fix(agent-loop): rematerialize adapter defaults

This commit is contained in:
Yichen Jiang
2026-07-30 21:49:58 +08:00
parent 95824545a6
commit 5fd34f9109
47 changed files with 348 additions and 100 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/architecture.md
architecture.md: 1fd9bd128d1bcc0dd91d46131981ea4fc331bd74
architecture.zh.md: 8521f09c6e415f9f8d1c0a44f7534b59c876decc
architecture.md: ef93b47702cdc8a2a6c68dc07edd629f0ab43170
architecture.zh.md: 4f1f05a3ce10f5de7edf192ff8d04e43a4dcb5c9

View File

@@ -94,7 +94,7 @@ forever:
assemble system prompt and tool schemas
snapshot the derived messages (the reconstruction boundary)
'step/start'
agent/request (config only) -> prepare reasoning/default under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
agent/request (config only) -> prepare adapter defaults/provenance under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
'assistant/chunk'
'assistant/message'
schedule tool calls by ctx.tools.executionMode:
@@ -143,7 +143,7 @@ Each agent owns scoped `agent.ctx`; shared storage overlays its tool, prompt, an
The session log is authoritative. `deriveMessages()` projects model history; raw `assistant/chunk` events preserve replay and UI fidelity. Fork, resume, transcript rendering, telemetry, and persistence derive from this stream.
**Model-visible ⟺ logged**: messages at `step/start` plus the folded `request/header` reconstruct every request; package-owned `dsh-agent-loop/invariant` can assert this through `ctx.invariants` ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
**Model-visible ⟺ logged**: messages at `step/start` plus the folded `request/header` reconstruct every request; the header also marks adapter-materialized defaults so the next proposal can discard them and resolve the selected route without losing explicit conversation settings. Package-owned `dsh-agent-loop/invariant` can assert reconstructability through `ctx.invariants` ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
Durability is a plugin concern. Backends eagerly drain synchronous `session/event` notifications. `session/flush` barriers precede each request and top-level tool dispatch, then follow `turn/end` before another queued turn or idle observation. `SessionPersistence` stores `SessionEvent` directly and metadata in `SessionHeader`; JSONL defaults to checksummed Zstandard, while SQLite shares the contract ([decision](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md)).

View File

@@ -94,7 +94,7 @@ forever:
assemble system prompt and tool schemas
snapshot the derived messages (the reconstruction boundary)
'step/start'
agent/request (config only) -> prepare reasoning/default under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
agent/request (config only) -> prepare adapter defaults/provenance under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
'assistant/chunk'
'assistant/message'
schedule tool calls by ctx.tools.executionMode:
@@ -143,7 +143,7 @@ idle inject:
会话日志是权威依据。`deriveMessages()` 投影出模型历史;原始 `assistant/chunk` 事件保证回放和 UI 保真。fork、恢复、transcript文本记录渲染、遥测和持久化均派生自该事件流。
**模型可见 ⟺ 已记录**`step/start` 时的消息与折叠后的 `request/header` 可以重建每个请求;该包的 `dsh-agent-loop/invariant` 可通过 `ctx.invariants` 断言这一点[可重建性](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md))。
**模型可见 ⟺ 已记录**`step/start` 时的消息与折叠后的 `request/header` 可以重建每个请求;该 header 还会标记适配器填入的默认值,使下一次提议可以丢弃这些值并解析所选路由,同时不丢失显式对话设置。该包的 `dsh-agent-loop/invariant` 可通过 `ctx.invariants` 断言可重建性[可重建性](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md))。
持久性由插件负责。后端会尽快排空同步的 `session/event` 通知。`session/flush` 屏障位于每次请求与顶层工具分发之前,并在 `turn/end` 之后、处理另一个已排队轮次或观察到空闲状态之前执行。`SessionPersistence` 直接存储 `SessionEvent`,并将元数据存入 `SessionHeader`JSONL 默认采用带校验和的 ZstandardSQLite 遵循同一契约([决策](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md))。

View File

