Problem
When using opencode run --format json with agents that invoke the task tool (subagents), the parent NDJSON stream receives no events from child sessions during execution — only the final tool_use event with status: completed after the subagent finishes.
This makes opencode run unsuitable as a headless runtime for multi-agent platforms that need to observe subagent progress in real time: tool calls, text tokens, step events.
We investigated every alternative:
- stdout during execution:
task tool only emits completed — no intermediate events
- stderr: opencode swallows child process stderr
- SQLite polling (
~/.local/share/opencode/opencode.db): text parts are written atomically at transaction commit — the data field is empty for the entire duration of subagent execution, only populated at the end
opencode serve + /event SSE: works, but requires a persistent process per session — not viable for stateless, ephemeral worker architectures
Root Cause
In cli/cmd/run.ts, the event loop filters out any event whose sessionID doesn't match the root session:
if (event.type === "message.part.updated") {
const part = event.properties.part
if (part.sessionID !== sessionID) continue // ← drops all child session events
Child session events (message.part.delta for streaming tokens, message.part.updated for completed parts) are published on the bus correctly — they're just silently discarded here.
The child sessionID is already available on the parent: task.ts stores it in ctx.metadata({ metadata: { sessionId: session.id } }), which surfaces as part.state.metadata.sessionId in the tool_use event when the task tool starts.
Proposed Solution
A minimal, additive change to run.ts — no changes to task.ts or the bus:
1. Track child session IDs as tasks start:
const childSessionIDs = new Set<string>()
// inside the message.part.updated block, before the sessionID filter:
if (part.type === "tool" && part.tool === "task" && part.state.status === "running") {
const meta = part.state.metadata as { sessionId?: string } | undefined
if (meta?.sessionId) childSessionIDs.add(meta.sessionId)
}
2. Forward child events with an envelope instead of dropping them:
if (part.sessionID !== sessionID) {
if (childSessionIDs.has(part.sessionID)) {
emit("subtask_event", { childSessionID: part.sessionID, part })
}
continue
}
3. Forward streaming 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,
})
}
}
This produces new NDJSON event types in --format json mode:
{"type":"subtask_event","sessionID":"ses_parent","childSessionID":"ses_child","part":{"type":"tool","tool":"bash",...}}
{"type":"subtask_delta","sessionID":"ses_parent","childSessionID":"ses_child","partID":"p_1","delta":"Analyzing"}
{"type":"subtask_delta","sessionID":"ses_parent","childSessionID":"ses_child","partID":"p_1","delta":" the code"}
{"type":"subtask_event","sessionID":"ses_parent","childSessionID":"ses_child","part":{"type":"text","text":"Done.",...}}
Why This Matters
opencode run is a compelling primitive for building multi-agent platforms: stateless, ephemeral, no persistent process per session. But without subagent observability it can only be used for fire-and-forget workflows where the final result is enough.
With this change, opencode run becomes a first-class runtime for platforms that need real-time visibility into every agent in the pipeline — same granularity as the SDK, none of the operational overhead of opencode serve.
Additional Notes
- Fully additive: existing behavior unchanged. Consumers that don't read
subtask_event/subtask_delta are unaffected.
- Opt-in flag: a
--subtask-events flag could gate this behavior to avoid unexpected stdout volume for users who don't need it.
- Tested scenario: we built a squad workflow experiment using a master agent + 3 subagents (planner/executor/reviewer) coordinated via a custom pipeline MCP. The missing observability is the only gap preventing production use.
We are happy to contribute the patch if the direction is approved.
/cc @cHIsIMun
Problem
When using
opencode run --format jsonwith agents that invoke thetasktool (subagents), the parent NDJSON stream receives no events from child sessions during execution — only the finaltool_useevent withstatus: completedafter the subagent finishes.This makes
opencode rununsuitable as a headless runtime for multi-agent platforms that need to observe subagent progress in real time: tool calls, text tokens, step events.We investigated every alternative:
tasktool only emitscompleted— no intermediate events~/.local/share/opencode/opencode.db): text parts are written atomically at transaction commit — thedatafield is empty for the entire duration of subagent execution, only populated at the endopencode serve+/eventSSE: works, but requires a persistent process per session — not viable for stateless, ephemeral worker architecturesRoot Cause
In
cli/cmd/run.ts, the event loop filters out any event whosesessionIDdoesn't match the root session:Child session events (
message.part.deltafor streaming tokens,message.part.updatedfor completed parts) are published on the bus correctly — they're just silently discarded here.The child
sessionIDis already available on the parent:task.tsstores it inctx.metadata({ metadata: { sessionId: session.id } }), which surfaces aspart.state.metadata.sessionIdin thetool_useevent when the task tool starts.Proposed Solution
A minimal, additive change to
run.ts— no changes totask.tsor the bus:1. Track child session IDs as tasks start:
2. Forward child events with an envelope instead of dropping them:
3. Forward streaming deltas from child sessions:
This produces new NDJSON event types in
--format jsonmode:{"type":"subtask_event","sessionID":"ses_parent","childSessionID":"ses_child","part":{"type":"tool","tool":"bash",...}} {"type":"subtask_delta","sessionID":"ses_parent","childSessionID":"ses_child","partID":"p_1","delta":"Analyzing"} {"type":"subtask_delta","sessionID":"ses_parent","childSessionID":"ses_child","partID":"p_1","delta":" the code"} {"type":"subtask_event","sessionID":"ses_parent","childSessionID":"ses_child","part":{"type":"text","text":"Done.",...}}Why This Matters
opencode runis a compelling primitive for building multi-agent platforms: stateless, ephemeral, no persistent process per session. But without subagent observability it can only be used for fire-and-forget workflows where the final result is enough.With this change,
opencode runbecomes a first-class runtime for platforms that need real-time visibility into every agent in the pipeline — same granularity as the SDK, none of the operational overhead ofopencode serve.Additional Notes
subtask_event/subtask_deltaare unaffected.--subtask-eventsflag could gate this behavior to avoid unexpected stdout volume for users who don't need it.We are happy to contribute the patch if the direction is approved.
/cc @cHIsIMun