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
@@ -555,6 +555,7 @@ test("advertises client capabilities in hello", async () => {
protocolVersion: 1,
capabilities: {
custom_mode_icons: true,
provider_subagents: true,
reasoning_merge_enum: true,
terminal_reflowable_snapshot: true,
browser_host: {
+76
View File
@@ -490,6 +490,22 @@ export interface FetchAgentTimelineOptions {
timeout?: number;
}
export type ProviderSubagentListPayload = Extract<
SessionOutboundMessage,
{ type: "agent.provider_subagents.list.response" }
>["payload"];
export type ProviderSubagentTimelinePayload = Extract<
SessionOutboundMessage,
{ type: "agent.provider_subagents.timeline.get.response" }
>["payload"];
export interface FetchProviderSubagentTimelineOptions {
direction?: ProviderSubagentTimelinePayload["direction"];
cursor?: FetchAgentTimelineCursor;
limit?: number;
requestId?: string;
timeout?: number;
}
// COMPAT(daemon-client-object-options): added in v0.1.102; remove after
// 2026-12-29 once SDK callers have migrated to object parameters.
function normalizeFetchAgentOptions(
@@ -2429,6 +2445,65 @@ export class DaemonClient {
return payload;
}
async listProviderSubagents(
parentAgentId: string,
options: { requestId?: string; timeout?: number } = {},
): Promise<ProviderSubagentListPayload> {
const requestId = this.createRequestId(options.requestId);
const message = SessionInboundMessageSchema.parse({
type: "agent.provider_subagents.list.request",
parentAgentId,
requestId,
});
const payload = await this.sendRequest({
requestId,
message,
timeout: options.timeout,
options: { skipQueue: true },
select: (response) =>
response.type === "agent.provider_subagents.list.response" &&
response.payload.requestId === requestId
? response.payload
: null,
});
if (payload.error) {
throw new Error(payload.error);
}
return payload;
}
async fetchProviderSubagentTimeline(
parentAgentId: string,
subagentId: string,
options: FetchProviderSubagentTimelineOptions = {},
): Promise<ProviderSubagentTimelinePayload> {
const requestId = this.createRequestId(options.requestId);
const message = SessionInboundMessageSchema.parse({
type: "agent.provider_subagents.timeline.get.request",
parentAgentId,
subagentId,
requestId,
...(options.direction ? { direction: options.direction } : {}),
...(options.cursor ? { cursor: options.cursor } : {}),
...(typeof options.limit === "number" ? { limit: options.limit } : {}),
});
const payload = await this.sendRequest({
requestId,
message,
timeout: options.timeout,
options: { skipQueue: true },
select: (response) =>
response.type === "agent.provider_subagents.timeline.get.response" &&
response.payload.requestId === requestId
? response.payload
: null,
});
if (payload.error) {
throw new Error(payload.error);
}
return payload;
}
async buildAgentForkContext(
agentId: string,
options: AgentForkContextOptions = {},
@@ -4672,6 +4747,7 @@ export class DaemonClient {
[CLIENT_CAPS.customModeIcons]: true,
[CLIENT_CAPS.reasoningMergeEnum]: true,
[CLIENT_CAPS.terminalReflowableSnapshot]: true,
[CLIENT_CAPS.providerSubagents]: true,
...this.config.capabilities,
},
...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),