Просмотр исходного кода

refactor(agent): rename agent/request-messages to agent/request-advice

tianyicui's review: the seam name did not say what the event or its
types do. 'advice' reads both ways — advisory content for the model,
and AOP before/after advice woven around a join point (here the
derived history) without modifying it — so RequestAdvice.before/after
are self-describing. Types follow: RequestAdvice / RequestAdviceContext;
the logged EpochHeader fields keep their positional names
(messagePrefix/messageSuffix).

Also sharpens the core.md wording the review flagged as ambiguous:
before-advice sits in front of the ENTIRE derived history, directly
after the system slot (the conventional home for session-stable openers
— an AGENTS.md digest, a skills catalog), after-advice follows the
history's last message. Catalogs and doc graphs regenerated.
Yichen Jiang 2 месяцев назад
Родитель
Сommit
e97fffeab7

+ 2 - 2
docs/architecture.md

@@ -72,7 +72,7 @@ forever:
       agent/pre-step
       'step/start'
       snapshot the derived messages (the reconstruction boundary)
-      agent/request (config only) -> agent/request-messages -> log request/header -> llm/stream (frozen)
+      agent/request (config only) -> agent/request-advice -> log request/header -> llm/stream (frozen)
         'assistant/chunk'
       agent/step-result
       'assistant/message'
@@ -141,7 +141,7 @@ New behavior should attach to a documented seam; changing the shipped loop requi
 | Add command execution | implement and register a `ctx.bash` backend |
 | Add filesystem access or policy | implement a `ctx.fs` provider or listen on `fs/*` policy events |
 | Intercept prompts, requests, tool use, or continuation | listen on the relevant `agent/*` or `tools/*` waterfall |
-| Add per-request context that must not become history | contribute request-only messages on `agent/request-messages`; logged on the request header |
+| Add per-request context that must not become history | contribute request-only messages on `agent/request-advice`; logged on the request header |
 | Add UI or editor integration | drive `ctx.agents` and render from `session/event` |
 | Add durable session state | add a `SessionEventMap` member and render/replay from the log |
 | Fork a live session | use `ctx.sessions.fork(source, boundary?, childSessionId?)` |

+ 17 - 17
docs/cordis-catalog/events.md

@@ -23,7 +23,7 @@ An agent was registered in the AgentRegistry and is ready to receive messages.
 
 Types: [Agent](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:308`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:313`](../../packages/core/agent/src/types.ts)
 
 ### `agent/disposed` — emit
 
@@ -35,7 +35,7 @@ An agent was disposed and removed from the registry; its fiber and any in-flight
 
 Types: [Agent](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:315`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:320`](../../packages/core/agent/src/types.ts)
 
 ### `agent/error` — emit
 
@@ -47,7 +47,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
 
 Types: [Agent](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:506`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:512`](../../packages/core/agent/src/types.ts)
 
 ### `agent/pre-step` — serial
 
@@ -61,7 +61,7 @@ Serial (awaited in registration order), not a waterfall: a listener mutates the
 
 Types: [Agent](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:393`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:398`](../../packages/core/agent/src/types.ts)
 
 ### `agent/prompt-submit` — waterfall
 
@@ -73,7 +73,7 @@ Waterfall: decide what happens to ONE drained queued message before it becomes a
 
 Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:406`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:411`](../../packages/core/agent/src/types.ts)
 
 ### `agent/queued` — emit
 
@@ -85,11 +85,11 @@ A message entered the agent's inbox (queued or steering). `source` is the resolv
 
 Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:333`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:338`](../../packages/core/agent/src/types.ts)
 
 ### `agent/request` — waterfall
 
-Waterfall: shape the step's call configuration — model switching, sampling overrides — by returning a replacement LlmCallConfig (the frozen seed is the config the loop would otherwise use). Config is ALL a listener shapes here: every request is a pure function of the session log (the reconstructability RFC), so model-visible content flows through the log channels — `inject()`, steering, prompt-submit `additionalContext`, prompt sections via `system-prompt/assemble`, or header-logged request-only messages via agent/request-messages — never through request mutation, and the loop records whatever config the request actually uses as a `request/header*` event before dispatch. The step's messages are already snapshotted when this fires (the `step/start` boundary): an `inject()` from a listener here lands in the log but joins the NEXT request. For surface mutation that must precede the snapshot (compaction), use agent/pre-step. Call `next()` to delegate, or return an LlmCallConfig without it to short-circuit.
+Waterfall: shape the step's call configuration — model switching, sampling overrides — by returning a replacement LlmCallConfig (the frozen seed is the config the loop would otherwise use). Config is ALL a listener shapes here: every request is a pure function of the session log (the reconstructability RFC), so model-visible content flows through the log channels — `inject()`, steering, prompt-submit `additionalContext`, prompt sections via `system-prompt/assemble`, or header-logged request-only messages via agent/request-advice — never through request mutation, and the loop records whatever config the request actually uses as a `request/header*` event before dispatch. The step's messages are already snapshotted when this fires (the `step/start` boundary): an `inject()` from a listener here lands in the log but joins the NEXT request. For surface mutation that must precede the snapshot (compaction), use agent/pre-step. Call `next()` to delegate, or return an LlmCallConfig without it to short-circuit.
 
 ```ts cordis-catalog
 'agent/request'(agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
@@ -97,23 +97,23 @@ Waterfall: shape the step's call configuration — model switching, sampling ove
 
 Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:430`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:435`](../../packages/core/agent/src/types.ts)
 
-### `agent/request-messages` — waterfall
+### `agent/request-advice` — waterfall
 
-Waterfall: contribute request-ONLY messages around the derived history — a RequestMessages whose `before` messages precede the boundary snapshot in `GenerateOptions.messages` and whose `after` messages follow it. Fires once per step, inside the open step, after the agent/request config waterfall and before the loop logs the request header. This is the seam for per-request advisory context the model must see NOW but that must NOT become durable history (a skills catalog, an environment reminder): contributions are recorded on the request's `request/header*` event (`EpochHeader.messagePrefix` / `messageSuffix`) — never as session messages — so `Session.deriveMessages()` stays untouched and the request remains reconstructable from the log.
+Waterfall: weave request-ONLY advice around the derived history — a RequestAdvice whose `before` messages sit in front of the ENTIRE boundary snapshot in `GenerateOptions.messages` and whose `after` messages follow its last message. Fires once per step, inside the open step, after the agent/request config waterfall and before the loop logs the request header. This is the seam for per-request advisory context the model must see NOW but that must NOT become durable history (a skills catalog, an environment reminder): contributions are recorded on the request's `request/header*` event (`EpochHeader.messagePrefix` / `messageSuffix`) — never as session messages — so `Session.deriveMessages()` stays untouched and the request remains reconstructable from the log.
 
-The seed is frozen and empty; a contributing listener returns a NEW RequestMessages extending `await next()` (spread its arrays — never mutate them), so contributions compose across plugins in registration order. The boundary snapshot is already taken when this fires: a `session.append`/`inject()` from a listener here lands in the log but joins the NEXT request — contribute through the returned value, not the session. Call `next()` to delegate, or return a RequestMessages without it to short-circuit.
+The seed is frozen and empty; a contributing listener returns a NEW RequestAdvice extending `await next()` (spread its arrays — never mutate them), so contributions compose across plugins in registration order. The boundary snapshot is already taken when this fires: a `session.append`/`inject()` from a listener here lands in the log but joins the NEXT request — contribute through the returned value, not the session. Call `next()` to delegate, or return a RequestAdvice without it to short-circuit.
 
 Pick the channel by change frequency (the cost model): a contribution rides the request's uncached tail, re-tokenized at full price on EVERY request it appears in — cheap only while small. Session-FROZEN content belongs in `before`, where it extends the cacheable prefix at zero marginal cost (but changing it mid-session invalidates the provider cache for the entire history after it). A LOW-FREQUENCY change notice belongs in durable history via `agent.inject()` — appended once, prefix-cached thereafter. Reserve `after` for small, frequently refreshed state snapshots, where a durable chain of stale copies would bloat the log and mislead the model.
 
 ```ts cordis-catalog
