* Add provider-owned subagent stream contract * OpenCode provider: detect child sessions, attach adopted children to the hosting server, synthesize external turns - Emit internal provider_child_session_detected / provider_session_deleted stream events from the global event stream (before the no-active-turn gate), including BFS hydration of existing descendants on subscribe - Track child sessions in a registry mapping child session id -> hosting server url; resumeSession attaches via acquireExisting() so adopted children share the parent's server generation and keep receiving live events - Synthesize external turns for adopted (externally driven) sessions so timeline, tool calls, and permission requests stream without a Paseo-initiated prompt - Honor launchContext.env in resumeSession (acquireDedicated parity with createSession) - Stop folding child tool parts into the parent's sub_agent action log; the parent row stays as a pointer * Render provider subagents in workspace timelines * fix provider subagent timeline reliability * fix provider subagent lifecycle edge cases * fix provider subagent event fidelity * Fix provider subagent history fidelity * Fix restored provider subagent state * Fix provider subagent visibility edges * Restore provider subagent timelines * Page provider subagent history * Forward subagent questions reliably * fix(subagents): preserve OpenCode child context * fix(subagents): harden provider timeline routing * fix(subagents): preserve provider page continuity * fix(subagents): retain live child prompts --------- Co-authored-by: Omer <639682+omercnet@users.noreply.github.com>
141 lines
4.4 KiB
TypeScript
141 lines
4.4 KiB
TypeScript
import { useEffect, useMemo } from "react";
|
|
import { usePendingArchiveAgentIds } from "@/hooks/use-archive-agent";
|
|
import equal from "fast-deep-equal";
|
|
import { useStoreWithEqualityFn } from "zustand/traditional";
|
|
import { useSessionStore, type Agent } from "@/stores/session-store";
|
|
import { refreshProviderSubagents, useProviderSubagentStore } from "./provider-store";
|
|
import type { ProviderSubagentDescriptorPayload } from "@getpaseo/protocol/messages";
|
|
|
|
export interface PaseoSubagentRow {
|
|
kind: "paseo";
|
|
id: Agent["id"];
|
|
provider: Agent["provider"];
|
|
title: Agent["title"];
|
|
status: Agent["status"];
|
|
requiresAttention: Agent["requiresAttention"];
|
|
createdAt: Agent["createdAt"];
|
|
}
|
|
|
|
export interface ProviderSubagentRow {
|
|
kind: "provider";
|
|
id: string;
|
|
parentAgentId: string;
|
|
provider: ProviderSubagentDescriptorPayload["provider"];
|
|
title: string | null;
|
|
status: ProviderSubagentDescriptorPayload["status"];
|
|
requiresAttention: boolean;
|
|
createdAt: Date;
|
|
}
|
|
|
|
export type SubagentRow = PaseoSubagentRow | ProviderSubagentRow;
|
|
|
|
type SessionStoreSnapshot = ReturnType<typeof useSessionStore.getState>;
|
|
type ProviderSubagentStoreSnapshot = ReturnType<typeof useProviderSubagentStore.getState>;
|
|
|
|
interface SelectSubagentsParams {
|
|
serverId: string;
|
|
parentAgentId: string;
|
|
}
|
|
|
|
const EMPTY_SUBAGENT_ROWS: SubagentRow[] = [];
|
|
const EMPTY_PROVIDER_SUBAGENT_ROWS: ProviderSubagentRow[] = [];
|
|
|
|
function toSubagentRow(agent: Agent): SubagentRow {
|
|
return {
|
|
kind: "paseo",
|
|
id: agent.id,
|
|
provider: agent.provider,
|
|
title: agent.title,
|
|
status: agent.status,
|
|
requiresAttention: agent.requiresAttention,
|
|
createdAt: agent.createdAt,
|
|
};
|
|
}
|
|
|
|
export function selectSubagentsForParent(
|
|
state: SessionStoreSnapshot,
|
|
params: SelectSubagentsParams,
|
|
pendingArchiveIds: ReadonlySet<string>,
|
|
): SubagentRow[] {
|
|
const agents = state.sessions[params.serverId]?.agents;
|
|
if (!agents || agents.size === 0) {
|
|
return EMPTY_SUBAGENT_ROWS;
|
|
}
|
|
|
|
const rows: SubagentRow[] = [];
|
|
for (const agent of agents.values()) {
|
|
if (
|
|
agent.archivedAt ||
|
|
pendingArchiveIds.has(agent.id) ||
|
|
agent.parentAgentId !== params.parentAgentId
|
|
) {
|
|
continue;
|
|
}
|
|
rows.push(toSubagentRow(agent));
|
|
}
|
|
|
|
if (rows.length === 0) {
|
|
return EMPTY_SUBAGENT_ROWS;
|
|
}
|
|
|
|
rows.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime());
|
|
return rows;
|
|
}
|
|
|
|
export function selectProviderSubagentsForParent(
|
|
state: ProviderSubagentStoreSnapshot,
|
|
params: SelectSubagentsParams,
|
|
supported: boolean,
|
|
): ProviderSubagentRow[] {
|
|
if (!supported) return EMPTY_PROVIDER_SUBAGENT_ROWS;
|
|
const rows: ProviderSubagentRow[] = [];
|
|
const prefix = `${params.serverId}\0${params.parentAgentId}\0`;
|
|
for (const [key, subagent] of state.descriptors) {
|
|
if (!key.startsWith(prefix)) continue;
|
|
rows.push({
|
|
kind: "provider",
|
|
id: subagent.id,
|
|
parentAgentId: subagent.parentAgentId,
|
|
provider: subagent.provider,
|
|
title: subagent.title ?? subagent.description,
|
|
status: subagent.status,
|
|
requiresAttention: subagent.status === "failed",
|
|
createdAt: new Date(subagent.createdAt),
|
|
});
|
|
}
|
|
rows.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime());
|
|
return rows;
|
|
}
|
|
|
|
export function useSubagentsForParent(params: SelectSubagentsParams): SubagentRow[] {
|
|
const pendingArchiveIds = usePendingArchiveAgentIds(params.serverId);
|
|
const paseoRows = useStoreWithEqualityFn(
|
|
useSessionStore,
|
|
(state) => selectSubagentsForParent(state, params, pendingArchiveIds),
|
|
equal,
|
|
);
|
|
const supported = useSessionStore(
|
|
(state) => state.sessions[params.serverId]?.serverInfo?.features?.providerSubagents === true,
|
|
);
|
|
const providerRows = useStoreWithEqualityFn(
|
|
useProviderSubagentStore,
|
|
(state) => selectProviderSubagentsForParent(state, params, supported),
|
|
equal,
|
|
);
|
|
const client = useSessionStore((state) => state.sessions[params.serverId]?.client ?? null);
|
|
|
|
useEffect(() => {
|
|
if (!client || !supported) return;
|
|
void refreshProviderSubagents(client, params.serverId, params.parentAgentId).catch(
|
|
() => undefined,
|
|
);
|
|
}, [client, params.parentAgentId, params.serverId, supported]);
|
|
|
|
return useMemo(() => {
|
|
if (providerRows.length === 0) return paseoRows;
|
|
const rows = [...paseoRows, ...providerRows];
|
|
rows.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime());
|
|
return rows;
|
|
}, [paseoRows, providerRows]);
|
|
}
|