refactor(agent-loop): drop the steering/message session event

Steer, inject, and followup now land as durable user/message events on the
session surface; the steering/message event type and its ConversationNode
kind are removed from the client projection. Update tests, docs, generated
catalogs, and agent notes to match, and align the steering e2e fixture and
prompt inventory assertions with the durable user/message landing.
This commit is contained in:
_Kerman
2026-08-03 16:04:26 +08:00
parent f4a2e0d10a
commit cb2f01f48c
86 changed files with 243 additions and 458 deletions
+1 -1
View File
@@ -444,7 +444,7 @@ Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/t
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`, `steering/message`).
* `assistant/message`, `tool/result`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
+1 -1
View File
@@ -450,7 +450,7 @@ interface LlmCallConfigAdapterDefaults {
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`, `steering/message`).
* `assistant/message`, `tool/result`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
+8 -11
View File
@@ -33,10 +33,11 @@ interface SessionEventMap {
'turn/start': { turn: number }
/**
* Closes turn `turn` after `step`, the last entered step (`0` when none),
* with the {@link TurnEndReason} that ended it. The loop awaits
* `session/flush` after an ordinary turn ends before claiming the next queued
* item. Success commits the turn; rejection is reported live and does not
* prevent later work.
* with the {@link TurnEndReason} that ended it. The loop does not await a
* flush at turn boundaries: `dsh-session-checkpoint-policy` owns the
* per-request durability checkpoint, and consumers that read storage after
* `whenIdle()` flush themselves. Success commits the turn; rejection is
* reported live and does not prevent later work.
*/
'turn/end': { turn: number; step: number; reason: TurnEndReason }
/** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */
@@ -84,8 +85,6 @@ interface SessionEventMap {
error?: { name: string; code: string }
meta?: JsonValue
}
/** Steering content injected between steps of a running turn. */
'steering/message': { turn: number; message: UserMessage }
/** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */
'todo/write': { todos: TodoItem[] }
/**
@@ -202,7 +201,7 @@ A proper discriminated union over `type` (not independent `type`/`data` unions),
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`, `steering/message`).
* `assistant/message`, `tool/result`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
@@ -236,7 +235,7 @@ For `assistant/message`, a present `sourceEventSeqs: []` is a complete known-emp
## Surface types
The four message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `steering/message`) carry surface metadata declaring how they join the ordered derived surface. See the [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md).
The three message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`) carry surface metadata declaring how they join the ordered derived surface. See the [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md).
### `SurfaceEventType` — the message-producing subset of event types
@@ -250,7 +249,6 @@ type SurfaceEventType =
| 'user/message'
| 'assistant/message'
| 'tool/result'
| 'steering/message'
```
### `SurfaceOp` — how an event entered the surface
@@ -260,7 +258,7 @@ type SurfaceEventType =
* How a session event entered the ordered surface. Only valid on
* {@link SurfaceEventType} events.
*
* - `'append'`: added to the tail — normal path for user/assistant/tool/steering
* - `'append'`: added to the tail — normal path for user/assistant/tool
* messages.
* - `{ op: 'replace', start, end }`: replaces surface nodes from `start`
* (inclusive) through `end` (inclusive) with this node. Both must exist as
@@ -498,7 +496,6 @@ declare class Session {
- `assistant/message` → an assistant message with the event's provider/model provenance and optional adapter-private replay state. Raw `assistant/chunk` events are replay/UI data and are **skipped** in derivation (the assembled message is authoritative). An **empty-content** `assistant/message` is also skipped — a max-tokens step cut off with no content still records an `assistant/message` to host its usage/provenance, but a content-less assistant turn must not enter the provider transcript.
- `tool/result` → a user message carrying a `tool-result` block.
- `user/message` (injected context, i.e. non-`user` source) → a user-role message carrying its `content` verbatim at its chronological position; provenance and domain data live in its typed source.
- `steering/message` → a user-role message carrying exact `content` at its chronological position; an optional envelope remains log-only display metadata.
Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and does not project into a message. Token accounting reads per-step `assistant/chunk { type: 'usage' }` records and treats `assistant/message.usage` as the committed-step fallback when no usage chunk exists; failed model-request attempts have no assistant message, so their usage chunk is the durable accounting record. Because this unreleased format intentionally has no compatibility promise, seed/load validation rejects request headers without provider+model and assistant messages without provider/model provenance instead of guessing a route for historical data.
+8 -11
View File
@@ -33,10 +33,11 @@ interface SessionEventMap {
'turn/start': { turn: number }
/**
* Closes turn `turn` after `step`, the last entered step (`0` when none),
* with the {@link TurnEndReason} that ended it. The loop awaits
* `session/flush` after an ordinary turn ends before claiming the next queued
* item. Success commits the turn; rejection is reported live and does not
* prevent later work.
* with the {@link TurnEndReason} that ended it. The loop does not await a
* flush at turn boundaries: `dsh-session-checkpoint-policy` owns the
* per-request durability checkpoint, and consumers that read storage after
* `whenIdle()` flush themselves. Success commits the turn; rejection is
* reported live and does not prevent later work.
*/
'turn/end': { turn: number; step: number; reason: TurnEndReason }
/** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */
@@ -84,8 +85,6 @@ interface SessionEventMap {
error?: { name: string; code: string }
meta?: JsonValue
}
/** Steering content injected between steps of a running turn. */
'steering/message': { turn: number; message: UserMessage }
/** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */
'todo/write': { todos: TodoItem[] }
/**
@@ -204,7 +203,7 @@ interface RequestContext {
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`, `steering/message`).
* `assistant/message`, `tool/result`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
@@ -238,7 +237,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
## Surface 类型
种产生消息的类型(`SurfaceEventType``user/message`、`assistant/message`、`tool/result`、`steering/message`)携带 surface 元数据,用来声明它们如何加入有序的派生 surface。见 [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md)。
种产生消息的类型(`SurfaceEventType``user/message`、`assistant/message`、`tool/result`)携带 surface 元数据,用来声明它们如何加入有序的派生 surface。见 [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md)。
### `SurfaceEventType`:事件类型中产生消息的子集
@@ -252,7 +251,6 @@ type SurfaceEventType =
| 'user/message'
| 'assistant/message'
| 'tool/result'
| 'steering/message'
```
### `SurfaceOp`:事件如何进入 surface
@@ -262,7 +260,7 @@ type SurfaceEventType =
* How a session event entered the ordered surface. Only valid on
* {@link SurfaceEventType} events.
*
* - `'append'`: added to the tail — normal path for user/assistant/tool/steering
* - `'append'`: added to the tail — normal path for user/assistant/tool
* messages.
* - `{ op: 'replace', start, end }`: replaces surface nodes from `start`
* (inclusive) through `end` (inclusive) with this node. Both must exist as
@@ -500,7 +498,6 @@ declare class Session {
- `assistant/message` → 一条 assistant 消息,包含事件的提供方/模型溯源信息和可选的适配器私有回放状态。原始 `assistant/chunk` 事件属于回放/UI 数据,在派生时会被**跳过**(组装后的消息才是权威)。**内容为空的** `assistant/message` 也会跳过:因 max-tokens 而截断且无内容的步骤仍会记录一条 `assistant/message` 以承载用量和溯源信息,但无内容的 assistant 轮次不得进入提供方 transcript(文本记录)。
- `tool/result` → 一条携带 `tool-result` 块的 user 消息。
- `user/message`(注入上下文,即非 `user` 来源)→ 按时间顺序在相应位置生成一条 user-role 消息,并原样承载其 `content`;溯源信息与领域数据都在其类型化的 source 中。
- `steering/message` → 按时间顺序在相应位置生成一条携带确切 `content` 的 user-role 消息;可选 envelope 仅作为日志中的展示元数据保留。
其余所有事件(`turn/*`、`step/*`、插件所有的 `llm/retry`)均为结构信息,不会投影为消息。token 记账读取每个步骤的 `assistant/chunk { type: 'usage' }` 记录;如果没有用量分片,则将 `assistant/message.usage` 作为已提交步骤的后备。失败的模型请求尝试没有 assistant 消息,因此其用量分片是持久化的记账记录。由于这一尚未发布的格式有意不提供兼容性承诺,seed/load 校验会拒绝缺少提供方和模型的请求头,以及缺少提供方/模型溯源信息的 assistant 消息,而不会猜测历史数据应走的提供方路由。