-'agent/request-messages'(agent: Agent, turn: number, step: number, messages: RequestMessages, context: RequestMessagesContext, next: () => Promise<RequestMessages>): Promise<RequestMessages>
+'agent/request-advice'(agent: Agent, turn: number, step: number, advice: RequestAdvice, context: RequestAdviceContext, next: () => Promise<RequestAdvice>): Promise<RequestAdvice>
 ```
 
 Types: [Agent](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:471`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:477`](../../packages/core/agent/src/types.ts)
 
 ### `agent/session-start` — emit
 
@@ -125,7 +125,7 @@ The agent's session lifecycle began, fired once before its first turn. `source`
 
 Types: [Agent](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:348`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:353`](../../packages/core/agent/src/types.ts)
 
 ### `agent/status` — emit
 
@@ -137,7 +137,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). Drive lifecycle
 
 Types: [Agent](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:324`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:329`](../../packages/core/agent/src/types.ts)
 
 ### `agent/step-result` — waterfall
 
@@ -149,7 +149,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va
 
 Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:481`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:487`](../../packages/core/agent/src/types.ts)
 
 ### `agent/turn-continuation` — waterfall
 
@@ -161,7 +161,7 @@ Waterfall: override the turn-continuation decision via a typed ContinuationDecis
 
 Types: [Agent](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/types.ts:494`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:500`](../../packages/core/agent/src/types.ts)
 
 ## `fs/*`
 

+ 9 - 9
docs/core-data-structures/core.md

@@ -193,9 +193,9 @@ The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition`
 
 ### The request envelope: `LlmCallConfig` and the logged header
 
-Requests are built by the loop, not shaped per call: the non-history half of a request — the `EpochHeader`: this call configuration plus the rendered system prompt, assembled tool schemas, and any request-only messages — is logged session state (`request/header` snapshot and delta events, [session.md](session.md#the-request-header-events-requestheader-and-requestheader-delta)), so every conversation request is a pure function of the session log ([reconstructability RFC](../rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)). The `agent/request` waterfall receives a frozen `LlmCallConfig` seed and a listener returns a replacement to switch model or sampling; the `agent/request-messages` waterfall contributes request-only messages framing the derived history (recorded as the header's `messagePrefix`/`messageSuffix`) — the loop logs whatever the request actually uses. Loop-built requests arrive at `llm/stream` deep-frozen; mutation throws.
+Requests are built by the loop, not shaped per call: the non-history half of a request — the `EpochHeader`: this call configuration plus the rendered system prompt, assembled tool schemas, and any request-only messages — is logged session state (`request/header` snapshot and delta events, [session.md](session.md#the-request-header-events-requestheader-and-requestheader-delta)), so every conversation request is a pure function of the session log ([reconstructability RFC](../rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)). The `agent/request` waterfall receives a frozen `LlmCallConfig` seed and a listener returns a replacement to switch model or sampling; the `agent/request-advice` waterfall weaves request-only advice around the derived history (recorded as the header's `messagePrefix`/`messageSuffix`) — the loop logs whatever the request actually uses. Loop-built requests arrive at `llm/stream` deep-frozen; mutation throws.
 
-On the wire, a loop-built request reads in this order: the `system` slot (the rendered prompt assembly) → `messagePrefix` (request-only `before` contributions) → the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps — → `messageSuffix` (request-only `after` contributions, the last thing the model reads). The framing arrays never enter the derived history; their durable record is the header events, and the dev invariant recomputes exactly this equation against every loop-built request.
+On the wire, a loop-built request reads in this order: the `system` slot (the rendered prompt assembly) → `messagePrefix` (the request-only `before` advice) → the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps — → `messageSuffix` (the request-only `after` advice, the last thing the model reads). The advice arrays never enter the derived history; their durable record is the header events, and the dev invariant recomputes exactly this equation against every loop-built request.
 
 FIXME(call-config-shape): revisit the exact definition of this type — which fields are genuinely epoch-level for cache purposes (`model` certainly; the sampling scalars sit here out of caution), and where provider-specific extras (reasoning options, extra body params) belong when an adapter needs them.
 
@@ -328,7 +328,7 @@ interface Agent {
 }
 ```
 
-`AgentStatus` is `'idle' | 'running' | 'disposed'`. `AgentId` is a branded string. `AgentOptions` (`model?`) is merge-extensible — plugins add creation options by declaration merging; the persona is NOT an agent option but the `dsh-system-prompt` plugin's `persona` config, shared context-wide. The `agent/*` event taxonomy (lifecycle emits incl. `agent/session-start`, the serial `agent/pre-step` surface-mutation seam, and the `agent/prompt-submit`/`agent/request`/`agent/request-messages`/`agent/step-result`/`agent/turn-continuation` waterfalls) is in [architecture.md § Event taxonomy](../architecture.md#event-taxonomy); turn/step boundaries are durable `session/event` records, not `agent/*` emits.
+`AgentStatus` is `'idle' | 'running' | 'disposed'`. `AgentId` is a branded string. `AgentOptions` (`model?`) is merge-extensible — plugins add creation options by declaration merging; the persona is NOT an agent option but the `dsh-system-prompt` plugin's `persona` config, shared context-wide. The `agent/*` event taxonomy (lifecycle emits incl. `agent/session-start`, the serial `agent/pre-step` surface-mutation seam, and the `agent/prompt-submit`/`agent/request`/`agent/request-advice`/`agent/step-result`/`agent/turn-continuation` waterfalls) is in [architecture.md § Event taxonomy](../architecture.md#event-taxonomy); turn/step boundaries are durable `session/event` records, not `agent/*` emits.
 
 ## Interception decisions
 
@@ -365,21 +365,21 @@ type ContinuationDecision =
 type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
 ```
 
-`agent/request-messages` returns a `RequestMessages` — request-only `before`/`after` messages framing the derived history for ONE request. Not a Decision union: the seam contributes content instead of vetoing, so the shape is the contribution itself; the loop records the non-empty arrays as the header's `messagePrefix`/`messageSuffix` ([the request envelope](#the-request-envelope-llmcallconfig-and-the-logged-header)), and `deriveMessages()` never returns them:
+`agent/request-advice` returns a `RequestAdvice` — the request-only advice woven around the derived history for ONE request (advice in both senses: advisory content for the model, attached before/after the join point like AOP advice, never modifying the history itself). Concretely, per request: `before` messages sit in front of the ENTIRE derived history, directly after the system slot — the conventional home for session-stable openers like an AGENTS.md digest or a skills catalog, re-contributed identically every step so the provider prefix cache holds; `after` messages follow the history's last message, closing the request. Not a Decision union: the seam contributes content instead of vetoing, so the shape is the contribution itself; the loop records the non-empty arrays as the header's `messagePrefix`/`messageSuffix` ([the request envelope](#the-request-envelope-llmcallconfig-and-the-logged-header)), and `deriveMessages()` never returns them:
 
 ```ts type-equiv
-interface RequestMessages {
-  /** Messages placed before the derived history in the request. */
+interface RequestAdvice {
+  /** Before-advice: messages placed ahead of the entire derived history. */
   before: Message[]
-  /** Messages placed after the derived history in the request. */
+  /** After-advice: messages placed after the derived history's last message. */
   after: Message[]
 }
 ```
 
-Listeners read the already-fixed request facts from a `RequestMessagesContext` (decide what to contribute from these; never mutate them):
+Listeners read the already-fixed request facts from a `RequestAdviceContext` (decide what to contribute from these; never mutate them):
 
 ```ts type-equiv
