Subagent lifecycle enrichment — lastAssistantMessage (observe-only)
Subagent 生命周期丰富化——lastAssistantMessage(仅观察)
The hooks subsystem ([interception seams Agent Note](2026-06-30-interception-seams.md)) 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](2026-06-21-subagent-capability-seam.md))
English
Problem
The hooks subsystem (interception seams Agent Note) 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 Agent Note enriches the end payload. It is deliberately observe-only: no control-flow change and no waterfall. 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 the readonly typed SubagentResult.output, so an observer sees what the child produced without holding the run. On an infrastructure rejection where no SubagentResult exists, it is absent and the event reports stopReason: 'error'. Providers and listeners are trusted same-process collaborators and honor the borrowed immutable payload contract.
Both events stay plain emits. Async SubagentService.start() attaches result observation to the ready provider run, emits subagent/start, and then returns the run; an in-process listener can therefore reach the published child via ctx.agents.get(info.id), while a remote provider need not have a local registry entry. A rejected provider start emits neither event. The callbacks remain observe-only and per-listener containment keeps one bad subscriber from stranding a live run or starving later listeners.
Alternatives considered
An agentType subagent-kind label (the harness analogue of CC's subagent_type) on the request + both lifecycle payloads — an earlier draft shipped it; dropped in review because it is a Claude-Code concept that does not fit our own seam (nothing here interprets it, and the only consumer was a CC-dialect bridge). The CC bridge instead feeds Claude Code's own default matcher value "general-purpose" for its SubagentStart/Stop agent_type matcher, so this Agent Note ships ONE enrichment: lastAssistantMessage.
A control-flow subagent/end — deferred; see below.
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 Agent Note already defers (the same redesign that unifies long-running-tool handling across subagents and bash). This Agent Note 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.
中文
问题
钩子子系统(拦截 seam Agent Note)允许插件在生命周期节点观察和拦截 agent(智能体)。Claude Code 和 Codex 都暴露了 SubagentStart / SubagentStop 钩子,且 CC 的钩子携带 subagent 的最终消息。harness 已经发出 subagent/start 和 subagent/end 生命周期事件(subagent 能力 seam),但其载荷极为精简(provider、id,以及 end 时的 stopReason),不足以让钩子桥接层在不单独访问活跃 run 的情况下报告 subagent 产出了什么。
本 Agent Note 丰富 end 载荷。它刻意限定为仅观察:不改变控制流,不引入 waterfall(瀑布式事件)。影响 run 的 subagent-stop 决策(续行、改变 run 的注入)属于另一个更大的重设计,不在本 Agent Note 范围内。
决策
在 SubagentRunEndInfo 中添加 lastAssistantMessage——子 agent 的最终输出。 在正常结束路径上,它是只读的类型化 SubagentResult.output,观察者无需持有 run 即可看到子 agent 产出了什么。在基础设施拒绝(不存在 SubagentResult)的情况下,该字段缺失,事件报告 stopReason: 'error'。提供方与监听方是受信任的同进程协作者,遵守借用不可变载荷的契约。
两个事件仍为普通 emit。异步的 SubagentService.start() 将结果观察附加到就绪的提供方 run 上,发出 subagent/start,然后返回该 run;进程内监听方因此可以通过 ctx.agents.get(info.id) 访问已发布的子 agent,而远程提供方无需在本地注册表中有对应条目。提供方启动被拒绝时不发出任何事件。回调保持仅观察,且逐监听方隔离确保一个异常订阅者不会阻塞活跃 run 或饿死后续监听方。
曾考虑的替代方案
agentType subagent 类别标签(CC 的 subagent_type 在 harness 中的对应物),放在请求与两个生命周期载荷上。早期草案曾包含它;评审中移除,因为它是 Claude Code 的概念,不适合我们自己的 seam(此处没有任何逻辑解释它,唯一消费方是 CC 方言桥接层)。CC 桥接层改为直接为其 SubagentStart/Stop 的 agent_type matcher 填入 Claude Code 自身的默认值 "general-purpose",因此本 Agent Note 只交付一项丰富化:lastAssistantMessage。
控制流式 subagent/end:推迟;见下文。
为何仅观察,以及推迟了什么
控制流式 subagent/end(一个被 await 的 waterfall,返回停止/继续决策,与其他拦截 seam 一致)需要:将 subagent/end 从 emit 改为 waterfall、重构 SubagentService.start 使其在结算前 await 监听方、在进程内提供方中实现 resume 能力以便「继续」能真正重新运行子 agent。这属于能力 seam Agent Note 已推迟的后台/steering(中途引导)subagent 重设计(同一个重设计还将统一 subagent 与 bash 之间的长时间运行工具处理)。本 Agent Note 交付钩子桥接层当前所需的仅观察丰富化;FIXME(subagent-continuation) / TODO 锚点标记了控制流版本在重设计发生时的落点。
后果
钩子桥接层(或原生插件)现在可以通过订阅既有 emit 将子 agent 的 lastAssistantMessage 转发给 SubagentStop 处理器,无需新的控制流接口。词汇新增记录在 docs/core-data-structures/subagent.md(事件行文部分)与两个 subagent README 中;catalog 已重新生成。生产行为无变化——事件触发方式与之前完全一致,end 载荷上多了一个可选字段——因此无需更新快照或 e2e 测试。