Skip to content

[FEATURE] opencode run: expose child session events (subtask_event/subtask_delta) in NDJSON stream #33397

Description

@cHIsIMun

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions