Skip to content

feat(agent-core): stream live subagent events into background task output#2130

Open
kaile9 wants to merge 2 commits into
MoonshotAI:mainfrom
kaile9:feat/agent-task-live-events
Open

feat(agent-core): stream live subagent events into background task output#2130
kaile9 wants to merge 2 commits into
MoonshotAI:mainfrom
kaile9:feat/agent-task-live-events

Conversation

@kaile9

@kaile9 kaile9 commented Jul 24, 2026

Copy link
Copy Markdown

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 Agent tool is still running, TaskOutput and the /tasks panel 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:

  1. Agent.onEvent(listener) (packages/agent-core/src/agent/index.ts)
    An in-process event subscription next to the existing RPC emitEvent fan-out. The fork in feat(agent-core): stream live subagent events into background task output buffer #667 monkey-patched agent.emitEvent from 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 existing records.restoring guard (so replayed history is not re-streamed), and listener exceptions are swallowed so event delivery can never break the agent loop.

  2. 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.

  3. 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.delta stream 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 / warning become one-line markers with whitespace-collapsed 200-char previews, so the log stays scannable and bounded.
    • Everything else (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.
    • The foreground Agent tool result contract is unchanged: the parent agent still receives only the subagent's final summary (formatForegroundResult now reads handle.completion rather than the task output buffer, which would otherwise return the whole transcript).
    • Two comments in background/index.ts that 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: TaskOutput in docs/{en,zh}/reference/tools.md now mentions that Agent tasks stream live.

Out of scope, deliberately: the experimental v2 engine (agent-core-v2) has its own subagent/task path, and swarm members (AgentSwarm via SubagentBatch) 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

  • New tests: test/agent/events.test.ts (delivery, unsubscribe, a throwing listener must not break emitEvent or 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 new subagent-host.test.ts case 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 of main (e.g. test/tools/image-originals.test.ts, test/tools/glob.test.ts).
  • pnpm -C packages/agent-core typecheck clean; oxlint on the touched files reports no new warnings.
  • End-to-end (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

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

…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-bot

changeset-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 76386a6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 after AgentTool.execution registers the task — synchronously right after spawn/resume returns.
  • On the spawn path, the child turn only starts after configureChild (which awaits prepareSystemPromptContext, i.e. real file I/O), and turn.started is emitted inside the async turn runner. On the resume path, triggerSubagentStart awaits first. Both give the microtask several event-loop turns to install the listener, so turn.started and everything after it are captured.
  • The one path where the turn can start synchronously is retry(), which the Agent tool 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(agent-core): stream live subagent events into background task output buffer

1 participant