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
@@ -0,0 +1,304 @@
import type {
AgentStreamEventPayload,
ProviderSubagentDescriptorPayload,
SessionOutboundMessage,
} from "@getpaseo/protocol/messages";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { create } from "zustand";
import { applyStreamEvent } from "@/types/stream";
import type { StreamItem } from "@/types/stream";
import type { AgentLifecycleStatus } from "@getpaseo/protocol/agent-lifecycle";
type ProviderSubagentTimelineItem = Extract<
Extract<SessionOutboundMessage, { type: "agent.provider_subagents.update" }>["payload"],
{ kind: "timeline" }
>["item"];
interface ProviderSubagentTimelineRow {
provider: ProviderSubagentDescriptorPayload["provider"];
item: ProviderSubagentTimelineItem;
timestamp: string;
}
export interface ProviderSubagentTimelineState {
tail: StreamItem[];
head: StreamItem[];
epoch: string | null;
lastSeq: number;
hasOlder: boolean;
rows: Map<number, ProviderSubagentTimelineRow>;
}
interface ProviderSubagentState {
descriptors: Map<string, ProviderSubagentDescriptorPayload>;
timelines: Map<string, ProviderSubagentTimelineState>;
replaceList(
serverId: string,
parentAgentId: string,
subagents: ProviderSubagentDescriptorPayload[],
): void;
applyUpdate(
serverId: string,
payload: Extract<
SessionOutboundMessage,
{ type: "agent.provider_subagents.update" }
>["payload"],
): void;
replaceTimeline(
serverId: string,
payload: Extract<
SessionOutboundMessage,
{ type: "agent.provider_subagents.timeline.get.response" }
>["payload"],
): void;
}
export function providerSubagentKey(
serverId: string,
parentAgentId: string,
subagentId: string,
): string {
return `${serverId}\0${parentAgentId}\0${subagentId}`;
}
export function providerSubagentLifecycleStatus(
status: ProviderSubagentDescriptorPayload["status"],
): AgentLifecycleStatus {
if (status === "running") return "running";
if (status === "failed") return "error";
return "idle";
}
type ProviderSubagentListClient = Pick<DaemonClient, "listProviderSubagents">;
const pendingListRequests = new WeakMap<ProviderSubagentListClient, Map<string, Promise<void>>>();
export function refreshProviderSubagents(
client: ProviderSubagentListClient,
serverId: string,
parentAgentId: string,
): Promise<void> {
const requestKey = `${serverId}\0${parentAgentId}`;
let clientRequests = pendingListRequests.get(client);
if (!clientRequests) {
clientRequests = new Map();
pendingListRequests.set(client, clientRequests);
}
const pending = clientRequests.get(requestKey);
if (pending) return pending;
const request = client
.listProviderSubagents(parentAgentId)
.then((payload) => {
useProviderSubagentStore.getState().replaceList(serverId, parentAgentId, payload.subagents);
return undefined;
})
.finally(() => {
clientRequests?.delete(requestKey);
});
clientRequests.set(requestKey, request);
return request;
}
function parentPrefix(serverId: string, parentAgentId: string): string {
return `${serverId}\0${parentAgentId}\0`;
}
const EMPTY_TIMELINE: ProviderSubagentTimelineState = {
tail: [],
head: [],
epoch: null,
lastSeq: 0,
hasOlder: false,
rows: new Map(),
};
function providerSubagentTerminalEvent(
subagent: ProviderSubagentDescriptorPayload,
): AgentStreamEventPayload | null {
if (subagent.status === "running") {
return null;
}
if (subagent.status === "failed") {
return { type: "turn_failed", provider: subagent.provider, error: "Subagent failed" };
}
if (subagent.status === "canceled") {
return { type: "turn_canceled", provider: subagent.provider, reason: "canceled" };
}
return { type: "turn_completed", provider: subagent.provider };
}
function buildTimelineState(
rows: ProviderSubagentTimelineState["rows"],
epoch: string | null,
descriptor?: ProviderSubagentDescriptorPayload,
hasOlder = false,
): ProviderSubagentTimelineState {
let timeline = { tail: [] as StreamItem[], head: [] as StreamItem[] };
for (const [, row] of [...rows].sort(([left], [right]) => left - right)) {
timeline = applyStreamEvent({
...timeline,
event: { type: "timeline", provider: row.provider, item: row.item },
timestamp: new Date(row.timestamp),
});
}
const terminalEvent = descriptor ? providerSubagentTerminalEvent(descriptor) : null;
if (terminalEvent && descriptor) {
timeline = applyStreamEvent({
...timeline,
event: terminalEvent,
timestamp: new Date(descriptor.updatedAt),
});
}
return {
...timeline,
epoch,
lastSeq: rows.size ? Math.max(...rows.keys()) : 0,
hasOlder,
rows,
};
}
function buildTimelineResponseRows(
existing: ProviderSubagentTimelineState | undefined,
payload: Extract<
SessionOutboundMessage,
{ type: "agent.provider_subagents.timeline.get.response" }
>["payload"],
provider: ProviderSubagentDescriptorPayload["provider"],
): ProviderSubagentTimelineState["rows"] {
const rows = new Map<number, ProviderSubagentTimelineRow>();
for (const row of payload.rows) {
rows.set(row.seq, { provider, item: row.item, timestamp: row.timestamp });
}
if (payload.reset || existing?.epoch !== payload.epoch) {
return rows;
}
if (payload.direction !== "tail") {
return new Map([...existing.rows, ...rows]);
}
let nextSeq = payload.rows.length
? Math.max(...payload.rows.map((row) => row.seq)) + 1
: payload.window.maxSeq + 1;
for (const [seq, row] of [...existing.rows].sort(([left], [right]) => left - right)) {
if (seq < nextSeq) continue;
if (seq !== nextSeq) break;
rows.set(seq, row);
nextSeq += 1;
}
return rows;
}
export const useProviderSubagentStore = create<ProviderSubagentState>((set) => ({
descriptors: new Map(),
timelines: new Map(),
replaceList(serverId, parentAgentId, subagents) {
set((state) => {
const prefix = parentPrefix(serverId, parentAgentId);
const descriptors = new Map(
[...state.descriptors].filter(([key]) => !key.startsWith(prefix)),
);
for (const subagent of subagents) {
descriptors.set(providerSubagentKey(serverId, parentAgentId, subagent.id), subagent);
}
const retainedKeys = new Set(descriptors.keys());
const timelines = new Map(
[...state.timelines].filter(([key]) => !key.startsWith(prefix) || retainedKeys.has(key)),
);
for (const subagent of subagents) {
const key = providerSubagentKey(serverId, parentAgentId, subagent.id);
const current = timelines.get(key);
const previous = state.descriptors.get(key);
if (current && previous?.status !== subagent.status) {
timelines.set(
key,
buildTimelineState(current.rows, current.epoch, subagent, current.hasOlder),
);
}
}
return { descriptors, timelines };
});
},
applyUpdate(serverId, payload) {
set((state) => {
if (payload.kind === "upsert") {
const key = providerSubagentKey(
serverId,
payload.subagent.parentAgentId,
payload.subagent.id,
);
const descriptors = new Map(state.descriptors);
const previous = descriptors.get(key);
descriptors.set(key, payload.subagent);
let timelines = state.timelines;
const current = state.timelines.get(key);
if (current && previous?.status !== payload.subagent.status) {
timelines = new Map(state.timelines);
timelines.set(
key,
buildTimelineState(current.rows, current.epoch, payload.subagent, current.hasOlder),
);
}
return { descriptors, timelines };
}
if (payload.kind === "remove") {
const key = providerSubagentKey(serverId, payload.parentAgentId, payload.subagentId);
const descriptors = new Map(state.descriptors);
const timelines = new Map(state.timelines);
descriptors.delete(key);
timelines.delete(key);
return { descriptors, timelines };
}
const key = providerSubagentKey(serverId, payload.parentAgentId, payload.subagentId);
const existing = state.timelines.get(key);
if (existing?.epoch && existing.epoch !== payload.epoch) {
return state;
}
const current = existing ?? EMPTY_TIMELINE;
if (payload.seq <= current.lastSeq) {
return state;
}
const rows = new Map(current.rows);
rows.set(payload.seq, {
provider: payload.provider,
item: payload.item,
timestamp: payload.timestamp,
});
const descriptor = state.descriptors.get(key);
const next =
descriptor && descriptor.status !== "running"
? buildTimelineState(rows, payload.epoch, descriptor, current.hasOlder)
: applyStreamEvent({
tail: current.tail,
head: current.head,
event: { type: "timeline", provider: payload.provider, item: payload.item },
timestamp: new Date(payload.timestamp),
});
const timelines = new Map(state.timelines);
timelines.set(key, {
...next,
epoch: payload.epoch,
lastSeq: payload.seq,
hasOlder: current.hasOlder,
rows,
});
return { timelines };
});
},
replaceTimeline(serverId, payload) {
const provider = payload.provider;
if (!provider) {
return;
}
set((state) => {
const key = providerSubagentKey(serverId, payload.parentAgentId, payload.subagentId);
const existing = state.timelines.get(key);
const rows = buildTimelineResponseRows(existing, payload, provider);
const descriptor = state.descriptors.get(key);
const timelines = new Map(state.timelines);
timelines.set(key, buildTimelineState(rows, payload.epoch, descriptor, payload.hasOlder));
return { timelines };
});
},
}));