-interface RequestMessagesContext {
+interface RequestAdviceContext {
   /** The rendered system prompt this request will carry. */
   system: string
   /** The prompt assembly the system prompt was rendered from (sections + tools). */

+ 2 - 2
docs/core-data-structures/session.md

@@ -111,7 +111,7 @@ export interface EpochHeader {
   tools?: ToolSchema[]
   /**
    * Request-only messages sent BEFORE the derived history (the
-   * `agent/request-messages` waterfall's `before` contributions). Not session
+   * `agent/request-advice` waterfall's `before` contributions). Not session
    * history — `deriveMessages()` never returns them — so the header is their
    * only durable record; absent when the request carried none.
    */
@@ -121,7 +121,7 @@ export interface EpochHeader {
 }
 ```
 
-Canonical form: an empty system prompt, an empty tool list, and empty request-only message arrays are ABSENT fields, matching how requests are built. `messagePrefix`/`messageSuffix` are the durable record of the `agent/request-messages` waterfall's contributions (the request is `messagePrefix + derived history + messageSuffix`); their deltas replace the array whole, an empty array encoding the transition back to absence. The other delta payloads (`SystemDelta` — a common-prefix/suffix line trim; `ToolsDelta` — name-keyed added/removed/changed) live beside the events in [`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts).
+Canonical form: an empty system prompt, an empty tool list, and empty request-only message arrays are ABSENT fields, matching how requests are built. `messagePrefix`/`messageSuffix` are the durable record of the `agent/request-advice` waterfall's contributions (the request is `messagePrefix + derived history + messageSuffix`); their deltas replace the array whole, an empty array encoding the transition back to absence. The other delta payloads (`SystemDelta` — a common-prefix/suffix line trim; `ToolsDelta` — name-keyed added/removed/changed) live beside the events in [`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts).
 
 ## `SessionEvent<T>` — one log entry
 

+ 12 - 12
docs/event-producer-consumer.md

@@ -7,18 +7,18 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 
 | Event | Mode | Declared in | Dispatchers | Listeners |
 | --- | --- | --- | --- | --- |
-| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:308`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`stdio-agent`](../packages/ui/stdio-agent) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:315`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`stdio-agent`](../packages/ui/stdio-agent) |
-| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:506`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
-| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:393`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic) |
-| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:406`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:333`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
-| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:430`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
-| `agent/request-messages` | `waterfall` | [`packages/core/agent/src/types.ts:471`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
-| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:348`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:324`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`stdio-agent`](../packages/ui/stdio-agent) |
-| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:481`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
-| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:494`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
+| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:313`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`stdio-agent`](../packages/ui/stdio-agent) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:320`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`stdio-agent`](../packages/ui/stdio-agent) |
+| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:512`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
+| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:398`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic) |
+| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:411`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
+| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:338`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
+| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:435`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
+| `agent/request-advice` | `waterfall` | [`packages/core/agent/src/types.ts:477`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
+| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:353`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
+| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:329`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`stdio-agent`](../packages/ui/stdio-agent) |
+| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:487`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
+| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:500`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
 | `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:123`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
 | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:138`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) |
 | `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:109`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |

+ 3 - 3
docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md

@@ -22,11 +22,11 @@ Prefix-cache stability is corollary #1, not the headline: an append-only log pro
 
 **The header.** The request's non-history half — `EpochHeader`: call config (`LlmCallConfig`: model + sampling scalars), rendered system prompt, assembled tool schemas, and any request-only messages (`messagePrefix`/`messageSuffix`, below) — is logged session state, in canonical form (empty system/tools/message arrays ≡ absent). Two log-only, turn-enclosed events in dsh-session carry it: `request/header`, a full snapshot with reason `'initial' | 'resume' | 'fallback'`, and `request/header-delta`, an amendment (`SystemDelta`: a common-prefix/suffix line trim; `ToolsDelta`: name-keyed added/removed/changed; `config`: replaced whole; `messagePrefix`/`messageSuffix`: replaced whole, an empty array encoding the transition to absence). The pure trio `foldRequestHeader` / `diffHeader` / `applyHeaderDelta` reconstructs; the live session tracks the fold with the same lazy cursor as the message cache. Snapshots anchor the fold where a fold needs anchors — conversation birth and process boundaries — and each loop instance appends one on its first request (`'initial'` when the log has none, `'resume'` otherwise, even when nothing changed: the boundary itself is a recorded fact, and cross-restart drift becomes attributable while an unchanged header resumes byte-identical). Deltas are an encoding optimization with a safety valve, never a correctness dependency: the writer verifies `applyHeaderDelta(prev, delta)` reproduces the new header exactly and records a `'fallback'` snapshot when the encoding cannot express a change (a pure tool reordering), so a well-formed log always folds.
 
-**The loop, transmission-stateless.** Per step: render assembly (every step — value comparison needs no change-signal discipline, and a section that varies per step surfaces as a *logged* header event per step instead of a silent bust) → `agent/pre-step` (compaction's surface mutations land before derivation) → **messages snapshot, then `step/start` appended as the next operation in the same synchronous frame** → seed the call config (first request of the instance: from `AgentOptions`, so explicit options always beat the logged baseline — fork model-overrides and resume reconfiguration stay correct; afterwards: from the folded header) → the `agent/request` waterfall, re-typed `(agent, turn, step, config: LlmCallConfig, next) → LlmCallConfig` — a frozen seed and a returned replacement are ALL a listener shapes; durable content flows through the log channels (`inject()`, steering, prompt-submit `additionalContext`, sections via `system-prompt/assemble`) — → the `agent/request-messages` waterfall — request-ONLY `before`/`after` messages framing the boundary snapshot (a frozen empty seed, contributions returned as an extension of `next()`; the per-request advisory channel: content the model must see now that must NOT become history — a skills catalog, an environment reminder) — → the header event the request owes the log, carrying those contributions as `messagePrefix`/`messageSuffix` (no session event carries them, so the header is their only durable record) → build `GenerateOptions` from `messagePrefix + snapshot + messageSuffix` + header, deep-freeze (`deepFreeze` exempts the `AbortSignal`, the one live control channel — freezing one breaks `AbortController.abort()`), dispatch. The loop's only in-process bookkeeping is one boolean: whether this instance has logged its anchoring snapshot.
+**The loop, transmission-stateless.** Per step: render assembly (every step — value comparison needs no change-signal discipline, and a section that varies per step surfaces as a *logged* header event per step instead of a silent bust) → `agent/pre-step` (compaction's surface mutations land before derivation) → **messages snapshot, then `step/start` appended as the next operation in the same synchronous frame** → seed the call config (first request of the instance: from `AgentOptions`, so explicit options always beat the logged baseline — fork model-overrides and resume reconfiguration stay correct; afterwards: from the folded header) → the `agent/request` waterfall, re-typed `(agent, turn, step, config: LlmCallConfig, next) → LlmCallConfig` — a frozen seed and a returned replacement are ALL a listener shapes; durable content flows through the log channels (`inject()`, steering, prompt-submit `additionalContext`, sections via `system-prompt/assemble`) — → the `agent/request-advice` waterfall — request-ONLY `before`/`after` messages framing the boundary snapshot (a frozen empty seed, contributions returned as an extension of `next()`; the per-request advisory channel: content the model must see now that must NOT become history — a skills catalog, an environment reminder) — → the header event the request owes the log, carrying those contributions as `messagePrefix`/`messageSuffix` (no session event carries them, so the header is their only durable record) → build `GenerateOptions` from `messagePrefix + snapshot + messageSuffix` + header, deep-freeze (`deepFreeze` exempts the `AbortSignal`, the one live control channel — freezing one breaks `AbortController.abort()`), dispatch. The loop's only in-process bookkeeping is one boolean: whether this instance has logged its anchoring snapshot.
 
 **The reconstruction boundary is `step/start`, unconditionally.** A step's messages are the derivation over `events[0..stepStartSeq)`. Because the snapshot precedes the `step/start` append in the same synchronous frame, nothing can enter this request past the boundary: an `agent.inject()` from an `agent/request` listener (or any concurrent task, or a `session/event` listener firing on `step/start` itself) lands in the log after the boundary and joins the NEXT request. For waterfall-window appends this matches the prior loop (it also derived before its waterfall); for a synchronous `step/start` listener it is a deliberate change — such a listener could previously reach the current request — and `agent/pre-step` is the sanctioned seam for content that must affect the CURRENT request. A step's header for reconstruction is the fold after its own `request/header*` event (which sits between its `step/start` and first response event) or the fold carried forward.
 
-**Enforcement.** Dev-mode ([dsh-invariants](../../../../packages/support/invariants/src/index.ts)), on `llm/stream`: a frozen request with a live `sessionId` — the loop-built marker; hand-built one-shots are unfrozen and skipped — must carry messages deep-equal to the folded header's `messagePrefix`, then the boundary derivation, then its `messageSuffix` — the derivation rebuilt through a FRESH `Session` over `events[0..stepStartSeq)` so the live cache cannot vouch for itself — and header fields equal to `foldRequestHeader` over the log. There is no divergence allowance and nothing to allow: no seam can put unlogged content into a request — the `agent/request-messages` seam's contributions enter only because the header event records them first. `prepend: true` only defends against the replay adapter's short-circuit (an append-registered listener); two prepended listeners have no defined mutual order in cordis, so correctness rests on the seq-bounded fold, never on listener timing. Measurement stays lean: the with-key e2e ([request-cache.e2e.ts](../../../../packages/core/agent-loop/tests/request-cache.e2e.ts)) proves `usage.cacheReadTokens > 0` on every request after the first against the live API, and per-step usage in the log is the production observable — a header event or compaction shows up as a cache-read collapse on the next step.
+**Enforcement.** Dev-mode ([dsh-invariants](../../../../packages/support/invariants/src/index.ts)), on `llm/stream`: a frozen request with a live `sessionId` — the loop-built marker; hand-built one-shots are unfrozen and skipped — must carry messages deep-equal to the folded header's `messagePrefix`, then the boundary derivation, then its `messageSuffix` — the derivation rebuilt through a FRESH `Session` over `events[0..stepStartSeq)` so the live cache cannot vouch for itself — and header fields equal to `foldRequestHeader` over the log. There is no divergence allowance and nothing to allow: no seam can put unlogged content into a request — the `agent/request-advice` seam's contributions enter only because the header event records them first. `prepend: true` only defends against the replay adapter's short-circuit (an append-registered listener); two prepended listeners have no defined mutual order in cordis, so correctness rests on the seq-bounded fold, never on listener timing. Measurement stays lean: the with-key e2e ([request-cache.e2e.ts](../../../../packages/core/agent-loop/tests/request-cache.e2e.ts)) proves `usage.cacheReadTokens > 0` on every request after the first against the live API, and per-step usage in the log is the production observable — a header event or compaction shows up as a cache-read collapse on the next step.
 
 ### The MiniCode shape: adopted, with the provenance arrow inverted
 
@@ -44,7 +44,7 @@ What survives from `LLMClient`: the conversation is maintained, not rebuilt —
 ## Consequences
 
 - A request that is not explained by the log cannot be constructed by accident — not by the loop, not by a listener; mutating a built request throws; every header change is a durable, diffable log event.
-- Choosing between the advisory channels is a change-frequency cost decision, and the seam does not hide it: an `agent/request-messages` contribution rides the request's uncached tail and is re-tokenized at full price on every request it appears in (a `before` contribution instead extends the cacheable prefix at zero marginal cost while stable, but a mid-session change invalidates the provider cache for the entire history after it), whereas an `inject()`ed `context/message` is paid once and prefix-cached thereafter at the price of accumulating durably in history and the log. Route session-frozen content to `before`, low-frequency change notices to `inject()`, and reserve `after` for small, frequently refreshed state snapshots where a durable chain of stale copies would bloat the log and mislead the model.
+- Choosing between the advisory channels is a change-frequency cost decision, and the seam does not hide it: an `agent/request-advice` contribution rides the request's uncached tail and is re-tokenized at full price on every request it appears in (a `before` contribution instead extends the cacheable prefix at zero marginal cost while stable, but a mid-session change invalidates the provider cache for the entire history after it), whereas an `inject()`ed `context/message` is paid once and prefix-cached thereafter at the price of accumulating durably in history and the log. Route session-frozen content to `before`, low-frequency change notices to `inject()`, and reserve `after` for small, frequently refreshed state snapshots where a durable chain of stale copies would bloat the log and mislead the model.
 - What still costs full price at the provider is inherent and logged: compaction (its `compact/*` events and replace node), a real prompt/tool change (`request/header-delta`), a config switch (ditto), a process boundary with drift (`'resume'` snapshot differing from its predecessor). The provider's own reasoning-content exclusion is managed server-side.
 - The `step/start`-listener behavior change (above) is the one observable semantics change for plugins; `agent/pre-step` is the current-request seam.
 - Tool-result trimming (planned) needs no new mechanism: a logged single-node surface replace (`start === end`) carrying a trimmed `tool/result` under the same `callId` — compaction-family, replay-correct, cache-bust batched by the same pressure logic.

+ 2 - 2
packages/core/agent-loop/README.md

@@ -59,7 +59,7 @@ forever:
       boundary = session.deriveMessages()   ⟵ reconstruction boundary: same sync frame,
       session('step/start')                     strictly before step/start
       config = waterfall agent/request       ⟵ frozen seed; return a replacement to switch
-      reqMsgs = waterfall agent/request-messages  ⟵ request-only before/after messages; recorded
+      reqMsgs = waterfall agent/request-advice  ⟵ request-only before/after messages; recorded
                                                 on the header, never session history
       session('request/header'[-delta])      ⟵ the header event this request owes the log
       stream llm.stream(freeze({header..., messages: before+boundary+after})) → session('assistant/chunk')
@@ -86,7 +86,7 @@ Cancellation: `agent.cancel()` is the single public stop primitive — it clears
 ### What is NOT here
 
 Everything that goes beyond "call the model, run the tools, repeat" belongs to plugins listening on the event taxonomy:
-- Hooks: `agent/session-start`, `agent/prompt-submit`, `agent/pre-step`, `agent/request`, `agent/request-messages`, `agent/step-result`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation`
+- Hooks: `agent/session-start`, `agent/prompt-submit`, `agent/pre-step`, `agent/request`, `agent/request-advice`, `agent/step-result`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation`
 - Compaction: `agent/pre-step`
 - Sandbox, permission, plan mode: `tools/pre-execute` (deny/ask gate), `tools/post-execute`
 - Sub-agents: implemented outside the loop as `ctx.subagents` providers; in-process providers use `ctx.agents.create()` and owned `AgentHandle` teardown, while child streaming/progress and background/poll collection remain deferred.

+ 22 - 19
packages/core/agent-loop/src/loop.ts

@@ -10,7 +10,7 @@
 import type { Context } from 'cordis'
 import type { FinishReason, GenerateOptions, LlmCallConfig, Message } from '@deepseek-ai/dsh-llm'
 import { BlockAssembler, HarnessError, deepFreeze } from '@deepseek-ai/dsh-llm'
-import type { ContinuationDecision, HookContext, PromptDecision, RequestMessages } from '@deepseek-ai/dsh-agent'
+import type { ContinuationDecision, HookContext, PromptDecision, RequestAdvice } from '@deepseek-ai/dsh-agent'
 import { canonicalHeader } from '@deepseek-ai/dsh-session'
 import type { Session, TurnEndReason, TurnTrigger } from '@deepseek-ai/dsh-session'
 import { createTransmissionLog, recordRequestHeader } from './request-log.ts'
@@ -161,7 +161,7 @@ export interface LoopHandle {
  *       boundary = session.deriveMessages()           ⟵ the reconstruction boundary: snapshot in the
  *       session('step/start')                            same sync frame, strictly before step/start
  *       config = waterfall agent/request(config)      ⟵ frozen seed; a returned replacement switches
- *       reqMsgs = waterfall agent/request-messages    ⟵ request-only before/after messages; logged on
+ *       advice = waterfall agent/request-advice       ⟵ request-only before/after advice; logged on
  *                                                        the header, never session history
  *       session('request/header'|'request/header-delta')  ⟵ the header event this request owes the
  *                                                        log (initial/resume anchor, delta, fallback)
@@ -720,32 +720,35 @@ async function runStep(
     throw new Error(`agent "${agent.id}" has no model: set AgentOptions.model or supply one via the agent/request waterfall`)
   }
 
-  // Collect request-ONLY messages: `before` contributions precede the boundary
-  // snapshot in the request, `after` contributions follow it. They are not
-  // session history — the header event below is their only durable record
-  // (EpochHeader.messagePrefix/messageSuffix), which keeps the request a pure
-  // function of the log. The frozen empty seed serves both the listener chain
-  // and the no-listener fallback: a contribution is a RETURNED extension of
-  // `await next()`, never an in-place push. Fired AFTER the boundary snapshot,
-  // so a listener's session append lands past the boundary and joins the NEXT
+  // Collect the request-ONLY advice: `before` messages go in front of the
+  // entire boundary snapshot, `after` messages follow its last message. Advice
+  // is not session history — the header event below is its only durable
+  // record (EpochHeader.messagePrefix/messageSuffix), which keeps the request
+  // a pure function of the log. The frozen empty seed serves both the
+  // listener chain and the no-listener fallback: a contribution is a RETURNED
+  // extension of `await next()`, never an in-place push. The context gets a
+  // frozen COPY of the boundary (the request is built from the internal
+  // snapshot), so a listener cannot smuggle unlogged content into the request
+  // by mutating what it was shown. Fired AFTER the boundary snapshot, so a
+  // listener's session append lands past the boundary and joins the NEXT
   // request — the same window rule as the `agent/request` waterfall.
-  const emptyRequestMessages: RequestMessages = deepFreeze({ before: [], after: [] })
-  const requestMessagesBoundary = deepFreeze([...boundaryMessages])
-  const requestMessages = await ctx.waterfall(
-    'agent/request-messages', agent, turn, step, emptyRequestMessages,
-    { system, assembly, boundaryMessages: requestMessagesBoundary, signal },
-    () => Promise.resolve(emptyRequestMessages),
+  const emptyRequestAdvice: RequestAdvice = deepFreeze({ before: [], after: [] })
+  const requestAdviceBoundary = deepFreeze([...boundaryMessages])
+  const requestAdvice = await ctx.waterfall(
+    'agent/request-advice', agent, turn, step, emptyRequestAdvice,
+    { system, assembly, boundaryMessages: requestAdviceBoundary, signal },
+    () => Promise.resolve(emptyRequestAdvice),
   )
 
   // The request header (the log's request/header* vocabulary): canonical form,
   // recorded before dispatch so the log always explains the request —
-  // including the request-only messages, which no other event carries.
+  // including the request-only advice, which no other event carries.
   const header = canonicalHeader({
     config,
     ...system ? { system } : {},
     ...assembly.tools.length > 0 ? { tools: assembly.tools } : {},
-    ...requestMessages.before.length > 0 ? { messagePrefix: requestMessages.before } : {},
-    ...requestMessages.after.length > 0 ? { messageSuffix: requestMessages.after } : {},
+    ...requestAdvice.before.length > 0 ? { messagePrefix: requestAdvice.before } : {},
+    ...requestAdvice.after.length > 0 ? { messageSuffix: requestAdvice.after } : {},
   })
   recordRequestHeader(session, transmission, header)
 

+ 9 - 9
packages/core/agent-loop/tests/interception.spec.ts

@@ -8,7 +8,7 @@ import AgentRegistry, {
   AgentId,
   type ContinuationDecision,
   type PromptDecision,
-  type RequestMessages,
+  type RequestAdvice,
   type SessionStartSource,
 } from '@deepseek-ai/dsh-agent'
 import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
@@ -311,7 +311,7 @@ describe('agent/session-start', () => {
   })
 })
 
-describe('agent/request-messages (RequestMessages)', () => {
+describe('agent/request-advice (RequestAdvice)', () => {
   it('frames the derived history: before precedes it, after follows it, and the header records both', async () => {
     const adapter = new MockAdapter([textResponse('ok')])
     const ctx = await harness(adapter)
@@ -319,7 +319,7 @@ describe('agent/request-messages (RequestMessages)', () => {
 
     const reminder: Message = { role: 'user', content: [{ type: 'text', text: '<system-reminder>catalog</system-reminder>' }] }
     const trailer: Message = { role: 'user', content: [{ type: 'text', text: 'trailing note' }] }
-    ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestMessages> => {
+    ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestAdvice> => {
       const result = await next()
       return { before: [...result.before, reminder], after: [...result.after, trailer] }
     })
@@ -351,7 +351,7 @@ describe('agent/request-messages (RequestMessages)', () => {
     const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
 
     const seen: { system: string; boundaryRoles: string[]; sectionCount: number }[] = []
-    ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, context, next): Promise<RequestMessages> => {
+    ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, context, next): Promise<RequestAdvice> => {
       const result = await next()
       seen.push({
         system: context.system,
@@ -360,7 +360,7 @@ describe('agent/request-messages (RequestMessages)', () => {
       })
       return { before: [{ role: 'user', content: [{ type: 'text', text: 'first' }] }, ...result.before], after: result.after }
     })
-    ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestMessages> => {
+    ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestAdvice> => {
       const result = await next()
       return { before: [...result.before, { role: 'user', content: [{ type: 'text', text: 'second' }] }], after: result.after }
     })
@@ -385,7 +385,7 @@ describe('agent/request-messages (RequestMessages)', () => {
     const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
 
     // A listener that delegates without contributing — the canonical no-op.
-    ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, _context, next) => next())
+    ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, _context, next) => next())
 
     send(agent, 'hi')
     await waitForIdle(ctx, agent)
@@ -402,7 +402,7 @@ describe('agent/request-messages (RequestMessages)', () => {
     const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
 
     let mutationError: unknown
-    ctx.on('agent/request-messages', async (_agent, _turn, _step, messages, _context, next): Promise<RequestMessages> => {
+    ctx.on('agent/request-advice', async (_agent, _turn, _step, messages, _context, next): Promise<RequestAdvice> => {
       try {
         messages.before.push({ role: 'user', content: [{ type: 'text', text: 'smuggled' }] })
       } catch (error: unknown) {
@@ -424,7 +424,7 @@ describe('agent/request-messages (RequestMessages)', () => {
     const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
 
     let mutationError: unknown
-    ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, context, next): Promise<RequestMessages> => {
+    ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, context, next): Promise<RequestAdvice> => {
       try {
         const mutableBoundary = context.boundaryMessages as Message[]
         mutableBoundary.push({ role: 'user', content: [{ type: 'text', text: 'smuggled' }] })
@@ -454,7 +454,7 @@ describe('agent/request-messages (RequestMessages)', () => {
     const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
 
     let step = 0
-    ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestMessages> => {
+    ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestAdvice> => {
       const result = await next()
       step += 1
       return { before: [...result.before, { role: 'user', content: [{ type: 'text', text: `reminder v${step}` }] }], after: result.after }

+ 1 - 1
packages/core/agent/README.md

@@ -45,7 +45,7 @@ Turn and step boundaries are NOT mirrored as `agent/*` emits: a consumer that ne
 - `agent/prompt-submit` — decide what happens to one drained queued message before it becomes a `user/message`: `PromptDecision` = `allow` (optionally rewriting the prompt `content` or attaching `additionalContext`) or `block` (drop it; a batch whose every prompt is blocked opens a zero-step turn that ends `rejected`). Maps onto Claude Code's `UserPromptSubmit`.
 - `agent/pre-step` (serial) — mutate the session surface before the step opens and history is derived (compaction). Fires after `turn/start` and before `step/start`, so a listener's appended events land outside the step.
 - `agent/request` — shape the call config before the model call: a frozen `LlmCallConfig` seed in, a replacement out (model switching, sampling overrides). Content is not shapeable here — every request is a pure function of the session log ([reconstructability RFC](../../../docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)); the loop logs whatever config the request actually uses as a `request/header*` event
-- `agent/request-messages` — contribute request-ONLY messages around the derived history: a frozen empty `RequestMessages` seed in, an extension of `await next()` out (`before` messages precede the boundary snapshot in the request, `after` messages follow it). For per-request advisory context the model must see now but that must not become durable history; the loop records the contributions on the request's `request/header*` event (`EpochHeader.messagePrefix`/`messageSuffix`), so `deriveMessages()` stays untouched and the request stays reconstructable. Cost model: contributions ride the request's uncached tail and are re-paid at full price on every request they appear in — put session-frozen content in `before` (cacheable prefix; a mid-session change busts the cache for everything after it), route low-frequency change notices through `agent.inject()` instead (paid once, prefix-cached thereafter), and reserve `after` for small, frequently refreshed state snapshots
+- `agent/request-advice` — contribute request-ONLY messages around the derived history: a frozen empty `RequestAdvice` seed in, an extension of `await next()` out (`before` messages precede the boundary snapshot in the request, `after` messages follow it). For per-request advisory context the model must see now but that must not become durable history; the loop records the contributions on the request's `request/header*` event (`EpochHeader.messagePrefix`/`messageSuffix`), so `deriveMessages()` stays untouched and the request stays reconstructable. Cost model: contributions ride the request's uncached tail and are re-paid at full price on every request they appear in — put session-frozen content in `before` (cacheable prefix; a mid-session change busts the cache for everything after it), route low-frequency change notices through `agent.inject()` instead (paid once, prefix-cached thereafter), and reserve `after` for small, frequently refreshed state snapshots
 - `agent/step-result` — post-process the assembled assistant message before tool dispatch (validates what the log records)
 - `agent/turn-continuation` — override the continue/stop decision via `ContinuationDecision` = `{action:'stop'}` or `{action:'continue', reason?}` (a `continue` `reason` is recorded as next-step steering in the same turn — the typed `/goal` pattern). Force-continue `/loop`, force-stop budget guard.
 

+ 33 - 27
packages/core/agent/src/types.ts

@@ -17,7 +17,7 @@
  *   consumer that wants the live transcript subscribes here.
  * - **`agent/*`** (this module) — the LIVE runtime surface. Always carries the
  *   live `Agent`. Two shapes: INTERCEPTION seams (the `agent/prompt-submit`/
- *   `agent/request`/`agent/request-messages`/`agent/step-result`/
+ *   `agent/request`/`agent/request-advice`/`agent/step-result`/
  *   `agent/turn-continuation` waterfalls and
  *   the serial `agent/pre-step`) that mutate/veto, and TRANSIENT emits
  *   (`agent/status`, `agent/error`, `agent/created`/
@@ -156,34 +156,39 @@ export type ContinuationDecision =
   | { action: 'continue'; reason?: HookContext }
 
 /**
- * Request-ONLY messages an `agent/request-messages` waterfall listener
- * contributes around the derived history of ONE LLM request: `before` messages
- * precede the derived history in `GenerateOptions.messages`, `after` messages
- * follow it. They are NOT session events — nothing here enters the session log
- * as durable history, `Session.deriveMessages()` never returns them, and the
- * next step recomputes them from scratch. The loop records the non-empty
- * arrays on the request's `request/header*` event (`EpochHeader.messagePrefix`
- * / `messageSuffix`), so the request stays reconstructable from the log (the
- * reconstructability RFC). For content that must become durable conversation
- * history, use the log channels instead: `agent.inject()`, steering, or
- * prompt-submit `additionalContext`.
+ * The request-only ADVICE an `agent/request-advice` waterfall listener weaves
+ * around the derived history of ONE LLM request — advice in both senses:
+ * advisory content for the model, attached before/after the join point like
+ * AOP advice, never modifying the history itself. In
+ * `GenerateOptions.messages` the `before` messages sit in front of the ENTIRE
+ * derived history (directly after the provider's system slot) and the `after`
+ * messages follow its last message (the newest user prompt on a turn's first
+ * step, the previous step's tool results afterwards). Advice is NOT session
+ * state — nothing here enters the session log as durable history,
+ * `Session.deriveMessages()` never returns it, and the next step recomputes
+ * it from scratch. The loop records the non-empty arrays on the request's
+ * `request/header*` event (`EpochHeader.messagePrefix` / `messageSuffix`), so
+ * the request stays reconstructable from the log (the reconstructability
+ * RFC). For content that must become durable conversation history, use the
+ * log channels instead: `agent.inject()`, steering, or prompt-submit
+ * `additionalContext`.
  */
