Files
deepseek-harness/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md
Tianyi Cui 84f3019310 refactor(subagent): drop the agentType lifecycle field
Address review: `agentType` was a Claude-Code concept (`subagent_type`) that
does not fit our own subagent seam — nothing in the harness interprets it, and
its only consumer was the CC-dialect hook bridge. Rather than let a foreign
concept sit on the core seam, remove it:

- `SubagentStartRequest`, `SubagentRunInfo`, `SubagentRunEndInfo`: drop the
  `agentType` field; the `subagent/start`/`subagent/end` payloads now carry
  `provider`/`id` (+ end `stopReason`/`lastAssistantMessage`) only.
- `dsh-tool-subagent`: drop `Config.agentType` and its request plumbing.
- Tests: keep the lastAssistantMessage / clone-containment / reject-path
  coverage (rewritten to not assert agentType); delete the two tool-subagent
  tests that only exercised agentType forwarding (dead behavior).
- Docs: retitle + rewrite the subagent-observe-enrich RFC to the one shipped
  enrichment (lastAssistantMessage), with a note on why agentType was dropped;
  update rfc/README index title, both subagent READMEs, and the
  core-data-structures/subagent.md type-equiv block + prose; regenerate catalog.

The CC bridge (PR-F) will feed Claude Code's own default matcher value
"general-purpose" for its SubagentStart/Stop agent_type matcher instead.
2026-07-02 05:52:02 +08:00

4.5 KiB

RFC: Subagent lifecycle enrichment — lastAssistantMessage (observe-only)

Status: implemented (accepted 2026-06-30)

Context

The hooks subsystem (interception seams RFC) lets a plugin observe and gate the agent at lifecycle points. Claude Code and Codex both expose SubagentStart / SubagentStop hooks, and CC's carry the subagent's final message. The harness already emits subagent/start and subagent/end lifecycle events (the subagent capability-seam), but their payloads were minimal (provider, id, and on end stopReason) — not enough for a hooks bridge to report WHAT a subagent produced without separately reaching for the live run.

This RFC enriches the end payload. It is deliberately observe-only: no control-flow change, no waterfall, no start() restructure. A run-affecting subagent-stop decision (continuation, injection that changes the run) is a separate, larger redesign and stays out of scope.

Decision

Add lastAssistantMessage — the child's final output — to SubagentRunEndInfo. On the settle path it is a DEEP CLONE of SubagentResult.output (so an observer sees WHAT the subagent produced without holding the run). On the REJECT path (an infrastructure fault where no SubagentResult was produced — the seam only knows stopReason: 'error') it is absent. The clone is load-bearing for observe-only: the subagent/end emit fires from a detached .then registered before start() returns, i.e. before the caller's own await run.result continuation — handing listeners the same array reference would let a mutating listener corrupt the caller's SubagentResult.output. structuredClone makes the event a read-only view (a regression test mutates the event's array and asserts the caller's result is untouched); a clone failure is contained (logged, the event still fires without lastAssistantMessage) rather than becoming an unhandled rejection on the detached .then.

Both events stay plain emits. subagent/end fires from a detached .then on run.result and awaits no listener, so it is genuinely observe-only by construction — a subagent/start listener can still reach the live child via ctx.agents.get(info.id) and inject() into it; a subagent/end listener can only observe (the run has settled). Per-listener containment (already in place) keeps one bad subscriber from stranding a live run or surfacing as an unhandled rejection on the detached settle hook.

Why observe-only, and what is deferred

A control-flow subagent/end (an awaited waterfall returning a stop/continue decision, like the other interception seams) would require: reshaping subagent/end from emit to waterfall, restructuring SubagentService.start to await listeners before settling, and implementing the resume capability in the in-process provider so a "continue" can actually re-run the child. That belongs to the background/steering subagent redesign the capability-seam RFC already defers (the same redesign that unifies long-running-tool handling across subagents and bash). This RFC ships the observe-only enrichment a hooks bridge needs today; FIXME(subagent-continuation) / TODO anchors mark where the control-flow version would land if and when that redesign happens.

Consequences

A hooks bridge (or a native plugin) can now forward the child's lastAssistantMessage to a SubagentStop handler by subscribing to the existing emits — no new control-flow surface. The vocabulary addition is documented in docs/core-data-structures/subagent.md (the events prose) and the two subagent READMEs; the catalog is regenerated. No production behavior changes — the events fire exactly as before, with one more (optional) field on the end payload — so no snapshot or e2e change is needed.