feat(agent-core): stream live subagent events into background task output#2130
feat(agent-core): stream live subagent events into background task output#2130kaile9 wants to merge 2 commits into
Conversation
…tput While a subagent launched via the Agent tool is still running, TaskOutput and the /tasks panel only showed a static preview; output appeared once the task completed. Subscribe to the child agent's events (via a small Agent.onEvent API exposed on SubagentHandle) and mirror them into the task output buffer: thinking/assistant text streams verbatim under [thinking]/[assistant] markers, turns, tool calls/results, retries, errors and warnings become one-line markers with bounded previews. Adapted from the approach outlined in MoonshotAI#667 by @thecannabisapp.
🦋 Changeset detectedLatest commit: 76386a6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 76386a65f1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| sink.signal.addEventListener('abort', requestAbort, { once: true }); | ||
| } | ||
|
|
||
| const unsubscribe = this.handle.subscribeToEvents?.(createEventStreamer(sink)); |
There was a problem hiding this comment.
Subscribe before starting the child turn
For normal spawn/resume/retry calls, the subagent completion promise is started in SessionSubagentHost.runWithActiveChild before the AgentBackgroundTask is registered, and TurnFlow.prompt() emits turn.started synchronously as soon as that run reaches the child prompt. Because this listener is attached later from the background task lifecycle, TaskOutput can miss the initial turn marker and any other early child events, so the live transcript begins mid-run. Attach/buffer the child event stream before kicking off the child run, or have the handle replay early events to the task.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — this race is real in principle, so I traced the exact ordering before choosing this shape:
- The subscription is installed in
registerTask → runTaskLifecycle, i.e. a microtask that runs immediately afterAgentTool.executionregisters the task — synchronously right afterspawn/resumereturns. - On the
spawnpath, the child turn only starts afterconfigureChild(which awaitsprepareSystemPromptContext, i.e. real file I/O), andturn.startedis emitted inside the async turn runner. On theresumepath,triggerSubagentStartawaits first. Both give the microtask several event-loop turns to install the listener, soturn.startedand everything after it are captured. - The one path where the turn can start synchronously is
retry(), which theAgenttool never calls — it only serves the swarm batch path — and even there the worst case is losing the first decorative[turn N started]marker, never tool calls or thinking content.
So I'd rather keep the simpler subscribe-at-task-start shape than add a per-child replay buffer for one cosmetic line. Happy to add a short code comment documenting this ordering if you'd find that useful.
Related Issue
Resolve #667
First of all: the diagnosis and the original fix idea come from @thecannabisapp's report in #667 (fork commit
003420b9). This PR is my attempt to rebase that idea onto the current architecture. I've tested it locally with care, but I'm new to this codebase — if any part of the approach misses a convention or a simpler seam, I'm genuinely happy to rework it.Problem
See linked issue for the original report. In short: while a subagent launched through the
Agenttool is still running,TaskOutputand the/taskspanel only show a static preview — its output appears only once the task completes or errors. There is no way to watch what a subagent is doing, including its thinking, in real time. This is especially uncomfortable for long-running subagents, where "no news" is indistinguishable from "stuck".What changed
Three small pieces, each kept as narrow as I could make them:
Agent.onEvent(listener)(packages/agent-core/src/agent/index.ts)An in-process event subscription next to the existing RPC
emitEventfan-out. The fork in feat(agent-core): stream live subagent events into background task output buffer #667 monkey-patchedagent.emitEventfrom the outside; I was hesitant to carry that over, since overriding an instance method breaks silently if the class ever changes. A tiny explicit API felt more honest: ~15 lines, listeners are invoked after the existingrecords.restoringguard (so replayed history is not re-streamed), and listener exceptions are swallowed so event delivery can never break the agent loop.SubagentHandle.subscribeToEvents(packages/agent-core/src/session/subagent-host.ts)The handle already abstracts everything a caller needs to await a subagent (
completion); exposing the child's event stream through the same handle keeps the change to one line per call site (spawn/resume/retry). The field is optional, so existing handle constructions (tests, other callers) compile unchanged.Live mirroring in
AgentBackgroundTask.start()(packages/agent-core/src/agent/background/agent-task.ts)The task subscribes on start and always unsubscribes in
finally(success, failure, and kill paths). Formatting choices, all open to pushback:thinking.delta/assistant.deltastream verbatim, grouped under a single[thinking]/[assistant]marker per uninterrupted run — the goal was a log that reads like a transcript rather than a pile of JSON events.turn.started/turn.ended(with the error message when a turn fails) /turn.step.retrying(attempt counts included, so "stuck on API retry" is visible) /turn.step.interrupted/tool.call.started/tool.result/error/warningbecome one-line markers with whitespace-collapsed 200-char previews, so the log stays scannable and bounded.tool.call.delta,agent.status.updated, …) is deliberately omitted to keep the volume down; if maintainers would rather see more (or less), that's a one-line change per event type.Agenttool result contract is unchanged: the parent agent still receives only the subagent's final summary (formatForegroundResultnow readshandle.completionrather than the task output buffer, which would otherwise return the whole transcript).background/index.tsthat claimed agent results are "appended once / bounded" were updated — that invariant no longer holds, and agent-task output is now intentionally uncapped (text-sized; killing a subagent over log volume would be worse than the leak).Buffer size stays governed by the existing output ring (1 MiB) and persistence accounting — the same mechanism that already bounds process-task stdout. I considered whether raw delta streaming could push a very long subagent past the 16 MiB persisted cap and get it killed; in practice a multi-hour run produces low single-digit MiB of text, but I'd rather name the trade-off explicitly than hide it.
Docs:
TaskOutputindocs/{en,zh}/reference/tools.mdnow mentions thatAgenttasks stream live.Out of scope, deliberately: the experimental v2 engine (
agent-core-v2) has its own subagent/task path, and swarm members (AgentSwarmviaSubagentBatch) never register background tasks — neither is touched here. If this lands, I'd be glad to follow up on either as a separate change.Verification
test/agent/events.test.ts(delivery, unsubscribe, a throwing listener must not breakemitEventor other listeners);test/agent/background/agent-task-events.test.ts(live output visible before completion, marker grouping across kind switches,[result error]+ 200-char truncation, unsubscribe on both the completed and the failed settle paths, no output appended after settle); one newsubagent-host.test.tscase proving the handle's subscription reaches the child agent.pnpm -C packages/agent-core test— all tests pass except a set of pre-existing Windows path-separator failures that I confirmed also fail on a clean checkout ofmain(e.g.test/tools/image-originals.test.ts,test/tools/glob.test.ts).pnpm -C packages/agent-core typecheckclean;oxlinton the touched files reports no new warnings.kimi -p, this repo as workspace): launched a background subagent and read its task output file while it was still running — it contained[turn 0 started], its[thinking](including a mid-run change of plan),[tool]/[result]lines as they happened, exactly the behavior feat(agent-core): stream live subagent events into background task output buffer #667 asks for.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.