docs(rfc): align scoped runtime contracts
This commit is contained in:
@@ -6,13 +6,13 @@ Status: implemented
|
||||
|
||||
The hooks subsystem ([interception seams RFC](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)), 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.
|
||||
This RFC 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 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`.
|
||||
**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 **`emit`s**. The service waits for `run.started` before firing `subagent/start`; an in-process listener can therefore reach the published child via `ctx.agents.get(info.id)` and `inject()` into it, while a remote provider need not have a local registry entry. It observes `run.result` immediately, snapshots the end payload before the caller can mutate it, and emits `subagent/end` only after start; readiness rejection emits neither event. The callbacks remain observe-only and per-listener containment keeps one bad subscriber from stranding a live run, surfacing as an unhandled rejection, or starving later listeners.
|
||||
Both events stay plain **`emit`s**. 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user