@@ -820,7 +820,7 @@ stream(options: GenerateOptions): AsyncIterable<StreamChunk>
Types: [GenerateOptions](../core-data-structures/core.md) · [LlmAdapter](../core-data-structures/llm-streaming.md) · [LlmCallConfig](../core-data-structures/core.md) · [LlmModelInfo](../core-data-structures/core.md) · [LlmProviderInfo](../core-data-structures/core.md) · [LlmResolvedModelInfo](../core-data-structures/core.md) · [PreparedLlmCall](../core-data-structures/llm-streaming.md) · [ResolvedRetryPolicy](../core-data-structures/llm-streaming.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
Source: [`packages/llm/llm/src/index.ts:191`](../../packages/llm/llm/src/index.ts)
Source: [`packages/llm/llm/src/index.ts:193`](../../packages/llm/llm/src/index.ts)
## `ctx.permission` — `PermissionService`
@@ -1590,7 +1590,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId):
Types: [CreateSessionOptions](../core-data-structures/persistence.md) · [Session](../core-data-structures/session.md) · [SessionId](../core-data-structures/core.md)
Source: [`packages/core/session/src/index.ts:713`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:738`](../../packages/core/session/src/index.ts)
## `ctx.sessionTitle` — `SessionTitleService`

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/core.md
core.md: 782d51b5178276ca1cb313607165b17b8e5d6a02
core.zh.md: d1c06447d9a0675d87eb263d7486b64cd4677373
core.md: 418d4102f9d39109069aa9ae6eb4a33c2f440b46
core.zh.md: 09a1790651b02cb4977135ba9d656443eb1c52d3

View File

@@ -340,9 +340,9 @@ The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition`
### The request envelope: `LlmCallConfig` and the logged header
The loop builds each request from logged state. `EpochHeader` records call config, rendered prompt, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
The loop builds each request from logged state. `EpochHeader` records call config, adapter-default provenance, rendered prompt, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. After the waterfall, the loop prepares the exact model capability under the turn signal, rejects unsupported explicit effort ids without clamping, materializes an adapter-configured default, and logs the effective value. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus provenance under the turn signal. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
On the wire, a loop-built request reads the `system` slot (the rendered prompt assembly) followed by 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. The dev invariant recomputes exactly this equation against every loop-built request.
@@ -365,6 +365,17 @@ interface LlmCallConfig {
}
```
```ts type-equiv
/**
* Effective config fields supplied by exact-model adapter resolution rather
* than by the caller's request proposal.
*/
interface LlmCallConfigAdapterDefaults {
reasoningEffort?: true
maxTokens?: true
}
```
## Sessions
A `Session` is an **append-only log** of typed `SessionEvent`s — the single source of truth. The LLM message history is *derived* from the log (`deriveMessages()`), not stored separately. The event vocabulary derives from `SessionEventMap`:

View File

@@ -346,9 +346,9 @@ interface ToolSchema {
### 请求信封:`LlmCallConfig` 与记录的 header
循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Noteagent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、适配器默认值来源、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Noteagent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
`agent/request` 接收冻结的调用配置种子并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 结束后,循环会在轮次信号控制下完成确切模型的能力准备,拒绝显式指定但不受支持的推理强度 ID不自动调整填入适配器配置的默认值,并记录最终生效值。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
`agent/request` 接收冻结的调用配置种子并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 开始前循环会移除标记为适配器默认值的值使确切模型准备过程填入所选路由的当前值未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID不自动调整并记录生效配置及其来源。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
在协议格式上,循环构建的请求先读取 `system` 槽位(渲染后的提示词组装),再读取派生历史——边界快照,其尾部在轮次首步是最新的 `user/message`,在后续步骤是上一步的工具结果。开发不变式针对每个循环构建的请求精确重算此等式。
@@ -371,6 +371,17 @@ interface LlmCallConfig {
}
```
```ts type-equiv
/**
* Effective config fields supplied by exact-model adapter resolution rather
* than by the caller's request proposal.
*/
interface LlmCallConfigAdapterDefaults {
reasoningEffort?: true
maxTokens?: true
}
```
## 会话
`Session` 是一份类型化 `SessionEvent` 的**仅追加日志**——唯一的真源。LLM大语言模型消息历史从日志*派生*`deriveMessages()`),而非单独存储。事件词汇从 `SessionEventMap` 派生:

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/llm-streaming.md
llm-streaming.md: d9c1772e2fb56de2f240b4e62fdc2e1aa12ae787
llm-streaming.zh.md: 824b7c46186f2e330ad2a3aafa7a046759ab9a89
llm-streaming.md: e7500a7985ea1916e206c41e05855701b48fcf00
llm-streaming.zh.md: 2b61815f2730afdfb93bc06b8ee8925d2f4cac25

View File