-export interface RequestMessages {
-  /** Messages placed before the derived history in the request. */
+export interface RequestAdvice {
+  /** Before-advice: messages placed ahead of the entire derived history. */
   before: Message[]
-  /** Messages placed after the derived history in the request. */
+  /** After-advice: messages placed after the derived history's last message. */
   after: Message[]
 }
 
 /**
- * Read-only facts about the request an `agent/request-messages` listener is
+ * Read-only facts about the request an `agent/request-advice` listener is
  * contributing to. Everything here is already fixed when the seam fires: the
  * step is open, the boundary snapshot is taken, and the system prompt is
  * assembled — a listener uses these to DECIDE what to contribute (e.g. render
  * a workspace-dependent reminder, or skip one already present in history),
  * never to mutate them.
  */
-export interface RequestMessagesContext {
+export interface RequestAdviceContext {
   /** The rendered system prompt this request will carry. */
   system: string
   /** The prompt assembly the system prompt was rendered from (sections + tools). */
@@ -412,7 +417,7 @@ declare module 'cordis' {
      * session log (the reconstructability RFC), so model-visible content
      * flows through the log channels — `inject()`, steering, prompt-submit
      * `additionalContext`, prompt sections via `system-prompt/assemble`, or
-     * header-logged request-only messages via {@link agent/request-messages}
+     * header-logged request-only messages via {@link agent/request-advice}
      * — never through request mutation, and the loop records whatever config
      * the request actually uses as a `request/header*` event before dispatch.
      * The step's messages are already snapshotted when this fires (the
@@ -429,10 +434,11 @@ declare module 'cordis' {
      */
     'agent/request'(agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
     /**
-     * Waterfall: contribute request-ONLY messages around the derived history —
-     * a {@link RequestMessages} whose `before` messages precede the boundary
-     * snapshot in `GenerateOptions.messages` and whose `after` messages follow
-     * it. Fires once per step, inside the open step, after the
+     * Waterfall: weave request-ONLY advice around the derived history — a
+     * {@link RequestAdvice} whose `before` messages sit in front of the
+     * ENTIRE boundary snapshot in `GenerateOptions.messages` and whose
+     * `after` messages follow its last message. Fires once per step, inside
+     * the open step, after the
      * {@link agent/request} config waterfall and before the loop logs the
      * request header. This is the seam for per-request advisory context the
      * model must see NOW but that must NOT become durable history (a skills
@@ -443,13 +449,13 @@ declare module 'cordis' {
      * reconstructable from the log.
      *
      * The seed is frozen and empty; a contributing listener returns a NEW
-     * {@link RequestMessages} extending `await next()` (spread its arrays —
+     * {@link RequestAdvice} extending `await next()` (spread its arrays —
      * never mutate them), so contributions compose across plugins in
      * registration order. The boundary snapshot is already taken when this
      * fires: a `session.append`/`inject()` from a listener here lands in the
      * log but joins the NEXT request — contribute through the returned value,
      * not the session. Call `next()` to delegate, or return a
-     * {@link RequestMessages} without it to short-circuit.
+     * {@link RequestAdvice} without it to short-circuit.
      *
      * Pick the channel by change frequency (the cost model): a contribution
      * rides the request's uncached tail, re-tokenized at full price on EVERY
@@ -464,11 +470,11 @@ declare module 'cordis' {
      * @param agent - the agent making the model call.
      * @param turn - the open turn number.
      * @param step - the step whose request this is.
-     * @param messages - the frozen empty seed; return an extended replacement to contribute.
-     * @param context - read-only request facts ({@link RequestMessagesContext}).
+     * @param advice - the frozen empty seed; return an extended replacement to contribute.
+     * @param context - read-only request facts ({@link RequestAdviceContext}).
      * @mode waterfall
      */
-    'agent/request-messages'(agent: Agent, turn: number, step: number, messages: RequestMessages, context: RequestMessagesContext, next: () => Promise<RequestMessages>): Promise<RequestMessages>
+    'agent/request-advice'(agent: Agent, turn: number, step: number, advice: RequestAdvice, context: RequestAdviceContext, next: () => Promise<RequestAdvice>): Promise<RequestAdvice>
     /**
      * Waterfall: post-process the assembled assistant {@link Message} before
      * tool dispatch (validation, content rewriting, …).

+ 1 - 1
packages/core/session/README.md

@@ -51,7 +51,7 @@ Plain class (not a Cordis Service). Create via `ctx.sessions.create()`.
 
 ### Request-header reconstruction (`request-header.ts`)
 
-The `request/header` (full `EpochHeader` snapshot with a `RequestHeaderReason`) and `request/header-delta` (system line-trim / name-keyed tools delta / whole config / whole request-only message arrays) events make the request envelope logged session state, so every conversation request is a pure function of the log. The pure trio reconstructs it: `foldRequestHeader(events)` folds a log (or any prefix) into the header in force; `diffHeader(prev, next)` encodes a change (undefined when equal); `applyHeaderDelta(prev, delta)` replays one. Writer contract: every logged delta is round-trip-verified (`apply(prev, delta)` deep-equals the new header) with a `'fallback'` snapshot when the encoding cannot express the change (a pure tool reordering), so folding never needs error recovery on a well-formed log. `canonicalHeader` pins the one representation of absence (empty system/tools/messagePrefix/messageSuffix ≡ absent fields; a delta's EMPTY message array encodes the transition back to absence). `EpochHeader.messagePrefix`/`messageSuffix` are the durable record of the `agent/request-messages` waterfall's request-only contributions — the request is `messagePrefix + derived history + messageSuffix`, and `deriveMessages()` never returns them.
+The `request/header` (full `EpochHeader` snapshot with a `RequestHeaderReason`) and `request/header-delta` (system line-trim / name-keyed tools delta / whole config / whole request-only message arrays) events make the request envelope logged session state, so every conversation request is a pure function of the log. The pure trio reconstructs it: `foldRequestHeader(events)` folds a log (or any prefix) into the header in force; `diffHeader(prev, next)` encodes a change (undefined when equal); `applyHeaderDelta(prev, delta)` replays one. Writer contract: every logged delta is round-trip-verified (`apply(prev, delta)` deep-equals the new header) with a `'fallback'` snapshot when the encoding cannot express the change (a pure tool reordering), so folding never needs error recovery on a well-formed log. `canonicalHeader` pins the one representation of absence (empty system/tools/messagePrefix/messageSuffix ≡ absent fields; a delta's EMPTY message array encodes the transition back to absence). `EpochHeader.messagePrefix`/`messageSuffix` are the durable record of the `agent/request-advice` waterfall's request-only contributions — the request is `messagePrefix + derived history + messageSuffix`, and `deriveMessages()` never returns them.
 
 ### Session event vocabulary (`types.ts`)
 

+ 1 - 1
packages/core/session/src/types.ts

@@ -203,7 +203,7 @@ export interface EpochHeader {
   tools?: ToolSchema[]
   /**
    * Request-only messages sent BEFORE the derived history (the
-   * `agent/request-messages` waterfall's `before` contributions). Not session
+   * `agent/request-advice` waterfall's `before` contributions). Not session
    * history — `deriveMessages()` never returns them — so the header is their
    * only durable record; absent when the request carried none.
    */

+ 1 - 1
packages/support/invariants/src/index.ts

@@ -368,7 +368,7 @@ export function apply(ctx: Context, config: Config = {}): void {
   // be EXACTLY what the session log reconstructs:
   //
   // - messages: the folded header's request-only messages (messagePrefix /
-  //   messageSuffix — the `agent/request-messages` contributions, logged on
+  //   messageSuffix — the `agent/request-advice` contributions, logged on
   //   the header because no session event carries them) framing the
   //   derivation over the log prefix strictly before the in-flight step's
   //   `step/start` (the reconstruction boundary). The derivation is compared

+ 2 - 2
scripts/type-equiv.manifest.json

@@ -15,8 +15,8 @@
     { "doc": "docs/core-data-structures/core.md", "symbol": "PromptDecision", "source": "packages/core/agent/src/types.ts" },
     { "doc": "docs/core-data-structures/core.md", "symbol": "ContinuationDecision", "source": "packages/core/agent/src/types.ts" },
     { "doc": "docs/core-data-structures/core.md", "symbol": "SessionStartSource", "source": "packages/core/agent/src/types.ts" },
-    { "doc": "docs/core-data-structures/core.md", "symbol": "RequestMessages", "source": "packages/core/agent/src/types.ts" },
-    { "doc": "docs/core-data-structures/core.md", "symbol": "RequestMessagesContext", "source": "packages/core/agent/src/types.ts" },
+    { "doc": "docs/core-data-structures/core.md", "symbol": "RequestAdvice", "source": "packages/core/agent/src/types.ts" },
+    { "doc": "docs/core-data-structures/core.md", "symbol": "RequestAdviceContext", "source": "packages/core/agent/src/types.ts" },
 
     { "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "StreamChunk", "source": "packages/llm/llm/src/types.ts" },
     { "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "TokenUsage", "source": "packages/llm/llm/src/types.ts" },