From 4f36177740026af563bf288b4cde986db7916de1 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 13 Oct 2026 17:04:55 +0800 Subject: [PATCH] feat(agent): publish step decisions as agent.step and add agent/step-boundary Each proposed step now dispatches the serial agent/step-boundary event after claiming input, then agent/model, and publishes the chosen model selection as the optional agent.step until the step ends or its proposal is rejected, fails, or is cancelled. agent/pre-step listeners, tool conditions, prompt sections, and the provider and model variables read agent.step, so the pre-step payload, AssembleContext, and ToolActivationContext carry no model selection field. Plan mode commits pending selections at the step boundary, before model choice, and still narrates the switch in the first admitted step. The system prompt assembles once after step/start, including runtime context, and system-prompt/prepare is removed. The runtime-context snapshot is admitted after the step's accepted messages, so a changed runtime context alone no longer opens a step. --- ...bles-and-tool-guidance-ownership.i18n.yaml | 4 +- ...t-variables-and-tool-guidance-ownership.md | 2 +- ...ariables-and-tool-guidance-ownership.zh.md | 2 +- ...6-07-05-reconstructable-requests.i18n.yaml | 4 +- .../2026-07-05-reconstructable-requests.md | 2 +- .../2026-07-05-reconstructable-requests.zh.md | 2 +- ...02-system-prompt-as-surface-node.i18n.yaml | 4 +- ...026-09-02-system-prompt-as-surface-node.md | 2 +- ...-09-02-system-prompt-as-surface-node.zh.md | 2 +- ...-model-chosen-before-preparation.i18n.yaml | 16 +- ...12-step-model-chosen-before-preparation.md | 17 +- ...step-model-chosen-before-preparation.zh.md | 17 +- apps/cli/tests/fixtures/dsh-badge/snapshot.ts | 2 +- docs/agent-lifecycle.md | 14 +- docs/agent-lifecycle.zh.md | 14 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 12 +- docs/architecture.zh.md | 12 +- docs/config-catalog.md | 6 +- docs/config-catalog.zh.md | 6 +- docs/event-producer-consumer.md | 32 +-- docs/event-producer-consumer.zh.md | 32 +-- docs/persistence-catalog.i18n.yaml | 12 +- docs/persistence-catalog.md | 6 +- docs/persistence-catalog.zh.md | 6 +- docs/persistence-schema.json | 4 +- docs/subsystems/core.i18n.yaml | 8 +- docs/subsystems/core.md | 66 ++++- docs/subsystems/core.zh.md | 66 ++++- docs/subsystems/plan.i18n.yaml | 14 +- docs/subsystems/plan.md | 22 +- docs/subsystems/plan.zh.md | 22 +- docs/subsystems/session-reference.md | 7 +- docs/subsystems/session-reference.zh.md | 7 +- docs/subsystems/system-prompt.md | 46 +--- docs/subsystems/system-prompt.zh.md | 46 +--- docs/subsystems/tools.md | 2 +- docs/subsystems/tools.zh.md | 2 +- .../conditional-tools/guide.i18n.yaml | 12 +- .../v0.2.1-alpha.2/conditional-tools/guide.md | 18 +- .../conditional-tools/guide.zh.md | 18 +- .../tests/archived-session-gate.host.spec.ts | 2 +- .../ui-model-selection/README.i18n.yaml | 4 +- packages/client/ui-model-selection/README.md | 2 +- .../client/ui-model-selection/README.zh.md | 2 +- .../tests/compaction-basic.spec.ts | 2 +- .../tests/agent-instructions.spec.ts | 30 +-- .../session-reference/README.i18n.yaml | 4 +- packages/context/session-reference/README.md | 2 +- .../context/session-reference/README.zh.md | 2 +- .../context/session-reference/src/index.ts | 26 +- .../tests/loader-composition.spec.ts | 2 +- .../tests/session-reference.spec.ts | 12 +- .../time-context/tests/time-context.spec.ts | 2 +- .../tmux-context/tests/tmux-context.spec.ts | 2 +- packages/core/agent-loop/README.i18n.yaml | 4 +- packages/core/agent-loop/README.md | 2 +- packages/core/agent-loop/README.zh.md | 2 +- packages/core/agent-loop/src/agent.ts | 136 +++++------ packages/core/agent-loop/src/index.ts | 4 +- packages/core/agent-loop/tests/loop.spec.ts | 18 +- .../core/agent-loop/tests/step-model.spec.ts | 231 ++++++++++++++++++ .../agent-loop/tests/tool-updates.spec.ts | 11 +- packages/core/agent/README.i18n.yaml | 8 +- packages/core/agent/README.md | 6 +- packages/core/agent/README.zh.md | 6 +- packages/core/agent/src/model-selection.ts | 11 +- packages/core/agent/src/runtime-types.ts | 45 +++- .../core/agent/tests/model-selection.spec.ts | 27 +- packages/core/system-prompt/README.i18n.yaml | 4 +- packages/core/system-prompt/README.md | 2 +- packages/core/system-prompt/README.zh.md | 2 +- packages/core/system-prompt/src/index.ts | 74 +++--- .../system-prompt/tests/system-prompt.spec.ts | 24 ++ packages/core/tools/README.i18n.yaml | 4 +- packages/core/tools/README.md | 4 +- packages/core/tools/README.zh.md | 4 +- packages/core/tools/src/index.ts | 10 +- packages/core/tools/tests/activation.spec.ts | 9 +- packages/core/tools/tests/ptc.spec.ts | 2 +- .../extensions/tool-cordis/src/api-catalog.ts | 62 ++--- packages/plan/plan-mode/README.i18n.yaml | 12 +- packages/plan/plan-mode/README.md | 6 +- packages/plan/plan-mode/README.zh.md | 6 +- packages/plan/plan-mode/src/index.ts | 69 +++--- .../plan/plan-mode/tests/plan-mode.spec.ts | 46 ++-- .../tests/session-checkpoint-policy.spec.ts | 2 +- .../working-directory/README.i18n.yaml | 8 +- packages/session/working-directory/README.md | 4 +- .../session/working-directory/README.zh.md | 4 +- .../session/working-directory/src/index.ts | 2 +- .../tests/working-directory.spec.ts | 10 +- .../skill/tool-skill/tests/tool-skill.spec.ts | 8 +- .../tests/model-selection-settings.spec.ts | 1 - scripts/fixtures/python-sdk-dynamic-tools.mjs | 2 +- scripts/gen-cordis-catalog.ts | 2 +- scripts/gen-doc-graphs.ts | 14 +- .../python-snapshot-workflow-order.spec.ts | 2 +- scripts/type-equiv.manifest.json | 5 + 99 files changed, 965 insertions(+), 636 deletions(-) create mode 100644 packages/core/agent-loop/tests/step-model.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml index f331bc345e..1c7d61e281 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml @@ -15,8 +15,8 @@ en: 46e67248692e333a zh: 51c5f2c6152c94d7 /agent-note-prompt-variables-and-tool-guidance-ownership/decision/prompt-variables: - en: f5cbf8a901143c06 - zh: 534836ed5176a8ec + en: b605f58d6fb3684a + zh: 294862fcf54e2665 /agent-note-prompt-variables-and-tool-guidance-ownership/decision/persona-as-the-order-0-section: en: c2d32c0ac5d28759 zh: 59cd252ba6da6e59 diff --git a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md index 73931b9c8e..ef6152c7dd 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md +++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md @@ -28,7 +28,7 @@ The assembled system prompt had four defects, all of one family: facts the harne Plugins register `{{name}}` values through `ctx.systemPrompt.variable(name, provider)`. Assembly resolves them into the waterfall-visible variable map. Rendering rejects unknown own-property references, registered providers that return `undefined`, malformed complete references, and unbalanced references that still contain a closing `}}`; a lone unmatched `{{` remains prose, and substituted values are not rescanned. Registration rejects invalid or duplicate variable names, and section names are unique. Sections may set `interpolate: false` to preserve generated documents literally; `tools:sdk` does so because tool descriptions and schemas may document their own `{{…}}` syntax. -`dsh-agent-loop` registers `provider` and `model` as pure projections of the assembly context: the step model selection that `agent/model` chose, or the agent's `options` pair outside a step. Example personas reference `{{model}}` so the model selection owns the model name. These variables remain on the loop plugin because they describe the agents it drives; a replacement loop supplies its own. The [Session working-directory decision](2026-09-13-session-working-directory.md) owns directory state and its literal user-context placement; the loop does not register `cwd`. +`dsh-agent-loop` registers `provider` and `model` as pure projections of the assembly context: the step model selection that `agent/model` chose, read from `agent.step`, or the agent's `options` pair outside a step. Example personas reference `{{model}}` so the model selection owns the model name. These variables remain on the loop plugin because they describe the agents it drives; a replacement loop supplies its own. The [Session working-directory decision](2026-09-13-session-working-directory.md) owns directory state and its literal user-context placement; the loop does not register `cwd`. ### Persona as the order-0 section diff --git a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md index 12944274fa..13306621d8 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md @@ -28,7 +28,7 @@ Status: implemented 插件通过 `ctx.systemPrompt.variable(name, provider)` 注册 `{{name}}` 值。组装过程将它们解析到 waterfall 可见的变量映射中。渲染阶段拒绝以下情况:引用未知的自有属性、已注册的提供方返回 `undefined`、格式错误的完整引用、以及仍包含闭合 `}}` 的不平衡引用;孤立的未匹配 `{{` 保留为行文,替换后的值不会被重新扫描。注册阶段拒绝无效或重复的变量名,section 名称也必须唯一。段可设置 `interpolate: false` 来原样保留生成的文档;`tools:sdk` 使用此设置,因为工具描述和 schema 可能会介绍自身的 `{{…}}` 语法。 -`dsh-agent-loop` 注册 `provider` 和 `model`,它们都是组装上下文的纯投影:步骤内取 `agent/model` 选定的步骤模型选择,步骤外取 agent 的 `options` 这一对值。示例 persona 引用 `{{model}}`,由模型选择拥有模型名称。这些变量保留在 loop 插件上,因为它们描述该循环驱动的 agent;替换循环自行提供自己的变量。[Session 工作目录决策](2026-09-13-session-working-directory.zh.md) 拥有目录状态及其字面用户上下文位置;循环不注册 `cwd`。 +`dsh-agent-loop` 注册 `provider` 和 `model`,它们都是组装上下文的纯投影:步骤内从 `agent.step` 读取 `agent/model` 选定的步骤模型选择,步骤外取 agent 的 `options` 这一对值。示例 persona 引用 `{{model}}`,由模型选择拥有模型名称。这些变量保留在 loop 插件上,因为它们描述该循环驱动的 agent;替换循环自行提供自己的变量。[Session 工作目录决策](2026-09-13-session-working-directory.zh.md) 拥有目录状态及其字面用户上下文位置;循环不注册 `cwd`。 ### Persona 作为 order-0 section diff --git a/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml index 5809b9a8d4..84445dd681 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml @@ -15,8 +15,8 @@ en: 6da15cf986d59a91 zh: 40282908b54295f9 /agent-note-every-llm-request-is-reconstructable-from-the-session-log/decision/the-mechanism: - en: b053c2b01866a634 - zh: cb15fa3a7fdefe60 + en: ba24d551138e75ac + zh: f9288da86f510199 /agent-note-every-llm-request-is-reconstructable-from-the-session-log/decision/the-minicode-shape-adopted-with-the-event-log-as-the-source: en: e5f2ffaa3d7de0cc zh: 5ba6b36787a0e189 diff --git a/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md index 022f6dcb06..d9c1970a66 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md +++ b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md @@ -24,7 +24,7 @@ Prefix-cache stability is corollary #1, not the headline: an append-only log pro `EpochHeader` records the request's non-history state: call config and tool schemas. Writers omit `tools: []` and `adapterDefaults: {}`; current acceptance rejects those fields and any `header.system`, rather than repairing them. Whitespace-only system-message content, `config.stop: []`, and nested extensions remain intact. The [V3 canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns historical conversion. The rendered system prompt is derived history — the `system/message` event at surface node 0, per the [surface-node Agent Note](2026-09-02-system-prompt-as-surface-node.md) — so a prompt change is a surface replacement rather than a header change. Adapter-supplied effort and token defaults retain their `adapterDefaults` ownership; a Web model selection restored from the log omits an adapter-owned effort so the next resolution cannot reclassify the same effective config as an explicit selection and a false change. `request/header` always writes a full snapshot: the first loop instance uses reason `initial`, later instances use `resume`, an in-instance change uses `change`, and an unchanged envelope beginning an explicitly declared message series or following a surface replacement uses `series`. A `change` snapshot carries `startsSeries: true` when the changed request also starts a series, preserving the two independent facts without a duplicate header. Ordinary append-only later Turns, further same-series Steps, and retries inherit the latest snapshot. `foldRequestHeader` selects the latest snapshot. Legacy `request/header-delta` events and the removed `fallback` reason are rejected when appended or loaded. -Each proposed step first claims its inbox batch, lets `agent/model` choose the step's provider, model, and reasoning effort, prepares runtime context, and runs `agent/pre-step`. Rejection opens no step; enter opens `step/start`, resolves the step's tools, assembles the system prompt, commits a changed prompt as the `system/message` append or node-0 replacement, records the final message batch as `user/message` events, and may use `startsRequestSeries: true` to declare a distinct series. `agent/request` may replace only the frozen call config's sampling controls; the provider, model, and reasoning effort stay the step's model selection. The loop records the owed initial, resume, change, or series full snapshot, builds `GenerateOptions` from derived messages (system message first) and that header with no `system` field, and freezes it while leaving `AbortSignal` live. The [request-freeze evidence reference](../../../../packages/core/agent-loop/README.md) owns reuse of completed message freezes and per-request local header freezing. The first call config starts from explicit `AgentOptions`, preserving fork overrides and resume reconfiguration; later calls start from the folded header. +Each proposed step first claims its inbox batch, commits pending selections at `agent/step-boundary`, lets `agent/model` choose the step's provider, model, and reasoning effort as `agent.step`, and runs `agent/pre-step`. Rejection opens no step; enter opens `step/start`, resolves the step's tools, assembles the system prompt and runtime context, commits a changed prompt as the `system/message` append or node-0 replacement, records the final message batch as `user/message` events, and may use `startsRequestSeries: true` to declare a distinct series. `agent/request` may replace only the frozen call config's sampling controls; the provider, model, and reasoning effort stay the step's model selection. The loop records the owed initial, resume, change, or series full snapshot, builds `GenerateOptions` from derived messages (system message first) and that header with no `system` field, and freezes it while leaving `AbortSignal` live. The [request-freeze evidence reference](../../../../packages/core/agent-loop/README.md) owns reuse of completed message freezes and per-request local header freezing. The first call config starts from explicit `AgentOptions`, preserving fork overrides and resume reconfiguration; later calls start from the folded header. **The open step is the reconstruction boundary.** Its entered `user/message` batch and any newly written `request/header` precede request dispatch. Injection after the atomic claim joins a later request, while a listener that must affect this request returns messages through `agent/pre-step`. Header reconstruction selects the step's `request/header`, or carries the prior snapshot when no new header is written. diff --git a/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md index 9b94859d12..f0a84c3d44 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md @@ -24,7 +24,7 @@ Status: implemented `EpochHeader` 记录请求的非历史状态:调用配置和工具 schema。写入方省略 `tools: []` 与 `adapterDefaults: {}`;当前接纳拒绝这些字段以及任何 `header.system`,而不修复它们。仅含空白的系统消息内容、`config.stop: []` 与嵌套扩展保持原样。[V3 规范信封决策](2026-09-06-v3-canonical-session-envelopes.zh.md)负责历史转换。渲染后的系统提示词是派生历史——surface 第 0 号节点上的 `system/message` 事件,见[surface 节点 Agent Note](2026-09-02-system-prompt-as-surface-node.zh.md)——因此提示词变更是 surface 替换而不是 header 变更。适配器提供的推理强度与 token 默认值会保留其 `adapterDefaults` 来源信息;Web 从日志恢复模型选择时会省略适配器持有的推理强度,因此下一次解析不会把相同的有效配置重新归类为显式选择并产生虚假变更。`request/header` 始终写入完整快照:首个循环实例使用 reason `initial`,后续实例使用 `resume`,实例内变更使用 `change`,内容未变的封装显式开启消息序列或跟随表层替换时使用 `series`。如果发生变化的请求同时开启序列,`change` 快照会携带 `startsSeries: true`,无需重复 header 即可保留这两个独立事实。普通的仅追加后续 Turn、同一序列内后续的 Step 与重试沿用最新快照。`foldRequestHeader` 选择最新快照。旧的 `request/header-delta` 事件和已移除的 `fallback` reason 在追加或加载时都会被拒绝。 -每个拟议步骤先领取其 inbox 批次,由 `agent/model` 选定该步骤的提供方、模型与推理强度,准备运行时上下文,再运行 `agent/pre-step`。reject 不打开步骤;enter 打开 `step/start`,解析该步骤的工具并组装系统提示词,把变化的提示词作为 `system/message` 追加或第 0 号节点替换提交,把最终消息批次记录为 `user/message` 事件,并可使用 `startsRequestSeries: true` 声明独立序列。`agent/request` 只能替换冻结调用配置中的采样控制参数;提供方、模型与推理强度保持为步骤的模型选择。循环记录所需的 initial、resume、change 或 series 完整快照,从派生消息(系统消息在先)与该不含 `system` 字段的 header 构建 `GenerateOptions`,冻结请求但保持 `AbortSignal` 活跃。[请求冻结证据说明](../../../../packages/core/agent-loop/README.zh.md)拥有消息完整冻结的复用规则和每次请求的本地 header 冻结规则。首次调用配置从显式的 `AgentOptions` 出发,保留 fork 覆盖和恢复重配置;后续调用从折叠后的 header 出发。 +每个拟议步骤先领取其 inbox 批次,在 `agent/step-boundary` 提交待生效的选择,由 `agent/model` 选定该步骤的提供方、模型与推理强度并发布为 `agent.step`,再运行 `agent/pre-step`。reject 不打开步骤;enter 打开 `step/start`,解析该步骤的工具并组装系统提示词与运行时上下文,把变化的提示词作为 `system/message` 追加或第 0 号节点替换提交,把最终消息批次记录为 `user/message` 事件,并可使用 `startsRequestSeries: true` 声明独立序列。`agent/request` 只能替换冻结调用配置中的采样控制参数;提供方、模型与推理强度保持为步骤的模型选择。循环记录所需的 initial、resume、change 或 series 完整快照,从派生消息(系统消息在先)与该不含 `system` 字段的 header 构建 `GenerateOptions`,冻结请求但保持 `AbortSignal` 活跃。[请求冻结证据说明](../../../../packages/core/agent-loop/README.zh.md)拥有消息完整冻结的复用规则和每次请求的本地 header 冻结规则。首次调用配置从显式的 `AgentOptions` 出发,保留 fork 覆盖和恢复重配置;后续调用从折叠后的 header 出发。 **已打开步骤是重建边界。** 进入步骤的 `user/message` 批次与任何新写入的 `request/header` 都位于请求分派之前。原子领取后发生的注入加入后续请求;必须影响本次请求的监听器则通过 `agent/pre-step` 返回消息。header 重建选择该步骤的 `request/header`,或在无新 header 写入时沿用前一个快照。 diff --git a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.i18n.yaml index ba1077a0ec..e1477a5f0e 100644 --- a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.i18n.yaml @@ -18,8 +18,8 @@ en: a217a08932938062 zh: 4924d99d55d9c973 /agent-note-the-system-prompt-is-surface-node-0/decision/ownership-in-the-loop: - en: 1626e01049f6d312 - zh: bb000a96944c8168 + en: ca45857bcd9424eb + zh: 37247c6966423dcb /agent-note-the-system-prompt-is-surface-node-0/decision/consumers: en: 39f9f4acffcccdc6 zh: 36b23a57e7d28950 diff --git a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md index dbcdaea1f2..6c61cbfcb4 100644 --- a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md +++ b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md @@ -36,7 +36,7 @@ When the initial rendered prompt is empty, the loop reserves an empty system hea `dsh-agent-loop` owns `SystemPromptProjection` beside `RuntimeContextProjection` in `packages/core/agent-loop/src/runtime-context.ts`. It reads the surviving `system/message` nodes from the current surface on every projection, so a compaction or replacement that ran earlier in the same step is already reflected. `project(rendered, { inHistory, startsSeries })` returns `{ message, intent }` — `intent` is `{ surfaceOp: 'append' }` when no system node survives or when the [in-history rule](../feature/2026-09-02-in-history-system-prompt-replacement.md) applies, otherwise a replacement of exactly the latest surviving system node — or `undefined` when the latest node already holds the rendered text. -In `packages/core/agent-loop/src/agent.ts`, `step()` renders the prompt with `renderPrompt(assembly)` after `step/start` and projects it on each attempt after `agent/request` and `prepareCall()`, so a compaction provider's replacement inside `agent/pre-step` or retry recovery is visible to reconciliation; the `system/message` is committed before the step's `user/message` events, so log order is wire order. `buildRequest` sets no `system` on the request: the request is `header.config`, `session.deriveMessages()` (system message first), and `header.tools`. The loop step order is: claim inbox → `agent/model` waterfall → `systemPrompt.prepare()` → `agent/pre-step` waterfall → `step/start` → resolve tools → `systemPrompt.assemble()` → `agent/request` waterfall → `prepareCall()` → commit `system/message` (when changed) → commit `user/message`s → `request/header` → `request/context` → stream. +In `packages/core/agent-loop/src/agent.ts`, `step()` renders the prompt with `renderPrompt(assembly)` after `step/start` and projects it on each attempt after `agent/request` and `prepareCall()`, so a compaction provider's replacement inside `agent/pre-step` or retry recovery is visible to reconciliation; the `system/message` is committed before the step's `user/message` events, so log order is wire order. `buildRequest` sets no `system` on the request: the request is `header.config`, `session.deriveMessages()` (system message first), and `header.tools`. The loop step order is: claim inbox → `agent/step-boundary` → `agent/model` waterfall (published as `agent.step`) → `agent/pre-step` waterfall → `step/start` → resolve tools → `systemPrompt.assemble()` → `agent/request` waterfall → `prepareCall()` → commit `system/message` (when changed) → commit `user/message`s and a changed runtime-context snapshot → `request/header` → `request/context` → stream. `dsh-token-meter` anchors usage to the priced surface immediately before the successful `assistant/message`, not to `step/start`. The loop admits the system prompt and user messages after step start, and retry recovery can replace nodes before rebuilding the request. Capturing that current surface includes every admitted input once; the embedded provider output remains separately priced so durable assistant rewrites retain their signed delta. The open step stores only turn and step for lifecycle validation, not a second node snapshot. diff --git a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md index fada9e3085..9bfb9df062 100644 --- a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md @@ -36,7 +36,7 @@ Status: implemented `dsh-agent-loop` 在 `packages/core/agent-loop/src/runtime-context.ts` 中与 `RuntimeContextProjection` 并列拥有 `SystemPromptProjection`。它在每次投影时从当前 surface 读取存活的 `system/message` 节点,因此同一步骤中更早运行的压缩或替换已经反映在内。`project(rendered, { inHistory, startsSeries })` 返回 `{ message, intent }`——没有系统节点存活或[历史内规则](../feature/2026-09-02-in-history-system-prompt-replacement.zh.md)适用时 `intent` 为 `{ surfaceOp: 'append' }`,否则是对最新存活系统节点的精确替换——最新节点已持有渲染文本时返回 `undefined`。 -在 `packages/core/agent-loop/src/agent.ts` 中,`step()` 在 `step/start` 之后用 `renderPrompt(assembly)` 渲染提示词,并在每次尝试的 `agent/request` 与 `prepareCall()` 之后投影它,因此压缩提供者在 `agent/pre-step` 内或重试恢复中做出的替换对协调可见;`system/message` 在该步骤的 `user/message` 事件之前提交,因此日志顺序即协议顺序。`buildRequest` 不在请求上设置 `system`:请求由 `header.config`、`session.deriveMessages()`(系统消息在先)和 `header.tools` 构成。循环步骤顺序为:领取收件箱 → `agent/model` waterfall → `systemPrompt.prepare()` → `agent/pre-step` waterfall → `step/start` → 解析工具 → `systemPrompt.assemble()` → `agent/request` waterfall → `prepareCall()` → 提交 `system/message`(有变化时) → 提交各条 `user/message` → `request/header` → `request/context` → 流式请求。 +在 `packages/core/agent-loop/src/agent.ts` 中,`step()` 在 `step/start` 之后用 `renderPrompt(assembly)` 渲染提示词,并在每次尝试的 `agent/request` 与 `prepareCall()` 之后投影它,因此压缩提供者在 `agent/pre-step` 内或重试恢复中做出的替换对协调可见;`system/message` 在该步骤的 `user/message` 事件之前提交,因此日志顺序即协议顺序。`buildRequest` 不在请求上设置 `system`:请求由 `header.config`、`session.deriveMessages()`(系统消息在先)和 `header.tools` 构成。循环步骤顺序为:领取收件箱 → `agent/step-boundary` → `agent/model` waterfall(发布为 `agent.step`) → `agent/pre-step` waterfall → `step/start` → 解析工具 → `systemPrompt.assemble()` → `agent/request` waterfall → `prepareCall()` → 提交 `system/message`(有变化时) → 提交各条 `user/message` 与变化后的运行时上下文快照 → `request/header` → `request/context` → 流式请求。 `dsh-token-meter` 把用量锚定到成功的 `assistant/message` 之前的已计价 surface,而不是 `step/start`。循环在步骤开始之后接纳系统提示词与用户消息,重试恢复还可能在重建请求之前替换节点。捕获当前 surface 会让每个已接纳输入恰好计入一次;内嵌的提供方输出仍单独计价,因此持久 assistant 改写保留其带符号增量。开放步骤只保存 turn 与 step 以验证生命周期,不保存第二份节点快照。 diff --git a/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.i18n.yaml index 3559e84201..829d4efb0f 100644 --- a/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.i18n.yaml @@ -6,17 +6,17 @@ en: f068fddb192978d2 zh: 601deb72d3d90263 /agent-note-choose-the-step-model-before-step-preparation/problem: - en: cb8d6f058dd5f47d - zh: bb70e1cfe6880da5 + en: 1c3fbc5343be9e19 + zh: ee1c1c032074f2b6 /agent-note-choose-the-step-model-before-step-preparation/decision: - en: d0ee43966ff59a93 - zh: 4f69bc120de87c44 + en: 5c62c30a1810dcf8 + zh: 5146e199e13b2dfb /agent-note-choose-the-step-model-before-step-preparation/alternatives-considered: - en: 3114082393f6a55f - zh: a3790bd3b18658e0 + en: e69897cbb8e51ea2 + zh: c71acdfffb7d0d60 /agent-note-choose-the-step-model-before-step-preparation/consequences: - en: c553f9931c8c467b - zh: 0d77c5c71d266d27 + en: bf8a702bc7559c51 + zh: e81657ff8566e16b /agent-note-choose-the-step-model-before-step-preparation/related: en: 00ef1147a7852c0a zh: 4bd89960e08b6eda diff --git a/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.md b/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.md index c9a6c8dde9..1c7f236cbe 100644 --- a/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.md +++ b/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.md @@ -6,29 +6,34 @@ English | [中文](2026-10-12-step-model-chosen-before-preparation.zh.md) ## Problem -Tool availability can depend on the route of the request that declares the tools: `web_search` must stay off routes without search credentials, and plan or team state can withhold other tools. Message preparation needs the same model: session references size snapshots by the model's context window, and model selection announces a switch. The loop assembled tools and prompt sections before `agent/pre-step` and decided the provider, model, and reasoning effort only in `agent/request` after `step/start`, so earlier consumers predicted them from model-selection state. A listener that replaced them in `agent/request`, including on a retry, made the assembled prompt and tool catalog describe another model. +Tool availability can depend on the route of the request that declares the tools: `web_search` must stay off routes without search credentials, and plan or team state can withhold other tools. Message preparation needs the same model: session references size snapshots by the model's context window, and model selection announces a switch. The loop assembled tools and prompt sections before `agent/pre-step` and decided the provider, model, and reasoning effort only in `agent/request` after `step/start`, so earlier consumers predicted them from model-selection state. A listener that replaced them in `agent/request`, including on a retry, made the assembled prompt and tool catalog describe another model. Plan state had the same problem: it was committed inside `agent/pre-step`, so plan guidance predicted it from the pending selection. ## Decision -The provider, model, and reasoning effort are a step fact, chosen together as the user chooses them before sending a message. After claiming input, the loop dispatches `agent/model` once per proposed step. Its default comes from the agent options before this loop logs a request and from the logged header afterwards, with an effort only when one was chosen for that exact model; `installModelSelection` returns its current selection there. The loop passes the resulting `ModelSelection` to `system-prompt/prepare` as `AssembleContext.modelSelection`, to `agent/pre-step` as `payload.modelSelection`, and to tool conditions as `ToolActivationContext.modelSelection`. The `{{provider}}` and `{{model}}` prompt variables read it. +The provider, model, and reasoning effort are a step fact, chosen together as the user chooses them before sending a message. After claiming input, the loop dispatches the serial `agent/step-boundary` event, where plugins commit selections that apply from this step, such as a pending plan-mode switch. It then dispatches `agent/model` once per proposed step. Its default comes from the agent options before this loop logs a request and from the logged header afterwards, with an effort only when one was chosen for that exact model; `installModelSelection` returns its current selection there. The loop publishes the result as `agent.step` and clears it when the step ends or its proposal is rejected, fails, or is cancelled. `agent/pre-step` listeners, tool conditions, prompt sections, and the `{{provider}}` and `{{model}}` variables read the selection from the agent. -After `agent/pre-step` accepts the step and `step/start` opens it, the loop resolves conditional tools and assembles the prompt once. Each request attempt then runs `agent/request` and `prepareCall()` immediately before admission. `agent/request` may change sampling controls; a result with another provider, model, or reasoning effort fails the step. Retries reuse the step's model selection, tool set, and rendered assembly, and the step-owned tool set closes after the step's tool batch settles. A selection made during a step applies from the next step. +After `agent/pre-step` accepts the step and `step/start` opens it, the loop resolves conditional tools and assembles the prompt once: sections, variables, runtime context, and tool declarations. The runtime-context snapshot is admitted after the step's accepted messages. Each request attempt then runs `agent/request` and `prepareCall()` immediately before admission. `agent/request` may change sampling controls; a result with another provider, model, or reasoning effort fails the step. Retries reuse the step's model selection, tool set, and rendered assembly, and the step-owned tool set closes after the step's tool batch settles. A selection made during a step applies from the next step. ## Alternatives considered - **Bind the route in `agent/request` before `agent/pre-step` and expose `Agent.currentRoute()`.** The prepared call captures the adapter registration and connection settings, so holding it across compaction, hooks, and session reads ignores settings changed meanwhile, and adapters were prepared for steps that `agent/pre-step` rejects. The getter was valid only inside a step and threw elsewhere, and every Agent implementation and test double had to carry it. -- **Record the model in `agent/pre-step`.** `system-prompt/prepare` runs before that waterfall, and model consumers inside it would depend on listener order. +- **Choose the model inside `agent/pre-step`.** Pre-step listeners consume the model for reference budgets and switch notices. Deciding and consuming it in one waterfall makes the value each listener sees depend on registration order. +- **Pass the selection to each consumer as a parameter.** `AssembleContext`, the pre-step payload, and `ToolActivationContext` each needed a field, and every later step decision would add one to all three. `agent.step` gives them one source. - **Register the model selection on the Agent without an event.** It adds a single-consumer method to the generic `Agent` interface and removes the listener path that supplies a model for agents created without one. - **Keep the decision in `agent/request` and predict it for earlier consumers.** A prediction diverges whenever a listener other than model selection changes the model. - **Choose only the provider and model per step and leave reasoning effort to `agent/request`.** An effort is valid only for the model it was chosen for. The split required `installModelSelection` to capture the selection in one waterfall and apply its effort in another only when no other listener had replaced the model, and it let a retry change effort within a step. +- **Prepare runtime context before `agent/pre-step` in a separate `system-prompt/prepare` phase.** It kept the runtime context between the claimed input and messages added by pre-step listeners, but split assembly into two hooks and made a changed runtime context alone open a step. +- **Commit pending plan selections inside `agent/pre-step`.** Phases and listeners that ran before the commit saw the previous plan state, and model choice could not depend on plan mode. ## Consequences - Tool conditions, prompt variables, reference budgets, and switch notices read the provider and model that the request header records. - `prepareCall()` stays adjacent to dispatch, so its adapter registration and connection snapshot cover only header logging and streaming. - A step cannot fail over to another provider or change reasoning effort on retry; a policy that needs this must run model selection, tool resolution, and assembly again for the retried attempt. -- The model selection cannot depend on `agent/pre-step` outcomes. Plan state commits inside that waterfall, so a plan-specific model would require committing plan selections before model selection. -- `agent/pre-step` listeners and direct dispatchers must handle the required `modelSelection` field. +- `agent.step` is undefined between steps and during `agent/model`; work that outlives its step reads it at its start. +- `agent/model` listeners can read plan state because the boundary commits it first, but model choice still cannot depend on `agent/pre-step` outcomes. +- A pending plan-mode selection is committed even when `agent/pre-step` rejects the step; its notice joins the next admitted step. +- The runtime-context snapshot follows the step's accepted messages, and a changed runtime context alone does not open a step. ## Related diff --git a/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.zh.md b/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.zh.md index 09ef27ac4f..e5ea4da203 100644 --- a/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-12-step-model-chosen-before-preparation.zh.md @@ -6,29 +6,34 @@ Status: implemented ## 问题 -工具是否可用可能取决于声明这些工具的请求所用的路由:`web_search` 不得出现在没有搜索凭据的路由上,计划或团队状态也可能撤下其他工具。消息准备同样需要这一模型:会话引用按模型的上下文窗口确定快照大小,模型选择会通告模型切换。循环原先在 `agent/pre-step` 之前组装工具与提示词段落,直到 `step/start` 之后才在 `agent/request` 中决定提供方、模型与推理强度,因此更早的消费方只能依据模型选择状态预测它们。若监听器在 `agent/request` 中(包括在重试时)替换它们,已组装的提示词与工具目录就会描述另一个模型。 +工具是否可用可能取决于声明这些工具的请求所用的路由:`web_search` 不得出现在没有搜索凭据的路由上,计划或团队状态也可能撤下其他工具。消息准备同样需要这一模型:会话引用按模型的上下文窗口确定快照大小,模型选择会通告模型切换。循环原先在 `agent/pre-step` 之前组装工具与提示词段落,直到 `step/start` 之后才在 `agent/request` 中决定提供方、模型与推理强度,因此更早的消费方只能依据模型选择状态预测它们。若监听器在 `agent/request` 中(包括在重试时)替换它们,已组装的提示词与工具目录就会描述另一个模型。计划状态也有同样的问题:它在 `agent/pre-step` 内提交,计划指引只能依据待提交的选择预测。 ## 决策 -提供方、模型与推理强度是步骤级事实,像用户在发送消息前那样一起选定。领取输入之后,循环为每个拟议步骤派发一次 `agent/model`。其默认值在本循环记录请求之前取自 agent 选项,之后取自已记录的请求头,且只有为该确切模型选定过的推理强度才会保留;`installModelSelection` 在这里返回其当前选择。循环把得到的 `ModelSelection` 作为 `AssembleContext.modelSelection` 传给 `system-prompt/prepare`,作为 `payload.modelSelection` 传给 `agent/pre-step`,作为 `ToolActivationContext.modelSelection` 传给工具条件。提示词变量 `{{provider}}` 与 `{{model}}` 读取该选择。 +提供方、模型与推理强度是步骤级事实,像用户在发送消息前那样一起选定。领取输入之后,循环派发串行事件 `agent/step-boundary`,插件在此提交从本步骤起生效的选择,例如待提交的计划模式切换。随后循环为每个拟议步骤派发一次 `agent/model`。其默认值在本循环记录请求之前取自 agent 选项,之后取自已记录的请求头,且只有为该确切模型选定过的推理强度才会保留;`installModelSelection` 在这里返回其当前选择。循环把结果发布为 `agent.step`,并在步骤结束或提案被拒绝、失败、取消时清除。`agent/pre-step` 监听器、工具条件、提示词段落以及 `{{provider}}` 与 `{{model}}` 变量都从 agent 读取该选择。 -`agent/pre-step` 接纳步骤且 `step/start` 打开步骤之后,循环解析条件工具并组装提示词,各执行一次。之后每次请求尝试都在准入之前紧接着运行 `agent/request` 与 `prepareCall()`。`agent/request` 可以修改采样控制参数;返回其他提供方、模型或推理强度会使步骤失败。重试复用该步骤的模型选择、工具集合与已渲染组装结果,步骤拥有的工具集合在该步骤的工具批次结束后关闭。步骤进行中做出的选择从下一个步骤开始生效。 +`agent/pre-step` 接纳步骤且 `step/start` 打开步骤之后,循环解析条件工具并一次性组装提示词:段落、变量、运行时上下文与工具声明。运行时上下文快照在该步骤已接纳的消息之后提交。之后每次请求尝试都在准入之前紧接着运行 `agent/request` 与 `prepareCall()`。`agent/request` 可以修改采样控制参数;返回其他提供方、模型或推理强度会使步骤失败。重试复用该步骤的模型选择、工具集合与已渲染组装结果,步骤拥有的工具集合在该步骤的工具批次结束后关闭。步骤进行中做出的选择从下一个步骤开始生效。 ## 考虑过的替代方案 - **在 `agent/pre-step` 之前通过 `agent/request` 绑定路由,并提供 `Agent.currentRoute()`。** 已准备调用会捕获适配器注册与连接设置,因此在压缩、钩子与会话读取期间一直持有它,会忽略期间修改的设置;被 `agent/pre-step` 拒绝的步骤也会准备适配器。该 getter 只在步骤内有效、在其他时刻抛错,且每个 Agent 实现与测试替身都必须提供它。 -- **在 `agent/pre-step` 内记录模型。** `system-prompt/prepare` 在该 waterfall 之前运行,而 waterfall 内的模型消费方将依赖监听器顺序。 +- **在 `agent/pre-step` 内选定模型。** pre-step 监听器自身就要用模型计算引用预算和切换通告。在同一个 waterfall 中既决定又使用模型,每个监听器看到的值就取决于注册顺序。 +- **把选择作为参数传给每个消费方。** `AssembleContext`、pre-step payload 与 `ToolActivationContext` 各需一个字段,之后每增加一项步骤决定都要同时改这三处。`agent.step` 为它们提供单一来源。 - **不使用事件,直接在 Agent 上登记模型选择。** 这会给通用 `Agent` 接口增加只有单一消费方的方法,并移除为创建时没有模型的 agent 提供模型的监听器途径。 - **决定留在 `agent/request`,为更早的消费方预测它。** 只要模型选择之外的监听器修改模型,预测就会偏离。 - **每个步骤只选定提供方与模型,推理强度留给 `agent/request`。** 推理强度只对选定它的模型有效。拆开后,`installModelSelection` 必须在一个 waterfall 中捕获选择,再在另一个 waterfall 中、且仅当其他监听器没有替换模型时应用推理强度;重试也可以在步骤内修改推理强度。 +- **在 `agent/pre-step` 之前用单独的 `system-prompt/prepare` 阶段准备运行时上下文。** 它把运行时上下文保留在已领取输入与 pre-step 监听器追加的消息之间,但把组装拆成了两个钩子,并让仅运行时上下文变化就能打开一个步骤。 +- **在 `agent/pre-step` 内提交待生效的计划选择。** 提交之前运行的阶段和监听器看到的是旧的计划状态,模型选择也无法依据计划模式。 ## 后果 - 工具条件、提示词变量、引用预算与切换通告读取的提供方与模型,与请求头记录的一致。 - `prepareCall()` 紧邻分发,因此其适配器注册与连接快照只覆盖请求头记录与流式请求。 - 步骤无法在重试时切换到其他提供方或修改推理强度;需要这种能力的策略必须为重试的尝试重新执行模型选择、工具解析与组装。 -- 模型选择不能依赖 `agent/pre-step` 的结果。计划状态在该 waterfall 内提交,因此按计划模式选择模型需要先在模型选择之前提交计划选择。 -- `agent/pre-step` 监听器与直接派发方必须处理必填的 `modelSelection` 字段。 +- `agent.step` 在两个步骤之间以及 `agent/model` 期间为 undefined;跨步骤运行的工作要在开始时读取它。 +- `agent/model` 监听器可以读取计划状态,因为步骤边界会先提交它;但模型选择仍不能依赖 `agent/pre-step` 的结果。 +- 即使 `agent/pre-step` 拒绝了该步骤,待生效的计划模式选择也已提交;其通告会出现在下一个被接纳的步骤中。 +- 运行时上下文快照位于该步骤已接纳的消息之后,仅运行时上下文变化不会打开步骤。 ## 相关 diff --git a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts index f6674047bf..c84912a9f1 100644 --- a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts +++ b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts @@ -43,7 +43,7 @@ try { } const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: new AbortController().signal }, + { messages: [], turn: 1, step: 1, signal: new AbortController().signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) const catalog = decision.kind === 'enter' diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index b1d099df0b..c91c486556 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -25,22 +25,22 @@ sequenceDiagram Note over Agent,Driver: claim pending next-step input plus one queued prompt Driver-->>SDK: agent/inbox/spliced pure deletion Driver-->>SDK: agent/inbox/claimed { message, turn } per message - Driver->>Hooks: agent/model waterfall chooses the step provider, model, and effort - Driver->>Prompt: system-prompt/prepare waterfall with the step model selection - Driver->>Hooks: agent/pre-step waterfall with the step model selection + Driver->>Hooks: agent/step-boundary serial commits selections pending for this step + Driver->>Hooks: agent/model waterfall chooses provider, model, and effort, published as agent.step + Driver->>Hooks: agent/pre-step waterfall reads agent.step Hooks-->>Driver: authoritative reject or enter(messages) alt proposed step rejected, first batch empty, or pre-step failed Driver-->>Driver: claimed batch stays removed, the open turn spends no step else enter proposed step Driver->>Session: step/start - Driver->>Tools: resolve({ agent, modelSelection, signal }) once per step - Driver->>Prompt: system-prompt/assemble with the step tool set + Driver->>Tools: resolve({ agent, signal }) once per step + Driver->>Prompt: system-prompt/assemble contexts, variables, sections, and the step tool set Driver->>Hooks: agent/request waterfall with the step model selection Driver->>LLM: prepareCall(config, signal) Note over Driver,LLM: cancellation during preparation commits neither system nor users Note over Driver,Session: synchronous admission using the prepared call capability Driver->>Session: system/message ordered per-node reconciliation - Driver->>Session: user/message per entered message + Driver->>Session: user/message per entered message, then a changed runtime-context snapshot Driver->>Session: request/header and request/context as needed Driver->>Driver: derive and freeze request from the log Driver->>LLM: bound prepared call through llm/stream waterfall @@ -75,8 +75,8 @@ sequenceDiagram opt next-step input is pending Driver-->>Driver: claim pending next-step input Driver-->>SDK: agent/inbox/claimed { message, turn } per message + Driver->>Hooks: agent/step-boundary serial Driver->>Hooks: agent/model waterfall - Driver->>Prompt: system-prompt/prepare waterfall Driver->>Hooks: agent/pre-step waterfall Hooks-->>Driver: authoritative reject or enter(messages) end diff --git a/docs/agent-lifecycle.zh.md b/docs/agent-lifecycle.zh.md index ebb6dbec73..4172d114eb 100644 --- a/docs/agent-lifecycle.zh.md +++ b/docs/agent-lifecycle.zh.md @@ -27,22 +27,22 @@ sequenceDiagram Note over Agent,Driver: claim pending next-step input plus one queued prompt Driver-->>SDK: agent/inbox/spliced pure deletion Driver-->>SDK: agent/inbox/claimed { message, turn } per message - Driver->>Hooks: agent/model waterfall chooses the step provider, model, and effort - Driver->>Prompt: system-prompt/prepare waterfall with the step model selection - Driver->>Hooks: agent/pre-step waterfall with the step model selection + Driver->>Hooks: agent/step-boundary serial commits selections pending for this step + Driver->>Hooks: agent/model waterfall chooses provider, model, and effort, published as agent.step + Driver->>Hooks: agent/pre-step waterfall reads agent.step Hooks-->>Driver: authoritative reject or enter(messages) alt proposed step rejected, first batch empty, or pre-step failed Driver-->>Driver: claimed batch stays removed, the open turn spends no step else enter proposed step Driver->>Session: step/start - Driver->>Tools: resolve({ agent, modelSelection, signal }) once per step - Driver->>Prompt: system-prompt/assemble with the step tool set + Driver->>Tools: resolve({ agent, signal }) once per step + Driver->>Prompt: system-prompt/assemble contexts, variables, sections, and the step tool set Driver->>Hooks: agent/request waterfall with the step model selection Driver->>LLM: prepareCall(config, signal) Note over Driver,LLM: cancellation during preparation commits neither system nor users Note over Driver,Session: synchronous admission using the prepared call capability Driver->>Session: system/message ordered per-node reconciliation - Driver->>Session: user/message per entered message + Driver->>Session: user/message per entered message, then a changed runtime-context snapshot Driver->>Session: request/header and request/context as needed Driver->>Driver: derive and freeze request from the log Driver->>LLM: bound prepared call through llm/stream waterfall @@ -77,8 +77,8 @@ sequenceDiagram opt next-step input is pending Driver-->>Driver: claim pending next-step input Driver-->>SDK: agent/inbox/claimed { message, turn } per message + Driver->>Hooks: agent/step-boundary serial Driver->>Hooks: agent/model waterfall - Driver->>Prompt: system-prompt/prepare waterfall Driver->>Hooks: agent/pre-step waterfall Hooks-->>Driver: authoritative reject or enter(messages) end diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index e3e6bbdaa4..8091ef489e 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -24,8 +24,8 @@ en: e89802f066e4955d zh: ea2fb4bef94e086d /deepseek-harness-architecture/turn-flow: - en: 5001f8affe8d84f0 - zh: de2632709bb4e93a + en: 0f496f867565ac9b + zh: 1c015bb06ce0a4b4 /deepseek-harness-architecture/session-log: en: a2e35c43438fec6d zh: e27bdac8e9feff32 diff --git a/docs/architecture.md b/docs/architecture.md index 3c120ed3b9..f5ad4e118c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -83,16 +83,16 @@ A **step** is one model request plus the tools it calls. A **turn** is zero or m ```text turn/start claim next-step input plus one queued message - agent/model -> step provider/model/effort - prepare variables + runtime context - -> agent/pre-step; project runtime context at fallback + agent/step-boundary -> commit pending selections + agent/model -> agent.step (provider/model/effort) + -> agent/pre-step reject | enter(messages, startsRequestSeries?) reject, or a first enter rewritten empty -> close the turn with no step step/start - resolve conditional tools + assemble prompt + resolve conditional tools + assemble prompt and context agent/request -> prepareCall reconcile system/message using the prepared call capability - append entered messages as user/message; reconcile retained runtime context + append entered messages as user/message, then changed runtime context log request/header and request/context as needed derive and freeze model history from the log stream the bound prepared call -> llm/stream -> agent/assistant-stream start @@ -109,7 +109,7 @@ turn/end One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents. -`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. `agent/model` fixes the step's provider, model, and effort before `agent/pre-step`; accepted steps resolve tools and assemble once. Each attempt's `agent/request` may change only sampling, and `prepareCall()` binds the adapter before prompt and user admission; cancellation in either commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes runtime facts, reconciles the prompt, admits users once, restores removed context, logs headers, and freezes the request before streaming; retries reuse the selection, tools, and assembly. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes append non-empty updates after cached history, including supported tool updates; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)). +`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. `agent/step-boundary` commits pending selections, then `agent/model` publishes `agent.step` (provider, model, effort) before `agent/pre-step`; accepted steps resolve tools and assemble once. Each attempt's `agent/request` may change only sampling, and `prepareCall()` binds the adapter before prompt and user admission; cancellation in either commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes runtime facts, reconciles the prompt, admits users once, restores removed context, logs headers, and freezes the request before streaming; retries reuse the selection, tools, and assembly. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes append non-empty updates after cached history, including supported tool updates; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)). The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes. diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 1d8e3718b9..c66ed3b344 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -87,16 +87,16 @@ AgentLoop 在启动已排队工作前等待串行 `agent/created`,初始化失 ```text turn/start claim next-step input plus one queued message - agent/model -> step provider/model/effort - prepare variables + runtime context - -> agent/pre-step; project runtime context at fallback + agent/step-boundary -> commit pending selections + agent/model -> agent.step (provider/model/effort) + -> agent/pre-step reject | enter(messages, startsRequestSeries?) reject, or a first enter rewritten empty -> close the turn with no step step/start - resolve conditional tools + assemble prompt + resolve conditional tools + assemble prompt and context agent/request -> prepareCall reconcile system/message using the prepared call capability - append entered messages as user/message; reconcile retained runtime context + append entered messages as user/message, then changed runtime context log request/header and request/context as needed derive and freeze model history from the log stream the bound prepared call -> llm/stream -> agent/assistant-stream start @@ -113,7 +113,7 @@ turn/end 输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。 -`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。`agent/model` 在 `agent/pre-step` 之前固定该步骤的提供方、模型与推理强度;被接纳的步骤随后解析工具并组装一次。每次尝试的 `agent/request` 只能修改采样控制参数,`prepareCall()` 在提示词与用户消息准入前绑定适配器;在任一阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求;重试复用模型选择、工具与组装结果。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新,包括同时发生的受支持工具更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。 +`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。`agent/step-boundary` 提交待生效的选择,随后 `agent/model` 在 `agent/pre-step` 之前发布 `agent.step`(提供方、模型、推理强度);被接纳的步骤随后解析工具并组装一次。每次尝试的 `agent/request` 只能修改采样控制参数,`prepareCall()` 在提示词与用户消息准入前绑定适配器;在任一阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求;重试复用模型选择、工具与组装结果。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新,包括同时发生的受支持工具更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。 循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 442dc7bc66..01d8364f5a 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2387,7 +2387,7 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` - `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:67`](../packages/plan/plan-mode/src/index.ts) +- `source`: [`packages/plan/plan-mode/src/index.ts:68`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -3492,7 +3492,7 @@ export interface Config { ## `@deepseek-ai/dsh-system-prompt` -- `source`: [`packages/core/system-prompt/src/index.ts:269`](../packages/core/system-prompt/src/index.ts) +- `source`: [`packages/core/system-prompt/src/index.ts:262`](../packages/core/system-prompt/src/index.ts) ```ts config-catalog /** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */ @@ -4171,7 +4171,7 @@ export interface Config { ## `@deepseek-ai/dsh-tools` - `inject`: `systemPrompt` -- `source`: [`packages/core/tools/src/index.ts:735`](../packages/core/tools/src/index.ts) +- `source`: [`packages/core/tools/src/index.ts:733`](../packages/core/tools/src/index.ts) ```ts config-catalog /** Plugin config: how the registered tools are presented to the model. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index f8a8c0c93f..7c09b28857 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2389,7 +2389,7 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` - `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:67`](../packages/plan/plan-mode/src/index.ts) +- `source`: [`packages/plan/plan-mode/src/index.ts:68`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -3494,7 +3494,7 @@ export interface Config { ## `@deepseek-ai/dsh-system-prompt` -- `source`: [`packages/core/system-prompt/src/index.ts:269`](../packages/core/system-prompt/src/index.ts) +- `source`: [`packages/core/system-prompt/src/index.ts:262`](../packages/core/system-prompt/src/index.ts) ```ts config-catalog /** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */ @@ -4173,7 +4173,7 @@ export interface Config { ## `@deepseek-ai/dsh-tools` - `inject`: `systemPrompt` -- `source`: [`packages/core/tools/src/index.ts:735`](../packages/core/tools/src/index.ts) +- `source`: [`packages/core/tools/src/index.ts:733`](../packages/core/tools/src/index.ts) ```ts config-catalog /** Plugin config: how the registered tools are presented to the model. */ diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index ac302aefdd..18eb0fdfc5 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -10,19 +10,20 @@ This matrix shows which packages dispatch each harness-owned event and which pac | --- | --- | --- | --- | --- | | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | -| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:273`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:282`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | -| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:311`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:319`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:300`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | -| `agent/model` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:352`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:333`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | -| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:373`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:292`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | -| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:426`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:284`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:293`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:456`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | +| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:322`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | +| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:311`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | +| `agent/model` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:379`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:344`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:400`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:416`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:303`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | +| `agent/step-boundary` | `serial` | [`packages/core/agent/src/runtime-types.ts:359`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`plan-mode`](../packages/plan/plan-mode) | +| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:444`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:624`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:631`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | @@ -72,9 +73,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | -| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime` | -| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:44`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | -| `system-prompt/prepare` | `waterfall` | [`packages/core/system-prompt/src/index.ts:38`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`working-directory`](../packages/session/working-directory) | +| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) | +| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:216`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:172`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:184`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index cb221a48b9..9d6c5c669a 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -12,19 +12,20 @@ | --- | --- | --- | --- | --- | | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | -| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:273`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:282`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | -| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:311`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:319`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:300`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | -| `agent/model` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:352`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:333`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | -| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:373`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:292`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | -| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:426`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:284`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:293`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:456`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | +| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:322`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | +| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:311`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | +| `agent/model` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:379`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:344`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:400`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:416`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:303`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | +| `agent/step-boundary` | `serial` | [`packages/core/agent/src/runtime-types.ts:359`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`plan-mode`](../packages/plan/plan-mode) | +| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:444`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:624`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:631`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | @@ -74,9 +75,8 @@ | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | -| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime` | -| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:44`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | -| `system-prompt/prepare` | `waterfall` | [`packages/core/system-prompt/src/index.ts:38`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`working-directory`](../packages/session/working-directory) | +| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) | +| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:216`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:172`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:184`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 647cb8b746..09b61ca9b7 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -153,8 +153,8 @@ en: 4294f78d26ed985b zh: 4294f78d26ed985b /session-persistence-event-catalog/events/plan/plan-mode-log-only: - en: f23bf0e5e6408adf - zh: a9442b4c5cdeef0b + en: 862175bfe8692ef7 + zh: a8ab123bf674cf77 /session-persistence-event-catalog/events/request: en: a8df82440ebdf5f8 zh: a8df82440ebdf5f8 @@ -1278,8 +1278,8 @@ en: f43a635a14568780 zh: 8d22226b045122eb /session-persistence-event-catalog/resolved-persistence-types/toolrestriction: - en: 0a56a07b1de985b4 - zh: 886c512054d12ad6 + en: 739d19df365ed5df + zh: b1e41e56995fe238 /session-persistence-event-catalog/resolved-persistence-types/toolresultmessage: en: 2258683c3cfa4ce3 zh: 5663aa76c911761b @@ -1389,8 +1389,8 @@ en: a74e3dfa51c987af zh: 6c3c87974d7a7260 /session-persistence-event-catalog/resolved-persistence-types/active~2: - en: 1bc41f689438a051 - zh: babdc92cc0ad089a + en: a3de6a6fe9672c7a + zh: a357481e27b3b81f /session-persistence-event-catalog/resolved-persistence-types/agentpreset: en: b8a0f8aac9708056 zh: f5f004bd205b41e1 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 1b2381bca8..0fd493b8b3 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -732,7 +732,7 @@ Source: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/ 'plan/mode': { active: boolean } ``` -Source: [`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts) +Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -5158,7 +5158,7 @@ Sources: [`packages/llm/llm/src/types.ts:127`](../packages/llm/llm/src/types.ts) SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f` -Sources: [`packages/core/tools/src/index.ts:761`](../packages/core/tools/src/index.ts) +Sources: [`packages/core/tools/src/index.ts:759`](../packages/core/tools/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -5734,7 +5734,7 @@ Array of `unknown` (opaque). SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -Sources: [`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts) +Sources: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index e839ff1ce7..dc9db33cbd 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -734,7 +734,7 @@ export type SessionEvent = { 'plan/mode': { active: boolean } ``` -来源:[`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -5160,7 +5160,7 @@ SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8` SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f` -来源:[`packages/core/tools/src/index.ts:761`](../packages/core/tools/src/index.ts) +来源:[`packages/core/tools/src/index.ts:759`](../packages/core/tools/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5736,7 +5736,7 @@ SHA-256: `b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7` SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -来源:[`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index aaebcb31cb..e53cc6a7dc 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -22809,7 +22809,7 @@ "digest": "20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5", "names": [], "sources": [ - "packages/plan/plan-mode/src/index.ts:51" + "packages/plan/plan-mode/src/index.ts:52" ] }, { @@ -23218,7 +23218,7 @@ "packages/core/tools/src/index.ts#ToolRestriction" ], "sources": [ - "packages/core/tools/src/index.ts:761" + "packages/core/tools/src/index.ts:759" ] }, { diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 35c730cee9..ba5927d5d8 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -12,14 +12,14 @@ en: ab09a38b4e1f0883 zh: 81e2d14c4036ab6c /core/the-agent-handle: - en: fbcbf559b0362d11 - zh: e2cc3f4e28f23687 + en: c0339e2de0246e58 + zh: 9e1def25d117ac0a /core/initiating-agent: en: 6c8c47ac78de2dde zh: c9d527a25a85d1d9 /core/interception-decisions: - en: f50b0e1933657f39 - zh: 961a69f1247951df + en: 4bfb9b4426027b9e + zh: f4af79c9c18afa94 /core/sessions: en: 9a38f693a00fa80d zh: d0f66664ac85ae64 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 34fc5d394d..6adb8ce35e 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -63,6 +63,13 @@ interface Agent { readonly id: SessionId /** Creation-time request options; `agent/model` may choose another provider, model, and effort for each step. */ readonly options: AgentOptions + /** + * The step the driver is preparing or running. The driver publishes it + * after `agent/model` and clears it when the step ends or its proposal is + * rejected, fails, or is cancelled, so it is undefined between steps and + * during `agent/model`. Work that outlives its step reads it at its start. + */ + readonly step?: AgentStep | undefined /** The live session this agent drives; its log is the durable source of truth. */ readonly session: Session /** Agent-owned access to durable pending work. */ @@ -208,7 +215,7 @@ interface AgentOptions { Each step requires `provider` and `model` after `agent/model`, which may supply or replace the options' provider, model, and effort. An explicit `reasoningEffort` seeds the first step's selection for that model; exact-model resolution validates it, while omission allows the adapter default to materialize. When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. An agent-scoped `deployment:persona-prefix` prompt section may shadow the global default persona. -The step keeps its model selection for prompt preparation, `agent/pre-step`, tool conditions, and every request attempt; `agent/request` may change only sampling controls such as `maxTokens`, `temperature`, and `stop`: +The driver publishes the step's selection as `agent.step`, which `agent/pre-step`, tool conditions, prompt assembly, and every request attempt read; `agent/request` may change only sampling controls such as `maxTokens`, `temperature`, and `stop`: ```ts type-equiv /** Complete provider, model, and optional reasoning effort; `agent/model` fixes one for each step. */ @@ -222,6 +229,14 @@ interface ModelSelection { } ``` +```ts type-equiv +/** Decisions the driver fixed for one proposed or running step. */ +interface AgentStep { + /** Provider, model, and reasoning effort that every request attempt in the step uses. */ + readonly modelSelection: ModelSelection +} +``` + The inbox is the delivery vocabulary — two ordered pending-message lists the agent owns as a durable projection: ```ts type-equiv @@ -325,7 +340,7 @@ Pre-step decisions use the same identified `UserMessage` type as durable user-ro Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) -`agent/pre-step` receives one payload carrying the exclusive claimed batch (`messages`), the proposed step's coordinates (`turn`, `step`), its `modelSelection`, and the current turn's cancellation `signal`. The initial proposal runs inside an open turn before any step; a tool continuation may submit an empty claimed batch between steps: +`agent/pre-step` receives one payload carrying the exclusive claimed batch (`messages`), the proposed step's coordinates (`turn`, `step`), and the current turn's cancellation `signal`; `payload.agent.step` already holds the step's model selection. The initial proposal runs inside an open turn before any step; a tool continuation may submit an empty claimed batch between steps: It returns a `PreStepDecision`. Reject opens no step. Enter supplies the complete message batch appended after `step/start`; claimed messages omitted by the final decision remain removed, while input inserted after the claim stays pending: @@ -943,7 +958,7 @@ Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/s #### `agent/model` — waterfall -Choose the provider, model, and reasoning effort for one proposed step. `await next()` yields the selection the machine would use: agent options before this loop logs a request, the logged header afterwards, with an effort only when one was chosen for that exact model. Return a replacement to switch; an effort belongs to its model, so a replacement that changes the model names that model's effort or omits it for the provider default. The loop runs this once per proposed step, before context preparation and `agent/pre-step`. Prompt preparation, tool conditions, and every request attempt in the step use the result, so a later choice applies from the next step. Cancellation here opens no step. +Choose the provider, model, and reasoning effort for one proposed step. `await next()` yields the selection the machine would use: agent options before this loop logs a request, the logged header afterwards, with an effort only when one was chosen for that exact model. Return a replacement to switch; an effort belongs to its model, so a replacement that changes the model names that model's effort or omits it for the provider default. The loop runs this once per proposed step, after `agent/step-boundary` and before `agent/pre-step`, then publishes the result as `agent.step`. Pre-step listeners, tool conditions, prompt assembly, and every request attempt in the step use it, so a later choice applies from the next step. Cancellation here opens no step. ```ts cordis-catalog /** @@ -953,10 +968,11 @@ Choose the provider, model, and reasoning effort for one proposed step. `await n * effort only when one was chosen for that exact model. Return a * replacement to switch; an effort belongs to its model, so a replacement * that changes the model names that model's effort or omits it for the - * provider default. The loop runs this once per proposed step, before - * context preparation and `agent/pre-step`. Prompt preparation, tool - * conditions, and every request attempt in the step use the result, so a - * later choice applies from the next step. Cancellation here opens no step. + * provider default. The loop runs this once per proposed step, after + * `agent/step-boundary` and before `agent/pre-step`, then publishes the + * result as `agent.step`. Pre-step listeners, tool conditions, prompt + * assembly, and every request attempt in the step use it, so a later + * choice applies from the next step. Cancellation here opens no step. * @param payload.agent - the agent proposing the step. * @param payload.turn - the turn that will own the step. * @param payload.step - the step proposed by the loop. @@ -975,22 +991,22 @@ Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/s #### `agent/pre-step` — waterfall -Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages. +Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages. `payload.agent.step` already holds the model selection that `agent/model` chose for this step. ```ts cordis-catalog /** * Reject a proposed step or replace the messages that enter it. Calling - * `next()` preserves the current messages. + * `next()` preserves the current messages. `payload.agent.step` already + * holds the model selection that `agent/model` chose for this step. * @param payload.agent - the agent proposing the step. * @param payload.messages - messages removed from the inbox for this step. * @param payload.turn - the turn that will own the step. * @param payload.step - the step proposed by the loop. - * @param payload.modelSelection - the provider, model, and effort `agent/model` chose for this step. * @param payload.signal - the current turn's cancellation signal. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode waterfall */ -'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; modelSelection: ModelSelection; signal: AbortSignal }, next: () => Promise): Promise +'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise ``` Types: [Scoped](scope.md) · [UserMessage](session.md) @@ -1083,6 +1099,34 @@ Types: [Scoped](scope.md) Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts) + + +#### `agent/step-boundary` — serial + +A proposed step reached its boundary: the loop claimed its input and is about to choose its model. Listeners commit selections that take effect from this step, such as a pending plan-mode switch, so `agent/model`, `agent/pre-step`, tool conditions, and prompt assembly read one state. Runs once per proposed step; a throw fails the proposal, and cancellation opens no step. + +```ts cordis-catalog +/** + * A proposed step reached its boundary: the loop claimed its input and is + * about to choose its model. Listeners commit selections that take effect + * from this step, such as a pending plan-mode switch, so `agent/model`, + * `agent/pre-step`, tool conditions, and prompt assembly read one state. + * Runs once per proposed step; a throw fails the proposal, and + * cancellation opens no step. + * @param payload.agent - the agent proposing the step. + * @param payload.turn - the turn that will own the step. + * @param payload.step - the step proposed by the loop. + * @param payload.signal - the current turn's explicit abort signal. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode serial + */ +'agent/step-boundary'(this: Scoped, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }): Promise | void +``` + +Types: [Scoped](scope.md) + +Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts) + #### `agent/turn-stopping` — serial diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 59c3b51e19..e090274546 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -67,6 +67,13 @@ interface Agent { readonly id: SessionId /** Creation-time request options; `agent/model` may choose another provider, model, and effort for each step. */ readonly options: AgentOptions + /** + * The step the driver is preparing or running. The driver publishes it + * after `agent/model` and clears it when the step ends or its proposal is + * rejected, fails, or is cancelled, so it is undefined between steps and + * during `agent/model`. Work that outlives its step reads it at its start. + */ + readonly step?: AgentStep | undefined /** The live session this agent drives; its log is the durable source of truth. */ readonly session: Session /** Agent-owned access to durable pending work. */ @@ -212,7 +219,7 @@ interface AgentOptions { 每个步骤在 `agent/model` 之后都要求 `provider` 与 `model` 都存在;该 waterfall 可以补上或替换 options 中的提供方、模型与推理强度。显式 `reasoningEffort` 会为该模型的首个步骤选择提供初始值;确切模型解析会校验该值,省略时则允许填入适配器默认值。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。agent 作用域的 `deployment:persona-prefix` 提示词段落可以遮蔽全局默认 persona。 -步骤在提示词准备、`agent/pre-step`、工具条件以及每次请求尝试中都沿用其模型选择;`agent/request` 只能修改 `maxTokens`、`temperature`、`stop` 等采样控制参数: +驱动把该步骤的模型选择发布为 `agent.step`,供 `agent/pre-step`、工具条件、提示词组装以及每次请求尝试读取;`agent/request` 只能修改 `maxTokens`、`temperature`、`stop` 等采样控制参数: ```ts type-equiv /** Complete provider, model, and optional reasoning effort; `agent/model` fixes one for each step. */ @@ -226,6 +233,14 @@ interface ModelSelection { } ``` +```ts type-equiv +/** Decisions the driver fixed for one proposed or running step. */ +interface AgentStep { + /** Provider, model, and reasoning effort that every request attempt in the step uses. */ + readonly modelSelection: ModelSelection +} +``` + inbox 即投递词汇——agent 以持久投影形式拥有的两条有序待处理消息列表: ```ts type-equiv @@ -333,7 +348,7 @@ pre-step 决策使用与持久 user-role 输入相同、带标识的 `UserMessag 源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) -`agent/pre-step` 接收一个 payload,携带独占的已领取批次(`messages`)、拟进入步骤的坐标(`turn`、`step`)、其模型选择 `modelSelection` 与当前轮次的取消 `signal`。首次提案在已打开的轮次内、任何步骤开始前运行;工具 continuation 可以在步骤之间提交空的已领取批次: +`agent/pre-step` 接收一个 payload,携带独占的已领取批次(`messages`)、拟进入步骤的坐标(`turn`、`step`)与当前轮次的取消 `signal`;`payload.agent.step` 已持有该步骤的模型选择。首次提案在已打开的轮次内、任何步骤开始前运行;工具 continuation 可以在步骤之间提交空的已领取批次: 它返回 `PreStepDecision`。reject 不会打开步骤。enter 提供在 `step/start` 后追加的完整消息批次;最终决策省略的已领取消息保持已删除,而领取后插入的输入仍留待后续处理: @@ -955,7 +970,7 @@ Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/s #### `agent/model` — waterfall -Choose the provider, model, and reasoning effort for one proposed step. `await next()` yields the selection the machine would use: agent options before this loop logs a request, the logged header afterwards, with an effort only when one was chosen for that exact model. Return a replacement to switch; an effort belongs to its model, so a replacement that changes the model names that model's effort or omits it for the provider default. The loop runs this once per proposed step, before context preparation and `agent/pre-step`. Prompt preparation, tool conditions, and every request attempt in the step use the result, so a later choice applies from the next step. Cancellation here opens no step. +Choose the provider, model, and reasoning effort for one proposed step. `await next()` yields the selection the machine would use: agent options before this loop logs a request, the logged header afterwards, with an effort only when one was chosen for that exact model. Return a replacement to switch; an effort belongs to its model, so a replacement that changes the model names that model's effort or omits it for the provider default. The loop runs this once per proposed step, after `agent/step-boundary` and before `agent/pre-step`, then publishes the result as `agent.step`. Pre-step listeners, tool conditions, prompt assembly, and every request attempt in the step use it, so a later choice applies from the next step. Cancellation here opens no step. ```ts cordis-catalog /** @@ -965,10 +980,11 @@ Choose the provider, model, and reasoning effort for one proposed step. `await n * effort only when one was chosen for that exact model. Return a * replacement to switch; an effort belongs to its model, so a replacement * that changes the model names that model's effort or omits it for the - * provider default. The loop runs this once per proposed step, before - * context preparation and `agent/pre-step`. Prompt preparation, tool - * conditions, and every request attempt in the step use the result, so a - * later choice applies from the next step. Cancellation here opens no step. + * provider default. The loop runs this once per proposed step, after + * `agent/step-boundary` and before `agent/pre-step`, then publishes the + * result as `agent.step`. Pre-step listeners, tool conditions, prompt + * assembly, and every request attempt in the step use it, so a later + * choice applies from the next step. Cancellation here opens no step. * @param payload.agent - the agent proposing the step. * @param payload.turn - the turn that will own the step. * @param payload.step - the step proposed by the loop. @@ -987,22 +1003,22 @@ Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/s #### `agent/pre-step` — waterfall -Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages. +Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages. `payload.agent.step` already holds the model selection that `agent/model` chose for this step. ```ts cordis-catalog /** * Reject a proposed step or replace the messages that enter it. Calling - * `next()` preserves the current messages. + * `next()` preserves the current messages. `payload.agent.step` already + * holds the model selection that `agent/model` chose for this step. * @param payload.agent - the agent proposing the step. * @param payload.messages - messages removed from the inbox for this step. * @param payload.turn - the turn that will own the step. * @param payload.step - the step proposed by the loop. - * @param payload.modelSelection - the provider, model, and effort `agent/model` chose for this step. * @param payload.signal - the current turn's cancellation signal. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode waterfall */ -'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; modelSelection: ModelSelection; signal: AbortSignal }, next: () => Promise): Promise +'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise ``` Types: [Scoped](scope.zh.md) · [UserMessage](session.zh.md) @@ -1095,6 +1111,34 @@ Types: [Scoped](scope.zh.md) Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts) + + +#### `agent/step-boundary` — serial + +A proposed step reached its boundary: the loop claimed its input and is about to choose its model. Listeners commit selections that take effect from this step, such as a pending plan-mode switch, so `agent/model`, `agent/pre-step`, tool conditions, and prompt assembly read one state. Runs once per proposed step; a throw fails the proposal, and cancellation opens no step. + +```ts cordis-catalog +/** + * A proposed step reached its boundary: the loop claimed its input and is + * about to choose its model. Listeners commit selections that take effect + * from this step, such as a pending plan-mode switch, so `agent/model`, + * `agent/pre-step`, tool conditions, and prompt assembly read one state. + * Runs once per proposed step; a throw fails the proposal, and + * cancellation opens no step. + * @param payload.agent - the agent proposing the step. + * @param payload.turn - the turn that will own the step. + * @param payload.step - the step proposed by the loop. + * @param payload.signal - the current turn's explicit abort signal. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode serial + */ +'agent/step-boundary'(this: Scoped, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }): Promise | void +``` + +Types: [Scoped](scope.zh.md) + +Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts) + #### `agent/turn-stopping` — serial diff --git a/docs/subsystems/plan.i18n.yaml b/docs/subsystems/plan.i18n.yaml index e5ce0f0902..17f616e621 100644 --- a/docs/subsystems/plan.i18n.yaml +++ b/docs/subsystems/plan.i18n.yaml @@ -8,15 +8,15 @@ /plan-mode/logged-state-and-recovery: en: dd1e8ba31e6fd535 zh: a22dbccc60c73bc9 -/plan-mode/pending-selections-and-the-pre-step-append: - en: 8d7e5b7824921ed5 - zh: 910fad43593f29f4 +/plan-mode/pending-selections-and-the-step-boundary-append: + en: a62c24bf1e46c0ff + zh: bbe0ff492ee1be65 /plan-mode/configuration: en: ee04468005fae8e1 zh: 1347d554102951f2 /plan-mode/the-exit-tool-and-the-plan-command: - en: b5bbc440552d7ade - zh: 17fb1cfed1bc48ef + en: 6739f69a1b71b495 + zh: a538adbe4c85b529 /plan-mode/the-service: - en: 07c3eec12e0dc75c - zh: 183d73f6672e2c5a + en: c798c709c5cb6454 + zh: bac179e6fd51a98f diff --git a/docs/subsystems/plan.md b/docs/subsystems/plan.md index cc25cfa64b..d37cf01a7f 100644 --- a/docs/subsystems/plan.md +++ b/docs/subsystems/plan.md @@ -10,11 +10,11 @@ Source: [`packages/plan/plan-mode/src/index.ts`](../../packages/plan/plan-mode/s `plan/mode` (`{ active: boolean }`) is a log-only, whole-value-replace [session event](session.md): durable and replayable, never in the model transcript. The optionally registered `plan` unit folds committed mode, command settlement, and the mode recorded at the latest request header. `ctx.planMode` reads that state through `stateOf()`; the first dependent access fails if the registry, `plan` key, or `turnBoundary` key is absent. Clients receive only `{ active, pending }`; resume, fork, and compaction recover both from the log. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md). -## Pending selections and the pre-step append +## Pending selections and the step-boundary append -Because every session event is turn-enclosed, a user selection remains pending until the next accepted in-turn pre-step appends it before request derivation, in whichever turn that occurs. A selection never forces continuation, so one made after a turn's final accepted pre-step is appended in a later turn. `set(agent, active)` records the pending selection (a no-op when the target equals the logged-or-already-pending state), and `get(agent)` returns `{ active: boolean; pending?: boolean }`: the logged state used to assemble the current step plus the selected state waiting to be appended. +Because every session event is turn-enclosed, `set(agent, active)` appends a selection immediately only between turns. During an open turn the selection remains pending until the next step boundary appends it, in whichever turn that occurs. A selection never forces continuation, so one made after a turn's final step boundary is appended in a later turn. `set` is a no-op when the target equals the logged-or-already-pending state, and `get(agent)` returns `{ active: boolean; pending?: boolean }`: the logged state used by the current step plus the selected state waiting to be appended. -The only append point while an agent is running is a prepended `agent/pre-step` listener. It observes every proposed request step, including turn 1 step 1 and request-recovery retries, calls downstream listeners first, and appends only after they accept the step. Prompt admission happens before a turn and cannot append `plan/mode`, so a selection made at the prompt is appended by the first accepted in-turn pre-step of the turn it starts. An append failure cannot block the turn, and the selection remains pending for a later accepted in-turn pre-step. An appended user selection also records one plugin-sourced `user/message` notice, but only when the last logged request header described the other state, so the model is told exactly when its context changed and never redundantly. A selection made after a turn's final accepted pre-step remains process-local and is lost if the process exits before another accepted in-turn pre-step ([README limitation](../../packages/plan/plan-mode/README.md#known-limitations-and-deferred-work)). +The only append point while an agent is running is an `agent/step-boundary` listener. It runs once for every proposed step, including turn 1 step 1, after the step claims its input and before `agent/model`, so model choice, `agent/pre-step`, tool conditions, and plan guidance read the committed state; same-step request-recovery retries do not run it. An append failure fails the proposal and keeps the selection pending for a later boundary. An appended user selection also records one plugin-sourced `user/message` notice in the first admitted step, but only when the last logged request header described the other state, so the model is told exactly when its context changed and never redundantly. A selection made after a turn's final step boundary remains process-local and is lost if the process exits before another boundary ([README limitation](../../packages/plan/plan-mode/README.md#known-limitations-and-deferred-work)). ## Configuration @@ -30,13 +30,13 @@ A missing, blank, or non-string `section` and any unknown key fail at plugin loa ## The exit tool and the `/plan` command -[`exit_plan_mode`](../tool-catalog.md#deepseek-aidsh-plan-mode) stays registered while plan mode is inactive, so entering or leaving plan mode changes only the prompt section, never the request tool catalog; execution outside plan mode fails. In plan mode it requires a complete markdown plan starting with a `#` heading and presents it for review through the [user-questions seam](user-questions.md). Approval returns `{ approved: true }` and records a silent (non-narrated) pending exit that is appended at the next accepted in-turn pre-step. Plan guidance therefore remains active for the rest of the assistant's current tool batch, and the tool result itself reports the transition. Keep-planning is a failed call carrying the user's feedback, so the model revises and presents again. A dismissed review returns `{ approved: false }` and concludes the turn, so plan mode stays active until the user's next message. A missing interaction channel and a service reload during review also fail the call rather than silently leaving plan mode. +[`exit_plan_mode`](../tool-catalog.md#deepseek-aidsh-plan-mode) is admitted only while the committed plan state is active, so entering or leaving plan mode changes the request tool declarations and the plan guidance together; Native and PTC execution use the same admitted set, and a call outside plan mode returns `UNKNOWN_TOOL`. In plan mode it requires a complete markdown plan starting with a `#` heading and presents it for review through the [user-questions seam](user-questions.md). Approval returns `{ approved: true }` and records a silent (non-narrated) pending exit that is appended at the next step boundary. Plan guidance therefore remains active for the rest of the assistant's current tool batch, and the tool result itself reports the transition. Keep-planning is a failed call carrying the user's feedback, so the model revises and presents again. A dismissed review returns `{ approved: false }` and concludes the turn, so plan mode stays active until the user's next message. A missing interaction channel and a service reload during review also fail the call rather than silently leaving plan mode. When [`ctx.commands`](commands.md) is composed, the plugin registers `/plan [off|message]`: bare `/plan` selects plan mode, any other non-empty message selects it and then submits the text through `agent.steer()` so it becomes the next step's ordinary logged user message under plan guidance, and the exact argument `off` selects inactive, which also cancels a pending entry before it is appended and becomes visible to a request. ## The service -`ctx.planMode` owns the logged plan state, applies and narrates selected state at step start, and owns the `plan:policy` section, the `/plan` command, and the stable exit tool; `get`/`set` signatures are in the generated [service catalog](#ctxplanmode--planmodecontroller). +`ctx.planMode` owns the logged plan state, applies selected state at the step boundary and narrates it in the admitted step, and owns the `plan:policy` section, the `/plan` command, and the conditional exit tool; `get`/`set` signatures are in the generated [service catalog](#ctxplanmode--planmodecontroller). @@ -55,7 +55,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ```ts cordis-catalog /** * Read the logged plan state and any selected state awaiting the next - * accepted in-turn pre-step. + * step boundary. * * @param agent The agent to read. * @returns Current logged state plus a pending selection, when present. @@ -64,17 +64,17 @@ get(agent: Agent): { active: boolean; pending?: boolean } /** * Select whether plan mode should be active. Between turns the method - * appends the change immediately because no in-turn pre-step will run until + * appends the change immediately because no step boundary will run until * another prompt starts a turn. The open-turn fold is the idle signal: * agent status stays `running` through post-turn checkpointing, when no - * further in-turn pre-step runs. During an open turn the selection remains - * pending until the next accepted in-turn pre-step. Repeated selection of - * the current or already-pending state is a no-op. + * further step boundary runs. During an open turn the selection remains + * pending until the next step boundary. Repeated selection of the current + * or already-pending state is a no-op. * * @param agent The agent to switch. * @param active Whether plan mode should be active. * @returns what happened: `committed` (logged now), `queued` (awaiting the - * next accepted in-turn pre-step), `cancelled` (an opposite pending selection + * next step boundary), `cancelled` (an opposite pending selection * was cleared; the logged state already matches), or `noop` (already in that * state). */ diff --git a/docs/subsystems/plan.zh.md b/docs/subsystems/plan.zh.md index 24047c0e48..dd97ec0a0c 100644 --- a/docs/subsystems/plan.zh.md +++ b/docs/subsystems/plan.zh.md @@ -10,11 +10,11 @@ `plan/mode`(`{ active: boolean }`)是仅记日志、整值替换的[会话事件](session.zh.md):持久且可回放,绝不进入模型 transcript(文本记录)。可选注册的 `plan` 单元折叠已提交模式、命令结算结果和最近一次请求头记录的模式。`ctx.planMode` 通过 `stateOf()` 读取该状态;注册表、`plan` key 或 `turnBoundary` key 缺失时,第一次依赖它们的访问会失败。客户端只接收 `{ active, pending }`;恢复、fork 与压缩(compaction)都能从日志恢复两者。完整事件声明见[持久化日志事件目录](../persistence-catalog.zh.md)。 -## 待生效选择与 pre-step 追加 +## 待生效选择与步骤边界追加 -由于每个会话事件都位于轮次之内,用户选择会保持待生效状态,直到下一个被接受的轮内 pre-step 在派生请求之前追加该选择,无论该 pre-step 位于哪个轮次。选择不会强制续行,因此在某轮最后一个被接受的 pre-step 之后作出的选择会在之后的轮次追加。`set(agent, active)` 记录待生效选择(目标值与已记录或已在等待的状态相同时不做任何事),`get(agent)` 返回 `{ active: boolean; pending?: boolean }`:用于组装当前步骤的已记录状态,以及等待追加的已选状态。 +由于每个会话事件都位于轮次之内,`set(agent, active)` 只在轮次之间立即追加选择。轮次进行中,选择保持待生效状态,直到下一个步骤边界追加它,无论该边界位于哪个轮次。选择不会强制续行,因此在某轮最后一个步骤边界之后作出的选择会在之后的轮次追加。目标值与已记录或已在等待的状态相同时,`set` 不做任何事;`get(agent)` 返回 `{ active: boolean; pending?: boolean }`:当前步骤使用的已记录状态,以及等待追加的已选状态。 -agent 运行时,唯一的追加点是前置(prepend)注册的 `agent/pre-step` 监听器。它会观察每个候选请求步骤,包括第 1 轮第 1 步和请求恢复重试;它先调用下游监听器,只在下游接受该步骤后追加。提示词准入发生在轮次开启之前,无法追加 `plan/mode`,因此在提示词处作出的选择由它开启的轮次内第一个被接受的 pre-step 追加。追加失败不能阻塞轮次,且该选择会继续等待之后被接受的轮内 pre-step。追加用户选择时还会记录一条插件来源的 `user/message` 通知,但仅当最后记录的请求头描述的是另一种状态时才记录,因此模型恰好在上下文变化时收到通知,且绝不重复。在某轮最后一个被接受的 pre-step 之后作出的选择只存在于进程内;如果进程在另一个被接受的轮内 pre-step 之前退出,该选择会丢失([README 限制](../../packages/plan/plan-mode/README.zh.md#known-limitations-and-deferred-work))。 +agent 运行时,唯一的追加点是 `agent/step-boundary` 监听器。它对每个拟议步骤运行一次(包括第 1 轮第 1 步),时机在该步骤领取输入之后、`agent/model` 之前,因此模型选择、`agent/pre-step`、工具条件与计划指引读到的都是已提交状态;同一步骤内的请求恢复重试不会运行它。追加失败会使该提案失败,选择继续等待之后的步骤边界。追加用户选择时还会在第一个被接纳的步骤中记录一条插件来源的 `user/message` 通知,但仅当最后记录的请求头描述的是另一种状态时才记录,因此模型恰好在上下文变化时收到通知,且绝不重复。在某轮最后一个步骤边界之后作出的选择只存在于进程内;如果进程在下一个步骤边界之前退出,该选择会丢失([README 限制](../../packages/plan/plan-mode/README.zh.md#known-limitations-and-deferred-work))。 ## 配置 @@ -30,13 +30,13 @@ interface PlanModeConfig { ## 退出工具与 `/plan` 命令 -[`exit_plan_mode`](../tool-catalog.zh.md#deepseek-aidsh-plan-mode) 在计划模式未激活时仍保持注册,因此进入或离开计划模式只改变提示词段落,绝不改变请求的工具目录;在计划模式之外执行会失败。在计划模式中,它要求一份以 `#` 标题开头的完整 markdown 计划,并通过[用户交互 seam](user-questions.zh.md) 呈交评审。批准返回 `{ approved: true }`,并记录一个静默(不叙述)的待生效退出,由下一个被接受的轮内 pre-step 追加。因此,计划指引在 assistant 当前这批工具调用的剩余部分继续生效,而工具结果本身会报告这次转换。「继续规划」则是一次携带用户反馈的失败调用,模型据此修订并再次呈交。放弃评审返回 `{ approved: false }` 并结束当前轮次,计划模式保持激活,直到用户发出下一条消息。评审期间交互通道缺失或服务重载同样使调用失败,而不是静默离开计划模式。 +[`exit_plan_mode`](../tool-catalog.zh.md#deepseek-aidsh-plan-mode) 只在已提交的计划状态处于激活时被准入,因此进入或离开计划模式会同时改变请求的工具声明与计划指引;原生与 PTC 执行使用同一准入集合,在计划模式之外调用会返回 `UNKNOWN_TOOL`。在计划模式中,它要求一份以 `#` 标题开头的完整 markdown 计划,并通过[用户交互 seam](user-questions.zh.md) 呈交评审。批准返回 `{ approved: true }`,并记录一个静默(不叙述)的待生效退出,由下一个步骤边界追加。因此,计划指引在 assistant 当前这批工具调用的剩余部分继续生效,而工具结果本身会报告这次转换。「继续规划」则是一次携带用户反馈的失败调用,模型据此修订并再次呈交。放弃评审返回 `{ approved: false }` 并结束当前轮次,计划模式保持激活,直到用户发出下一条消息。评审期间交互通道缺失或服务重载同样使调用失败,而不是静默离开计划模式。 当 [`ctx.commands`](commands.zh.md) 被组合时,插件注册 `/plan [off|message]`:单独的 `/plan` 选择计划模式;任何其他非空消息先选择计划模式,再通过 `agent.steer()` 提交该文本,使其在计划指引下成为下一步骤的普通已记录用户消息;确切参数 `off` 选择未激活,这还会在待生效条目被追加并对请求可见之前将其取消。 ## 服务 -`ctx.planMode` 拥有已记录的计划状态,在步骤开始时应用并叙述选中的状态,还拥有 `plan:policy` 段落、`/plan` 命令和稳定注册的退出工具;`get`/`set` 签名见生成的[服务目录](#ctxplanmode--planmodecontroller)。 +`ctx.planMode` 拥有已记录的计划状态,在步骤边界应用选中的状态并在被接纳的步骤中叙述,还拥有 `plan:policy` 段落、`/plan` 命令和按条件准入的退出工具;`get`/`set` 签名见生成的[服务目录](#ctxplanmode--planmodecontroller)。 @@ -55,7 +55,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ```ts cordis-catalog /** * Read the logged plan state and any selected state awaiting the next - * accepted in-turn pre-step. + * step boundary. * * @param agent The agent to read. * @returns Current logged state plus a pending selection, when present. @@ -64,17 +64,17 @@ get(agent: Agent): { active: boolean; pending?: boolean } /** * Select whether plan mode should be active. Between turns the method - * appends the change immediately because no in-turn pre-step will run until + * appends the change immediately because no step boundary will run until * another prompt starts a turn. The open-turn fold is the idle signal: * agent status stays `running` through post-turn checkpointing, when no - * further in-turn pre-step runs. During an open turn the selection remains - * pending until the next accepted in-turn pre-step. Repeated selection of - * the current or already-pending state is a no-op. + * further step boundary runs. During an open turn the selection remains + * pending until the next step boundary. Repeated selection of the current + * or already-pending state is a no-op. * * @param agent The agent to switch. * @param active Whether plan mode should be active. * @returns what happened: `committed` (logged now), `queued` (awaiting the - * next accepted in-turn pre-step), `cancelled` (an opposite pending selection + * next step boundary), `cancelled` (an opposite pending selection * was cleared; the logged state already matches), or `noop` (already in that * state). */ diff --git a/docs/subsystems/session-reference.md b/docs/subsystems/session-reference.md index 517b395729..56db8ea11b 100644 --- a/docs/subsystems/session-reference.md +++ b/docs/subsystems/session-reference.md @@ -205,7 +205,7 @@ async listCandidates( agent: Agent, query: string = '', limit: number = this.con /** * Snapshot all references for one accepted direct message and return one aggregated durable context. - * Automatic budgets use the step's model selection, or agent options for direct preparation. + * Automatic budgets use the model of the agent's current step, or agent options outside a step. * Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation. * Truncated previews include omission facts and a full-snapshot spill locator, or an explicit unavailable notice. * Cancellation prevents context publication, including when storage completes after cancellation. @@ -213,13 +213,12 @@ async listCandidates( agent: Agent, query: string = '', limit: number = this.con * @param content - already host-normalized readable message content. * @param references - structured source sessions in mention order. * @param signal - optional cancellation boundary for the active turn. - * @param modelSelection - provider and model of the step admitting the message; omitted for direct preparation. * @returns detached content and optional referenced-session context. */ -async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, modelSelection?: ModelSelection, ): Promise +async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise ``` -Types: [Agent](core.md) · [ContentBlock](llm-streaming.md) · [ModelSelection](core.md) +Types: [Agent](core.md) · [ContentBlock](llm-streaming.md) Source: [`packages/context/session-reference/src/index.ts`](../../packages/context/session-reference/src/index.ts) diff --git a/docs/subsystems/session-reference.zh.md b/docs/subsystems/session-reference.zh.md index f76a9a7371..ccac4a0b7a 100644 --- a/docs/subsystems/session-reference.zh.md +++ b/docs/subsystems/session-reference.zh.md @@ -205,7 +205,7 @@ async listCandidates( agent: Agent, query: string = '', limit: number = this.con /** * Snapshot all references for one accepted direct message and return one aggregated durable context. - * Automatic budgets use the step's model selection, or agent options for direct preparation. + * Automatic budgets use the model of the agent's current step, or agent options outside a step. * Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation. * Truncated previews include omission facts and a full-snapshot spill locator, or an explicit unavailable notice. * Cancellation prevents context publication, including when storage completes after cancellation. @@ -213,13 +213,12 @@ async listCandidates( agent: Agent, query: string = '', limit: number = this.con * @param content - already host-normalized readable message content. * @param references - structured source sessions in mention order. * @param signal - optional cancellation boundary for the active turn. - * @param modelSelection - provider and model of the step admitting the message; omitted for direct preparation. * @returns detached content and optional referenced-session context. */ -async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, modelSelection?: ModelSelection, ): Promise +async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise ``` -Types: [Agent](core.zh.md) · [ContentBlock](llm-streaming.zh.md) · [ModelSelection](core.zh.md) +Types: [Agent](core.zh.md) · [ContentBlock](llm-streaming.zh.md) Source: [`packages/context/session-reference/src/index.ts`](../../packages/context/session-reference/src/index.ts) diff --git a/docs/subsystems/system-prompt.md b/docs/subsystems/system-prompt.md index 76d9fed53e..0eda0c8ae2 100644 --- a/docs/subsystems/system-prompt.md +++ b/docs/subsystems/system-prompt.md @@ -189,24 +189,20 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine * @param context - the same scope and current plugin-defined assembly fields. * @returns the accepted assembly with current runtime-context provider text. */ -refreshContext(assembly: T, context: AssembleContext = {}): T +refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly /** - * Prepare variables and runtime facts before accepting a step. No tool - * conditions or prompt sections are evaluated in this phase. - * @param context - calling scope and request cancellation. - * @returns prepared inputs after the preparation waterfall. + * Assemble global and scoped providers, detach tool parameters, apply + * canonical ordering, then run the presentation waterfall. Scoped sections + * and variables shadow globals. The waterfall's text is authoritative except + * that an effective complete section is restored afterwards as the sole + * prompt section, current suppression removes optional contexts, and + * required contexts keep their provider values; tool declarations stay + * provider-owned. + * @param context - the optional scope, step tool set, and plugin-defined assembly fields. + * @returns the post-waterfall assembly with ordered tool declarations. */ -async prepare(context: AssembleContext = {}): Promise - -/** - * Assemble prompt sections and tool declarations after request preparation. - * The presentation waterfall cannot replace provider-owned tools. - * @param context - calling scope, route-bound agent, and resolved tools. - * @param preparation - accepted variables and runtime facts; prepared here when omitted. - * @returns the complete assembly with ordered tool declarations. - */ -async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise +async assemble(context: AssembleContext = {}): Promise ``` Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts) @@ -256,25 +252,5 @@ Emitted when any prompt provider changes. This registry notification is unfilter 'system-prompt/change'(): void ``` -Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts) - - - -#### `system-prompt/prepare` — waterfall - -Prepare variables and runtime context before step admission. - -```ts cordis-catalog -/** - * Prepare variables and runtime context before step admission. - * @param preparation - registered variables and context contributions. - * @param context - calling scope and cancellation. - * @mode waterfall - */ -'system-prompt/prepare'(this: Scoped, preparation: PromptPreparation, context: AssembleContext, next: () => Promise): Promise -``` - -Types: [Scoped](scope.md) - Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts) diff --git a/docs/subsystems/system-prompt.zh.md b/docs/subsystems/system-prompt.zh.md index 12ce785206..3ea3ae2732 100644 --- a/docs/subsystems/system-prompt.zh.md +++ b/docs/subsystems/system-prompt.zh.md @@ -189,24 +189,20 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine * @param context - the same scope and current plugin-defined assembly fields. * @returns the accepted assembly with current runtime-context provider text. */ -refreshContext(assembly: T, context: AssembleContext = {}): T +refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly /** - * Prepare variables and runtime facts before accepting a step. No tool - * conditions or prompt sections are evaluated in this phase. - * @param context - calling scope and request cancellation. - * @returns prepared inputs after the preparation waterfall. + * Assemble global and scoped providers, detach tool parameters, apply + * canonical ordering, then run the presentation waterfall. Scoped sections + * and variables shadow globals. The waterfall's text is authoritative except + * that an effective complete section is restored afterwards as the sole + * prompt section, current suppression removes optional contexts, and + * required contexts keep their provider values; tool declarations stay + * provider-owned. + * @param context - the optional scope, step tool set, and plugin-defined assembly fields. + * @returns the post-waterfall assembly with ordered tool declarations. */ -async prepare(context: AssembleContext = {}): Promise - -/** - * Assemble prompt sections and tool declarations after request preparation. - * The presentation waterfall cannot replace provider-owned tools. - * @param context - calling scope, route-bound agent, and resolved tools. - * @param preparation - accepted variables and runtime facts; prepared here when omitted. - * @returns the complete assembly with ordered tool declarations. - */ -async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise +async assemble(context: AssembleContext = {}): Promise ``` Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts) @@ -256,25 +252,5 @@ Emitted when any prompt provider changes. This registry notification is unfilter 'system-prompt/change'(): void ``` -Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts) - - - -#### `system-prompt/prepare` — waterfall - -Prepare variables and runtime context before step admission. - -```ts cordis-catalog -/** - * Prepare variables and runtime context before step admission. - * @param preparation - registered variables and context contributions. - * @param context - calling scope and cancellation. - * @mode waterfall - */ -'system-prompt/prepare'(this: Scoped, preparation: PromptPreparation, context: AssembleContext, next: () => Promise): Promise -``` - -Types: [Scoped](scope.zh.md) - Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts) diff --git a/docs/subsystems/tools.md b/docs/subsystems/tools.md index 5350cc8f7d..7aff5f398b 100644 --- a/docs/subsystems/tools.md +++ b/docs/subsystems/tools.md @@ -569,7 +569,7 @@ get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | * Resolve conditions once for this step without changing shared registrations. * Cancellation or a rejected condition publishes no partial selection. Removed * registrations cannot be restored by a late condition result. - * @param context - preparing agent, step model selection, and cancellation. + * @param context - preparing agent and cancellation. * @returns the complete selection; the caller disposes it after its tool batch settles. */ async resolve(context: ToolActivationContext): Promise diff --git a/docs/subsystems/tools.zh.md b/docs/subsystems/tools.zh.md index 5d33028bde..39a93905fe 100644 --- a/docs/subsystems/tools.zh.md +++ b/docs/subsystems/tools.zh.md @@ -569,7 +569,7 @@ get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | * Resolve conditions once for this step without changing shared registrations. * Cancellation or a rejected condition publishes no partial selection. Removed * registrations cannot be restored by a late condition result. - * @param context - preparing agent, step model selection, and cancellation. + * @param context - preparing agent and cancellation. * @returns the complete selection; the caller disposes it after its tool batch settles. */ async resolve(context: ToolActivationContext): Promise diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.i18n.yaml index d97d05ce94..c2b1b153ac 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.i18n.yaml @@ -3,14 +3,14 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.md /: - en: b56fc42e23dfd970 - zh: cbe86c0b3af8eee7 + en: d8520be9ef9ace75 + zh: 7aaf5e3eff54b19d /update-step-model-selection-conditional-tools-and-prompt-hooks: en: 81e0b8c4355e86a3 zh: 2563f89226c11cfb /update-step-model-selection-conditional-tools-and-prompt-hooks/change: - en: b2a7c15a9971ce4a - zh: ef217bd430dfb48f + en: be511d31b6f02e4f + zh: b25075b04ef28234 /update-step-model-selection-conditional-tools-and-prompt-hooks/migration: - en: f818cf83d987ae4b - zh: 7d4f431cdb10dd91 + en: c849786a9f5e5100 + zh: 15aa7729b7802ed4 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.md index cff2857104..ab52391610 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.md @@ -1,6 +1,6 @@ --- kind: upgrade-guide -description: "Step provider, model, and reasoning-effort selection moves from `agent/request` to `agent/model`, and custom tool and prompt plugins must use step-owned tool admission." +description: "Each step's provider, model, and reasoning effort come from `agent/model` through `agent.step`, plan selections commit at `agent/step-boundary`, and custom tool and prompt plugins must use step-owned tool admission." --- # Update step model selection, conditional tools, and prompt hooks @@ -9,16 +9,16 @@ English | [中文](guide.zh.md) ## Change -`agent/model` returns each step's `ModelSelection` (provider, model, and optional reasoning effort) before `system-prompt/prepare` and `agent/pre-step`. Prompt preparation, `agent/pre-step` through its new `modelSelection` payload field, tool conditions, and every request attempt in the step use that selection. `agent/request` still runs for each attempt after prompt assembly and `step/start`, but it may change only sampling controls such as `maxTokens`, `temperature`, and `stop`; returning another provider, model, or reasoning effort fails the step. An agent without options provider and model must receive them from `agent/model`. Plugins that replaced the provider, model, or effort in `agent/request`, including on a retry, are affected. `installModelSelection` applies its selection through `agent/model`, and `ModelSelectionRef` no longer has `assembled`. +`agent/model` returns each step's `ModelSelection` (provider, model, and optional reasoning effort) after the new serial `agent/step-boundary` event and before `agent/pre-step`. The loop publishes it as `agent.step` until the step ends; `agent/pre-step` listeners, tool conditions, prompt sections, and every request attempt read it there. `agent/request` still runs for each attempt after prompt assembly and `step/start`, but it may change only sampling controls such as `maxTokens`, `temperature`, and `stop`; returning another provider, model, or reasoning effort fails the step. An agent without options provider and model must receive them from `agent/model`. Plugins that replaced the provider, model, or effort in `agent/request`, including on a retry, are affected. `installModelSelection` applies its selection through `agent/model`, and `ModelSelectionRef` no longer has `assembled`. -Runtime context preparation uses `system-prompt/prepare`; `system-prompt/assemble` receives presentation fields without a tool array. Tool declarations remain owned by registered providers. Conditional tools require a resolved step set for execution, including nested PTC calls. Replacing a registration does not authorize an outstanding call to run the replacement. +`system-prompt/assemble` runs once per step after `step/start` and receives presentation fields without a tool array; tool declarations remain owned by registered providers. Pre-step decisions no longer contain the runtime-context message: the snapshot is admitted after the step's accepted messages, and a changed runtime context alone no longer opens a step. Conditional tools require a resolved step set for execution, including nested PTC calls. Replacing a registration does not authorize an outstanding call to run the replacement. -`exit_plan_mode` is offered only while committed plan mode is active. A failed plan-state write stops step admission and keeps the pending selection. Existing Sessions require no format migration. +`exit_plan_mode` is offered only while committed plan mode is active. A plan selection made during a turn is committed at the next `agent/step-boundary`, even when `agent/pre-step` rejects that step; a failed write fails the proposal and keeps the selection pending. Existing Sessions require no format migration. ## Migration -1. Move provider, model, and reasoning-effort replacement from `agent/request` listeners to `agent/model` listeners that return `{ provider, model, reasoningEffort? }`; an omitted effort uses the model's provider default. Keep only sampling changes in `agent/request`. Read the selection from `payload.modelSelection` in `agent/pre-step` and from `AssembleContext.modelSelection` in prompt providers. -2. Declare tool conditions with `ctx.tools.register(definition, { when })`; the condition receives `{ agent, modelSelection, signal }`. -3. Move runtime-context preparation listeners to `system-prompt/prepare`. Keep presentation changes in `system-prompt/assemble` and stop replacing its tool list. -4. For programmatic conditional calls, resolve `{ agent, modelSelection, signal }` through `ctx.tools.resolve()`, pass the resulting `toolSet` to assembly and execution, and dispose it after the batch settles. AgentLoop does this once per step for model calls. Pass `context.toolSet` when tool guidance queries `ctx.tools.get()` or `schemas()`. -5. Remove `assembled` from `ModelSelectionRef` objects, and add `modelSelection` to `agent/pre-step` payloads that tests dispatch directly. Verify that inactive tools are absent from declarations, that a historical name returns `UNKNOWN_TOOL` without reaching the implementation, and that an `agent/request` listener changing the model or effort ends the turn with an error naming `agent/model`. +1. Move provider, model, and reasoning-effort replacement from `agent/request` listeners to `agent/model` listeners that return `{ provider, model, reasoningEffort? }`; an omitted effort uses the model's provider default. Keep only sampling changes in `agent/request`. Read the step's selection from `agent.step` in `agent/pre-step` listeners, tool conditions, and prompt providers. +2. Declare tool conditions with `ctx.tools.register(definition, { when })`; the condition receives `{ agent, signal }`. +3. Keep presentation changes in `system-prompt/assemble` and stop replacing its tool list. Do not look for the runtime-context message in `agent/pre-step` decisions. +4. For programmatic conditional calls, resolve `{ agent, signal }` through `ctx.tools.resolve()`, pass the resulting `toolSet` to assembly and execution, and dispose it after the batch settles. AgentLoop does this once per step for model calls. Pass `context.toolSet` when tool guidance queries `ctx.tools.get()` or `schemas()`. +5. Commit state that applies from the next step in an `agent/step-boundary` listener. Remove `assembled` from `ModelSelectionRef` objects, and set `step` on test Agents whose pre-step listeners read the model. Verify that inactive tools are absent from declarations, that a historical name returns `UNKNOWN_TOOL` without reaching the implementation, and that an `agent/request` listener changing the model or effort ends the turn with an error naming `agent/model`. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.zh.md index 01fa5cfec8..4c2b6bbba4 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/conditional-tools/guide.zh.md @@ -1,6 +1,6 @@ --- kind: upgrade-guide -description: "步骤的提供方、模型与推理强度选择从 `agent/request` 移到 `agent/model`,自定义工具和提示词插件必须使用步骤拥有的工具准入集合。" +description: "每个步骤的提供方、模型与推理强度由 `agent/model` 决定并通过 `agent.step` 读取,计划选择在 `agent/step-boundary` 提交,自定义工具和提示词插件必须使用步骤拥有的工具准入集合。" --- # 更新步骤模型选择、条件工具与提示词钩子 @@ -9,16 +9,16 @@ description: "步骤的提供方、模型与推理强度选择从 `agent/request ## 变更 -`agent/model` 在 `system-prompt/prepare` 与 `agent/pre-step` 之前返回每个步骤的 `ModelSelection`(提供方、模型与可选的推理强度)。提示词准备、`agent/pre-step`(通过新增的 payload 字段 `modelSelection`)、工具条件以及该步骤的每次请求尝试都使用这一选择。`agent/request` 仍在提示词组装与 `step/start` 之后为每次尝试运行,但只能修改 `maxTokens`、`temperature`、`stop` 等采样控制参数;返回其他提供方、模型或推理强度会使步骤失败。options 中没有提供方与模型的 agent 必须由 `agent/model` 提供。在 `agent/request` 中替换提供方、模型或推理强度的插件(包括在重试时替换的插件)会受到影响。`installModelSelection` 通过 `agent/model` 应用其选择,`ModelSelectionRef` 不再包含 `assembled`。 +`agent/model` 在新增的串行事件 `agent/step-boundary` 之后、`agent/pre-step` 之前返回每个步骤的 `ModelSelection`(提供方、模型与可选的推理强度)。循环把它发布为 `agent.step`,直到步骤结束;`agent/pre-step` 监听器、工具条件、提示词段落以及每次请求尝试都从这里读取。`agent/request` 仍在提示词组装与 `step/start` 之后为每次尝试运行,但只能修改 `maxTokens`、`temperature`、`stop` 等采样控制参数;返回其他提供方、模型或推理强度会使步骤失败。options 中没有提供方与模型的 agent 必须由 `agent/model` 提供。在 `agent/request` 中替换提供方、模型或推理强度的插件(包括在重试时替换的插件)会受到影响。`installModelSelection` 通过 `agent/model` 应用其选择,`ModelSelectionRef` 不再包含 `assembled`。 -运行时上下文准备使用 `system-prompt/prepare`;`system-prompt/assemble` 接收不含工具数组的展示字段。工具声明仍由已注册提供方持有。条件工具执行需要已解析的步骤集合,包括嵌套 PTC 调用。替换注册不会授权尚未执行的旧调用使用新实现。 +`system-prompt/assemble` 在 `step/start` 之后每个步骤运行一次,接收不含工具数组的展示字段;工具声明仍由已注册提供方持有。pre-step 决策中不再包含运行时上下文消息:快照在该步骤已接纳的消息之后提交,仅运行时上下文变化也不再打开步骤。条件工具执行需要已解析的步骤集合,包括嵌套 PTC 调用。替换注册不会授权尚未执行的旧调用使用新实现。 -`exit_plan_mode` 只在已提交的计划模式启用时提供。计划状态写入失败会停止步骤接纳并保留待提交选择。现有 Session 无需格式迁移。 +`exit_plan_mode` 只在已提交的计划模式启用时提供。轮次进行中作出的计划选择在下一个 `agent/step-boundary` 提交,即使 `agent/pre-step` 拒绝了该步骤;写入失败会使该提案失败并保留待提交选择。现有 Session 无需格式迁移。 ## 迁移 -1. 把 `agent/request` 监听器中的提供方、模型与推理强度替换移到返回 `{ provider, model, reasoningEffort? }` 的 `agent/model` 监听器;省略推理强度时使用该模型的提供方默认值。`agent/request` 只保留采样参数的修改。在 `agent/pre-step` 中从 `payload.modelSelection` 读取选择,在提示词提供方中从 `AssembleContext.modelSelection` 读取选择。 -2. 使用 `ctx.tools.register(definition, { when })` 声明工具条件;条件接收 `{ agent, modelSelection, signal }`。 -3. 将运行时上下文准备监听器迁移到 `system-prompt/prepare`。展示变更留在 `system-prompt/assemble`,停止替换它的工具列表。 -4. 程序化调用条件工具时,通过 `ctx.tools.resolve()` 解析 `{ agent, modelSelection, signal }`,将返回的 `toolSet` 传给组装与执行,并在批次结束后释放。AgentLoop 会为模型调用在每个步骤完成一次这些操作。工具指导查询 `ctx.tools.get()` 或 `schemas()` 时传入 `context.toolSet`。 -5. 从 `ModelSelectionRef` 对象中删除 `assembled`,并为测试直接派发的 `agent/pre-step` payload 补上 `modelSelection`。验证未启用工具不出现在声明中,使用历史名称调用会返回 `UNKNOWN_TOOL` 且不会进入实现,并验证修改模型或推理强度的 `agent/request` 监听器会让轮次以指向 `agent/model` 的错误结束。 +1. 把 `agent/request` 监听器中的提供方、模型与推理强度替换移到返回 `{ provider, model, reasoningEffort? }` 的 `agent/model` 监听器;省略推理强度时使用该模型的提供方默认值。`agent/request` 只保留采样参数的修改。在 `agent/pre-step` 监听器、工具条件与提示词提供方中从 `agent.step` 读取步骤的模型选择。 +2. 使用 `ctx.tools.register(definition, { when })` 声明工具条件;条件接收 `{ agent, signal }`。 +3. 展示变更留在 `system-prompt/assemble`,停止替换它的工具列表。不要在 `agent/pre-step` 的决策中查找运行时上下文消息。 +4. 程序化调用条件工具时,通过 `ctx.tools.resolve()` 解析 `{ agent, signal }`,将返回的 `toolSet` 传给组装与执行,并在批次结束后释放。AgentLoop 会为模型调用在每个步骤完成一次这些操作。工具指导查询 `ctx.tools.get()` 或 `schemas()` 时传入 `context.toolSet`。 +5. 从下一个步骤起生效的状态在 `agent/step-boundary` 监听器中提交。从 `ModelSelectionRef` 对象中删除 `assembled`;若测试中的 pre-step 监听器读取模型,为测试 Agent 设置 `step`。验证未启用工具不出现在声明中,使用历史名称调用会返回 `UNKNOWN_TOOL` 且不会进入实现,并验证修改模型或推理强度的 `agent/request` 监听器会让轮次以指向 `agent/model` 的错误结束。 diff --git a/packages/api/session-controller/tests/archived-session-gate.host.spec.ts b/packages/api/session-controller/tests/archived-session-gate.host.spec.ts index 3c9d962f9e..79bad41b7c 100644 --- a/packages/api/session-controller/tests/archived-session-gate.host.spec.ts +++ b/packages/api/session-controller/tests/archived-session-gate.host.spec.ts @@ -47,7 +47,7 @@ async function liveAgent(ctx: Context, session: Session): Promise { /** Propose one model step for the Agent; the innermost decision enters. */ const proposed = (ctx: Context, agent: Agent) => ctx.waterfall( 'agent/pre-step', - { agent, messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: new AbortController().signal }, + { agent, messages: [], turn: 1, step: 1, signal: new AbortController().signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) diff --git a/packages/client/ui-model-selection/README.i18n.yaml b/packages/client/ui-model-selection/README.i18n.yaml index f9e88d76d0..84def19bb7 100644 --- a/packages/client/ui-model-selection/README.i18n.yaml +++ b/packages/client/ui-model-selection/README.i18n.yaml @@ -33,8 +33,8 @@ en: 14acd4692e923480 zh: ff89aed852aa0da8 /deepseek-ai-dsh-client-ui-model-selection/model-experience: - en: 782f79ed910f426a - zh: 54a2e3e32a747544 + en: da858b97c79bf854 + zh: d0f7996f88679175 /deepseek-ai-dsh-client-ui-model-selection/model-experience/kv-cache-effect: en: 68e54c00412a40d1 zh: 0ac96f57fd194d96 diff --git a/packages/client/ui-model-selection/README.md b/packages/client/ui-model-selection/README.md index a32460cdb9..cc0b6725e8 100644 --- a/packages/client/ui-model-selection/README.md +++ b/packages/client/ui-model-selection/README.md @@ -86,7 +86,7 @@ Read these pages when the model surface is not enough. They move from the browse ## Model Experience -Indirectly, through the `session.selectModel` selection both entries submit: the Host snapshots the complete `ModelSelection` at the next prompt-assembly boundary and owns the model-visible effect, while a running step keeps its assembled selection. +Indirectly, through the `session.selectModel` selection both entries submit: the Host applies the complete `ModelSelection` when the next step chooses its model and owns the model-visible effect, while a running step keeps the selection it started with. #### KV Cache effect diff --git a/packages/client/ui-model-selection/README.zh.md b/packages/client/ui-model-selection/README.zh.md index 21b13ec2d5..a65645f690 100644 --- a/packages/client/ui-model-selection/README.zh.md +++ b/packages/client/ui-model-selection/README.zh.md @@ -86,7 +86,7 @@ composer 的 `ModelSelect` 与 `/model` 选项构建器共用[提供方排序](s ## 模型体验 -两个入口提交的 `session.selectModel` 选择会间接影响模型:Host 会在下一次提示词组装边界为完整的 `ModelSelection` 创建快照,并负责使其对模型生效;运行中的步骤则保留已组装的选择。 +两个入口提交的 `session.selectModel` 选择会间接影响模型:Host 会在下一个步骤选择模型时应用完整的 `ModelSelection`,并负责使其对模型生效;运行中的步骤则保留开始时的选择。 #### KV Cache 影响 diff --git a/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts b/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts index a1d1d28162..3736aa5c5e 100644 --- a/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts +++ b/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts @@ -1675,7 +1675,7 @@ describe('default one-shot summarizer', () => { describe('automatic listener and loader composition', () => { function preStep(ctx: Context, owner: Agent, signal = SIGNAL) { return agentEvents(ctx, owner).waterfall( - 'agent/pre-step', { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal }, + 'agent/pre-step', { messages: [], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) } diff --git a/packages/context/agent-instructions/tests/agent-instructions.spec.ts b/packages/context/agent-instructions/tests/agent-instructions.spec.ts index ef3da9000f..721866df73 100644 --- a/packages/context/agent-instructions/tests/agent-instructions.spec.ts +++ b/packages/context/agent-instructions/tests/agent-instructions.spec.ts @@ -310,7 +310,7 @@ async function agentInstructionsOf(agent: Agent): Promise { async function syncAgentInstructions(ctx: Context, agent: Agent): Promise { await agentEvents(ctx, agent).waterfall( - 'agent/pre-step', { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: testToolSignal }, + 'agent/pre-step', { messages: [], turn: 1, step: 1, signal: testToolSignal }, async () => ({ kind: 'enter' as const, messages: [] }), ) } @@ -345,13 +345,13 @@ async function composeBaselinePrefix(ctx: Context, agent: TestAgent): Promise Promise.resolve({ kind: 'enter' as const, messages: [] }), ) const claimed = claimInbox(agent, 'next-step') const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: claimed, turn: 1, step: 2, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages: claimed, turn: 1, step: 2, signal }, () => Promise.resolve({ kind: 'enter' as const, messages: claimed }), ) const entered = decision.kind === 'enter' ? decision.messages : [] @@ -1724,7 +1724,7 @@ describe('workspace context request injection', () => { const original = await stubAgent(root) await agentEvents(ctx, original).waterfall( 'agent/pre-step', - { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: AbortSignal.timeout(requestTimeoutMs) }, + { messages: [], turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) const inserted = original.inbox.nextStep[0] @@ -1738,7 +1738,7 @@ describe('workspace context request injection', () => { const claimed = claimInbox(resumed, 'next-step') const decision = await agentEvents(ctx, resumed).waterfall( 'agent/pre-step', - { messages: claimed, turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: AbortSignal.timeout(requestTimeoutMs) }, + { messages: claimed, turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) }, () => Promise.resolve({ kind: 'enter' as const, messages: claimed }), ) if (decision.kind !== 'enter') throw new Error('recovered baseline was rejected') @@ -1773,7 +1773,7 @@ describe('workspace context request injection', () => { const original = await stubAgent(root) await agentEvents(ctx, original).waterfall( 'agent/pre-step', - { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: AbortSignal.timeout(requestTimeoutMs) }, + { messages: [], turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) const stale = original.inbox.nextStep[0] @@ -1788,7 +1788,7 @@ describe('workspace context request injection', () => { const staleClaim = claimInbox(resumed, 'next-step') const staleDecision = await agentEvents(ctx, resumed).waterfall( 'agent/pre-step', - { messages: staleClaim, turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: AbortSignal.timeout(requestTimeoutMs) }, + { messages: staleClaim, turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) }, () => Promise.resolve({ kind: 'enter' as const, messages: staleClaim }), ) @@ -1831,7 +1831,7 @@ describe('workspace context request injection', () => { const original = await stubAgent(root) await agentEvents(originalCtx, original).waterfall( 'agent/pre-step', - { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: AbortSignal.timeout(requestTimeoutMs) }, + { messages: [], turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) const stale = original.inbox.nextStep[0] @@ -1846,7 +1846,7 @@ describe('workspace context request injection', () => { const claimed = claimInbox(resumed, 'next-step') const decision = await agentEvents(resumedCtx, resumed).waterfall( 'agent/pre-step', - { messages: claimed, turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: AbortSignal.timeout(requestTimeoutMs) }, + { messages: claimed, turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) }, () => Promise.resolve({ kind: 'enter' as const, messages: claimed }), ) @@ -1959,7 +1959,7 @@ describe('workspace context request injection', () => { const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [prompt], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: AbortSignal.timeout(requestTimeoutMs) }, + { messages: [prompt], turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) }, () => Promise.resolve(downstream), ) @@ -2551,7 +2551,7 @@ describe('workspace context request injection', () => { const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: AbortSignal.timeout(requestTimeoutMs) }, + { messages: [], turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) }, () => Promise.resolve(downstream), ) @@ -2673,7 +2673,7 @@ describe('workspace context request injection', () => { const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [prompt], turn: 2, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: AbortSignal.timeout(requestTimeoutMs) }, + { messages: [prompt], turn: 2, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) }, () => Promise.resolve({ kind: 'enter' as const, messages: [prompt] }), ) @@ -3137,7 +3137,7 @@ describe('workspace context request injection', () => { const reason = new Error('cancel prefix') const pending = agentEvents(ctx, await stubAgent(root)).waterfall( 'agent/pre-step', - { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: controller.signal }, + { messages: [], turn: 1, step: 1, signal: controller.signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) @@ -5770,7 +5770,7 @@ describe('workspace context inbox synchronization', () => { controller.abort(new Error('abort pre-step reconciliation')) await expect(agentEvents(ctx, agent).waterfall( - 'agent/pre-step', { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: controller.signal }, + 'agent/pre-step', { messages: [], turn: 1, step: 1, signal: controller.signal }, async () => ({ kind: 'enter' as const, messages: [] }), )).rejects.toThrow('abort pre-step reconciliation') @@ -5889,7 +5889,7 @@ describe('workspace context inbox synchronization', () => { const downstream = { kind: 'enter' as const, messages: claimed } const decision = await agentEvents(ctx, agent).waterfall( - 'agent/pre-step', { messages: claimed, turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: testToolSignal }, + 'agent/pre-step', { messages: claimed, turn: 1, step: 1, signal: testToolSignal }, async () => downstream, ) diff --git a/packages/context/session-reference/README.i18n.yaml b/packages/context/session-reference/README.i18n.yaml index 64a394e658..d54bf729b6 100644 --- a/packages/context/session-reference/README.i18n.yaml +++ b/packages/context/session-reference/README.i18n.yaml @@ -33,8 +33,8 @@ en: db349483e453caf7 zh: 70035c291e1efe84 /deepseek-ai-dsh-session-reference/understand-the-implementation/design-concept: - en: 7171fcac9b67e148 - zh: 0ccd05e624ca8ab9 + en: 1c50eff549fbb26f + zh: 8caefc6c2634ced0 /deepseek-ai-dsh-session-reference/understand-the-implementation/source-map: en: 7c7b413082507150 zh: 0229eb73d441b305 diff --git a/packages/context/session-reference/README.md b/packages/context/session-reference/README.md index 32a387f879..f5187910c9 100644 --- a/packages/context/session-reference/README.md +++ b/packages/context/session-reference/README.md @@ -70,7 +70,7 @@ Preparation reads each referenced session's current surface exactly once, when t The resolver discovers optional storage through `ctx.get("spillStore")` and saves only truncated references. Storage ownership is the target session; the source descriptor identifies the referenced session and label, without a fabricated tool call. Cancellation is checked after the asynchronous save and prevents publication even if an artifact was written. Artifact expiry remains the backend's existing policy. -The budget uses the step model that `agent/model` chose, which `agent/pre-step` carries as `modelSelection`. Direct `prepare` calls without a model selection use agent options; session headers do not select the budget model. +The budget uses the step model that `agent/model` chose, which the agent exposes as `agent.step` during `agent/pre-step`. Direct `prepare` calls outside a step use agent options; session headers do not select the budget model. ### Source map diff --git a/packages/context/session-reference/README.zh.md b/packages/context/session-reference/README.zh.md index 129db83741..f17baaebce 100644 --- a/packages/context/session-reference/README.zh.md +++ b/packages/context/session-reference/README.zh.md @@ -70,7 +70,7 @@ kind: "package-reference" 解析器通过 `ctx.get("spillStore")` 获取可选存储,只保存被截断的引用。存储归目标会话所有;来源信息标识被引用的源会话与标签,不伪造工具调用。异步保存后会检查取消,即使产物已写入,也会阻止发布。产物过期仍遵循后端既有策略。 -预算使用 `agent/model` 选定、由 `agent/pre-step` 以 `modelSelection` 携带的步骤模型。未传入模型选择的直接 `prepare` 调用使用 agent 选项;Session 请求头不决定预算模型。 +预算使用 `agent/model` 选定的步骤模型,`agent/pre-step` 期间 agent 通过 `agent.step` 公开它。步骤之外的直接 `prepare` 调用使用 agent 选项;Session 请求头不决定预算模型。 ### 源码地图 diff --git a/packages/context/session-reference/src/index.ts b/packages/context/session-reference/src/index.ts index 2d67bc8197..34ff2f7ba9 100644 --- a/packages/context/session-reference/src/index.ts +++ b/packages/context/session-reference/src/index.ts @@ -7,7 +7,7 @@ import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' -import type { Agent, ModelSelection, PreStepDecision } from '@deepseek-ai/dsh-agent' +import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import { createUserMessage, freezeMessage, LlmError } from '@deepseek-ai/dsh-llm' import type { ContentBlock, LlmResolvedModelInfo, UserMessage } from '@deepseek-ai/dsh-llm' @@ -122,12 +122,12 @@ export class SessionReferenceResolver extends TypertRemoteService { 'SESSION_REFERENCE_INVALID_CONFIG', ) } - ctx.on('agent/pre-step', async ({ agent, modelSelection, signal }, next): Promise => { + ctx.on('agent/pre-step', async ({ agent, signal }, next): Promise => { const decision = await next() if (decision.kind === 'reject') return decision return { ...decision, - messages: await this.prepareDirectMessages(agent, modelSelection, decision.messages, signal), + messages: await this.prepareDirectMessages(agent, decision.messages, signal), } }, { prepend: true }) } @@ -136,14 +136,12 @@ export class SessionReferenceResolver extends TypertRemoteService { * Replace canonical mentions in direct user messages and place each prepared * snapshot immediately after the message that cited it. * @param agent - agent entering the model step. - * @param modelSelection - provider and model of the step. * @param messages - messages accepted by downstream pre-step listeners. * @param signal - active turn cancellation. * @returns direct messages followed by their session-reference context in citation order. */ private async prepareDirectMessages( agent: Agent, - modelSelection: ModelSelection, messages: readonly UserMessage[], signal: AbortSignal, ): Promise { @@ -157,7 +155,7 @@ export class SessionReferenceResolver extends TypertRemoteService { return { type: 'text', text: parsed.text } }) if (references.length === 0) return [message] - const resolved = await this.prepare(agent, content, references, signal, modelSelection) + const resolved = await this.prepare(agent, content, references, signal) const direct = freezeMessage({ ...message, content: resolved.content }) /* v8 ignore if -- a parsed canonical mention always leaves one normalized reference */ if (resolved.additionalContext === undefined) { @@ -278,7 +276,7 @@ export class SessionReferenceResolver extends TypertRemoteService { /** * Snapshot all references for one accepted direct message and return one aggregated durable context. - * Automatic budgets use the step's model selection, or agent options for direct preparation. + * Automatic budgets use the model of the agent's current step, or agent options outside a step. * Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation. * Truncated previews include omission facts and a full-snapshot spill locator, or an explicit unavailable notice. * Cancellation prevents context publication, including when storage completes after cancellation. @@ -286,7 +284,6 @@ export class SessionReferenceResolver extends TypertRemoteService { * @param content - already host-normalized readable message content. * @param references - structured source sessions in mention order. * @param signal - optional cancellation boundary for the active turn. - * @param modelSelection - provider and model of the step admitting the message; omitted for direct preparation. * @returns detached content and optional referenced-session context. */ async prepare( @@ -294,13 +291,12 @@ export class SessionReferenceResolver extends TypertRemoteService { content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, - modelSelection?: ModelSelection, ): Promise { const acceptedContent = structuredClone(content) const inputs = normalizeReferences(agent.id, references, this.config.maxReferences) if (inputs.length === 0) return { content: acceptedContent } assertNotCancelled(signal) - const maxReferenceBytes = await this.referenceBudget(agent, modelSelection, signal) + const maxReferenceBytes = await this.referenceBudget(agent, signal) assertNotCancelled(signal) let prepared: PreparedSource[] try { @@ -351,14 +347,10 @@ export class SessionReferenceResolver extends TypertRemoteService { return { content: acceptedContent, additionalContext } } - private async referenceBudget( - agent: Agent, - modelSelection: ModelSelection | undefined, - signal: AbortSignal | undefined, - ): Promise { + private async referenceBudget(agent: Agent, signal: AbortSignal | undefined): Promise { if (this.config.maxReferenceBytes !== undefined) return this.config.maxReferenceBytes - // Options seed direct preparation; the step's model selection owns model-step preparation. - const { provider, model } = modelSelection ?? agent.options + // Options seed preparation outside a step; the step's model selection owns step preparation. + const { provider, model } = agent.step?.modelSelection ?? agent.options const llm = this.ctx.get('llm') if (provider === undefined || model === undefined || llm === undefined) return DEFAULT_MAX_REFERENCE_BYTES let info: LlmResolvedModelInfo diff --git a/packages/context/session-reference/tests/loader-composition.spec.ts b/packages/context/session-reference/tests/loader-composition.spec.ts index f387fdd1f7..3c8cfa6e15 100644 --- a/packages/context/session-reference/tests/loader-composition.spec.ts +++ b/packages/context/session-reference/tests/loader-composition.spec.ts @@ -77,7 +77,7 @@ describe('session-reference real Loader composition', () => { source: { kind: 'user' }, }) const decision = await agentEvents(ctx, agent).waterfall('agent/pre-step', { - messages: [direct], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: new AbortController().signal, + messages: [direct], turn: 1, step: 1, signal: new AbortController().signal, }, () => Promise.resolve({ kind: 'enter' as const, messages: [direct] })) expect(decision.kind).toBe('enter') if (decision.kind !== 'enter') throw new Error('Expected admitted reference') diff --git a/packages/context/session-reference/tests/session-reference.spec.ts b/packages/context/session-reference/tests/session-reference.spec.ts index 643c868caa..a69b71e998 100644 --- a/packages/context/session-reference/tests/session-reference.spec.ts +++ b/packages/context/session-reference/tests/session-reference.spec.ts @@ -466,7 +466,7 @@ describe('session reference spill outcomes', () => { }) const direct = createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: formatSessionReferenceMention({ sessionId: source.id }) }] }) const pending = agentEvents(ctx, fakeAgent(target)).waterfall('agent/pre-step', - { messages: [direct], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: controller.signal }, + { messages: [direct], turn: 1, step: 1, signal: controller.signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [direct] })) const rejected = expect(pending).rejects.toThrow(expectCode('SESSION_REFERENCE_CANCELLED')) await started.promise @@ -538,7 +538,7 @@ describe('model-relative reference budgets', () => { let modelSelection = { provider: 'selected', model: 'large' } const message = createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: formatSessionReferenceMention({ sessionId: source.id }) }] }) const signal = new AbortController().signal - const enter = () => agentEvents(ctx, agent).waterfall('agent/pre-step', { messages: [message], turn: 1, step: 1, modelSelection, signal }, + const enter = () => agentEvents(ctx, { ...agent, step: { modelSelection } }).waterfall('agent/pre-step', { messages: [message], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [message] })) const first = await enter() expect(first.kind).toBe('enter') @@ -621,7 +621,7 @@ describe('model-relative reference budgets', () => { expect(resolve).toHaveBeenLastCalledWith('seed', 'seed', undefined) const message = createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: formatSessionReferenceMention({ sessionId: source.id }) }] }) const seed = { kind: 'enter' as const, messages: [message] } - await expect(agentEvents(ctx, agent).waterfall('agent/pre-step', { messages: [message], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal: new AbortController().signal }, + await expect(agentEvents(ctx, agent).waterfall('agent/pre-step', { messages: [message], turn: 1, step: 1, signal: new AbortController().signal }, () => Promise.resolve(seed))).resolves.toBe(seed) }) @@ -877,7 +877,7 @@ describe('session reference discovery and preparation', () => { const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [direct, ordinary, plugin], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages: [direct, ordinary, plugin], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [direct, ordinary, plugin] }), ) @@ -913,14 +913,14 @@ describe('session reference discovery and preparation', () => { await expect(agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [malformed], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages: [malformed], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'reject' as const }), )).resolves.toEqual({ kind: 'reject' }) expect(readSurface).not.toHaveBeenCalled() await expect(agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [malformed], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages: [malformed], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [malformed] }), )).rejects.toThrow(/invalid session reference URI/) }) diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts index a50195aff8..92499007d1 100644 --- a/packages/context/time-context/tests/time-context.spec.ts +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -98,7 +98,7 @@ async function fire( }) const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [proposed], turn, step, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages: [proposed], turn, step, signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [proposed] }), ) if (decision.kind === 'enter') { diff --git a/packages/context/tmux-context/tests/tmux-context.spec.ts b/packages/context/tmux-context/tests/tmux-context.spec.ts index 50990452e3..ae15f20a46 100644 --- a/packages/context/tmux-context/tests/tmux-context.spec.ts +++ b/packages/context/tmux-context/tests/tmux-context.spec.ts @@ -151,7 +151,7 @@ async function fire( ): Promise { const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [], turn, step, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages: [], turn, step, signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) if (decision.kind === 'enter') { diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 62ace68991..43cddfd1dd 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -45,8 +45,8 @@ en: 3c5721bc00a0bbf4 zh: 711f832343ffbb75 /deepseek-ai-dsh-agent-loop/understand-the-implementation/turn-and-step-flow: - en: 0063a07435bc4028 - zh: c873fd82c927a7c2 + en: db754159572b2c69 + zh: c9358e9df71ea589 /deepseek-ai-dsh-agent-loop/understand-the-implementation/failure-and-cancellation: en: eea56903c4ff82f5 zh: c0d5abb5767780ed diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index c42100eae7..1db9b2c0c6 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -117,7 +117,7 @@ The loop is the production acquisition point for session write handles. When `ct ### Turn and step flow -The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`. `AgentLoop` registers the standard `inbox` projection for its service lifetime, so cold reads work before any Agent exists and after all Agents unload. Its package-internal `ReactLoopInbox` uses that shared projection for structural commands and loop-only claims. At a turn boundary it opens the durable turn, then atomically claims pending next-step input plus one queued prompt; between steps it claims only next-step input. The driver chooses the step's provider, model, and reasoning effort through `agent/model`, prepares variables and runtime context with that selection, then runs `agent/pre-step`; its fallback projects that context. A rejected decision or empty first batch opens no step. Cancellation during model selection opens no step. After acceptance, `step/start` precedes asynchronous tool activation and final prompt assembly, both once per step. Each attempt then runs the `agent/request` waterfall and `prepareCall()` with the step's model selection; a listener that returns another provider, model, or reasoning effort fails the step, and cancellation during either async phase commits neither the pending system prompt nor accepted users. For every attempt, the loop then synchronously reconciles the rendered prompt against the surviving `system/message` nodes using the prepared call capability, refreshes registered runtime facts, appends the accepted `user/message` batch only on the first attempt with its owned context candidate replaced in place, and restores removed runtime context, logs the header and context as needed, and derives and freezes the request before streaming through that bound prepared call. Retries reuse the step's model selection, tool set, and rendered assembly without repeating model selection, tool activation, assembly, `agent/pre-step`, or user admission. The step-owned tool set is closed after the step's tool batch settles. Reconciliation sees pre-step and retry compaction; a broken series consolidates the prompt at the head rather than appending an update after already-committed users. The request is `header.config`, `deriveMessages()`, and `header.tools`; it carries no `system` field. Each model attempt emits one process-local `start`, emits every `chunk` only after the matching durable assistant-frame settlement, and emits exactly one terminal `end`; final assembly or message-append failure settles it as `aborted`, while `committed` follows the durable `assistant/message`. Each successful model call appends one message anchor, and a cancelled stream appends an `interrupted: true` anchor with the delivered prefix so the next request contains what the user saw. Within a step, exclusive calls form barriers and parallel-safe calls use the bounded rolling pool; policy, durable results... (line truncated to 2000 chars) +The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`. `AgentLoop` registers the standard `inbox` projection for its service lifetime, so cold reads work before any Agent exists and after all Agents unload. Its package-internal `ReactLoopInbox` uses that shared projection for structural commands and loop-only claims. At a turn boundary it opens the durable turn, then atomically claims pending next-step input plus one queued prompt; between steps it claims only next-step input. The driver dispatches `agent/step-boundary`, chooses the step's provider, model, and reasoning effort through `agent/model`, publishes them as `agent.step`, then runs `agent/pre-step`, whose fallback enters the claimed batch unchanged. A rejected decision or empty first batch opens no step; a changed runtime context alone does not open one. Cancellation at the boundary or during model selection opens no step. `agent.step` is cleared when the step ends or its proposal is rejected, fails, or is cancelled. After acceptance, `step/start` precedes asynchronous tool activation and one prompt assembly of sections, variables, runtime context, and tool declarations, both once per step. Each attempt then runs the `agent/request` waterfall and `prepareCall()` with the step's model selection; a listener that returns another provider, model, or reasoning effort fails the step, and cancellation during either async phase commits neither the pending system prompt nor accepted users. For every attempt, the loop then synchronously reconciles the rendered prompt against the surviving `system/message` nodes using the prepared call capability, refreshes registered runtime facts, appends the accepted `user/message` batch only on the first attempt, then appends the runtime-context snapshot when it changed or was removed, logs the header and context as needed, and derives and freezes the request before streaming through that bound prepared call. Retries reuse the step's model selection, tool set, and rendered assembly without repeating model selection, tool activation, assembly, `agent/pre-step`, or user admission. The step-owned tool set is closed after the step's tool batch settles. Reconciliation sees pre-step and retry compaction; a broken series consolidates the prompt at the head rather than appending an update after already-committed users. The request is `header.config`, `deriveMessages()`, and `header.tools`; it carries no `system` field. Each model attempt emits one process-local `start`, emits every `chunk` only after the matching durable assistant-frame settlement, and emits exactly one terminal `end`; final assembly or message-append failure settles it as `aborted`, while `committed` follows the durable `assistant/message`. Each successful model call appends one message anchor, and a cancelled stream appends an `interrupted: true` anchor with the delivered prefix so the next request contains what the user saw. Within a step, exclusive calls form barriers and parallel-safe calls use the bounded rolling pool; policy, durable results... (line truncated to 2000 chars) Prompt admission uses the actual `prepareCall()` result, not the preceding `request/context`. With no system node, even an empty prompt is appended (reserving node 0 without a wire message). On an incapable route or at a new request series, a non-empty rendering is consolidated at the first system node: each non-empty later system node receives a logged per-node empty replacement, then the head is rewritten if needed. Dormant empty tails need no replacement and do not determine the effective text. Consolidation applies even when the latest effective text is unchanged. On a continuing `in-history` series, an unchanged effective prompt produces no event and a non-empty change appends. An empty rendering clears every non-empty later system node through a logged per-node empty replacement, then empties the head if needed, regardless of route or series state. No older instructions remain model-visible. An empty head with no active later system node means no prompt; repeated clears and resume leave it empty. A restored non-empty prompt follows the same route/series rule: a continuing capable route may append it, while an incapable route or new series refills the head. A step starts a series when the pre-step decision declares `startsRequestSeries` or `session.surface.contentGeneration` changed since attachment or the last request (a replacement or image-offload decision). Visible tool-schema changes force prompt consolidation only when the prepared route lacks `toolUpdate`. Routes supporting both tool and system-prompt updates can append a changed prompt and activate added tools without rewriting earlier messages. If another admission condition replaces a system node, the request starts a new series with current tool declarations. Resume and a provider or model swap alone continue the series; the prepared route still governs admission. Per-node empty replacements preserve intervening history without a surface delete operation. diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index 3427ef23c7..03b5f3b15f 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -117,7 +117,7 @@ const handle = await ctx.agents.create({ ### 轮次与步骤流程 -驱动器在其整个生命周期内拥有一个 agent,并在 `ctx.agents.withInitiator(agent, ...)` 内运行。`AgentLoop` 在服务生命周期内注册标准 `inbox` 投影,因此冷读取在没有 Agent 和所有 Agent 卸载后都可用。其包内部 `ReactLoopInbox` 使用该共享投影执行结构化命令与仅供 loop 使用的领取操作。在轮次边界,它先打开持久轮次,再原子领取待处理的 next-step 输入与一条排队提示词;在步骤之间则只领取 next-step 输入。驱动器通过 `agent/model` 选定步骤的提供方、模型与推理强度,按该选择准备变量和运行时上下文,然后运行 `agent/pre-step`;fallback 投影该上下文。被拒绝的决定或空的首批输入不打开步骤。模型选择期间取消不会打开步骤。接纳后,`step/start` 先于异步工具激活与最终提示词组装,两者每个步骤各执行一次。每次尝试随后按步骤的模型选择运行 `agent/request` waterfall 与 `prepareCall()`;监听器若返回其他提供方、模型或推理强度,步骤会失败,而在任一异步阶段取消都不会提交待定的系统提示词或已接纳用户消息。每次尝试时,循环随后依据已准备调用的能力,同步将渲染后的提示词与存活的 `system/message` 节点协调一致、刷新已注册运行时事实、仅在首次尝试追加已接纳的 `user/message` 批次并原位替换循环自有上下文候选、恢复被移除的运行时上下文、按需记录 header 与 context,再派生并冻结请求,通过该绑定的已准备调用发起流式请求。重试复用该步骤的模型选择、工具集合与已渲染组装结果,不重复模型选择、工具激活、组装、`agent/pre-step` 或用户消息接纳。步骤拥有的工具集合在该步骤的工具批次结束后关闭。协调过程可见 pre-step 与重试中的压缩;序列中断时将提示词归并到头部,而非在已提交用户消息之后追加更新。请求由 `header.config`、`deriveMessages()` 与 `header.tools` 构成,不携带 `system` 字段。每次模型尝试会发出一个进程本地 `start`,仅在匹配的持久 assistant-frame 结算之后发出各个 `chunk`,并恰好发出一个终态 `end`;最终组装或消息追加失败时以 `aborted` 结算,`committed` 则出现在持久 `assistant/message` 之后。每次成功的模型调用都恰好追加一个 message 锚点,被取消的流则追加带 `interrupted: true` 的锚点并携带已交付前缀,使下一次请求包含用户看到的内容。在步骤内,独占调用形成屏障,并行安全调用使用有界滚动池;策略、持久结果与结果上下文保持模型顺序。 +驱动器在其整个生命周期内拥有一个 agent,并在 `ctx.agents.withInitiator(agent, ...)` 内运行。`AgentLoop` 在服务生命周期内注册标准 `inbox` 投影,因此冷读取在没有 Agent 和所有 Agent 卸载后都可用。其包内部 `ReactLoopInbox` 使用该共享投影执行结构化命令与仅供 loop 使用的领取操作。在轮次边界,它先打开持久轮次,再原子领取待处理的 next-step 输入与一条排队提示词;在步骤之间则只领取 next-step 输入。驱动器派发 `agent/step-boundary`,通过 `agent/model` 选定步骤的提供方、模型与推理强度并发布为 `agent.step`,然后运行 `agent/pre-step`,其 fallback 原样接纳已领取批次。被拒绝的决定或空的首批输入不打开步骤;仅运行时上下文变化也不会打开步骤。在步骤边界或模型选择期间取消不会打开步骤。步骤结束或提案被拒绝、失败、取消时,`agent.step` 会被清除。接纳后,`step/start` 先于异步工具激活与一次提示词组装(段落、变量、运行时上下文与工具声明),两者每个步骤各执行一次。每次尝试随后按步骤的模型选择运行 `agent/request` waterfall 与 `prepareCall()`;监听器若返回其他提供方、模型或推理强度,步骤会失败,而在任一异步阶段取消都不会提交待定的系统提示词或已接纳用户消息。每次尝试时,循环随后依据已准备调用的能力,同步将渲染后的提示词与存活的 `system/message` 节点协调一致、刷新已注册运行时事实、仅在首次尝试追加已接纳的 `user/message` 批次,随后在运行时上下文变化或被移除时追加其快照、按需记录 header 与 context,再派生并冻结请求,通过该绑定的已准备调用发起流式请求。重试复用该步骤的模型选择、工具集合与已渲染组装结果,不重复模型选择、工具激活、组装、`agent/pre-step` 或用户消息接纳。步骤拥有的工具集合在该步骤的工具批次结束后关闭。协调过程可见 pre-step 与重试中的压缩;序列中断时将提示词归并到头部,而非在已提交用户消息之后追加更新。请求由 `header.config`、`deriveMessages()` 与 `header.tools` 构成,不携带 `system` 字段。每次模型尝试会发出一个进程本地 `start`,仅在匹配的持久 assistant-frame 结算之后发出各个 `chunk`,并恰好发出一个终态 `end`;最终组装或消息追加失败时以 `aborted` 结算,`committed` 则出现在持久 `assistant/message` 之后。每次成功的模型调用都恰好追加一个 message 锚点,被取消的流则追加带 `interrupted: true` 的锚点并携带已交付前缀,使下一次请求包含用户看到的内容。在步骤内,独占调用形成屏障,并行安全调用使用有界滚动池;策略、持久结果与结果上下文保持模型顺序。 提示词准入依据实际的 `prepareCall()` 结果,而非先前的 `request/context`。没有系统节点时,即使提示词为空也追加(预留第 0 号节点,但不产生协议消息)。在不具备能力的路由上或新请求序列开始时,非空渲染文本归并到首个系统节点:每个非空的后续系统节点分别收到有日志记录的空内容替换,随后按需重写头节点。未生效的空尾节点无需替换,也不决定有效文本。即使最新有效文本未变,也执行归并。延续中的 `in-history` 序列在有效提示词不变时不产生事件,非空变更则追加。无论路由或序列状态如何,空渲染文本都会通过有日志记录的逐节点空内容替换清除每个非空的后续系统节点,再按需清空头节点。模型不会继续看到旧指令。空头节点且没有生效的后续系统节点表示没有提示词;重复清除与恢复会话都保持为空。重新提供的非空提示词遵循同一路由/序列规则:延续中的具备能力路由可以追加它,不具备能力的路由或新序列则重新填充头节点。以下情况开启序列:pre-step 决定声明 `startsRequestSeries`,或 `session.surface.contentGeneration` 自附接或上次请求以来发生变化(替换或图片省略决定)。仅当已准备路由未声明 `toolUpdate` 时,可见工具 schema 变化才强制归并提示词。同时支持工具和系统提示词更新的路由可以追加变更后的提示词并激活新增工具,无需改写此前消息。如果其他准入条件替换系统节点,请求会以当前工具声明开启新序列。恢复与单纯的提供方或模型切换都延续序列;准入仍由已准备的路由决定。逐节点的空内容替换保留其间历史,无需 surface 删除操作。 diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 2eac31c365..0592cdf9b6 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -10,6 +10,7 @@ import type { AgentEventDispatch, AgentOptions, AgentStatus, + AgentStep, CancelOptions, InboxTarget, ModelSelection, @@ -31,7 +32,7 @@ import { createScope } from '@deepseek-ai/dsh-scope' import type { EpochHeader, RequestContext, Session, SessionId, SessionSeq, TurnEndReason, UserMessage } from '@deepseek-ai/dsh-session' import { canonicalHeader, headerEquals, ToolCallRecovery } from '@deepseek-ai/dsh-session' import { joinContextSections, renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt' -import type { PromptAssembly, PromptPreparation } from '@deepseek-ai/dsh-system-prompt' +import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt' import type {} from '@deepseek-ai/dsh-session-projection' import type { Context } from '@deepseek-ai/cordis' import { ReactLoopInbox } from './inbox.ts' @@ -65,7 +66,6 @@ type PreparedStep = messages: UserMessage[] startsRequestSeries?: true modelSelection: ModelSelection - preparation: PromptPreparation } /** Name a provider, model, and reasoning effort in diagnostics. */ @@ -122,6 +122,8 @@ export class ReactLoopAgent implements Agent { /** Whether this loop instance has appended its initial/resume request anchor. */ private requestHeaderLogged = false + /** Decisions published for the step being proposed or run; cleared between steps. */ + private currentStep: AgentStep | undefined /** Surface generation at attachment or the preceding built request. */ private requestSurfaceGeneration: number private readonly runtimeContext: RuntimeContextProjection @@ -154,6 +156,10 @@ export class ReactLoopAgent implements Agent { return this.phase.kind === 'idle' || this.phase.kind === 'maintenance' ? 'idle' : 'running' } + get step(): AgentStep | undefined { + return this.currentStep + } + /** Commit a phase and publish its externally visible status transition. */ private setPhase(next: Phase): void { const previousStatus = this.status @@ -282,23 +288,17 @@ export class ReactLoopAgent implements Agent { if (this.phase.kind !== 'running') throw new Error(`agent "${this.id}": pre-step outside running phase`) const signal = this.phase.abort.signal const claimed = this.inbox.claim(target, position.turn) - const modelSelection = await this.chooseModel(position.turn, position.step, signal) - const preparation = await this.loopCtx.systemPrompt.prepare({ ...assembleContextFor(this, signal), modelSelection }) + await this.dispatch.serial('agent/step-boundary', { ...position, signal }) signal.throwIfAborted() + const modelSelection = await this.chooseModel(position.turn, position.step, signal) + this.currentStep = Object.freeze({ modelSelection }) const decision = await this.dispatch.waterfall( - 'agent/pre-step', { messages: claimed, ...position, modelSelection, signal }, - (): Promise => { - const sections = renderContextSections(preparation) - const context = this.runtimeContext.project(joinContextSections(sections), sections) - return Promise.resolve({ - kind: 'enter', - messages: context === undefined ? claimed : [...claimed, context], - }) - }, + 'agent/pre-step', { messages: claimed, ...position, signal }, + () => Promise.resolve({ kind: 'enter', messages: claimed }), ) signal.throwIfAborted() if (decision.kind === 'reject') return decision - return { ...decision, modelSelection, preparation } + return { ...decision, modelSelection } } /** Choose the provider, model, and effort that the proposed step keeps for every request attempt. */ @@ -348,47 +348,51 @@ export class ReactLoopAgent implements Agent { while (true) { signal.throwIfAborted() const step = phase.step + 1 - const decision = await this.preStep(target, { turn, step }) - if (decision.kind === 'reject') { - turnEnds = { kind: 'blocked' } - return false - } - if (turnEnds && decision.messages.length === 0) break - // A removed waking message or an enter decision rewritten to empty - // still owns the initial turn boundary, but it spends no model call. - if (phase.step === 0 && decision.messages.length === 0) { - turnEnds = { kind: 'completed' } - return false - } - signal.throwIfAborted() - this.session.append('step/start', { turn, step }) - phase.step = step - const toolRecovery = new ToolCallRecovery() - const stopRecovery = this.ctx.on('session/event', (session, event) => { - if (session === this.session) toolRecovery.observe(event) - }) try { - // max-tokens is sticky: once any step hits the ceiling, later steps - // that complete normally must not downgrade the turn outcome. - const stepEnd = await this.step(decision) - // max-tokens stays sticky: a later completed step must not - // downgrade the turn outcome. - if (turnEnds === null || turnEnds.kind !== 'max-tokens') turnEnds = stepEnd - } catch (error: unknown) { + const decision = await this.preStep(target, { turn, step }) + if (decision.kind === 'reject') { + turnEnds = { kind: 'blocked' } + return false + } + if (turnEnds && decision.messages.length === 0) break + // A removed waking message or an enter decision rewritten to empty + // still owns the initial turn boundary, but it spends no model call. + if (phase.step === 0 && decision.messages.length === 0) { + turnEnds = { kind: 'completed' } + return false + } + signal.throwIfAborted() + this.session.append('step/start', { turn, step }) + phase.step = step + const toolRecovery = new ToolCallRecovery() + const stopRecovery = this.ctx.on('session/event', (session, event) => { + if (session === this.session) toolRecovery.observe(event) + }) try { - for (const event of toolRecovery.results()) { - this.session.append('tool/result', event.data, { - surfaceOp: 'append', - ...event.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: event.sourceEventSeqs }, - }) + // max-tokens is sticky: once any step hits the ceiling, later steps + // that complete normally must not downgrade the turn outcome. + const stepEnd = await this.runStep(decision) + // max-tokens stays sticky: a later completed step must not + // downgrade the turn outcome. + if (turnEnds === null || turnEnds.kind !== 'max-tokens') turnEnds = stepEnd + } catch (error: unknown) { + try { + for (const event of toolRecovery.results()) { + this.session.append('tool/result', event.data, { + surfaceOp: 'append', + ...event.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: event.sourceEventSeqs }, + }) + } + } catch (recoveryError: unknown) { + throw new AggregateError([error, recoveryError], 'Step failed and its pending tool results could not be recorded', { cause: error }) } - } catch (recoveryError: unknown) { - throw new AggregateError([error, recoveryError], 'Step failed and its pending tool results could not be recorded', { cause: error }) + throw error + } finally { + stopRecovery() + this.session.append('step/end', { turn, step }) } - throw error } finally { - stopRecovery() - this.session.append('step/end', { turn, step }) + this.currentStep = undefined } signal.throwIfAborted() if (turnEnds && this.inbox.nextStep.length === 0) { @@ -428,24 +432,21 @@ export class ReactLoopAgent implements Agent { return true } - private async step(decision: Extract): Promise { + private async runStep(decision: Extract): Promise { /* v8 ignore next -- private callers establish the running phase before executing a step */ if (this.phase.kind !== 'running') throw new Error(`agent "${this.id}": step outside running phase`) const { turn, step, abort: { signal } } = this.phase signal.throwIfAborted() - const { modelSelection } = decision - const toolSet = await this.loopCtx.tools.resolve({ agent: this, modelSelection, signal }) + const toolSet = await this.loopCtx.tools.resolve({ agent: this, signal }) try { - const assembly = await this.loopCtx.systemPrompt.assemble( - { ...assembleContextFor(this, signal), modelSelection, toolSet }, - decision.preparation, - ) + const assembleContext = { ...assembleContextFor(this, signal), toolSet } + const assembly = await this.loopCtx.systemPrompt.assemble(assembleContext) const renderedPrompt = renderPrompt(assembly) let firstAttempt = true while (true) { - const { config, preparedCall } = await this.prepareRequest(turn, step, modelSelection, signal) - const currentContext = this.loopCtx.systemPrompt.refreshContext(assembly, { ...assembleContextFor(this, signal), modelSelection }) + const { config, preparedCall } = await this.prepareRequest(turn, step, decision.modelSelection, signal) + const currentContext = this.loopCtx.systemPrompt.refreshContext(assembly, assembleContext) const sections = renderContextSections(currentContext) const context = this.runtimeContext.project(joinContextSections(sections), sections) signal.throwIfAborted() @@ -459,22 +460,11 @@ export class ReactLoopAgent implements Agent { for (const { message, intent } of commits) { this.session.append('system/message', { turn, step, message }, intent) } - let contextAdmitted = false if (firstAttempt) { - for (const message of decision.messages) { - if (message.source.kind === 'runtime-context') { - if (context !== undefined && !contextAdmitted) { - this.session.append('user/message', context, { surfaceOp: 'append' }) - contextAdmitted = true - } - } else { - this.session.append('user/message', message, { surfaceOp: 'append' }) - } - } - } - if (context !== undefined && !contextAdmitted) { - this.session.append('user/message', context, { surfaceOp: 'append' }) + for (const message of decision.messages) this.session.append('user/message', message, { surfaceOp: 'append' }) } + // The runtime-context snapshot follows the admitted batch. + if (context !== undefined) this.session.append('user/message', context, { surfaceOp: 'append' }) firstAttempt = false const request = this.buildRequest(config, preparedCall, assembly.tools, { turn, step }, startsRequestSeries, signal) const live = new AssistantStreamAttempt( diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 34e7879654..85b4ab9162 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -386,8 +386,8 @@ export class AgentLoop extends Service implements AgentFactory { }) }, 'agentLoop.transactions()') ctx.effect(() => ctx.agents.setFactory(this), 'agentLoop.setFactory()') - ctx.systemPrompt.variable('provider', context => context.modelSelection?.provider ?? context.agent?.options.provider) - ctx.systemPrompt.variable('model', context => context.modelSelection?.model ?? context.agent?.options.model) + ctx.systemPrompt.variable('provider', context => context.agent?.step?.modelSelection.provider ?? context.agent?.options.provider) + ctx.systemPrompt.variable('model', context => context.agent?.step?.modelSelection.model ?? context.agent?.options.model) for (const { id, sessionId, cwd, resumeSessionId, ...options } of this.config.agents) { const meta = cwd === undefined ? {} : { cwd } diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index 193544367f..6bcc77323b 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -796,7 +796,7 @@ describe('agent loop', () => { } }) - it('replaces a stale owned snapshot at its accepted position and admits it once', async () => { + it('admits the runtime-context snapshot current at admission after the accepted batch', async () => { const adapter = new MockAdapter([textResponse('done')]) const ctx = await harness(adapter) try { @@ -807,16 +807,20 @@ describe('agent loop', () => { const decision = await next() current = 'after' if (decision.kind === 'reject') return decision - const context = decision.messages.find(message => message.source.kind === 'runtime-context')! - return { ...decision, messages: [context, ...decision.messages.filter(message => message !== context), context] } + expect(decision.messages.map(message => message.source.kind)).toEqual(['user']) + const appended = createUserMessage({ content: [{ type: 'text', text: 'appended' }], source: { kind: 'user' } }) + return { ...decision, messages: [...decision.messages, appended] } }) send(agent, 'accepted user') await agent.whenIdle() const users = adapter.requests[0]?.messages.filter(message => message.role === 'user') ?? [] - expect(users.map(message => message.source?.kind)).toEqual(['runtime-context', 'user']) - expect(users[0]?.content).toEqual([{ type: 'text', text: 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nafter' }]) - expect(users[1]?.content).toEqual([{ type: 'text', text: 'accepted user' }]) - expect(agent.session.snapshotEvents().filter(event => event.type === 'user/message')).toHaveLength(2) + expect(users.map(message => message.content)).toEqual([ + [{ type: 'text', text: 'accepted user' }], + [{ type: 'text', text: 'appended' }], + [{ type: 'text', text: 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nafter' }], + ]) + expect(users.at(-1)?.source?.kind).toBe('runtime-context') + expect(agent.session.snapshotEvents().filter(event => event.type === 'user/message')).toHaveLength(3) } finally { await ctx.fiber.dispose() } diff --git a/packages/core/agent-loop/tests/step-model.spec.ts b/packages/core/agent-loop/tests/step-model.spec.ts new file mode 100644 index 0000000000..97f345c280 --- /dev/null +++ b/packages/core/agent-loop/tests/step-model.spec.ts @@ -0,0 +1,231 @@ +/** Each proposed step commits boundary selections, then publishes its model selection as `agent.step`. */ +import { afterEach, describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import LlmRuntime, { createUserMessage } from '@deepseek-ai/dsh-llm' +import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools' +import AgentRegistry, { type Agent, type AgentStep } from '@deepseek-ai/dsh-agent' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' + +const contexts: Context[] = [] +afterEach(async () => { + for (const ctx of contexts.splice(0)) await ctx.fiber.dispose() +}) + +async function harness(adapter: MockAdapter): Promise { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(LlmRuntime) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentLoop, { agents: [] }) + ctx.llm.registerAdapter(['mock'], adapter) + return ctx +} + +function send(agent: Agent, text: string): void { + agent.followup(createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } })) +} + +function eventTypes(agent: Agent): string[] { + return agent.session.snapshotEvents().map(event => event.type) +} + +function lastTurnEnd(agent: Agent) { + const event = agent.session.snapshotEvents().findLast(entry => entry.type === 'turn/end') + return event?.type === 'turn/end' ? event.data.reason : undefined +} + +describe('agent.step', () => { + it('publishes one frozen selection from agent/model through its step and clears it between steps', async () => { + const adapter = new MockAdapter([toolCallResponse('c1', 'probe', {}), textResponse('done')]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('step-visibility'), { provider: 'mock', model: 'mock' }) + const seen = { + boundary: [] as (AgentStep | undefined)[], + model: [] as (AgentStep | undefined)[], + preStep: [] as (AgentStep | undefined)[], + condition: [] as (AgentStep | undefined)[], + section: [] as (AgentStep | undefined)[], + request: [] as (AgentStep | undefined)[], + execute: [] as (AgentStep | undefined)[], + stopping: [] as (AgentStep | undefined)[], + } + ctx.on('agent/step-boundary', ({ agent: subject }) => { seen.boundary.push(subject.step) }) + ctx.on('agent/model', async ({ agent: subject }, next) => { + seen.model.push(subject.step) + return next() + }) + ctx.on('agent/pre-step', async ({ agent: subject }, next) => { + seen.preStep.push(subject.step) + return next() + }) + ctx.on('agent/request', async ({ agent: subject }, next) => { + seen.request.push(subject.step) + return next() + }) + ctx.on('agent/turn-stopping', ({ agent: subject }) => { seen.stopping.push(subject.step) }) + ctx.systemPrompt.section({ + name: 'probe', + order: 0, + text: (context) => { + seen.section.push(context.agent?.step) + return '' + }, + }) + ctx.tools.register(defineContentToolFixture({ + name: 'probe', description: 'Record the calling step.', parameters: {}, + async execute(_args, exec) { + seen.execute.push(exec.agent?.step) + return [] + }, + }), { + when: ({ agent: subject }) => { + seen.condition.push(subject.step) + return true + }, + }) + + send(agent, 'go') + await agent.whenIdle() + + expect(seen.boundary).toEqual([undefined, undefined]) + expect(seen.model).toEqual([undefined, undefined]) + const [first, second] = seen.preStep + expect(first).toEqual({ modelSelection: { provider: 'mock', model: 'mock' } }) + expect(Object.isFrozen(first)).toBe(true) + expect(Object.isFrozen(first?.modelSelection)).toBe(true) + expect(second).toEqual(first) + expect(second).not.toBe(first) + for (const phase of [seen.condition, seen.section, seen.request]) { + expect(phase).toHaveLength(2) + expect(phase[0]).toBe(first) + expect(phase[1]).toBe(second) + } + expect(seen.execute).toHaveLength(1) + expect(seen.execute[0]).toBe(first) + expect(seen.stopping).toEqual([undefined]) + expect(agent.step).toBeUndefined() + }) + + it('clears the selection when a proposal is rejected or fails', async () => { + const adapter = new MockAdapter([]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('step-cleared'), { provider: 'mock', model: 'mock' }) + let outcome: 'reject' | 'throw' = 'reject' + ctx.on('agent/pre-step', async (_payload, next) => { + await next() + if (outcome === 'throw') throw new Error('pre-step failed') + return { kind: 'reject' } + }) + + send(agent, 'rejected') + await agent.whenIdle() + expect(agent.step).toBeUndefined() + expect(lastTurnEnd(agent)?.kind).toBe('blocked') + + outcome = 'throw' + send(agent, 'failed') + await agent.whenIdle() + expect(agent.step).toBeUndefined() + expect(lastTurnEnd(agent)?.kind).toBe('error') + expect(adapter.requests).toEqual([]) + }) +}) + +describe('agent/step-boundary', () => { + it('runs after the claim and before agent/model once per proposed step', async () => { + const adapter = new MockAdapter([toolCallResponse('c1', 'noop', {}), textResponse('done')]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('boundary-order'), { provider: 'mock', model: 'mock' }) + ctx.tools.register(defineContentToolFixture({ + name: 'noop', description: 'Do nothing.', parameters: {}, + execute: async () => [], + })) + const order: string[] = [] + ctx.on('agent/inbox/claimed', () => { order.push('claimed') }) + ctx.on('agent/step-boundary', ({ step }) => { order.push(`boundary:${String(step)}`) }) + ctx.on('agent/model', async ({ step }, next) => { + order.push(`model:${String(step)}`) + return next() + }) + ctx.on('agent/pre-step', async ({ step }, next) => { + order.push(`pre-step:${String(step)}`) + return next() + }) + + send(agent, 'go') + await agent.whenIdle() + + expect(order).toEqual(['claimed', 'boundary:1', 'model:1', 'pre-step:1', 'boundary:2', 'model:2', 'pre-step:2']) + }) + + it('fails the proposal before model choice when a listener throws', async () => { + const adapter = new MockAdapter([]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('boundary-failure'), { provider: 'mock', model: 'mock' }) + let modelChosen = false + ctx.on('agent/step-boundary', () => { throw new Error('boundary commit failed') }) + ctx.on('agent/model', async (_payload, next) => { + modelChosen = true + return next() + }) + + send(agent, 'go') + await agent.whenIdle() + + expect(modelChosen).toBe(false) + const reason = lastTurnEnd(agent) + expect(reason?.kind === 'error' ? reason.error.message : undefined).toContain('boundary commit failed') + expect(eventTypes(agent)).not.toContain('step/start') + expect(adapter.requests).toEqual([]) + }) + + it('opens no step when the turn is cancelled during the boundary', async () => { + const adapter = new MockAdapter([]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('boundary-cancel'), { provider: 'mock', model: 'mock' }) + const entered = Promise.withResolvers() + const release = Promise.withResolvers() + ctx.on('agent/step-boundary', async () => { + entered.resolve(undefined) + await release.promise + }) + + send(agent, 'cancelled') + await entered.promise + agent.cancel({ kind: 'user' }) + release.resolve(undefined) + await agent.whenIdle() + + expect(lastTurnEnd(agent)?.kind).toBe('aborted') + expect(eventTypes(agent)).not.toContain('step/start') + expect(agent.step).toBeUndefined() + expect(adapter.requests).toEqual([]) + }) +}) + +describe('runtime context admission', () => { + it('opens no step for a removed wake even when the runtime context changed', async () => { + const adapter = new MockAdapter([]) + const ctx = await harness(adapter) + ctx.systemPrompt.context({ name: 'fact', order: 0, text: 'changed' }) + const agent = await ctx.agentLoop.create(SessionId('context-only'), { provider: 'mock', model: 'mock' }) + // The driver claims synchronously on wake, so the insertion observer removes the message first. + ctx.on('agent/inbox/inserted', ({ agent: subject, message }) => { subject.inbox.remove(message.id) }) + + send(agent, 'removed wake') + await agent.whenIdle() + + expect(adapter.requests).toEqual([]) + expect(eventTypes(agent)).toContain('turn/start') + expect(eventTypes(agent)).not.toContain('step/start') + expect(eventTypes(agent)).not.toContain('user/message') + }) +}) diff --git a/packages/core/agent-loop/tests/tool-updates.spec.ts b/packages/core/agent-loop/tests/tool-updates.spec.ts index 4044aa1e92..09d201f6ff 100644 --- a/packages/core/agent-loop/tests/tool-updates.spec.ts +++ b/packages/core/agent-loop/tests/tool-updates.spec.ts @@ -131,11 +131,12 @@ describe('step-time tool activation', () => { calls++ return [{ type: 'text', text: 'done' }] }, - }), { when: async ({ modelSelection }) => { - expect(Object.isFrozen(modelSelection)).toBe(true) - selectedModels.push(modelSelection.model) + }), { when: async ({ agent: subject }) => { + const selection = subject.step?.modelSelection + expect(Object.isFrozen(selection)).toBe(true) + selectedModels.push(selection?.model ?? '') await Promise.resolve() - return modelSelection.model === 'enabled' + return selection?.model === 'enabled' } }) await send(agent, 'enabled') model = 'disabled' @@ -191,7 +192,7 @@ describe('step-time tool activation', () => { }), { when: () => { counts.conditions++; return retried } }) ctx.systemPrompt.section({ name: 'retry:guidance', order: 0, complete: true, - text: context => `${context.modelSelection?.model}:${ctx.tools.schemas(agent, context.toolSet).map(tool => tool.name).join(',')}`, + text: context => `${context.agent?.step?.modelSelection.model}:${ctx.tools.schemas(agent, context.toolSet).map(tool => tool.name).join(',')}`, }) await send(agent, 'retry once') expect(adapter.requests.map(request => request.model)).toEqual(['first', 'first']) diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index 5d7936aae2..c2df453d65 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -27,8 +27,8 @@ en: 2594a1e02adea801 zh: cae8860369728efb /deepseek-ai-dsh-agent/use-this-package/intercept-or-observe-work-in-flight: - en: bb854bd10523e158 - zh: b59580450c764598 + en: f5045f7c9f1c2f7a + zh: bc6ceeb549eb0f4d /deepseek-ai-dsh-agent/understand-the-implementation: en: ce7e776e9d516bdb zh: 59d2751743db7795 @@ -36,8 +36,8 @@ en: 7c850051d2857bfa zh: 4cab817553114e15 /deepseek-ai-dsh-agent/understand-the-implementation/step-admission: - en: 324af18f63a4f537 - zh: eec28e7f8362a919 + en: 0271968a215a5b72 + zh: 36e7535632a166dc /deepseek-ai-dsh-agent/understand-the-implementation/durable-inbox: en: f6802c7f94f623b3 zh: cc3885db80765970 diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index a7b91fc1b4..9ac3fa06e8 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -64,7 +64,7 @@ await handle.agent.whenIdle() ### Intercept or observe work in flight -The `agent/*` events let plugins act on live work without depending on the loop package. `agent/model` chooses each step's provider, model, and reasoning effort; `agent/pre-step` can reject a proposed step or replace the messages entering it; `agent/request-error` lets a listener retry a failed model request; `agent/turn-stopping` runs before an otherwise completed turn closes and can steer to keep it open. `agent/assistant-stream` carries one process-local Assistant attempt's ordered start, transient chunk, and end frames. Start names the attempt's turn and step, chunk indexes are dense from zero, and `end.index` is the next chunk position. The loop commits the complete compact stream as one `assistant/message` or `assistant/attempt` before a committed end frame, so the live event remains presentation data rather than the replay source. `agent/status`, `agent/created`, and `agent/disposed` drive UI and coordination state, and the per-message `agent/inbox/*` notifications keep inbox projections in sync. Exact signatures, dispatch modes, and payload contracts live in the generated region of the [core subsystem page](../../../docs/subsystems/core.md#cordis-surface). +The `agent/*` events let plugins act on live work without depending on the loop package. `agent/step-boundary` commits selections that apply from the next step, such as a pending plan-mode switch; `agent/model` chooses each step's provider, model, and reasoning effort, which the agent then exposes as `agent.step`; `agent/pre-step` can reject a proposed step or replace the messages entering it; `agent/request-error` lets a listener retry a failed model request; `agent/turn-stopping` runs before an otherwise completed turn closes and can steer to keep it open. `agent/assistant-stream` carries one process-local Assistant attempt's ordered start, transient chunk, and end frames. Start names the attempt's turn and step, chunk indexes are dense from zero, and `end.index` is the next chunk position. The loop commits the complete compact stream as one `assistant/message` or `assistant/attempt` before a committed end frame, so the live event remains presentation data rather than the replay source. `agent/status`, `agent/created`, and `agent/disposed` drive UI and coordination state, and the per-message `agent/inbox/*` notifications keep inbox projections in sync. Exact signatures, dispatch modes, and payload contracts live in the generated region of the [core subsystem page](../../../docs/subsystems/core.md#cordis-surface). ----- @@ -82,9 +82,9 @@ The package is built on one separation: the public `Agent` surface and registry ### Step admission -`PreStepDecision` is either `{ kind: 'reject' }` or `{ kind: 'enter', messages, startsRequestSeries? }`. The enter branch contains the complete identified, frozen message batch. `agent/model` chooses the step's provider, model, and reasoning effort before `agent/pre-step`, whose payload carries that `modelSelection`. Acceptance does not commit the batch: after `step/start`, tool conditions, prompt assembly, `agent/request`, and `prepareCall()` must succeed before system and user messages are logged. Cancellation during model selection opens no step, and cancellation during a later async phase commits neither message kind. `startsRequestSeries: true` declares a distinct model-message series; a wrapping listener preserves that declaration and the batch unless it intentionally replaces either one. Claiming removes offered messages from the inbox, while messages inserted after the claim remain pending for a later boundary. +`PreStepDecision` is either `{ kind: 'reject' }` or `{ kind: 'enter', messages, startsRequestSeries? }`. The enter branch contains the complete identified, frozen message batch. `agent/step-boundary` and `agent/model` run before `agent/pre-step`, so pre-step listeners read the step's model selection from `agent.step`. Acceptance does not commit the batch: after `step/start`, tool conditions, prompt assembly, `agent/request`, and `prepareCall()` must succeed before system and user messages are logged. Cancellation at the boundary or during model selection opens no step, and cancellation during a later async phase commits neither message kind. `startsRequestSeries: true` declares a distinct model-message series; a wrapping listener preserves that declaration and the batch unless it intentionally replaces either one. Claiming removes offered messages from the inbox, while messages inserted after the claim remain pending for a later boundary. -The step's model selection supplies or replaces the creation-time provider, model, and effort. Prompt preparation, `agent/pre-step`, tool conditions, and every request attempt in the step use it; an `agent/request` listener that returns another provider, model, or reasoning effort fails the step. `installModelSelection` returns its current selection, effort included, when `agent/model` runs, so a selection made during a step applies from the next step. +The step's model selection supplies or replaces the creation-time provider, model, and effort. The driver publishes it as `agent.step` after `agent/model` and clears it when the step ends or its proposal is rejected, fails, or is cancelled; `agent/pre-step`, tool conditions, prompt assembly, and every request attempt in the step read it. An `agent/request` listener that returns another provider, model, or reasoning effort fails the step. `installModelSelection` returns its current selection, effort included, when `agent/model` runs, so a selection made during a step applies from the next step. ### Durable inbox diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index becdb8417b..72256e0cc1 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -64,7 +64,7 @@ await handle.agent.whenIdle() ### 拦截或观察进行中的工作 -`agent/*` 事件让插件无需依赖循环包即可作用于实时工作。`agent/model` 选定每个步骤的提供方、模型与推理强度;`agent/pre-step` 可以拒绝拟进入的步骤或替换进入它的消息;`agent/request-error` 让监听器重试失败的模型请求;`agent/turn-stopping` 在本可完成的轮次关闭前运行,并可通过 steer 使其保持打开。`agent/assistant-stream` 携带一个进程本地 Assistant attempt 的有序 start、瞬态分片与 end frame。start 给出该 attempt 的轮次与步骤,分片索引从零开始密集递增,`end.index` 则是下一个分片位置。loop 会在 committed end frame 前把完整紧凑流提交为一个 `assistant/message` 或 `assistant/attempt`,因此实时事件仍是呈现数据而非回放来源。`agent/status`、`agent/created` 与 `agent/disposed` 驱动 UI 与协调状态,逐消息的 `agent/inbox/*` 通知则让收件箱投影保持同步。确切签名、分发 mode 与 payload 约定见 [core 子系统页](../../../docs/subsystems/core.zh.md#cordis-surface) 的生成区块。 +`agent/*` 事件让插件无需依赖循环包即可作用于实时工作。`agent/step-boundary` 提交从本步骤起生效的选择,例如待提交的计划模式切换;`agent/model` 选定每个步骤的提供方、模型与推理强度,agent 随后通过 `agent.step` 公开它;`agent/pre-step` 可以拒绝拟进入的步骤或替换进入它的消息;`agent/request-error` 让监听器重试失败的模型请求;`agent/turn-stopping` 在本可完成的轮次关闭前运行,并可通过 steer 使其保持打开。`agent/assistant-stream` 携带一个进程本地 Assistant attempt 的有序 start、瞬态分片与 end frame。start 给出该 attempt 的轮次与步骤,分片索引从零开始密集递增,`end.index` 则是下一个分片位置。loop 会在 committed end frame 前把完整紧凑流提交为一个 `assistant/message` 或 `assistant/attempt`,因此实时事件仍是呈现数据而非回放来源。`agent/status`、`agent/created` 与 `agent/disposed` 驱动 UI 与协调状态,逐消息的 `agent/inbox/*` 通知则让收件箱投影保持同步。确切签名、分发 mode 与 payload 约定见 [core 子系统页](../../../docs/subsystems/core.zh.md#cordis-surface) 的生成区块。 ----- @@ -82,9 +82,9 @@ await handle.agent.whenIdle() ### 步骤准入 -`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages, startsRequestSeries? }`。enter 分支包含完整、带标识且冻结的消息批次。`agent/model` 在 `agent/pre-step` 之前选定该步骤的提供方、模型与推理强度,`agent/pre-step` 的 payload 携带该 `modelSelection`。接纳不等于提交:`step/start` 之后,工具条件、提示词组装、`agent/request` 与 `prepareCall()` 都成功后,才记录系统提示词与用户批次。在模型选择期间取消不会打开步骤,在之后任一异步阶段取消也不会提交这两种消息。`startsRequestSeries: true` 声明一个独立的模型消息序列;包装下游 enter 的监听器会保留该声明与批次,除非有意替换其中一项。领取会从 inbox 移除候选消息,领取后插入的消息则等待后续边界。 +`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages, startsRequestSeries? }`。enter 分支包含完整、带标识且冻结的消息批次。`agent/step-boundary` 与 `agent/model` 在 `agent/pre-step` 之前运行,因此 pre-step 监听器从 `agent.step` 读取该步骤的模型选择。接纳不等于提交:`step/start` 之后,工具条件、提示词组装、`agent/request` 与 `prepareCall()` 都成功后,才记录系统提示词与用户批次。在步骤边界或模型选择期间取消不会打开步骤,在之后任一异步阶段取消也不会提交这两种消息。`startsRequestSeries: true` 声明一个独立的模型消息序列;包装下游 enter 的监听器会保留该声明与批次,除非有意替换其中一项。领取会从 inbox 移除候选消息,领取后插入的消息则等待后续边界。 -步骤的模型选择会补上或替换创建时的提供方、模型与推理强度。提示词准备、`agent/pre-step`、工具条件以及该步骤的每次请求尝试都使用它;`agent/request` 监听器若返回其他提供方、模型或推理强度,步骤会失败。`installModelSelection` 在 `agent/model` 运行时返回当前选择(含推理强度),因此步骤进行中做出的选择从下一个步骤开始生效。 +步骤的模型选择会补上或替换创建时的提供方、模型与推理强度。驱动在 `agent/model` 之后把它发布为 `agent.step`,并在步骤结束或提案被拒绝、失败、取消时清除;`agent/pre-step`、工具条件、提示词组装以及该步骤的每次请求尝试都读取它。`agent/request` 监听器若返回其他提供方、模型或推理强度,步骤会失败。`installModelSelection` 在 `agent/model` 运行时返回当前选择(含推理强度),因此步骤进行中做出的选择从下一个步骤开始生效。 ### 持久 inbox diff --git a/packages/core/agent/src/model-selection.ts b/packages/core/agent/src/model-selection.ts index d0e30909aa..1c04bdf103 100644 --- a/packages/core/agent/src/model-selection.ts +++ b/packages/core/agent/src/model-selection.ts @@ -51,8 +51,8 @@ function modelSwitchNotice(previous: ModelSelection, selected: ModelSelection) { * model's provider default. * * A provider/model change appends a durable user-role notice to the next - * admitted request. It compares the step's selection with the latest request - * header; effort-only changes and empty no-request decisions add no notice. + * admitted request. It compares `agent.step` with the latest request header; + * effort-only changes and empty no-request decisions add no notice. * Failure before header persistence repeats the notice on the next request. * * @param agentCtx - The selected Agent's scoped context. @@ -67,14 +67,15 @@ export function installModelSelection(agentCtx: Context, selection: ModelSelecti }) const disposeNotice = agentCtx.on( 'agent/pre-step', - async ({ agent, messages, modelSelection, signal, step }, next): Promise => { + async ({ agent, messages, signal, step }, next): Promise => { const decision = await next() if (decision.kind === 'reject' || signal.aborted) return decision // The loop skips an empty first step and an emptied offered continuation. if (decision.messages.length === 0 && (step === 1 || messages.length > 0)) return decision + const selected = agent.step?.modelSelection const previous = agent.session.requestHeader()?.config - if (previous === undefined || sameModel(modelSelection, previous)) return decision - return { ...decision, messages: [...decision.messages, modelSwitchNotice(previous, modelSelection)] } + if (selected === undefined || previous === undefined || sameModel(selected, previous)) return decision + return { ...decision, messages: [...decision.messages, modelSwitchNotice(previous, selected)] } }, { prepend: true }, ) diff --git a/packages/core/agent/src/runtime-types.ts b/packages/core/agent/src/runtime-types.ts index c710d95592..3a8111c1e8 100644 --- a/packages/core/agent/src/runtime-types.ts +++ b/packages/core/agent/src/runtime-types.ts @@ -19,8 +19,6 @@ declare module '@deepseek-ai/dsh-system-prompt' { interface AssembleContext { /** Agent for this assembly; absent on diagnostics. When present, `scope` must identify the same agent. */ agent?: Agent - /** Model selection of the step being assembled; absent outside a step. */ - modelSelection?: ModelSelection } } @@ -34,6 +32,12 @@ export interface ModelSelection { reasoningEffort?: ReasoningEffortId } +/** Decisions the driver fixed for one proposed or running step. */ +export interface AgentStep { + /** Provider, model, and reasoning effort that every request attempt in the step uses. */ + readonly modelSelection: ModelSelection +} + /** Merge-extensible agent creation options. Persona belongs to system-prompt sections. */ export interface AgentOptions { /** Provider route (must have a registered adapter at call time). */ @@ -176,6 +180,13 @@ declare module './types.ts' { interface Agent { /** Creation-time request options; `agent/model` may choose another provider, model, and effort for each step. */ readonly options: AgentOptions + /** + * The step the driver is preparing or running. The driver publishes it + * after `agent/model` and clears it when the step ends or its proposal is + * rejected, fails, or is cancelled, so it is undefined between steps and + * during `agent/model`. Work that outlives its step reads it at its start. + */ + readonly step?: AgentStep | undefined /** The live session this agent drives; its log is the durable source of truth. */ readonly session: Session /** Agent-owned access to durable pending work. */ @@ -320,17 +331,32 @@ declare module '@deepseek-ai/cordis' { // ---- the machine's extension points ---- /** * Reject a proposed step or replace the messages that enter it. Calling - * `next()` preserves the current messages. + * `next()` preserves the current messages. `payload.agent.step` already + * holds the model selection that `agent/model` chose for this step. * @param payload.agent - the agent proposing the step. * @param payload.messages - messages removed from the inbox for this step. * @param payload.turn - the turn that will own the step. * @param payload.step - the step proposed by the loop. - * @param payload.modelSelection - the provider, model, and effort `agent/model` chose for this step. * @param payload.signal - the current turn's cancellation signal. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode waterfall */ - 'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; modelSelection: ModelSelection; signal: AbortSignal }, next: () => Promise): Promise + 'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise + /** + * A proposed step reached its boundary: the loop claimed its input and is + * about to choose its model. Listeners commit selections that take effect + * from this step, such as a pending plan-mode switch, so `agent/model`, + * `agent/pre-step`, tool conditions, and prompt assembly read one state. + * Runs once per proposed step; a throw fails the proposal, and + * cancellation opens no step. + * @param payload.agent - the agent proposing the step. + * @param payload.turn - the turn that will own the step. + * @param payload.step - the step proposed by the loop. + * @param payload.signal - the current turn's explicit abort signal. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode serial + */ + 'agent/step-boundary'(this: Scoped, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }): Promise | void /** * Choose the provider, model, and reasoning effort for one proposed step. * `await next()` yields the selection the machine would use: agent options @@ -338,10 +364,11 @@ declare module '@deepseek-ai/cordis' { * effort only when one was chosen for that exact model. Return a * replacement to switch; an effort belongs to its model, so a replacement * that changes the model names that model's effort or omits it for the - * provider default. The loop runs this once per proposed step, before - * context preparation and `agent/pre-step`. Prompt preparation, tool - * conditions, and every request attempt in the step use the result, so a - * later choice applies from the next step. Cancellation here opens no step. + * provider default. The loop runs this once per proposed step, after + * `agent/step-boundary` and before `agent/pre-step`, then publishes the + * result as `agent.step`. Pre-step listeners, tool conditions, prompt + * assembly, and every request attempt in the step use it, so a later + * choice applies from the next step. Cancellation here opens no step. * @param payload.agent - the agent proposing the step. * @param payload.turn - the turn that will own the step. * @param payload.step - the step proposed by the loop. diff --git a/packages/core/agent/tests/model-selection.spec.ts b/packages/core/agent/tests/model-selection.spec.ts index 23663ee2e1..fc453c0c8f 100644 --- a/packages/core/agent/tests/model-selection.spec.ts +++ b/packages/core/agent/tests/model-selection.spec.ts @@ -5,6 +5,7 @@ import { agentEvents, installModelSelection, type Agent, + type AgentStep, type ModelSelection, type ModelSelectionRef, } from '../src/index.ts' @@ -24,13 +25,25 @@ function harness(current?: ModelSelection, previous?: ModelSelection) { contexts.push(ctx) const selection: ModelSelectionRef = { current } const dispose = installModelSelection(ctx, selection) - const agent = { session: Session.create(SessionId('model-selection')) } as Agent + const state: { session: Session; step?: AgentStep } = { session: Session.create(SessionId('model-selection')) } + const agent = state as Agent if (previous) agent.session.append('request/header', { header: { config: previous }, reason: 'initial' }) const choose = (step: number) => agentEvents(ctx, agent).waterfall('agent/model', { turn: 1, step, signal }, () => Promise.resolve(seed)) - const preStep = (modelSelection: ModelSelection, messages = [input], offered = [input], step = 1, stepSignal = signal) => - agentEvents(ctx, agent).waterfall('agent/pre-step', { turn: 1, step, messages: offered, modelSelection, signal: stepSignal }, - () => Promise.resolve({ kind: 'enter' as const, messages })) + /** Dispatch pre-step for a step whose published model selection is `modelSelection`. */ + const preStep = ( + modelSelection: ModelSelection | undefined, + messages = [input], + offered = [input], + step = 1, + stepSignal = signal, + decision: 'enter' | 'reject' = 'enter', + ) => { + if (modelSelection === undefined) delete state.step + else state.step = { modelSelection } + return agentEvents(ctx, agent).waterfall('agent/pre-step', { turn: 1, step, messages: offered, signal: stepSignal }, + () => Promise.resolve(decision === 'enter' ? { kind: 'enter' as const, messages } : { kind: 'reject' as const })) + } return { ctx, agent, selection, dispose, choose, preStep } } @@ -63,7 +76,7 @@ describe('installModelSelection()', () => { await expect(h.preStep({ provider: 'beta', model: 'b1' })).resolves.toMatchObject({ messages: [input, notice('alpha/a0', 'beta/b1')] }) }) - it('preserves initial, effort-only, rejected, cancelled, and disposed decisions', async () => { + it('preserves initial, effort-only, unpublished, rejected, cancelled, and disposed decisions', async () => { const h = harness() const initial = { provider: 'alpha', model: 'a0' } await expect(h.preStep(initial)).resolves.toEqual({ kind: 'enter', messages: [input] }) @@ -72,9 +85,9 @@ describe('installModelSelection()', () => { reason: 'initial', }) await expect(h.preStep({ ...initial, reasoningEffort: ReasoningEffortId('low') })).resolves.toEqual({ kind: 'enter', messages: [input] }) + await expect(h.preStep(undefined)).resolves.toEqual({ kind: 'enter', messages: [input] }) const switched = { provider: 'alpha', model: 'a1' } - await expect(agentEvents(h.ctx, h.agent).waterfall('agent/pre-step', { turn: 1, step: 3, messages: [], modelSelection: switched, signal }, - () => Promise.resolve({ kind: 'reject' as const }))).resolves.toEqual({ kind: 'reject' }) + await expect(h.preStep(switched, [], [], 3, signal, 'reject')).resolves.toEqual({ kind: 'reject' }) await expect(h.preStep(switched, [input], [input], 3, AbortSignal.abort())).resolves.toEqual({ kind: 'enter', messages: [input] }) h.dispose() await expect(h.preStep(switched)).resolves.toEqual({ kind: 'enter', messages: [input] }) diff --git a/packages/core/system-prompt/README.i18n.yaml b/packages/core/system-prompt/README.i18n.yaml index 8abb19e207..ed2d640874 100644 --- a/packages/core/system-prompt/README.i18n.yaml +++ b/packages/core/system-prompt/README.i18n.yaml @@ -21,8 +21,8 @@ en: 87e28893e6cf58d7 zh: 06f476b38c4d2038 /deepseek-ai-dsh-system-prompt/use-this-package/contribute-a-prompt-section: - en: 1d970a1e99cd71bb - zh: 62f30c25e82bdbea + en: d9185fd5c60cf3d5 + zh: 54a816d078495344 /deepseek-ai-dsh-system-prompt/use-this-package/contribute-a-prompt-variable: en: b3e4c9b4ced59488 zh: d80c791e72bd5821 diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index 70c465cdaf..890e5e983b 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -67,7 +67,7 @@ Set `interpolate: false` on a section or dynamic context to preserve its text li `refreshContext(assembly, context)` refreshes registered runtime facts after pre-step and route preparation. It preserves accepted sections, tools, variables, and contexts contributed only by the assembly waterfall, applies current optional-context suppression, and restores missing required entries. Put changing facts in context providers; registered context text is read again before admission. -`prepare(context)` resolves variables and runtime facts through `system-prompt/prepare` before step admission. After `agent/pre-step` accepts the step and commits its selections, `assemble(context, preparation)` resolves sections and tool declarations. Presentation listeners receive sections, contexts, and variables; tools remain provider-owned. Tool-aware section callbacks use `context.toolSet` to inspect the same step selection as execution. +The agent loop calls `assemble(context)` once per step after `step/start`, when the step's model selection (`context.agent.step`) and admitted tool set (`context.toolSet`) are fixed. Presentation listeners receive sections, contexts, and variables; tools remain provider-owned. Tool-aware section callbacks use `context.toolSet` to inspect the same step selection as execution. ### Contribute a prompt variable diff --git a/packages/core/system-prompt/README.zh.md b/packages/core/system-prompt/README.zh.md index 1ec9b81050..f831e8c1c0 100644 --- a/packages/core/system-prompt/README.zh.md +++ b/packages/core/system-prompt/README.zh.md @@ -67,7 +67,7 @@ ctx.systemPrompt.section({ `refreshContext(assembly, context)` 在 pre-step 与路由准备之后刷新已注册的运行时事实。它保留已接纳的段、工具、变量及仅由组装 waterfall 添加的上下文,应用当前的可选上下文抑制,并恢复缺失的必需条目。将变化的事实放在上下文 provider 中;已注册上下文的文本会在准入前再次读取。 -`prepare(context)` 在步骤接纳前通过 `system-prompt/prepare` 解析变量与运行时事实。`agent/pre-step` 接纳步骤并提交其选择后,`assemble(context, preparation)` 解析段落与工具声明。展示监听器接收段落、上下文与变量;工具声明仍由提供方持有。依赖工具的段落回调通过 `context.toolSet` 读取与执行相同的步骤集合。 +agent loop 在每个步骤的 `step/start` 之后调用一次 `assemble(context)`,此时该步骤的模型选择(`context.agent.step`)与准入工具集合(`context.toolSet`)都已确定。展示监听器接收段落、上下文与变量;工具声明仍由提供方持有。依赖工具的段落回调通过 `context.toolSet` 读取与执行相同的步骤集合。 ### 贡献提示词变量 diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts index 9032691280..6abf8bbc0b 100644 --- a/packages/core/system-prompt/src/index.ts +++ b/packages/core/system-prompt/src/index.ts @@ -29,13 +29,6 @@ declare module '@deepseek-ai/cordis' { * @mode waterfall */ 'system-prompt/assemble'(this: Scoped, assembly: PromptPresentation, context: AssembleContext, next: () => Promise): Promise - /** - * Prepare variables and runtime context before step admission. - * @param preparation - registered variables and context contributions. - * @param context - calling scope and cancellation. - * @mode waterfall - */ - 'system-prompt/prepare'(this: Scoped, preparation: PromptPreparation, context: AssembleContext, next: () => Promise): Promise /** * Emitted when any prompt provider changes. This registry notification is * unfiltered because a global change affects every scope. @@ -124,18 +117,18 @@ export interface ToolProviderResult { readonly knownNames?: readonly string[] } -/** Prepared runtime facts and variables; context text remains uninterpolated. */ -export interface PromptPreparation { +/** + * Prompt text contributions that the assembly waterfall may transform. + * Sections and contexts remain uninterpolated until rendered; tool + * declarations are owned by their providers. + */ +export interface PromptPresentation { + sections: AssembledSection[] contexts: AssembledContext[] variables: Record } -/** Prompt text contributions; tool declarations are owned by their providers. */ -export interface PromptPresentation extends PromptPreparation { - sections: AssembledSection[] -} - -/** Completed model inputs, including provider-owned tool declarations. */ +/** Completed model inputs: the presentation plus tool declarations in canonical order. */ export interface PromptAssembly extends PromptPresentation { tools: ToolSchema[] } @@ -338,7 +331,7 @@ export function joinContextSections(sections: readonly ContextSnapshotSection[]) * @param assembly - the assembly whose contexts and variables to render. * @returns one entry per contributing context that rendered to non-empty text. */ -export function renderContextSections(assembly: PromptPreparation): ContextSnapshotSection[] { +export function renderContextSections(assembly: PromptAssembly): ContextSnapshotSection[] { return assembly.contexts .map(context => ({ name: context.name, @@ -590,7 +583,7 @@ export class SystemPrompt extends Service { * @param context - the same scope and current plugin-defined assembly fields. * @returns the accepted assembly with current runtime-context provider text. */ - refreshContext(assembly: T, context: AssembleContext = {}): T { + refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly { const providers = this.layers.merge(context.scope, layer => layer.contexts) const suppressed = !this.layers.global.runtimeContextSuppressors.isEmpty() || this.layers.chainLayers(context.scope).some(layer => !layer.runtimeContextSuppressors.isEmpty()) @@ -613,12 +606,18 @@ export class SystemPrompt extends Service { } /** - * Prepare variables and runtime facts before accepting a step. No tool - * conditions or prompt sections are evaluated in this phase. - * @param context - calling scope and request cancellation. - * @returns prepared inputs after the preparation waterfall. + * Assemble global and scoped providers, detach tool parameters, apply + * canonical ordering, then run the presentation waterfall. Scoped sections + * and variables shadow globals. The waterfall's text is authoritative except + * that an effective complete section is restored afterwards as the sole + * prompt section, current suppression removes optional contexts, and + * required contexts keep their provider values; tool declarations stay + * provider-owned. + * @param context - the optional scope, step tool set, and plugin-defined assembly fields. + * @returns the post-waterfall assembly with ordered tool declarations. */ - async prepare(context: AssembleContext = {}): Promise { + // Keep configuration failures on the declared asynchronous error path. + async assemble(context: AssembleContext = {}): Promise { const scope = context.scope const scopeLayers = this.layers.chainLayers(scope) const runtimeContextSuppressed = !this.layers.global.runtimeContextSuppressors.isEmpty() @@ -634,28 +633,6 @@ export class SystemPrompt extends Service { variables[name] = provider(context) } } - const contexts = [...this.layers.merge(scope, layer => layer.contexts).values()] - .filter(entry => !runtimeContextSuppressed || entry.required === true) - .sort((a, b) => a.order - b.order) - .map(entry => resolveContext(entry, context)) - const preparation = { contexts, variables } - return this.ctx.waterfall(scopeTarget(this, scope), 'system-prompt/prepare', preparation, context, - () => Promise.resolve(preparation)) - } - - /** - * Assemble prompt sections and tool declarations after request preparation. - * The presentation waterfall cannot replace provider-owned tools. - * @param context - calling scope, route-bound agent, and resolved tools. - * @param preparation - accepted variables and runtime facts; prepared here when omitted. - * @returns the complete assembly with ordered tool declarations. - */ - async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise { - const prepared = preparation ?? await this.prepare(context) - const scope = context.scope - const scopeLayers = this.layers.chainLayers(scope) - const runtimeContextSuppressed = !this.layers.global.runtimeContextSuppressors.isEmpty() - || scopeLayers.some(layer => !layer.runtimeContextSuppressors.isEmpty()) // Scoped sections shadow globals before the deterministic order sort. const sectionByName = this.layers.merge(scope, layer => layer.sections) const contextByName = this.layers.merge(scope, layer => layer.contexts) @@ -695,7 +672,14 @@ export class SystemPrompt extends Service { return assembled }) const tools = orderTools(collected, this.toolOrder, knownNames) - const assembly: PromptPresentation = { ...prepared, sections } + const assembly: PromptPresentation = { + sections, + contexts: [...contextByName.values()] + .filter(entry => !runtimeContextSuppressed || entry.required === true) + .sort((a, b) => a.order - b.order) + .map(entry => resolveContext(entry, context)), + variables, + } const transformed = await this.ctx.waterfall( scopeTarget(this, scope), 'system-prompt/assemble', assembly, context, () => Promise.resolve(assembly), diff --git a/packages/core/system-prompt/tests/system-prompt.spec.ts b/packages/core/system-prompt/tests/system-prompt.spec.ts index a8bef706d1..33fbb153c7 100644 --- a/packages/core/system-prompt/tests/system-prompt.spec.ts +++ b/packages/core/system-prompt/tests/system-prompt.spec.ts @@ -51,6 +51,30 @@ describe('SystemPrompt', () => { await ctx.fiber.dispose() } }) + it('keeps required contexts at their provider values after presentation listeners', async () => { + const ctx = new Context() + try { + await ctx.plugin(SystemPrompt) + ctx.systemPrompt.context({ name: 'directory', order: 0, required: true, text: 'Directory: /project' }) + ctx.systemPrompt.context({ name: 'optional', order: 1, text: 'optional' }) + let rewrite = true + ctx.on('system-prompt/assemble', async (_assembly, _context, next) => { + const accepted = await next() + return rewrite + ? { ...accepted, contexts: accepted.contexts.map(entry => ({ ...entry, text: 'rewritten' })) } + : { ...accepted, contexts: accepted.contexts.filter(entry => entry.name !== 'directory') } + }) + expect((await ctx.systemPrompt.assemble()).contexts).toEqual([ + { name: 'directory', text: 'Directory: /project' }, { name: 'optional', text: 'rewritten' }, + ]) + rewrite = false + expect((await ctx.systemPrompt.assemble()).contexts).toEqual([ + { name: 'optional', text: 'optional' }, { name: 'directory', text: 'Directory: /project' }, + ]) + } finally { + await ctx.fiber.dispose() + } + }) it('refreshes registered context without reassembling accepted sections, schemas, variables, or expert additions', async () => { const ctx = new Context() try { diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml index 9103091c77..c0d8299e41 100644 --- a/packages/core/tools/README.i18n.yaml +++ b/packages/core/tools/README.i18n.yaml @@ -21,8 +21,8 @@ en: 9d2e6caa39c53d62 zh: 096a777f828c7565 /deepseek-ai-dsh-tools/use-this-package/conditional-tools: - en: 33618b68f90858c0 - zh: 10e20f40a5871555 + en: 1f8835ef3c0e16d2 + zh: 0e5fe6d06b403188 /deepseek-ai-dsh-tools/use-this-package/configure-the-presentation-mode: en: 4e27c45ae889369e zh: 7a0cd7a9dc7fe139 diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index d15bbaf35e..e4fd422699 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -65,9 +65,9 @@ The unified schema DSL supports `string`, `number`, `integer`, `boolean`, `null` ### Conditional tools -`register(definition, { when })` evaluates a synchronous or asynchronous condition once for each model step. The condition receives `{ agent, modelSelection, signal }`, where `modelSelection` is the provider, model, and reasoning effort that `agent/model` chose for every request attempt in the step. False omits the tool from Native declarations, PTC bindings, and execution. A thrown condition fails step preparation as `TOOL_ACTIVATION_FAILED`; it is not treated as ordinary unavailability. Conditions must observe cancellation and must not change registrations or perform tool work. +`register(definition, { when })` evaluates a synchronous or asynchronous condition once for each model step. The condition receives `{ agent, signal }`; `agent.step.modelSelection` is the provider, model, and reasoning effort that `agent/model` chose for every request attempt in the step. False omits the tool from Native declarations, PTC bindings, and execution. A thrown condition fails step preparation as `TOOL_ACTIVATION_FAILED`; it is not treated as ordinary unavailability. Conditions must observe cancellation and must not change registrations or perform tool work. -`resolve({ agent, modelSelection, signal })` returns a step-owned `ResolvedToolSet`. Pass it as `toolSet` to prompt assembly and every Native or PTC execution, then dispose it after the step's batch settles; retries in the step reuse it. Registrations stay effect-owned; each selection admits only the original registration in its exact agent scope. Disposal, new restrictions, cancellation, or same-name replacement prevent unstarted calls from using it. Started operations retain their cooperative cancellation obligations. Conditional calls without a resolved set return `UNKNOWN_TOOL`. +`resolve({ agent, signal })` returns a step-owned `ResolvedToolSet`. Pass it as `toolSet` to prompt assembly and every Native or PTC execution, then dispose it after the step's batch settles; retries in the step reuse it. Registrations stay effect-owned; each selection admits only the original registration in its exact agent scope. Disposal, new restrictions, cancellation, or same-name replacement prevent unstarted calls from using it. Started operations retain their cooperative cancellation obligations. Conditional calls without a resolved set return `UNKNOWN_TOOL`. `get(name, scope, toolSet)` and `schemas(scope, toolSet)` inspect an admitted set; omitting `toolSet` inspects registered definitions without evaluating conditions. Tool guidance receives the same selection as `AssembleContext.toolSet`. A presentation transform cannot add or remove callable tools. diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md index 3b60e0dbec..61c3f64bf6 100644 --- a/packages/core/tools/README.zh.md +++ b/packages/core/tools/README.zh.md @@ -65,9 +65,9 @@ ctx.tools.register(defineTool({ ### 条件工具 -`register(definition, { when })` 为每个模型步骤计算一次同步或异步条件。条件接收 `{ agent, modelSelection, signal }`,其中 `modelSelection` 是 `agent/model` 为该步骤所有请求尝试选定的提供方、模型与推理强度。返回 false 会从原生声明、PTC 绑定和执行集合中排除工具。条件抛错会以 `TOOL_ACTIVATION_FAILED` 中止步骤准备,不会被当成正常不可用。条件必须响应取消,不得改变注册或执行工具操作。 +`register(definition, { when })` 为每个模型步骤计算一次同步或异步条件。条件接收 `{ agent, signal }`;`agent.step.modelSelection` 是 `agent/model` 为该步骤所有请求尝试选定的提供方、模型与推理强度。返回 false 会从原生声明、PTC 绑定和执行集合中排除工具。条件抛错会以 `TOOL_ACTIVATION_FAILED` 中止步骤准备,不会被当成正常不可用。条件必须响应取消,不得改变注册或执行工具操作。 -`resolve({ agent, modelSelection, signal })` 返回步骤拥有的 `ResolvedToolSet`。将它作为 `toolSet` 传给提示词组装以及每次原生或 PTC 执行,步骤的批次结束后释放;同一步骤内的重试复用它。注册仍由 effect 持有;每份集合只准入原 agent scope 中的原注册。注销、新的限制、取消或同名替换会阻止尚未开始的调用继续使用它。已经开始的操作保留协作取消义务。没有已解析集合的条件工具调用返回 `UNKNOWN_TOOL`。 +`resolve({ agent, signal })` 返回步骤拥有的 `ResolvedToolSet`。将它作为 `toolSet` 传给提示词组装以及每次原生或 PTC 执行,步骤的批次结束后释放;同一步骤内的重试复用它。注册仍由 effect 持有;每份集合只准入原 agent scope 中的原注册。注销、新的限制、取消或同名替换会阻止尚未开始的调用继续使用它。已经开始的操作保留协作取消义务。没有已解析集合的条件工具调用返回 `UNKNOWN_TOOL`。 `get(name, scope, toolSet)` 和 `schemas(scope, toolSet)` 读取已准入集合;省略 `toolSet` 时只查看已注册定义,不计算条件。工具指导文本通过 `AssembleContext.toolSet` 接收同一集合。展示变换不能增删可调用工具。 diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 90d484052f..aa50a31ecd 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -11,7 +11,7 @@ import { AnonymousEntries, NamedEntries, ScopedLayers, scopeOf, scopeTarget } fr import type { ScopeKey, ScopeLayer, Scoped } from '@deepseek-ai/dsh-scope' import type { ToolCallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm' import { HarnessError } from '@deepseek-ai/dsh-llm' -import type { Agent, ModelSelection } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' import type { UserMessage } from '@deepseek-ai/dsh-session' import { assertNever, deepFreeze, snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values' import type { PromptSection, ToolProviderResult } from '@deepseek-ai/dsh-system-prompt' @@ -229,10 +229,8 @@ export interface ToolOutputDefinition { /** Inputs for a tool's step-time activation condition. */ export interface ToolActivationContext { - /** Agent preparing the step. */ + /** Agent preparing the step; `agent.step` holds the step's model selection. */ readonly agent: Agent - /** Provider, model, and reasoning effort that every request attempt in the step uses. */ - readonly modelSelection: ModelSelection /** Cancellation of step preparation; asynchronous conditions must observe it. */ readonly signal: AbortSignal } @@ -243,7 +241,7 @@ export interface ToolRegistrationOptions { * Decide whether the step's requests may call the tool. Omission enables * it. False excludes it from both declarations and dispatch; rejection fails * step preparation. Conditions must not mutate registrations or start tool work. - * @param context - preparing agent, step model selection, and cancellation signal. + * @param context - preparing agent and cancellation signal. * @returns whether the tool participates in this step. */ readonly when?: (context: ToolActivationContext) => boolean | Promise @@ -1311,7 +1309,7 @@ export class ToolRuntime extends Service { * Resolve conditions once for this step without changing shared registrations. * Cancellation or a rejected condition publishes no partial selection. Removed * registrations cannot be restored by a late condition result. - * @param context - preparing agent, step model selection, and cancellation. + * @param context - preparing agent and cancellation. * @returns the complete selection; the caller disposes it after its tool batch settles. */ async resolve(context: ToolActivationContext): Promise { diff --git a/packages/core/tools/tests/activation.spec.ts b/packages/core/tools/tests/activation.spec.ts index c9c4125aca..2c591987b7 100644 --- a/packages/core/tools/tests/activation.spec.ts +++ b/packages/core/tools/tests/activation.spec.ts @@ -19,7 +19,6 @@ async function setup() { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) const agent = { id: SessionId('activation') } as Agent - const modelSelection = { provider: 'test', model: 'test' } let scope!: Scope await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, agent) }, { inject: ['tools'] })) const controller = new AbortController() @@ -30,8 +29,8 @@ async function setup() { async execute() { calls++; return 'done' }, }) const input = { agent, name: tool.name, arguments: {}, callId: ToolCallId('call'), signal: controller.signal } - const resolve = () => ctx.tools.resolve({ agent, modelSelection, signal: controller.signal }) - return { ctx, agent, modelSelection, scope, controller, tool, input, resolve, calls: () => calls } + const resolve = () => ctx.tools.resolve({ agent, signal: controller.signal }) + return { ctx, agent, scope, controller, tool, input, resolve, calls: () => calls } } it('activates and deactivates a registered tool without allowing historical calls', async () => { @@ -104,8 +103,8 @@ it('does not restore a registration removed during its asynchronous condition', it('propagates cancellation after an asynchronous condition settles', async () => { const h = await setup() const condition = Promise.withResolvers() - h.ctx.tools.register(h.tool, { when: ({ modelSelection, signal }) => { - expect(modelSelection).toBe(h.modelSelection) + h.ctx.tools.register(h.tool, { when: ({ agent, signal }) => { + expect(agent).toBe(h.agent) expect(signal).toBe(h.controller.signal) return condition.promise } }) diff --git a/packages/core/tools/tests/ptc.spec.ts b/packages/core/tools/tests/ptc.spec.ts index 15045bf56f..dbd633de86 100644 --- a/packages/core/tools/tests/ptc.spec.ts +++ b/packages/core/tools/tests/ptc.spec.ts @@ -187,7 +187,7 @@ describe('mode-aware wire contribution', () => { const unregister = ctx.tools.register(definition, { when: () => active }) for (const enabled of [false, true]) { active = enabled - const toolSet = await ctx.tools.resolve({ agent, modelSelection: { provider: 'test', model: 'test' }, signal: testToolSignal }) + const toolSet = await ctx.tools.resolve({ agent, signal: testToolSignal }) try { const assembly = await systemPrompt.assemble({ scope: agent, agent, toolSet }) const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? '' diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index b3553b8aee..56ab68c929 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1660,15 +1660,15 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ methods: [ { signature: 'get(agent: Agent): { active: boolean; pending?: boolean }', - description: 'Read the logged plan state and any selected state awaiting the next accepted in-turn pre-step.', + description: 'Read the logged plan state and any selected state awaiting the next step boundary.', parameters: [{ name: 'agent', description: 'The agent to read.' }], returns: 'Current logged state plus a pending selection, when present.', }, { signature: 'set(agent: Agent, active: boolean): \'committed\' | \'queued\' | \'cancelled\' | \'noop\'', - description: 'Select whether plan mode should be active. Between turns the method appends the change immediately because no in-turn pre-step will run until another prompt starts a turn. The open-turn fold is the idle signal: agent status stays `running` through post-turn checkpointing, when no further in-turn pre-step runs. During an open turn the selection remains pending until the next accepted in-turn pre-step. Repeated selection of the current or already-pending state is a no-op.', + description: 'Select whether plan mode should be active. Between turns the method appends the change immediately because no step boundary will run until another prompt starts a turn. The open-turn fold is the idle signal: agent status stays `running` through post-turn checkpointing, when no further step boundary runs. During an open turn the selection remains pending until the next step boundary. Repeated selection of the current or already-pending state is a no-op.', parameters: [{ name: 'agent', description: 'The agent to switch.' }, { name: 'active', description: 'Whether plan mode should be active.' }], - returns: 'what happened: `committed` (logged now), `queued` (awaiting the next accepted in-turn pre-step), `cancelled` (an opposite pending selection was cleared; the logged state already matches), or `noop` (already in that state).', + returns: 'what happened: `committed` (logged now), `queued` (awaiting the next step boundary), `cancelled` (an opposite pending selection was cleared; the logged state already matches), or `noop` (already in that state).', }, ], }, @@ -2393,9 +2393,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'mention-carrying candidates in rank order.', }, { - signature: 'async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, modelSelection?: ModelSelection, ): Promise', - description: 'Snapshot all references for one accepted direct message and return one aggregated durable context. Automatic budgets use the step\'s model selection, or agent options for direct preparation. Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation. Truncated previews include omission facts and a full-snapshot spill locator, or an explicit unavailable notice. Cancellation prevents context publication, including when storage completes after cancellation.', - parameters: [{ name: 'agent', description: 'target agent; references to it are rejected.' }, { name: 'content', description: 'already host-normalized readable message content.' }, { name: 'references', description: 'structured source sessions in mention order.' }, { name: 'signal', description: 'optional cancellation boundary for the active turn.' }, { name: 'modelSelection', description: 'provider and model of the step admitting the message; omitted for direct preparation.' }], + signature: 'async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise', + description: 'Snapshot all references for one accepted direct message and return one aggregated durable context. Automatic budgets use the model of the agent\'s current step, or agent options outside a step. Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation. Truncated previews include omission facts and a full-snapshot spill locator, or an explicit unavailable notice. Cancellation prevents context publication, including when storage completes after cancellation.', + parameters: [{ name: 'agent', description: 'target agent; references to it are rejected.' }, { name: 'content', description: 'already host-normalized readable message content.' }, { name: 'references', description: 'structured source sessions in mention order.' }, { name: 'signal', description: 'optional cancellation boundary for the active turn.' }], returns: 'detached content and optional referenced-session context.', }, ], @@ -3095,22 +3095,16 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'the exact Cordis effect disposer.', }, { - signature: 'refreshContext(assembly: T, context: AssembleContext = {}): T', + signature: 'refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly', description: 'Refresh accepted registered runtime facts for request admission. Contexts added only by the assembly waterfall retain their accepted values. Current suppression removes optional contexts, and missing required registrations are restored in registry order. Sections, tools, and interpolation variables retain their accepted values during this refresh.', parameters: [{ name: 'assembly', description: 'accepted assembly for this scope and step.' }, { name: 'context', description: 'the same scope and current plugin-defined assembly fields.' }], returns: 'the accepted assembly with current runtime-context provider text.', }, { - signature: 'async prepare(context: AssembleContext = {}): Promise', - description: 'Prepare variables and runtime facts before accepting a step. No tool conditions or prompt sections are evaluated in this phase.', - parameters: [{ name: 'context', description: 'calling scope and request cancellation.' }], - returns: 'prepared inputs after the preparation waterfall.', - }, - { - signature: 'async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise', - description: 'Assemble prompt sections and tool declarations after request preparation. The presentation waterfall cannot replace provider-owned tools.', - parameters: [{ name: 'context', description: 'calling scope, route-bound agent, and resolved tools.' }, { name: 'preparation', description: 'accepted variables and runtime facts; prepared here when omitted.' }], - returns: 'the complete assembly with ordered tool declarations.', + signature: 'async assemble(context: AssembleContext = {}): Promise', + description: 'Assemble global and scoped providers, detach tool parameters, apply canonical ordering, then run the presentation waterfall. Scoped sections and variables shadow globals. The waterfall\'s text is authoritative except that an effective complete section is restored afterwards as the sole prompt section, current suppression removes optional contexts, and required contexts keep their provider values; tool declarations stay provider-owned.', + parameters: [{ name: 'context', description: 'the optional scope, step tool set, and plugin-defined assembly fields.' }], + returns: 'the post-waterfall assembly with ordered tool declarations.', }, ], }, @@ -3354,7 +3348,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ { signature: 'async resolve(context: ToolActivationContext): Promise', description: 'Resolve conditions once for this step without changing shared registrations. Cancellation or a rejected condition publishes no partial selection. Removed registrations cannot be restored by a late condition result.', - parameters: [{ name: 'context', description: 'preparing agent, step model selection, and cancellation.' }], + parameters: [{ name: 'context', description: 'preparing agent and cancellation.' }], returns: 'the complete selection; the caller disposes it after its tool batch settles.', }, { @@ -3985,15 +3979,15 @@ export const EVENT_API: readonly EventApiEntry[] = [ mode: 'waterfall', signature: '\'agent/model\'(this: Scoped, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise', summary: 'Choose the provider, model, and reasoning effort for one proposed step.', - description: 'Choose the provider, model, and reasoning effort for one proposed step. `await next()` yields the selection the machine would use: agent options before this loop logs a request, the logged header afterwards, with an effort only when one was chosen for that exact model. Return a replacement to switch; an effort belongs to its model, so a replacement that changes the model names that model\'s effort or omits it for the provider default. The loop runs this once per proposed step, before context preparation and `agent/pre-step`. Prompt preparation, tool conditions, and every request attempt in the step use the result, so a later choice applies from the next step. Cancellation here opens no step.', + description: 'Choose the provider, model, and reasoning effort for one proposed step. `await next()` yields the selection the machine would use: agent options before this loop logs a request, the logged header afterwards, with an effort only when one was chosen for that exact model. Return a replacement to switch; an effort belongs to its model, so a replacement that changes the model names that model\'s effort or omits it for the provider default. The loop runs this once per proposed step, after `agent/step-boundary` and before `agent/pre-step`, then publishes the result as `agent.step`. Pre-step listeners, tool conditions, prompt assembly, and every request attempt in the step use it, so a later choice applies from the next step. Cancellation here opens no step.', parameters: [{ name: 'payload', description: '.signal - the current turn\'s explicit abort signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }], }, { name: 'agent/pre-step', mode: 'waterfall', - signature: '\'agent/pre-step\'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; modelSelection: ModelSelection; signal: AbortSignal }, next: () => Promise): Promise', + signature: '\'agent/pre-step\'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise', summary: 'Reject a proposed step or replace the messages that enter it.', - description: 'Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages.', + description: 'Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages. `payload.agent.step` already holds the model selection that `agent/model` chose for this step.', parameters: [{ name: 'payload', description: '.signal - the current turn\'s cancellation signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }], }, { @@ -4020,6 +4014,14 @@ export const EVENT_API: readonly EventApiEntry[] = [ description: 'Agent status changed (`idle` ⇄ `running`). A waking delivery enters `running` synchronously after reserving cancellation; `idle` means no driver remains scheduled or active.', parameters: [{ name: 'payload', description: '.status - the status just entered (the transition\'s destination). Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }], }, + { + name: 'agent/step-boundary', + mode: 'serial', + signature: '\'agent/step-boundary\'(this: Scoped, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }): Promise | void', + summary: 'A proposed step reached its boundary: the loop claimed its input and is about to choose its model.', + description: 'A proposed step reached its boundary: the loop claimed its input and is about to choose its model. Listeners commit selections that take effect from this step, such as a pending plan-mode switch, so `agent/model`, `agent/pre-step`, tool conditions, and prompt assembly read one state. Runs once per proposed step; a throw fails the proposal, and cancellation opens no step.', + parameters: [{ name: 'payload', description: '.signal - the current turn\'s explicit abort signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }], + }, { name: 'agent/turn-stopping', mode: 'serial', @@ -4436,14 +4438,6 @@ export const EVENT_API: readonly EventApiEntry[] = [ description: 'Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope.', parameters: [], }, - { - name: 'system-prompt/prepare', - mode: 'waterfall', - signature: '\'system-prompt/prepare\'(this: Scoped, preparation: PromptPreparation, context: AssembleContext, next: () => Promise): Promise', - summary: 'Prepare variables and runtime context before step admission.', - description: 'Prepare variables and runtime context before step admission.', - parameters: [{ name: 'preparation', description: 'registered variables and context contributions.' }, { name: 'context', description: 'calling scope and cancellation.' }], - }, { name: 'tools/change', mode: 'emit', @@ -6384,13 +6378,9 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'PromptOrigin', declaration: 'export type PromptOrigin = {\n kind: \'composer\';\n} | {\n kind: \'plugin\';\n name: string;\n};', }, - { - name: 'PromptPreparation', - declaration: 'export interface PromptPreparation {\n contexts: AssembledContext[];\n variables: Record;\n}', - }, { name: 'PromptPresentation', - declaration: 'export interface PromptPresentation extends PromptPreparation {\n sections: AssembledSection[];\n}', + declaration: 'export interface PromptPresentation {\n sections: AssembledSection[];\n contexts: AssembledContext[];\n variables: Record;\n}', }, { name: 'PromptSection', @@ -7778,7 +7768,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SystemPrompt', - declaration: 'export class SystemPrompt extends Service {\n static Config: z;\n constructor(ctx: Context, config: Config);\n section(section: PromptSection): () => void;\n getSectionOrder(name: PromptSectionOrderName): number;\n getContextOrder(name: PromptContextOrderName): number;\n context(context: PromptContext): () => void;\n suppressRuntimeContext(): () => void;\n tools(provider: (context: AssembleContext) => ToolProviderResult): () => void;\n variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void;\n refreshContext(assembly: T, context: AssembleContext = {}): T;\n async prepare(context: AssembleContext = {}): Promise;\n async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise;\n}', + declaration: 'export class SystemPrompt extends Service {\n static Config: z;\n constructor(ctx: Context, config: Config);\n section(section: PromptSection): () => void;\n getSectionOrder(name: PromptSectionOrderName): number;\n getContextOrder(name: PromptContextOrderName): number;\n context(context: PromptContext): () => void;\n suppressRuntimeContext(): () => void;\n tools(provider: (context: AssembleContext) => ToolProviderResult): () => void;\n variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void;\n refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly;\n async assemble(context: AssembleContext = {}): Promise;\n}', }, { name: 'SystemPromptMessageSource', @@ -7966,7 +7956,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolActivationContext', - declaration: 'export interface ToolActivationContext {\n readonly agent: Agent;\n readonly modelSelection: ModelSelection;\n readonly signal: AbortSignal;\n}', + declaration: 'export interface ToolActivationContext {\n readonly agent: Agent;\n readonly signal: AbortSignal;\n}', }, { name: 'ToolAdditionBlock', diff --git a/packages/plan/plan-mode/README.i18n.yaml b/packages/plan/plan-mode/README.i18n.yaml index 9fadebb38e..33b681c436 100644 --- a/packages/plan/plan-mode/README.i18n.yaml +++ b/packages/plan/plan-mode/README.i18n.yaml @@ -39,14 +39,14 @@ en: 0acc88b042a99380 zh: e4cf66b77f2fc7ae /deepseek-ai-dsh-plan-mode/understand-the-implementation/durable-state-and-step-boundary-appends: - en: 6d0adadc354f211e - zh: 86fa49a9f4377368 + en: 1d2abc9a8ef4a1bf + zh: e09bc45a1d94934f /deepseek-ai-dsh-plan-mode/understand-the-implementation/the-plan-command: en: a7bf35c2a9da52bc zh: dea9f4b3a41f1a6d /deepseek-ai-dsh-plan-mode/understand-the-implementation/the-exit-tool: - en: 86b26413acb2e835 - zh: a84d876efefa5bea + en: cd036af680065de1 + zh: eed170bfef995d80 /deepseek-ai-dsh-plan-mode/understand-the-implementation/session-projection-unit: en: 74cd6bdf50af51f5 zh: 67609cf7202ececb @@ -99,8 +99,8 @@ en: 9d9626fbf4cdc8de zh: 8237cbc3ca048024 /deepseek-ai-dsh-plan-mode/known-limitations-and-deferred-work: - en: a87aabfede0087fd - zh: 201e9b3766980005 + en: 985f29a245876400 + zh: f7c9d7ed6eab1f55 /deepseek-ai-dsh-plan-mode/known-limitations-and-deferred-work/dev-note: en: 7de02cd33a767bd6 zh: d9e54003f230f725 diff --git a/packages/plan/plan-mode/README.md b/packages/plan/plan-mode/README.md index 26b24ed4c3..2c642ee0be 100644 --- a/packages/plan/plan-mode/README.md +++ b/packages/plan/plan-mode/README.md @@ -82,7 +82,7 @@ Plan mode is a product package, not a capability seam: there is no swappable bac ### Durable state and step-boundary appends -The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next accepted in-turn pre-step — the only append point while an agent runs — and an append failure stops step admission while retaining the pending selection. The `set`/`get` service methods and their exact return states live in [`src/index.ts`](src/index.ts) and read the registered `plan` projection; the first dependent access fails explicitly when the registry or key is absent. +The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next step boundary — the only append point while an agent runs, before the step chooses its model — and an append failure fails that proposal while retaining the pending selection. The `set`/`get` service methods and their exact return states live in [`src/index.ts`](src/index.ts) and read the registered `plan` projection; the first dependent access fails explicitly when the registry or key is absent. ### The `/plan` command @@ -90,7 +90,7 @@ The command child activates only when a commands service is composed. It maps ba ### The exit tool -`exit_plan_mode` is conditionally admitted only while the committed plan state is active. Entering or leaving changes the tool declarations and plan guidance together; Native and PTC execution use the same admitted set. An approved review records a silent pending exit that the next accepted in-turn pre-step appends, keeping plan guidance for the rest of the current tool batch. Without a user-questions channel, or after a service reload while the review is pending, the call fails closed and `/plan off` remains the manual escape. +`exit_plan_mode` is conditionally admitted only while the committed plan state is active. Entering or leaving changes the tool declarations and plan guidance together; Native and PTC execution use the same admitted set. An approved review records a silent pending exit that the next step boundary appends, keeping plan guidance for the rest of the current tool batch. Without a user-questions channel, or after a service reload while the review is pending, the call fails closed and `/plan off` remains the manual escape. ### Session projection unit @@ -182,7 +182,7 @@ Entering plan mode adds the exit tool through the route’s tool-update protocol These limits describe when plan mode does not behave as you might expect or needs extra care. They are current package constraints, not a roadmap. - **Guidance, not enforcement** — plan mode restrains through text only; deployments that need enforced restrictions configure sandbox mode and approval policy independently. -- **Pending selections are process-local** — a selection made after the turn's final accepted pre-step is lost if the process exits before another accepted in-turn pre-step; the UI must reapply it. +- **Pending selections are process-local** — a selection made after the turn's final step boundary is lost if the process exits before another step boundary; the UI must reapply it. - **No creation-time plan option** — forked agents inherit logged plan state, while newly spawned agents begin inactive. - **Live children cannot open the review** — a child owned by another live agent fails the `exit_plan_mode` call and is told to include the unresolved decision in its final result; durable fork lineage alone does not prevent a session resumed as a runtime root from opening the review. - **One specialized review renderer** — only the Web UI has a `plan-review` presentation; another interaction provider presents the same request through its generic option flow. diff --git a/packages/plan/plan-mode/README.zh.md b/packages/plan/plan-mode/README.zh.md index 3239d57803..758a5153db 100644 --- a/packages/plan/plan-mode/README.zh.md +++ b/packages/plan/plan-mode/README.zh.md @@ -82,7 +82,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并 ### 持久状态与步骤边界追加 -本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有打开的轮次时立即记录模式变化;轮次内则等待下一个被接纳的 pre-step。写入失败会停止步骤接纳并保留待提交选择。`set`/`get` 的精确返回状态见 [`src/index.ts`](src/index.ts);它们读取已注册的 `plan` 投影,缺少注册表或投影键时在首次依赖访问处明确失败。 +本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有打开的轮次时立即记录模式变化;轮次内则等待下一个步骤边界,这是 agent 运行时唯一的写入点,位于该步骤选择模型之前。写入失败会使该提案失败,并保留待提交选择。`set`/`get` 的精确返回状态见 [`src/index.ts`](src/index.ts);它们读取已注册的 `plan` 投影,缺少注册表或投影键时在首次依赖访问处明确失败。 ### `/plan` 命令 @@ -90,7 +90,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并 ### 退出工具 -`exit_plan_mode` 只在已提交的计划状态为 active 时准入。进入或离开会同时改变工具声明与计划指导;原生和 PTC 执行使用同一准入集合。批准评审后记录静默的待退出选择,由下一个被接纳的轮次内 pre-step 提交,当前工具批次的剩余调用保留计划指导。没有用户问答通道,或评审期间服务重载时,调用失败;`/plan off` 仍可手动退出。 +`exit_plan_mode` 只在已提交的计划状态为 active 时准入。进入或离开会同时改变工具声明与计划指导;原生和 PTC 执行使用同一准入集合。批准评审后记录静默的待退出选择,由下一个步骤边界提交,当前工具批次的剩余调用保留计划指导。没有用户问答通道,或评审期间服务重载时,调用失败;`/plan off` 仍可手动退出。 ### 会话投影单元 @@ -182,7 +182,7 @@ schema 仅在启用时按 ToolRuntime 模式计入开销;每份计划参数与 这些限制描述计划模式在哪些情况下不符合你的预期,或需要额外的注意。它们是当前包约束,不是路线图。 - **引导而非强制**——计划模式只通过文本约束;需要强制限制的部署要分别配置沙箱模式与审批策略。 -- **待生效选择只存在于进程内**——某轮最后一个被接受的 pre-step 之后作出的选择,若进程在另一个被接受的轮内 pre-step 之前退出就会丢失;UI 必须重新应用它。 +- **待生效选择只存在于进程内**——某轮最后一个步骤边界之后作出的选择,若进程在下一个步骤边界之前退出就会丢失;UI 必须重新应用它。 - **没有创建时 plan 选项**——fork 的 agent 继承已记录的计划状态,新 spawn 的 agent 则从未激活开始。 - **存活的子级无法打开评审**——由另一个存活 agent 所有的子级调用 `exit_plan_mode` 会失败,并被要求把尚未解决的决策包含进最终结果;仅有持久化 fork 谱系并不能阻止恢复为运行时根的会话打开该评审。 - **只有一个专用评审渲染器**——只有 Web UI 具备 `plan-review` 呈现;其他交互提供方通过其通用选项流程呈现同一请求。 diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts index e99f53f95a..717f459661 100644 --- a/packages/plan/plan-mode/src/index.ts +++ b/packages/plan/plan-mode/src/index.ts @@ -7,10 +7,11 @@ * state. * * The `plan` projection folds the session log, so resume and fork restore the - * state. User selections remain pending until the next accepted in-turn - * pre-step. The service appends `plan/mode` from `agent/pre-step` before - * step-time tool conditions and plan guidance read that committed state. - * Same-step retries do not commit pending selections. + * state. User selections made during a turn remain pending until the next + * step boundary. The service appends `plan/mode` from `agent/step-boundary`, + * before the step chooses its model, so `agent/model`, `agent/pre-step`, tool + * conditions, and plan guidance read the committed state. Same-step retries + * do not commit pending selections. * * The exit tool is admitted only for requests whose committed plan state is * active. Native declarations, PTC bindings, and execution share that selection. @@ -177,32 +178,40 @@ export class PlanModeController extends Service { private readonly section: string /** - * Latest selection per session awaiting the next accepted in-turn pre-step. + * Latest selection per session awaiting the next step boundary. * `narrate` is true for user selections and false for the exit tool, whose * result already narrates the transition. */ private readonly pendingIntents = new WeakMap() + /** Sessions whose boundary-committed user switch still owes a notice in an admitted step. */ + private readonly unannounced = new WeakSet() constructor(ctx: Context, config: PlanModeConfig = { section: '' }) { super(ctx, 'planMode') this.section = resolveConfig(config).section let disposed = false - // Pre-step is outside Session.append publication, so it can append the - // log-only mode event inside an open turn without re-entering the session. - // A failed append remains pending and stops admission before guidance or - // tool availability could describe an uncommitted state. + // The step boundary is outside Session.append publication, so it can append + // the log-only mode event inside an open turn without re-entering the + // session. A failed append remains pending and fails the proposal before + // model choice, guidance, or tool availability could read an uncommitted state. + ctx.on('agent/step-boundary', ({ agent }) => { + const pending = this.pendingIntents.get(agent.session) + if (pending === undefined) return + this.onBoundary(agent.session) + if (pending.narrate) this.unannounced.add(agent.session) + else this.unannounced.delete(agent.session) + }) + // The notice joins the first admitted step after the commit; a rejected + // proposal leaves it owed to the next one. ctx.on('agent/pre-step', async ( { agent, signal }, next, ): Promise => { const decision = await next() - const pending = this.pendingIntents.get(agent.session) - if (decision.kind === 'reject' || signal.aborted || pending === undefined) return decision - const narration = this.narration(agent.session, pending.active) - this.onBoundary(agent.session) - return !pending.narrate || narration === undefined - ? decision - : { ...decision, messages: [...decision.messages, narration] } + if (decision.kind === 'reject' || signal.aborted || !this.unannounced.has(agent.session)) return decision + this.unannounced.delete(agent.session) + const narration = this.narration(agent.session, this.loggedActive(agent.session)) + return narration === undefined ? decision : { ...decision, messages: [...decision.messages, narration] } }) ctx.effect(() => () => { disposed = true }, 'dsh-plan-mode: close service lifetime') @@ -239,7 +248,7 @@ export class PlanModeController extends Service { return { kind: 'success', text: 'Plan mode entry cancelled.' } case 'noop': // Repeat the queued wording while an exit still awaits the - // next accepted pre-step; only a truly inactive session reads + // next step boundary; only a truly inactive session reads // idempotent. return this.loggedActive(agent.session) ? { kind: 'success', text: 'Leaving plan mode (applies from the next step).' } @@ -328,8 +337,8 @@ export class PlanModeController extends Service { } throw cause } - // A review may outlive this plugin fiber. Without its pre-step listener, - // an approved selection could never be appended, so fail and keep planning. + // A review may outlive this plugin fiber. Without its step-boundary + // listener, an approved selection could never be appended, so fail and keep planning. if (disposed) { throw new Error('the plan-mode service was reloaded while the plan was under review; present the plan again') } @@ -342,8 +351,8 @@ export class PlanModeController extends Service { : `The user chose to keep planning; their feedback: ${feedback}`) } // Keep plan guidance for the rest of this assistant tool batch. The - // silent selection is appended at the next accepted in-turn pre-step, - // before its tool and prompt assembly. + // silent selection is appended at the next step boundary, before that + // step chooses its model, tools, and prompt. this.pendingIntents.set(agent.session, { active: false, narrate: false }) return { approved: true } }, @@ -384,7 +393,7 @@ export class PlanModeController extends Service { /** * Read the logged plan state and any selected state awaiting the next - * accepted in-turn pre-step. + * step boundary. * * @param agent The agent to read. * @returns Current logged state plus a pending selection, when present. @@ -397,17 +406,17 @@ export class PlanModeController extends Service { /** * Select whether plan mode should be active. Between turns the method - * appends the change immediately because no in-turn pre-step will run until + * appends the change immediately because no step boundary will run until * another prompt starts a turn. The open-turn fold is the idle signal: * agent status stays `running` through post-turn checkpointing, when no - * further in-turn pre-step runs. During an open turn the selection remains - * pending until the next accepted in-turn pre-step. Repeated selection of - * the current or already-pending state is a no-op. + * further step boundary runs. During an open turn the selection remains + * pending until the next step boundary. Repeated selection of the current + * or already-pending state is a no-op. * * @param agent The agent to switch. * @param active Whether plan mode should be active. * @returns what happened: `committed` (logged now), `queued` (awaiting the - * next accepted in-turn pre-step), `cancelled` (an opposite pending selection + * next step boundary), `cancelled` (an opposite pending selection * was cleared; the logged state already matches), or `noop` (already in that * state). */ @@ -433,7 +442,7 @@ export class PlanModeController extends Service { return 'committed' } - /** Append one pending selection before the next request assembly. */ + /** Append one pending selection at a step boundary. */ private onBoundary(session: Session): void { const pending = this.pendingIntents.get(session) if (pending === undefined) return @@ -443,8 +452,8 @@ export class PlanModeController extends Service { return } session.append('plan/mode', { active: target }) - // Delete only after append succeeds so a later accepted in-turn pre-step - // can retry a failed durable write. + // Delete only after append succeeds so a later step boundary can retry a + // failed durable write. this.pendingIntents.delete(session) } diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts index 562dedadf8..cc9e21b61d 100644 --- a/packages/plan/plan-mode/tests/plan-mode.spec.ts +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -19,7 +19,6 @@ import type { PlanModeConfig } from '../src/index.ts' import type { PlanUnitState } from '../src/types.ts' const testToolSignal = new AbortController().signal -const TEST_MODEL_SELECTION = { provider: 'test', model: 'test' } const TEST_PLAN_SECTION = 'Test plan mode instructions.' const PLAN_CONFIG = { section: TEST_PLAN_SECTION } satisfies PlanModeConfig @@ -36,8 +35,8 @@ function registerQuestionAnswerer(ctx: Context, answerer: QuestionAnswerer): () * Drives the REAL plugin: mounts `dsh-plan-mode` beside real `SystemPrompt` and * `ToolRuntime` services, with fake Agents carrying real `Session`s and a * real scoped `agent.ctx` minted through `createScope`. - * Request boundaries are simulated by dispatching the real pre-step waterfall - * and the following `step/start` session event used by the loop. + * Request boundaries are simulated by dispatching the real step-boundary and + * pre-step events and the following `step/start` session event used by the loop. */ async function agentWithSession( @@ -76,7 +75,7 @@ async function agentWithSession( } async function assembleFor(ctx: Context, agent: Agent) { - const toolSet = await ctx.tools.resolve({ agent, modelSelection: TEST_MODEL_SELECTION, signal: testToolSignal }) + const toolSet = await ctx.tools.resolve({ agent, signal: testToolSignal }) return ctx.systemPrompt.assemble({ agent, scope: agent, toolSet }) } @@ -106,19 +105,25 @@ async function setup(config: PlanModeConfig = PLAN_CONFIG): Promise { } /** - * Dispatch pre-step processing and optionally its following step-start commit. + * Dispatch a step boundary, its pre-step processing, and optionally the following step-start commit. */ -async function boundary(ctx: Context, agent: Agent & { session: Session }, type: 'pre-step' | 'step-start'): Promise { +async function boundary( + ctx: Context, + agent: Agent & { session: Session }, + type: 'pre-step' | 'step-start', + proposal: 'enter' | 'reject' = 'enter', +): Promise { const events = agentEvents(ctx, agent) const message = createUserMessage({ content: [{ type: 'text', text: 'boundary probe' }], source: { kind: 'user' }, }) const signal = new AbortController().signal + await events.serial('agent/step-boundary', { turn: 1, step: 1, signal }) const decision = await events.waterfall( 'agent/pre-step', - { messages: [message], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal }, - () => Promise.resolve({ kind: 'enter' as const, messages: [message] }), + { messages: [message], turn: 1, step: 1, signal }, + () => Promise.resolve(proposal === 'enter' ? { kind: 'enter' as const, messages: [message] } : { kind: 'reject' as const }), ) if (decision.kind === 'enter') { for (const message of decision.messages.slice(1)) { @@ -327,7 +332,7 @@ describe('the boundary flush', () => { expect(agent.session.snapshotEvents().some(event => event.type === 'plan/mode')).toBe(false) }) - it('flushes from pre-step before the following step/start', async () => { + it('flushes at the step boundary before the following step/start', async () => { const ctx = await setup() const agent = await agentWithSession(ctx) openTurn(agent.session) @@ -337,7 +342,7 @@ describe('the boundary flush', () => { expect(ctx.planMode.get(agent)).toEqual({ active: true }) }) - it('removes the pre-step flush when the plugin fiber is disposed', async () => { + it('removes the boundary flush when the plugin fiber is disposed', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) @@ -400,6 +405,19 @@ describe('the boundary flush', () => { expect(noticeTexts(agent.session)).toEqual(['The user switched this session back to the default mode.']) }) + it('commits at a rejected proposal and owes the notice to the next admitted step', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + header(agent.session) + openTurn(agent.session) + ctx.planMode.set(agent, true) + await boundary(ctx, agent, 'pre-step', 'reject') + expect(foldPlanMode(agent.session.snapshotEvents())).toBe(true) + expect(noticeTexts(agent.session)).toEqual([]) + await boundary(ctx, agent, 'pre-step') + expect(noticeTexts(agent.session)).toEqual(['The user switched this session to plan mode.']) + }) + it('stays silent when the header already reflects the flushed mode', async () => { const ctx = await setup() const agent = await agentWithSession(ctx) @@ -439,7 +457,7 @@ describe('the boundary flush', () => { expect(ctx.planMode.get(agent).pending).toBeUndefined() }) - it('rejects a pre-step append failure and keeps the intent pending', async () => { + it('rejects a boundary append failure and keeps the intent pending', async () => { const ctx = await setup() const warn = vi.fn() ctx.logger.warn = warn as never @@ -798,7 +816,7 @@ describe('exit_plan_mode', () => { async function callExit(ctx: Context, agent: Agent | undefined, plan = '# The plan\n\ndo things') { const signal = new AbortController().signal - const toolSet = agent === undefined ? undefined : await ctx.tools.resolve({ agent, modelSelection: TEST_MODEL_SELECTION, signal }) + const toolSet = agent === undefined ? undefined : await ctx.tools.resolve({ agent, signal }) return ctx.tools.execute({ ...toolSet === undefined ? {} : { toolSet }, callId: ToolCallId(`call-exit-${++callCounter}`), @@ -933,7 +951,7 @@ describe('exit_plan_mode', () => { const agent = await agentWithSession(ctx, 'ptc-exit', { active: true }) const result = await ctx.tools.execute({ - toolSet: await ctx.tools.resolve({ agent, modelSelection: TEST_MODEL_SELECTION, signal: testToolSignal }), + toolSet: await ctx.tools.resolve({ agent, signal: testToolSignal }), callId: ToolCallId(`call-exit-${++callCounter}`), name: RUN_CODE_NAME, arguments: { code: `return await tools.${EXIT_PLAN_MODE}({ plan: ${JSON.stringify(plan)} })`, description: 'Submit the plan for review' }, @@ -1083,7 +1101,7 @@ describe('exit_plan_mode', () => { const { ctx, agent, asked } = await setupWithReview({ selected: ['Approve'] }) const controller = new AbortController() const result = await ctx.tools.execute({ - toolSet: await ctx.tools.resolve({ agent, modelSelection: TEST_MODEL_SELECTION, signal: testToolSignal }), + toolSet: await ctx.tools.resolve({ agent, signal: testToolSignal }), callId: ToolCallId(`call-exit-${++callCounter}`), name: EXIT_PLAN_MODE, arguments: { plan: '# P' }, diff --git a/packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts b/packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts index d7991f7c0c..654b1ab18f 100644 --- a/packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts +++ b/packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts @@ -220,7 +220,7 @@ describe('session-checkpoint-policy tool and step boundaries', () => { ctx.on('session/flush', (current) => { flushed.push(current.id) }) const signal = new AbortController().signal await agentEvents(ctx, agent).waterfall( - 'agent/pre-step', { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal }, + 'agent/pre-step', { messages: [], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'enter', messages: [] }), ) expect(flushed).toEqual([session.id]) diff --git a/packages/session/working-directory/README.i18n.yaml b/packages/session/working-directory/README.i18n.yaml index 2a03b75013..a593c37b80 100644 --- a/packages/session/working-directory/README.i18n.yaml +++ b/packages/session/working-directory/README.i18n.yaml @@ -18,8 +18,8 @@ en: 0cd615b8f987f950 zh: ccf4964df789011c /deepseek-ai-dsh-working-directory/understand-the-implementation: - en: 6f7b264aef175243 - zh: e2798c0aab0eb21b + en: f0458147d29631b1 + zh: 6a288670dd084ffb /deepseek-ai-dsh-working-directory/further-exploration: en: eabff683146786c8 zh: 7997dec17802ef64 @@ -39,8 +39,8 @@ en: f5a095acef81df3b zh: 758796edbc4337fc /deepseek-ai-dsh-working-directory/known-limitations-and-deferred-work: - en: 17ecae217e34f3cd - zh: 92d52f4ced97d044 + en: a0f7bcd972f4a4fb + zh: 051ec0eb0fa92bea /deepseek-ai-dsh-working-directory/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: a4b126f9a4eaca6f diff --git a/packages/session/working-directory/README.md b/packages/session/working-directory/README.md index 8866317c01..1f54ad8c92 100644 --- a/packages/session/working-directory/README.md +++ b/packages/session/working-directory/README.md @@ -39,7 +39,7 @@ Mount alongside `fs`, `sessionProjections`, and `systemPrompt`. `defaultDirector
Implementation internals — click to expand -The Session projection owns the effective directory and exposes it in Session observations, so cold readers can use it without activating an Agent; the header continues to identify the original project. Changes serialize per Session, validate through the filesystem provider, check cancellation and Agent lifetime, commit an event, and queue a user-context notice. If notice queuing fails after commit, the operation returns the committed directory and logs a warning; the next request or resume still receives the required directory context. `system-prompt/prepare` validates the current directory before step admission and before publishing its required context contribution. Existing processes and sandbox grants keep their independent ownership. The [service implementation](src/index.ts) defines the API. +The Session projection owns the effective directory and exposes it in Session observations, so cold readers can use it without activating an Agent; the header continues to identify the original project. Changes serialize per Session, validate through the filesystem provider, check cancellation and Agent lifetime, commit an event, and queue a user-context notice. If notice queuing fails after commit, the operation returns the committed directory and logs a warning; the next request or resume still receives the required directory context. Prompt assembly validates the current directory before publishing its required context contribution. Existing processes and sandbox grants keep their independent ownership. The [service implementation](src/index.ts) defines the API. No runtime invariant companion is published because the service reads its only directory state from the Session projection and checks filesystem validity at use. @@ -77,7 +77,7 @@ Directory transitions append context without replacing the stable system-prompt -- **Missing original project** — when the current directory and the original project are both unavailable, step preparation rejects every model turn, including chat-only turns. The service does not choose or recreate a repository. Restore the original directory, or use SDK `setWorkingDirectory` (`set_working_directory` in Python) or `ctx.workingDirectory.set` to select an existing absolute directory before retrying. Existing shells retain their process-local directories. +- **Missing original project** — when the current directory and the original project are both unavailable, prompt assembly rejects every model turn, including chat-only turns. The service does not choose or recreate a repository. Restore the original directory, or use SDK `setWorkingDirectory` (`set_working_directory` in Python) or `ctx.workingDirectory.set` to select an existing absolute directory before retrying. Existing shells retain their process-local directories. ### Dev Note diff --git a/packages/session/working-directory/README.zh.md b/packages/session/working-directory/README.zh.md index f3e65358b5..ad92f21b59 100644 --- a/packages/session/working-directory/README.zh.md +++ b/packages/session/working-directory/README.zh.md @@ -39,7 +39,7 @@ kind: "package-reference"
实现细节 — 点击展开 -Session 投影拥有当前目录,并通过 Session observation 暴露它,因此冷态读取方无需激活 Agent 即可使用;header 继续标识原始项目。每个 Session 的变更串行执行,通过文件系统 provider 验证,并检查取消状态与 Agent 生命周期,提交事件后排入用户上下文通知。若提交后通知入队失败,操作仍返回已提交的目录并记录警告;下一次请求或恢复仍会收到必需的目录上下文。`system-prompt/prepare` 在步骤接纳以及发布必需的上下文条目前验证当前目录。已有进程与沙箱授权继续由各自的服务拥有。[服务实现](src/index.ts) 定义 API。 +Session 投影拥有当前目录,并通过 Session observation 暴露它,因此冷态读取方无需激活 Agent 即可使用;header 继续标识原始项目。每个 Session 的变更串行执行,通过文件系统 provider 验证,并检查取消状态与 Agent 生命周期,提交事件后排入用户上下文通知。若提交后通知入队失败,操作仍返回已提交的目录并记录警告;下一次请求或恢复仍会收到必需的目录上下文。提示词组装在发布必需的上下文条目前验证当前目录。已有进程与沙箱授权继续由各自的服务拥有。[服务实现](src/index.ts) 定义 API。 不发布 `./invariant` 伴生模块,因为服务从 Session 投影读取唯一的目录状态,并在使用时检查文件系统有效性。 @@ -77,7 +77,7 @@ Session 投影拥有当前目录,并通过 Session observation 暴露它,因 -- **原始项目缺失** — 当前目录与原始项目均不可用时,步骤准备会拒绝每个模型轮次,包括纯聊天轮次。服务不会另选或重建仓库。请先恢复原始目录,或通过 SDK 的 `setWorkingDirectory`(Python 中为 `set_working_directory`)或 `ctx.workingDirectory.set` 选择现有的绝对目录,再重试。已有 shell 保留各自进程的目录。 +- **原始项目缺失** — 当前目录与原始项目均不可用时,提示词组装会拒绝每个模型轮次,包括纯聊天轮次。服务不会另选或重建仓库。请先恢复原始目录,或通过 SDK 的 `setWorkingDirectory`(Python 中为 `set_working_directory`)或 `ctx.workingDirectory.set` 选择现有的绝对目录,再重试。已有 shell 保留各自进程的目录。 ### 开发备注 diff --git a/packages/session/working-directory/src/index.ts b/packages/session/working-directory/src/index.ts index 90156169ed..7346feb274 100644 --- a/packages/session/working-directory/src/index.ts +++ b/packages/session/working-directory/src/index.ts @@ -94,7 +94,7 @@ export class WorkingDirectoryService extends Service { interpolate: false, text: context => context.agent === undefined ? '' : this.contextText(context.agent.session), }) - ctx.on('system-prompt/prepare', async (_assembly, context, next) => { + ctx.on('system-prompt/assemble', async (_assembly, context, next) => { if (context.agent === undefined) return next() await this.ensure(context.agent, context.signal) const assembly = await next() diff --git a/packages/session/working-directory/tests/working-directory.spec.ts b/packages/session/working-directory/tests/working-directory.spec.ts index a3fd4281b5..dbd6775312 100644 --- a/packages/session/working-directory/tests/working-directory.spec.ts +++ b/packages/session/working-directory/tests/working-directory.spec.ts @@ -9,7 +9,7 @@ import WorkingDirectory, { type Config as WorkingDirectoryConfig } from '@deepse import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit' import { SessionId } from '@deepseek-ai/dsh-session' import { ToolCallId } from '@deepseek-ai/dsh-llm' -import { renderContextSnapshot, renderPrompt, type AssembleContext, type PromptPreparation } from '@deepseek-ai/dsh-system-prompt' +import { renderContextSnapshot, renderPrompt } from '@deepseek-ai/dsh-system-prompt' import * as WorkingDirectoryTool from '@deepseek-ai/dsh-tool-working-directory' async function fixture(config?: (root: string) => WorkingDirectoryConfig) { @@ -177,13 +177,9 @@ describe('Session working directories', () => { })]) }) - it.each((['system-prompt/prepare', 'system-prompt/assemble'] as const).flatMap(event => - (['remove', 'replace'] as const).map(change => ({ event, change })), - ))('retains required current-directory context after $event can $change it', async ({ event, change }) => { + it.each(['remove', 'replace'] as const)('retains required current-directory context after an assembly transform can %s it', async (change) => { await using f = await fixture() - f.ctx.on(event, async ( - _assembly: Assembly, _context: AssembleContext, next: () => Promise, - ) => { + f.ctx.on('system-prompt/assemble', async (_assembly, _context, next) => { const assembled = await next() return { ...assembled, diff --git a/packages/skill/tool-skill/tests/tool-skill.spec.ts b/packages/skill/tool-skill/tests/tool-skill.spec.ts index b1bfb6776f..e3362cda0a 100644 --- a/packages/skill/tool-skill/tests/tool-skill.spec.ts +++ b/packages/skill/tool-skill/tests/tool-skill.spec.ts @@ -116,7 +116,7 @@ async function fireStep(ctx: Context, agent: Agent, turn: number, step: number): const signal = new AbortController().signal const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [], turn, step, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages: [], turn, step, signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) if (decision.kind === 'enter') { @@ -134,7 +134,7 @@ async function proposeStep( const signal = new AbortController().signal return await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages, turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages, turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'enter' as const, messages }), ) } @@ -166,7 +166,7 @@ async function composePrefix(ctx: Context, cwd: string, signal = new AbortContro async function composePrefixForAgent(ctx: Context, agent: Agent, signal = new AbortController().signal): Promise { const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages: [], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'enter' as const, messages: [] }), ) if (decision.kind === 'enter') { @@ -1104,7 +1104,7 @@ describe('user-explicit invocation injection', () => { const signal = new AbortController().signal const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', - { messages: [gesture('/hidden-demo blocked step')], turn: 1, step: 1, modelSelection: { provider: 'test', model: 'test' }, signal }, + { messages: [gesture('/hidden-demo blocked step')], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'reject' as const }), ) expect(decision).toEqual({ kind: 'reject' }) diff --git a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts index 4f5b02bd1d..66ec1314e2 100644 --- a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts @@ -275,7 +275,6 @@ describe('SubagentModelSelectionConfig', () => { messages: [], turn: 1, step: 1, - modelSelection: { provider: 'test', model: 'test' }, signal: new AbortController().signal, } await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next)) diff --git a/scripts/fixtures/python-sdk-dynamic-tools.mjs b/scripts/fixtures/python-sdk-dynamic-tools.mjs index 99fc3722db..83b8439ac7 100644 --- a/scripts/fixtures/python-sdk-dynamic-tools.mjs +++ b/scripts/fixtures/python-sdk-dynamic-tools.mjs @@ -14,7 +14,7 @@ export function apply(ctx) { render(_args, value) { return [{ type: 'text', text: value }] }, }, async execute() { return 'pong' }, - }, { when: async ({ modelSelection }) => active && modelSelection.provider === 'deepseek-official' }) + }, { when: async ({ agent }) => active && agent.step?.modelSelection.provider === 'deepseek-official' }) ctx.on('tools/post-execute', async (exec, result, next) => { const downstream = await next() if (result.isError) return downstream diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 7b886ad3fd..41d6cf8657 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -320,6 +320,7 @@ export const LINK_MAP: Readonly> = { AgentFactory: 'core.md', AgentHandle: 'core.md', ModelSelection: 'core.md', + AgentStep: 'core.md', AllowedModelRoute: 'subagent.md', SubagentModelSelectionSettings: 'subagent.md', AgentOptions: 'core.md', @@ -941,7 +942,6 @@ export const TYPE_LINK_EXEMPTIONS: Readonly> = { ToolRegistrationOptions: 'conditional registration options are owned by packages/core/tools/README.md', ResolvedToolSet: 'step-owned admission is owned by packages/core/tools/README.md', PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md', - PromptPreparation: 'preparation inputs are owned by packages/core/system-prompt/README.md', PromptPresentation: 'presentation inputs are owned by packages/core/system-prompt/README.md', RequestRunId: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts', RpcReceipt: 'carrier-layer receipt is owned by packages/client/connection/src/rpc.ts', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 01c3f8376b..8191d85fd3 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -1548,22 +1548,22 @@ function renderLifecycle(): string { ' Note over Agent,Driver: claim pending next-step input plus one queued prompt', ` Driver-->>SDK: ${mermaidCode('agent/inbox/spliced')} pure deletion`, ` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`, - ` Driver->>Hooks: ${mermaidCode('agent/model')} waterfall chooses the step provider, model, and effort`, - ` Driver->>Prompt: ${mermaidCode('system-prompt/prepare')} waterfall with the step model selection`, - ` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall with the step model selection`, + ` Driver->>Hooks: ${mermaidCode('agent/step-boundary')} serial commits selections pending for this step`, + ` Driver->>Hooks: ${mermaidCode('agent/model')} waterfall chooses provider, model, and effort, published as agent.step`, + ` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall reads agent.step`, ' Hooks-->>Driver: authoritative reject or enter(messages)', ' alt proposed step rejected, first batch empty, or pre-step failed', ' Driver-->>Driver: claimed batch stays removed, the open turn spends no step', ' else enter proposed step', ` Driver->>Session: ${mermaidCode('step/start')}`, - ' Driver->>Tools: resolve({ agent, modelSelection, signal }) once per step', - ` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} with the step tool set`, + ' Driver->>Tools: resolve({ agent, signal }) once per step', + ` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} contexts, variables, sections, and the step tool set`, ` Driver->>Hooks: ${mermaidCode('agent/request')} waterfall with the step model selection`, ' Driver->>LLM: prepareCall(config, signal)', ' Note over Driver,LLM: cancellation during preparation commits neither system nor users', ' Note over Driver,Session: synchronous admission using the prepared call capability', ` Driver->>Session: ${mermaidCode('system/message')} ordered per-node reconciliation`, - ` Driver->>Session: ${mermaidCode('user/message')} per entered message`, + ` Driver->>Session: ${mermaidCode('user/message')} per entered message, then a changed runtime-context snapshot`, ` Driver->>Session: ${mermaidCode('request/header')} and ${mermaidCode('request/context')} as needed`, ' Driver->>Driver: derive and freeze request from the log', ` Driver->>LLM: bound prepared call through ${mermaidCode('llm/stream')} waterfall`, @@ -1598,8 +1598,8 @@ function renderLifecycle(): string { ' opt next-step input is pending', ' Driver-->>Driver: claim pending next-step input', ` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`, + ` Driver->>Hooks: ${mermaidCode('agent/step-boundary')} serial`, ` Driver->>Hooks: ${mermaidCode('agent/model')} waterfall`, - ` Driver->>Prompt: ${mermaidCode('system-prompt/prepare')} waterfall`, ` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`, ' Hooks-->>Driver: authoritative reject or enter(messages)', ' end', diff --git a/scripts/python-snapshot-workflow-order.spec.ts b/scripts/python-snapshot-workflow-order.spec.ts index 3461e5faf6..b82796cb52 100644 --- a/scripts/python-snapshot-workflow-order.spec.ts +++ b/scripts/python-snapshot-workflow-order.spec.ts @@ -44,7 +44,7 @@ async function harness(direct = false) { runId: WorkflowRunId('run'), seq: 1, label: 'workflow-child', childId, }) const step = (overrides = {}) => agentEvents(ctx, agent).waterfall('agent/pre-step', { - turn: 1, step: 1, messages, modelSelection: { provider: 'test', model: 'test' }, signal: controller.signal, ...overrides, + turn: 1, step: 1, messages, signal: controller.signal, ...overrides, }, next) return { ctx, fiber, parent, other, session, agent, controller, decision, next, start, step } } diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 997dd53cfe..bd35522ab7 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -2181,6 +2181,11 @@ "symbol": "ModelSelection", "source": "packages/core/agent/src/runtime-types.ts" }, + { + "doc": "docs/subsystems/core.md", + "symbol": "AgentStep", + "source": "packages/core/agent/src/runtime-types.ts" + }, { "doc": "docs/subsystems/feedback.md", "symbol": "MessageFeedbackVersion",