From bec5fdcf571f1b6f0b3248ac60b89eec6e1dbc1d Mon Sep 17 00:00:00 2001 From: Lucas Vinicius Date: Mon, 22 Jun 2026 13:45:10 -0300 Subject: [PATCH] feat(run): forward child session events to NDJSON stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the `task` tool spawns a subagent, its child session events are published on the bus but filtered out of the parent NDJSON stream (`opencode run --format json`). This makes `opencode run` unusable as a headless runtime for multi-agent platforms that need real-time visibility into subagent execution. Two new event types are emitted in `--format json` mode: - `subtask_delta` — streaming token delta from a child session (`childSessionID`, `partID`, `delta`) - `subtask_event` — completed/updated part from a child session (`childSessionID`, `part`) Child sessions are discovered when a `task` tool part transitions to `running` and its metadata exposes `sessionId`. Only events from known child sessions are forwarded; the filter is additive and does not affect any existing event types. Closes #33397 --- packages/opencode/src/cli/cmd/run.ts | 29 +++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index fad09c3a7add..f2aa5737180c 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -680,6 +680,9 @@ export const RunCommand = effectCmd({ async function loop(client: OpencodeClient, events: Awaited>) { const toggles = new Map() let error: string | undefined + // Track child session IDs spawned by the task tool so their events + // can be forwarded to the parent NDJSON stream. + const childSessionIDs = new Set() for await (const event of events.stream) { if ( @@ -695,9 +698,33 @@ export const RunCommand = effectCmd({ toggles.set("start", true) } + // Forward streaming token deltas from child sessions. + if (event.type === "message.part.delta") { + if (childSessionIDs.has(event.properties.sessionID)) { + emit("subtask_delta", { + childSessionID: event.properties.sessionID, + partID: event.properties.partID, + delta: event.properties.delta, + }) + } + } + if (event.type === "message.part.updated") { const part = event.properties.part - if (part.sessionID !== sessionID) continue + + // Register child session ID when a task tool starts running. + if (part.type === "tool" && part.tool === "task" && part.state.status === "running") { + const meta = (part.state as { metadata?: { sessionId?: string } }).metadata + if (meta?.sessionId) childSessionIDs.add(meta.sessionId) + } + + // Forward completed/updated parts from child sessions. + if (part.sessionID !== sessionID) { + if (childSessionIDs.has(part.sessionID)) { + emit("subtask_event", { childSessionID: part.sessionID, part }) + } + continue + } if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) { if (emit("tool_use", { part })) continue