@@ -169,6 +169,8 @@ declare class BlockAssembler {
interface PreparedLlmCall {
/** Detached, deep-frozen config with any adapter-owned default materialized. */
readonly config: LlmCallConfig
/** Config fields materialized by the captured adapter rather than proposed by the caller. */
readonly adapterDefaults: LlmCallConfigAdapterDefaults
/**
* Dispatch this call once through the registration captured during
* preparation. The request's call-config fields must match {@link config};

View File

@@ -169,6 +169,8 @@ declare class BlockAssembler {
interface PreparedLlmCall {
/** Detached, deep-frozen config with any adapter-owned default materialized. */
readonly config: LlmCallConfig
/** Config fields materialized by the captured adapter rather than proposed by the caller. */
readonly adapterDefaults: LlmCallConfigAdapterDefaults
/**
* Dispatch this call once through the registration captured during
* preparation. The request's call-config fields must match {@link config};

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/session.md
session.md: 769d5db301e3e81664c732ab1685c859a00cceb2
session.zh.md: 7af459949eda1b939596c20adf1c9e55f6d2b2b4
session.md: d7cd2c216c35a89024a312805759929b26edb53d
session.zh.md: 1a163f96872ba18f230d6aa395e86c0f4c56240d

View File

@@ -144,7 +144,7 @@ interface TodoItem {
### The request header event: `request/header`
The request envelope — the `EpochHeader` (call config + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
The request envelope — the `EpochHeader` (call config + adapter-default provenance + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
```ts type-equiv
/**
@@ -155,6 +155,8 @@ The request envelope — the `EpochHeader` (call config + rendered system prompt
interface EpochHeader {
/** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
config: LlmCallConfig
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
adapterDefaults?: LlmCallConfigAdapterDefaults
/** Rendered system prompt text; absent for a system-less request. */
system?: string
/** Assembled tool schemas; absent for a tool-less request. */

View File

@@ -146,7 +146,7 @@ interface TodoItem {
### 请求头事件:`request/header`
请求信封(即 `EpochHeader`:调用配置 + 渲染后的系统提示词 + 已组装的工具 schema会作为会话状态写入日志因此每个对话请求都是日志的纯函数见可重建性 Agent Note。带有 reason `'initial'` 或 `'resume'` 的完整 `request/header` 快照记录每个 agent loop 实例的边界;之后请求发生变化时,系统会以 reason `'change'` 记录另一份完整快照。`foldRequestHeader(events)` 通过选择最新快照重建请求头。该事件不是 `SurfaceEventType`,不产生 LLM 消息。
请求信封(即 `EpochHeader`:调用配置 + 适配器默认值来源 + 渲染后的系统提示词 + 已组装的工具 schema会作为会话状态写入日志因此每个对话请求都是日志的纯函数见可重建性 Agent Note。带有 reason `'initial'` 或 `'resume'` 的完整 `request/header` 快照记录每个 agent loop 实例的边界;之后请求发生变化时,系统会以 reason `'change'` 记录另一份完整快照。`foldRequestHeader(events)` 通过选择最新快照重建请求头。该事件不是 `SurfaceEventType`,不产生 LLM 消息。
```ts type-equiv
/**
@@ -157,6 +157,8 @@ interface TodoItem {
interface EpochHeader {
/** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
config: LlmCallConfig
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
adapterDefaults?: LlmCallConfigAdapterDefaults
/** Rendered system prompt text; absent for a system-less request. */
system?: string
/** Assembled tool schemas; absent for a tool-less request. */

View File

@@ -78,7 +78,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}[T]
```
Sources: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:315`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:347`](../packages/core/session/src/types.ts)
Sources: [`packages/core/session/src/types.ts:282`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:289`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:318`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:350`](../packages/core/session/src/types.ts)
## Events
@@ -154,7 +154,7 @@ Source: [`packages/ui/user-approval/src/index.ts:67`](../packages/ui/user-approv
Types: [StreamChunk](core-data-structures/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:212`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:215`](../packages/core/session/src/types.ts)
#### `assistant/message` — surface
@@ -170,7 +170,7 @@ Source: [`packages/core/session/src/types.ts:212`](../packages/core/session/src/
Types: [TokenUsage](core-data-structures/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:219`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:222`](../packages/core/session/src/types.ts)
### `command/*`
@@ -379,7 +379,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/s
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:255`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -432,7 +432,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s
'session/end-seed': Record<string, never>
```
Source: [`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:278`](../packages/core/session/src/types.ts)
#### `session/title` — log-only
@@ -468,7 +468,7 @@ Source: [`packages/session-title/session-title-llm/src/index.ts:43`](../packages
'steering/message': { turn: number; message: UserMessage }
```
Source: [`packages/core/session/src/types.ts:245`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:248`](../packages/core/session/src/types.ts)
### `step/*`
@@ -479,7 +479,7 @@ Source: [`packages/core/session/src/types.ts:245`](../packages/core/session/src/
'step/end': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:204`](../packages/core/session/src/types.ts)
#### `step/start` — log-only
@@ -488,7 +488,7 @@ Source: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/
'step/start': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:199`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:202`](../packages/core/session/src/types.ts)
### `todo/*`
@@ -501,7 +501,7 @@ Source: [`packages/core/session/src/types.ts:199`](../packages/core/session/src/
Types: [TodoItem](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:250`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -518,7 +518,7 @@ Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/
Types: [CallId](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:225`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:228`](../packages/core/session/src/types.ts)
#### `tool/code-dispatch` — log-only
@@ -591,7 +591,7 @@ Source: [`packages/core/tools/src/code-mode.ts:33`](../packages/core/tools/src/c
}
```
Source: [`packages/core/session/src/types.ts:237`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:240`](../packages/core/session/src/types.ts)
### `turn/*`
@@ -609,7 +609,7 @@ Source: [`packages/core/session/src/types.ts:237`](../packages/core/session/src/
Types: [TurnEndReason](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:200`](../packages/core/session/src/types.ts)
#### `turn/start` — log-only
@@ -622,7 +622,7 @@ Source: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/
Types: [TurnTrigger](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:190`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:193`](../packages/core/session/src/types.ts)
### `user/*`
@@ -640,4 +640,4 @@ Source: [`packages/core/session/src/types.ts:190`](../packages/core/session/src/
'user/message': UserMessage
```
Source: [`packages/core/session/src/types.ts:210`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:213`](../packages/core/session/src/types.ts)