Show provider subagents in the subagents track (#2013)

* 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>
This commit is contained in:
Mohamed Boudra
2026-07-13 03:13:00 +08:00
committed by GitHub
co-authored by Omer
parent 41d882859c
commit 66445adc07
46 changed files with 5056 additions and 337 deletions
+70 -2
View File
@@ -1,9 +1,13 @@
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 SubagentRow {
export interface PaseoSubagentRow {
kind: "paseo";
id: Agent["id"];
provider: Agent["provider"];
title: Agent["title"];
@@ -12,7 +16,21 @@ export interface SubagentRow {
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;
@@ -20,9 +38,11 @@ interface SelectSubagentsParams {
}
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,
@@ -62,11 +82,59 @@ export function selectSubagentsForParent(
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);
return useStoreWithEqualityFn(
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]);
}