diff --git a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts index c84912a9f1..87eb1a73b5 100644 --- a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts +++ b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts @@ -27,6 +27,7 @@ try { const agentId = SessionId('dsh-badge-snapshot') const session = ctx.sessions.create(agentId, { meta: { cwd: process.cwd() } }) const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, ctx, id: agentId, options: {}, diff --git a/docs/agent-lifecycle.i18n.yaml b/docs/agent-lifecycle.i18n.yaml index cb75783c9c..3871250205 100644 --- a/docs/agent-lifecycle.i18n.yaml +++ b/docs/agent-lifecycle.i18n.yaml @@ -6,5 +6,5 @@ en: 41d772192075e133 zh: 305a6826a598b96d /agent-turn-and-step-lifecycle: - en: a1389c8ed7e37324 - zh: c2944b9192af4ec9 + en: 8b6b2eff5a5c93b9 + zh: f63a8b82e1888d12 diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index 6ffe3c2b47..eceb9229fc 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -25,16 +25,18 @@ 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->>Prompt: system-prompt/assemble waterfall + Driver->>Hooks: agent/request waterfall + Driver->>LLM: prepareCall(config, signal), bind Agent.currentRoute() + Driver->>Prompt: system-prompt/prepare waterfall Driver->>Hooks: agent/pre-step waterfall 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->>Hooks: agent/request waterfall - Driver->>LLM: prepareCall(config, signal) - Note over Driver,LLM: cancellation during either async phase commits neither system nor users + Driver->>Tools: resolve({ agent, signal }) + Driver->>Prompt: system-prompt/assemble with resolved tool set + 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 @@ -48,7 +50,7 @@ sequenceDiagram Driver-->>SDK: agent/assistant-stream committed end Driver->>Hooks: agent/request-error waterfall Hooks-->>Driver: return retry action or preserve the original error - Note over Driver,LLM: retry in the open step: prepare and reconcile the same rendered assembly without repeating pre-step or users + Note over Driver,LLM: retry resolves route and tools. Unchanged inputs reuse the prompt without repeating pre-step or users else model request succeeded Driver->>Session: assistant/message Driver-->>SDK: agent/assistant-stream committed end @@ -56,7 +58,7 @@ sequenceDiagram loop barriers and bounded rolling pool, reclassify before start opt call starts Driver->>Session: tool/call - Driver->>Tools: ordered pre, concurrent execute + Driver->>Tools: ordered pre, concurrent execute with request tool set Tools-->>Session: tool-owned events when applicable end opt next model-order result ready @@ -64,13 +66,16 @@ sequenceDiagram Driver->>Session: tool/result end end + Driver->>Tools: dispose request tool set Driver->>Session: step/end + Driver->>Driver: clear bound route opt natural stop and next-step inbox empty Driver->>Hooks: agent/turn-stopping serial terminal checkpoint end opt next-step input is pending Driver-->>Driver: claim pending next-step input Driver-->>SDK: agent/inbox/claimed { message, turn } per message + Driver->>Driver: prepare route and runtime context for the next step Driver->>Hooks: agent/pre-step waterfall Hooks-->>Driver: authoritative reject or enter(messages) end @@ -82,7 +87,7 @@ sequenceDiagram The `assistant/message` event records every successful provider call, including content-less and `max-tokens` finishes, and embeds the exact compact timed stream. Empty content stays out of derived history. A failed, retried, cancelled, or stream-error attempt that reaches settlement without a surface message records its stream as `assistant/attempt`. Live `agent/assistant-stream` chunk frames are transient; replay reads either durable settlement, and a hard process loss before settlement leaves no durable attempt stream. -`dsh-compaction-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery runs within the open step and retries only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative. Each retry prepares its call and reconciles the retained rendered assembly before request derivation, without repeating assembly, pre-step, or user admission. +`dsh-compaction-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery runs within the open step and retries only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative. Each retry prepares its call and resolves tool conditions before request derivation, without repeating pre-step or user admission. Prompt assembly repeats only when the bound route or tool schemas change. The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages and `startsRequestSeries` unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch. diff --git a/docs/agent-lifecycle.zh.md b/docs/agent-lifecycle.zh.md index c4b2fa70db..74b8fb05f6 100644 --- a/docs/agent-lifecycle.zh.md +++ b/docs/agent-lifecycle.zh.md @@ -27,16 +27,18 @@ 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->>Prompt: system-prompt/assemble waterfall + Driver->>Hooks: agent/request waterfall + Driver->>LLM: prepareCall(config, signal), bind Agent.currentRoute() + Driver->>Prompt: system-prompt/prepare waterfall Driver->>Hooks: agent/pre-step waterfall 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->>Hooks: agent/request waterfall - Driver->>LLM: prepareCall(config, signal) - Note over Driver,LLM: cancellation during either async phase commits neither system nor users + Driver->>Tools: resolve({ agent, signal }) + Driver->>Prompt: system-prompt/assemble with resolved tool set + 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 @@ -50,7 +52,7 @@ sequenceDiagram Driver-->>SDK: agent/assistant-stream committed end Driver->>Hooks: agent/request-error waterfall Hooks-->>Driver: return retry action or preserve the original error - Note over Driver,LLM: retry in the open step: prepare and reconcile the same rendered assembly without repeating pre-step or users + Note over Driver,LLM: retry resolves route and tools. Unchanged inputs reuse the prompt without repeating pre-step or users else model request succeeded Driver->>Session: assistant/message Driver-->>SDK: agent/assistant-stream committed end @@ -58,7 +60,7 @@ sequenceDiagram loop barriers and bounded rolling pool, reclassify before start opt call starts Driver->>Session: tool/call - Driver->>Tools: ordered pre, concurrent execute + Driver->>Tools: ordered pre, concurrent execute with request tool set Tools-->>Session: tool-owned events when applicable end opt next model-order result ready @@ -66,13 +68,16 @@ sequenceDiagram Driver->>Session: tool/result end end + Driver->>Tools: dispose request tool set Driver->>Session: step/end + Driver->>Driver: clear bound route opt natural stop and next-step inbox empty Driver->>Hooks: agent/turn-stopping serial terminal checkpoint end opt next-step input is pending Driver-->>Driver: claim pending next-step input Driver-->>SDK: agent/inbox/claimed { message, turn } per message + Driver->>Driver: prepare route and runtime context for the next step Driver->>Hooks: agent/pre-step waterfall Hooks-->>Driver: authoritative reject or enter(messages) end @@ -84,7 +89,7 @@ sequenceDiagram `assistant/message` 事件会记录每次成功的提供方调用,包括返回空内容或以 `max-tokens` 结束的调用,并嵌入精确的紧凑带时间 stream。空内容不会进入派生历史。失败、重试、取消或 stream error attempt 到达 settlement 时,如果没有 surface message,就会把 stream 记录为 `assistant/attempt`。实时 `agent/assistant-stream` chunk frame 是瞬态数据;回放读取任一种持久 settlement,如果进程在 settlement 前硬中断,则不会留下持久 attempt stream。 -`dsh-compaction-basic` 在派生请求之前通过 `agent/pre-step` 处理压力,而 `agent/request-error` 仅用于规范的上下文溢出。任一触发条件满足后,系统都会先执行可选的工具结果剪枝,再选择摘要。恢复发生在仍打开的步骤内,只有剪枝或摘要生成推进 surface replacement generation 时才重试,否则仍以原始请求错误为准。每次重试都会准备调用,并在派生请求之前协调保留的已渲染组装结果,不重复组装、pre-step 或用户消息准入。 +`dsh-compaction-basic` 在派生请求之前通过 `agent/pre-step` 处理压力,而 `agent/request-error` 仅用于规范的上下文溢出。任一触发条件满足后,系统都会先执行可选的工具结果剪枝,再选择摘要。恢复发生在仍打开的步骤内,只有剪枝或摘要生成推进 surface replacement generation 时才重试,否则仍以原始请求错误为准。每次重试都会在派生请求之前准备调用并解析工具条件,不重复 pre-step 或用户消息准入。只有已绑定路由或工具 schema 变化时才重新组装提示词。 以返回的 `agent/pre-step` 决策为准;通过包装 `next()` 的监听器会保留下游消息与 `startsRequestSeries`,除非有意替换。steering(中途引导)和注入的上下文在后续的认领操作取得其下一步骤批次后,会经过同一 waterfall(瀑布式事件)。 diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 8206f3e6dc..a1eca06e56 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: e6402b28c0ed15fa - zh: 6c07be9047b42056 + en: e7f529503233d805 + zh: f1d1e4a58e44a2ec /deepseek-harness-architecture/session-log: en: a2e35c43438fec6d zh: e27bdac8e9feff32 diff --git a/docs/architecture.md b/docs/architecture.md index 03d88148c7..9157ba13a9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -83,12 +83,13 @@ 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 - assemble prompt sections + tool schemas + agent/request -> prepareCall -> bind Agent.currentRoute() + prepare variables + runtime context -> agent/pre-step; project runtime context at fallback reject | enter(messages, startsRequestSeries?) reject, or a first enter rewritten empty -> close the turn with no step step/start - agent/request -> prepareCall (cancellation commits neither system nor users) + resolve conditional tools + assemble prompt using that tool set reconcile system/message using the prepared call capability append entered messages as user/message; reconcile retained runtime context log request/header and request/context as needed @@ -107,7 +108,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 }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. 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/request` and `prepareCall()` bind the route before context preparation and `agent/pre-step`; cancellation commits no step or messages. Accepted steps resolve tool conditions before final assembly. 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 do not repeat `agent/pre-step`; prompt assembly repeats only when the resolved route or tool schemas change. 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 870fa1c996..58f110c928 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -87,12 +87,13 @@ AgentLoop 在启动已排队工作前等待串行 `agent/created`,初始化失 ```text turn/start claim next-step input plus one queued message - assemble prompt sections + tool schemas + agent/request -> prepareCall -> bind Agent.currentRoute() + prepare variables + runtime context -> agent/pre-step; project runtime context at fallback reject | enter(messages, startsRequestSeries?) reject, or a first enter rewritten empty -> close the turn with no step step/start - agent/request -> prepareCall (cancellation commits neither system nor users) + resolve conditional tools + assemble prompt using that tool set reconcile system/message using the prepared call capability append entered messages as user/message; reconcile retained runtime context log request/header and request/context as needed @@ -111,7 +112,7 @@ turn/end 输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。 -`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 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 }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复 `agent/pre-step`;仅在已解析路由或工具 schema 改变时重新组装提示词。附接后的 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 17856848d5..28b8224ae3 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2372,7 +2372,7 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` - `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts) +- `source`: [`packages/plan/plan-mode/src/index.ts:67`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -3475,7 +3475,7 @@ export interface Config { ## `@deepseek-ai/dsh-system-prompt` -- `source`: [`packages/core/system-prompt/src/index.ts:254`](../packages/core/system-prompt/src/index.ts) +- `source`: [`packages/core/system-prompt/src/index.ts:266`](../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). */ @@ -4154,7 +4154,7 @@ export interface Config { ## `@deepseek-ai/dsh-tools` - `inject`: `systemPrompt` -- `source`: [`packages/core/tools/src/index.ts:675`](../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 c4ab85cdda..1db597f106 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2374,7 +2374,7 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` - `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts) +- `source`: [`packages/plan/plan-mode/src/index.ts:67`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -3477,7 +3477,7 @@ export interface Config { ## `@deepseek-ai/dsh-system-prompt` -- `source`: [`packages/core/system-prompt/src/index.ts:254`](../packages/core/system-prompt/src/index.ts) +- `source`: [`packages/core/system-prompt/src/index.ts:266`](../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). */ @@ -4156,7 +4156,7 @@ export interface Config { ## `@deepseek-ai/dsh-tools` - `inject`: `systemPrompt` -- `source`: [`packages/core/tools/src/index.ts:675`](../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 9691933dda..2702e3b3ec 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -10,18 +10,18 @@ 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:363`](../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:261`](../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:270`](../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:393`](../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:299`](../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:307`](../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:288`](../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/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../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:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../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:280`](../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:381`](../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:369`](../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:267`](../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:276`](../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:399`](../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:305`](../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:313`](../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:294`](../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/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:326`](../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:343`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:359`](../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:286`](../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:387`](../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:619`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:599`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:626`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | @@ -71,14 +71,15 @@ 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`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`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:209`](../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:165`](../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:177`](../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) | -| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | -| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | +| `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) | +| `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) | +| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:161`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | +| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:206`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` | | `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index cb8ea84aac..c393e8340c 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -12,18 +12,18 @@ | --- | --- | --- | --- | --- | | `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:363`](../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:261`](../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:270`](../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:393`](../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:299`](../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:307`](../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:288`](../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/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../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:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../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:280`](../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:381`](../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:369`](../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:267`](../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:276`](../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:399`](../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:305`](../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:313`](../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:294`](../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/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:326`](../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:343`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:359`](../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:286`](../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:387`](../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:619`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:599`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:626`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | @@ -73,14 +73,15 @@ | `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`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`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:209`](../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:165`](../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:177`](../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) | -| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | -| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | +| `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) | +| `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) | +| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:161`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | +| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:206`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` | | `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index dab112d89b..61fc4845c0 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -144,8 +144,8 @@ en: 4294f78d26ed985b zh: 4294f78d26ed985b /session-persistence-event-catalog/events/plan/plan-mode-log-only: - en: 862175bfe8692ef7 - zh: a8ab123bf674cf77 + en: f23bf0e5e6408adf + zh: a9442b4c5cdeef0b /session-persistence-event-catalog/events/request: en: a8df82440ebdf5f8 zh: a8df82440ebdf5f8 @@ -1239,8 +1239,8 @@ en: f43a635a14568780 zh: 8d22226b045122eb /session-persistence-event-catalog/resolved-persistence-types/toolrestriction: - en: d7d3c0238d9b373e - zh: f49d93ee54e7a780 + en: 739d19df365ed5df + zh: b1e41e56995fe238 /session-persistence-event-catalog/resolved-persistence-types/toolresultmessage: en: 2258683c3cfa4ce3 zh: 5663aa76c911761b @@ -1347,8 +1347,8 @@ en: a74e3dfa51c987af zh: 6c3c87974d7a7260 /session-persistence-event-catalog/resolved-persistence-types/active~2: - en: a3de6a6fe9672c7a - zh: a357481e27b3b81f + en: 1bc41f689438a051 + zh: babdc92cc0ad089a /session-persistence-event-catalog/resolved-persistence-types/agentpreset: en: b8a0f8aac9708056 zh: f5f004bd205b41e1 @@ -1779,8 +1779,8 @@ en: 08be4349548406a9 zh: 8e7cf9966198ef78 /session-persistence-event-catalog/resolved-persistence-types/kind-tool-registry: - en: 79db83dd2c430faf - zh: 173f568241f5ce86 + en: bbcefadaa064365a + zh: 0285b8b877d6193d /session-persistence-event-catalog/resolved-persistence-types/kind-user: en: cb5d3fbab4debac6 zh: 72add55254356149 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 6d4c694309..b9be17476b 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -700,7 +700,7 @@ Source: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/ 'plan/mode': { active: boolean } ``` -Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +Source: [`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -4996,7 +4996,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:701`](../packages/core/tools/src/index.ts) +Sources: [`packages/core/tools/src/index.ts:759`](../packages/core/tools/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -5556,7 +5556,7 @@ Array of `unknown` (opaque). SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -Sources: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +Sources: [`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -7329,7 +7329,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49` SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543` -Sources: [`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/index.ts) +Sources: [`packages/core/tools/src/index.ts:42`](../packages/core/tools/src/index.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 92024676bd..da78fe36f3 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -702,7 +702,7 @@ export type SessionEvent = { 'plan/mode': { active: boolean } ``` -来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -4998,7 +4998,7 @@ SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8` SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f` -来源:[`packages/core/tools/src/index.ts:701`](../packages/core/tools/src/index.ts) +来源:[`packages/core/tools/src/index.ts:759`](../packages/core/tools/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5558,7 +5558,7 @@ SHA-256: `b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7` SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7331,7 +7331,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49` SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543` -来源:[`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/index.ts) +来源:[`packages/core/tools/src/index.ts:42`](../packages/core/tools/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index e9046242ff..2467ac78ef 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -22518,7 +22518,7 @@ "digest": "20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5", "names": [], "sources": [ - "packages/plan/plan-mode/src/index.ts:52" + "packages/plan/plan-mode/src/index.ts:51" ] }, { @@ -22908,7 +22908,7 @@ "packages/core/tools/src/index.ts#ToolRestriction" ], "sources": [ - "packages/core/tools/src/index.ts:701" + "packages/core/tools/src/index.ts:759" ] }, { @@ -24142,7 +24142,7 @@ "digest": "8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543", "names": [], "sources": [ - "packages/core/tools/src/index.ts:35" + "packages/core/tools/src/index.ts:42" ] }, { diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index c6c02c6c42..882e8d364b 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -61,8 +61,14 @@ Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types interface Agent { /** Session-backed Agent identity. */ readonly id: SessionId - /** The provider route and model this agent's requests use. */ + /** Initial request options; currentRoute() reports the bound request route. */ readonly options: AgentOptions + /** + * Read the immutable route bound to this request and its tool batch. + * @returns the actual provider and model selected by prepareCall(). + * @throws when no main request is prepared; never falls back to prior requests. + */ + currentRoute(): Readonly> /** The live session this agent drives; its log is the durable source of truth. */ readonly session: Session /** Agent-owned access to durable pending work. */ @@ -952,16 +958,16 @@ Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/s #### `agent/request` — waterfall -Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. On step admission, this runs after assembly and `step/start`, before the system prompt and accepted user batch are committed. Cancellation here or during subsequent `prepareCall()` resolution commits neither. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages. +Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. On step admission, this runs before context preparation and `agent/pre-step`. Cancellation here or during subsequent `prepareCall()` resolution opens no step and commits no system prompt or user batch. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages. ```ts cordis-catalog /** * Replace the frozen call configuration. `await next()` yields the config * the machine would use (agent options on the first request, the logged * header afterwards); return a replacement to switch. On step admission, - * this runs after assembly and `step/start`, before the system prompt and - * accepted user batch are committed. Cancellation here or during subsequent - * `prepareCall()` resolution commits neither. The prepared call capability + * this runs before context preparation and `agent/pre-step`. Cancellation + * here or during subsequent `prepareCall()` resolution opens no step and + * commits no system prompt or user batch. The prepared call capability * governs prompt admission. Model-visible content must use logged channels; * this waterfall cannot mutate messages. * @param payload.agent - the agent making the model call. diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 582a817f1f..2e9bf255f3 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -65,8 +65,14 @@ interface AgentHandle { interface Agent { /** Session-backed Agent identity. */ readonly id: SessionId - /** The provider route and model this agent's requests use. */ + /** Initial request options; currentRoute() reports the bound request route. */ readonly options: AgentOptions + /** + * Read the immutable route bound to this request and its tool batch. + * @returns the actual provider and model selected by prepareCall(). + * @throws when no main request is prepared; never falls back to prior requests. + */ + currentRoute(): Readonly> /** The live session this agent drives; its log is the durable source of truth. */ readonly session: Session /** Agent-owned access to durable pending work. */ @@ -962,16 +968,16 @@ Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/s #### `agent/request` — waterfall -Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. On step admission, this runs after assembly and `step/start`, before the system prompt and accepted user batch are committed. Cancellation here or during subsequent `prepareCall()` resolution commits neither. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages. +Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. On step admission, this runs before context preparation and `agent/pre-step`. Cancellation here or during subsequent `prepareCall()` resolution opens no step and commits no system prompt or user batch. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages. ```ts cordis-catalog /** * Replace the frozen call configuration. `await next()` yields the config * the machine would use (agent options on the first request, the logged * header afterwards); return a replacement to switch. On step admission, - * this runs after assembly and `step/start`, before the system prompt and - * accepted user batch are committed. Cancellation here or during subsequent - * `prepareCall()` resolution commits neither. The prepared call capability + * this runs before context preparation and `agent/pre-step`. Cancellation + * here or during subsequent `prepareCall()` resolution opens no step and + * commits no system prompt or user batch. The prepared call capability * governs prompt admission. Model-visible content must use logged channels; * this waterfall cannot mutate messages. * @param payload.agent - the agent making the model call. diff --git a/docs/subsystems/plan.md b/docs/subsystems/plan.md index 1c29a87851..bf19460d13 100644 --- a/docs/subsystems/plan.md +++ b/docs/subsystems/plan.md @@ -50,7 +50,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.planMode` — `PlanModeController` -`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool. Client carriers expose the projection's cropped `{ active, pending }` view. +`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the conditional exit tool. Client carriers expose the projection's cropped `{ active, pending }` view. ```ts cordis-catalog /** diff --git a/docs/subsystems/plan.zh.md b/docs/subsystems/plan.zh.md index db2a7e1c2b..11cc861a1f 100644 --- a/docs/subsystems/plan.zh.md +++ b/docs/subsystems/plan.zh.md @@ -50,7 +50,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.planMode` — `PlanModeController` -`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool. Client carriers expose the projection's cropped `{ active, pending }` view. +`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the conditional exit tool. Client carriers expose the projection's cropped `{ active, pending }` view. ```ts cordis-catalog /** diff --git a/docs/subsystems/system-prompt.md b/docs/subsystems/system-prompt.md index c529c150bb..76d9fed53e 100644 --- a/docs/subsystems/system-prompt.md +++ b/docs/subsystems/system-prompt.md @@ -184,23 +184,29 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine * 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 the accepted assembly; their providers and waterfall do not rerun. + * retain their accepted values during this refresh. * @param assembly - accepted assembly for this scope and step. * @param context - the same scope and current plugin-defined assembly fields. * @returns the accepted assembly with current runtime-context provider text. */ -refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly +refreshContext(assembly: T, context: AssembleContext = {}): T /** - * Assemble global and scoped providers, detach tool parameters, apply - * canonical ordering, then run the assembly waterfall. Scoped sections and - * variables shadow globals. The returned waterfall value is authoritative - * except that an effective complete section is restored afterwards as the - * sole prompt section. - * @param context - the optional scope and plugin-defined assembly fields. - * @returns the post-waterfall assembly with any complete prompt enforced. + * 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. */ -async assemble(context: AssembleContext = {}): Promise +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 ``` Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts) @@ -213,13 +219,13 @@ Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system- #### `system-prompt/assemble` — waterfall -Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt. +Presentation waterfall over assembled sections, contexts, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt. ```ts cordis-catalog /** - * Expert waterfall over the assembled sections, contexts, tools, and variables. + * Presentation waterfall over assembled sections, contexts, and variables. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners - * receive only that scope's assemblies. The returned value is authoritative. + * receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned. * A supplied signal controls only this explicit assembly request and must not * be retained to control later turns. A registered complete section is * restored after this waterfall, so listeners cannot add to or replace @@ -228,7 +234,7 @@ Expert waterfall over the assembled sections, contexts, tools, and variables. Sc * @param context - the caller's per-assembly context. * @mode waterfall */ -'system-prompt/assemble'(this: Scoped, assembly: PromptAssembly, context: AssembleContext, next: () => Promise): Promise +'system-prompt/assemble'(this: Scoped, assembly: PromptPresentation, context: AssembleContext, next: () => Promise): Promise ``` Types: [Scoped](scope.md) @@ -250,5 +256,25 @@ 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 a805c1d771..12ce785206 100644 --- a/docs/subsystems/system-prompt.zh.md +++ b/docs/subsystems/system-prompt.zh.md @@ -184,23 +184,29 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine * 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 the accepted assembly; their providers and waterfall do not rerun. + * retain their accepted values during this refresh. * @param assembly - accepted assembly for this scope and step. * @param context - the same scope and current plugin-defined assembly fields. * @returns the accepted assembly with current runtime-context provider text. */ -refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly +refreshContext(assembly: T, context: AssembleContext = {}): T /** - * Assemble global and scoped providers, detach tool parameters, apply - * canonical ordering, then run the assembly waterfall. Scoped sections and - * variables shadow globals. The returned waterfall value is authoritative - * except that an effective complete section is restored afterwards as the - * sole prompt section. - * @param context - the optional scope and plugin-defined assembly fields. - * @returns the post-waterfall assembly with any complete prompt enforced. + * 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. */ -async assemble(context: AssembleContext = {}): Promise +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 ``` Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts) @@ -213,13 +219,13 @@ Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system- #### `system-prompt/assemble` — waterfall -Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt. +Presentation waterfall over assembled sections, contexts, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt. ```ts cordis-catalog /** - * Expert waterfall over the assembled sections, contexts, tools, and variables. + * Presentation waterfall over assembled sections, contexts, and variables. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners - * receive only that scope's assemblies. The returned value is authoritative. + * receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned. * A supplied signal controls only this explicit assembly request and must not * be retained to control later turns. A registered complete section is * restored after this waterfall, so listeners cannot add to or replace @@ -228,7 +234,7 @@ Expert waterfall over the assembled sections, contexts, tools, and variables. Sc * @param context - the caller's per-assembly context. * @mode waterfall */ -'system-prompt/assemble'(this: Scoped, assembly: PromptAssembly, context: AssembleContext, next: () => Promise): Promise +'system-prompt/assemble'(this: Scoped, assembly: PromptPresentation, context: AssembleContext, next: () => Promise): Promise ``` Types: [Scoped](scope.zh.md) @@ -250,5 +256,25 @@ 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 585ce1582a..c9e0a46a73 100644 --- a/docs/subsystems/tools.md +++ b/docs/subsystems/tools.md @@ -206,6 +206,8 @@ interface ToolExecutionInput { readonly arguments: unknown /** The agent on whose behalf the call runs (set by the agent loop). */ readonly agent?: Agent + /** Request admission shared by Native and PTC dispatch. Conditional tools require it. */ + readonly toolSet?: ResolvedToolSet /** * Opaque token of the enclosing transport execution, when one exists. PTC * mode sets this on SDK sub-dispatches so commit-style observers can wait for @@ -525,9 +527,10 @@ presentAs(mode: ToolPresentationMode): () => void * Register globally or in the calling agent scope. Scoped tools shadow * globals; duplicates within one layer and the reserved `run_code` name fail. * @param definition - tool schema, execution, and optional finalization/presentation callbacks. + * @param options - optional request-time activation condition. * @returns the exact disposer that unregisters the tool. */ -register(definition: ToolDefinition): () => void +register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void /** * Restrict global tools for the calling agent scope. Empty filters, unknown @@ -557,17 +560,28 @@ guard(guard: ToolGuard): () => void * actually executed. * @param name - the tool name as registered. * @param scope - the viewing scope (the agent); omitted = the global view. + * @param toolSet - request admission; omit only to inspect registered definitions. * @returns the definition the scope resolves, or undefined when none is visible. */ -get(name: string, scope?: ScopeKey): ToolDefinition | undefined +get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined + +/** + * Resolve conditions once for this request 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 - agent with a bound route and request cancellation. + * @returns the complete selection; the caller disposes it after its tool batch settles. + */ +async resolve(context: ToolActivationContext): Promise /** * Project visible definitions onto the allowlisted model-facing schema fields, * excluding execution and presentation callbacks. * @param scope - the viewing scope (the agent); omitted = the global view. + * @param toolSet - request admission, or omitted for registered-definition inspection. * @returns one deep-cloned schema per visible tool. */ -schemas(scope?: ScopeKey): ToolSchema[] +schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[] /** * Classify a pending call through the caller's visible tool definition. Only diff --git a/docs/subsystems/tools.zh.md b/docs/subsystems/tools.zh.md index 4ee27fcb4d..81a1408320 100644 --- a/docs/subsystems/tools.zh.md +++ b/docs/subsystems/tools.zh.md @@ -206,6 +206,8 @@ interface ToolExecutionInput { readonly arguments: unknown /** The agent on whose behalf the call runs (set by the agent loop). */ readonly agent?: Agent + /** Request admission shared by Native and PTC dispatch. Conditional tools require it. */ + readonly toolSet?: ResolvedToolSet /** * Opaque token of the enclosing transport execution, when one exists. PTC * mode sets this on SDK sub-dispatches so commit-style observers can wait for @@ -525,9 +527,10 @@ presentAs(mode: ToolPresentationMode): () => void * Register globally or in the calling agent scope. Scoped tools shadow * globals; duplicates within one layer and the reserved `run_code` name fail. * @param definition - tool schema, execution, and optional finalization/presentation callbacks. + * @param options - optional request-time activation condition. * @returns the exact disposer that unregisters the tool. */ -register(definition: ToolDefinition): () => void +register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void /** * Restrict global tools for the calling agent scope. Empty filters, unknown @@ -557,17 +560,28 @@ guard(guard: ToolGuard): () => void * actually executed. * @param name - the tool name as registered. * @param scope - the viewing scope (the agent); omitted = the global view. + * @param toolSet - request admission; omit only to inspect registered definitions. * @returns the definition the scope resolves, or undefined when none is visible. */ -get(name: string, scope?: ScopeKey): ToolDefinition | undefined +get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined + +/** + * Resolve conditions once for this request 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 - agent with a bound route and request cancellation. + * @returns the complete selection; the caller disposes it after its tool batch settles. + */ +async resolve(context: ToolActivationContext): Promise /** * Project visible definitions onto the allowlisted model-facing schema fields, * excluding execution and presentation callbacks. * @param scope - the viewing scope (the agent); omitted = the global view. + * @param toolSet - request admission, or omitted for registered-definition inspection. * @returns one deep-cloned schema per visible tool. */ -schemas(scope?: ScopeKey): ToolSchema[] +schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[] /** * Classify a pending call through the caller's visible tool definition. Only diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.i18n.yaml new file mode 100644 index 0000000000..94da8216ed --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# 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.1/conditional-tools/guide.md +/: + en: 0a9569f33806d6bf + zh: 3bd885a0d518aad8 +/update-conditional-tools-and-prompt-hooks: + en: a292521a564f5f0b + zh: 2e15d7b96d966b29 +/update-conditional-tools-and-prompt-hooks/change: + en: 018c4ade3568fbc5 + zh: e2f0f9c1467250be +/update-conditional-tools-and-prompt-hooks/migration: + en: abe994f27c41f65e + zh: 106d04a591a0faa2 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.md new file mode 100644 index 0000000000..b9cc48a160 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Custom tool and prompt plugins must use request-owned tool admission and the earlier route preparation stage." +--- + +# Update conditional tools and prompt hooks + +English | [中文](guide.zh.md) + +## Change + +`agent/request` and `prepareCall()` bind the route before `agent/pre-step`. 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 request 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. + +## Migration + +1. Declare tool conditions with `ctx.tools.register(definition, { when })`. Read the bound route through `agent.currentRoute()`; never infer it from initial options or the preceding Session header. +2. Move runtime-context preparation listeners to `system-prompt/prepare`. Keep presentation changes in `system-prompt/assemble` and stop replacing its tool list. +3. 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 for model calls. Pass `context.toolSet` when tool guidance queries `ctx.tools.get()` or `schemas()`. +4. Update custom Agent implementations with `currentRoute()`, valid only while a main request is bound. Verify that inactive tools are absent from declarations and that a historical name returns `UNKNOWN_TOOL` without reaching the implementation. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.zh.md new file mode 100644 index 0000000000..1d4b35bca4 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.zh.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "自定义工具和提示词插件必须使用请求拥有的工具准入集合以及提前的路由准备阶段。" +--- + +# 更新条件工具与提示词钩子 + +[English](guide.md) | 中文 + +## 变更 + +`agent/request` 与 `prepareCall()` 在 `agent/pre-step` 之前绑定路由。运行时上下文准备使用 `system-prompt/prepare`;`system-prompt/assemble` 接收不含工具数组的展示字段。工具声明仍由已注册提供方持有。条件工具执行需要已解析的请求集合,包括嵌套 PTC 调用。替换注册不会授权尚未执行的旧调用使用新实现。 + +`exit_plan_mode` 只在已提交的计划模式启用时提供。计划状态写入失败会停止步骤接纳并保留待提交选择。现有 Session 无需格式迁移。 + +## 迁移 + +1. 使用 `ctx.tools.register(definition, { when })` 声明工具条件。通过 `agent.currentRoute()` 读取已绑定路由,不从初始选项或先前 Session 请求头推断。 +2. 将运行时上下文准备监听器迁移到 `system-prompt/prepare`。展示变更留在 `system-prompt/assemble`,停止替换它的工具列表。 +3. 程序化调用条件工具时,通过 `ctx.tools.resolve()` 解析 `{ agent, signal }`,将返回的 `toolSet` 传给组装与执行,并在批次结束后释放。AgentLoop 会为模型调用完成这些操作。工具指导查询 `ctx.tools.get()` 或 `schemas()` 时传入 `context.toolSet`。 +4. 为自定义 Agent 实现补充 `currentRoute()`,仅在主请求已绑定期间可用。验证未启用工具不出现在声明中,且使用历史名称调用会返回 `UNKNOWN_TOOL`,不会进入实现。 diff --git a/packages/api/job-controller/tests/host-harness.ts b/packages/api/job-controller/tests/host-harness.ts index 85609166b0..79776efbd9 100644 --- a/packages/api/job-controller/tests/host-harness.ts +++ b/packages/api/job-controller/tests/host-harness.ts @@ -21,6 +21,7 @@ export async function registerAgent(ctx: Context, rawId: string): Promise Promise } = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/api/job-controller/tests/observe.host.spec.ts b/packages/api/job-controller/tests/observe.host.spec.ts index 583862eeac..51b24ff063 100644 --- a/packages/api/job-controller/tests/observe.host.spec.ts +++ b/packages/api/job-controller/tests/observe.host.spec.ts @@ -226,6 +226,7 @@ describe('JobController', () => { async function liveOwner(ctx: Context, rawId: string): Promise { const session = ctx.sessions.create(SessionId(rawId)) const owner: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, 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 79bad41b7c..8209040d68 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 @@ -26,6 +26,7 @@ async function harness(): Promise { /** A registered idle Agent double: the gate reads only its Session header. */ async function liveAgent(ctx: Context, session: Session): Promise { const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, diff --git a/packages/bundle/headless/tests/headless.spec.ts b/packages/bundle/headless/tests/headless.spec.ts index cc73507d5f..e290f7a342 100644 --- a/packages/bundle/headless/tests/headless.spec.ts +++ b/packages/bundle/headless/tests/headless.spec.ts @@ -149,6 +149,7 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{ let idle = Promise.resolve() let running = false const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: createOptions.agentOptions ?? {}, session, diff --git a/packages/context/session-reference/README.i18n.yaml b/packages/context/session-reference/README.i18n.yaml index 51063bf115..3b7443317e 100644 --- a/packages/context/session-reference/README.i18n.yaml +++ b/packages/context/session-reference/README.i18n.yaml @@ -27,14 +27,14 @@ en: 169f100ba62f2120 zh: 3fd4fc0bd73b2370 /deepseek-ai-dsh-session-reference/use-this-package/configuration: - en: d9abc1b28c3feb40 - zh: ac85539027b37130 + en: 5dfb1f0c52d300bf + zh: ae731dd6fcd93027 /deepseek-ai-dsh-session-reference/understand-the-implementation: en: db349483e453caf7 zh: 70035c291e1efe84 /deepseek-ai-dsh-session-reference/understand-the-implementation/design-concept: - en: 5c7712ca91eaae1c - zh: 2d923b605afa7a88 + en: d7ff10eae41712ad + zh: 2b91a16dee11808e /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 4c594635b5..79221efeec 100644 --- a/packages/context/session-reference/README.md +++ b/packages/context/session-reference/README.md @@ -50,7 +50,7 @@ For a truncated reference, an optional spill backend saves the full captured tex | `maxReferenceBytes` | automatic | Explicit maximum serialized JSON bytes per source; overrides the automatic budget exactly | | `referenceContextFraction` | `0.2` | Context-window fraction per source, from `0` to `1` | -The automatic budget is `max(65536, floor(contextWindow × 4 × referenceContextFraction))` bytes per source. Model context capacity is measured in tokens; four bytes per token is a sizing heuristic, not an exact token conversion. A missing route, LLM service, adapter, or capacity uses 64 KiB; other model metadata lookup errors and cancellation fail preparation. +The automatic budget is `max(65536, floor(contextWindow × 4 × referenceContextFraction))` bytes per source. Model context capacity is measured in tokens; four bytes per token is a sizing heuristic, not an exact token conversion. A missing LLM service, adapter, or capacity uses 64 KiB; other model metadata lookup errors and cancellation fail preparation. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-reference) is the exhaustive source for every accepted field and its JSDoc. @@ -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 provider and model captured after `system-prompt/assemble` completes for the target agent. Direct `prepare` calls before any assembly use agent options; session headers do not select the budget model. Diagnostic assemblies without an agent do not affect captured routes. +The budget uses `agent.currentRoute()`, bound before step input preparation. The agent must have a prepared main request when a model-relative budget is needed; initial options and stored 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 c1f8b58d59..edd4e673b4 100644 --- a/packages/context/session-reference/README.zh.md +++ b/packages/context/session-reference/README.zh.md @@ -50,7 +50,7 @@ kind: "package-reference" | `maxReferenceBytes` | 自动 | 每个来源的最大序列化 JSON 字节数;显式设置时精确覆盖自动预算 | | `referenceContextFraction` | `0.2` | 每个来源的上下文窗口比例,范围为 `0` 到 `1` | -自动预算为每个来源 `max(65536, floor(contextWindow × 4 × referenceContextFraction))` 字节。模型上下文容量以 token 计量;每个 token 四字节是容量估算,不是精确的 token 换算。缺少路由、LLM(大语言模型)服务、适配器或容量时使用 64 KiB;其他模型元数据查询错误与取消会使准备失败。 +自动预算为每个来源 `max(65536, floor(contextWindow × 4 × referenceContextFraction))` 字节。模型上下文容量以 token 计量;每个 token 四字节是容量估算,不是精确的 token 换算。缺少 LLM(大语言模型)服务、适配器或容量时使用 64 KiB;其他模型元数据查询错误与取消会使准备失败。 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-reference)是每个受支持字段及其 JSDoc 的穷尽式真源。 @@ -70,7 +70,7 @@ kind: "package-reference" 解析器通过 `ctx.get("spillStore")` 获取可选存储,只保存被截断的引用。存储归目标会话所有;来源信息标识被引用的源会话与标签,不伪造工具调用。异步保存后会检查取消,即使产物已写入,也会阻止发布。产物过期仍遵循后端既有策略。 -预算使用目标 agent 的 `system-prompt/assemble` 完成后捕获的提供方与模型。首次组装前直接调用 `prepare` 时使用 agent 选项;会话头不决定预算模型。不带 agent 的诊断组装不会影响已捕获路由。 +预算使用步骤输入准备前绑定的 `agent.currentRoute()`。需要模型相对预算时,agent 必须已有准备完成的主请求;初始选项与已存储请求头不决定预算模型。 ### 源码地图 diff --git a/packages/context/session-reference/src/index.ts b/packages/context/session-reference/src/index.ts index f2082d85e2..df9a66e850 100644 --- a/packages/context/session-reference/src/index.ts +++ b/packages/context/session-reference/src/index.ts @@ -92,7 +92,6 @@ export class SessionReferenceResolver extends TypertRemoteService { }) private readonly config: Required> & { maxReferenceBytes: number | undefined } - private readonly assembledRoutes = new WeakMap() constructor(ctx: Context, config: Config = {}) { super(ctx, 'sessionReferenceResolver') @@ -123,15 +122,6 @@ export class SessionReferenceResolver extends TypertRemoteService { 'SESSION_REFERENCE_INVALID_CONFIG', ) } - // Prepend observes model-selection overrides after downstream assembly completes. - ctx.on('system-prompt/assemble', async (_assembly, context, next) => { - const assembly = await next() - if (context.agent !== undefined) { - const { provider, model } = assembly.variables - this.assembledRoutes.set(context.agent, { provider, model }) - } - return assembly - }, { prepend: true }) ctx.on('agent/pre-step', async ({ agent, signal }, next): Promise => { const decision = await next() if (decision.kind === 'reject') return decision @@ -359,10 +349,9 @@ export class SessionReferenceResolver extends TypertRemoteService { private async referenceBudget(agent: Agent, signal: AbortSignal | undefined): Promise { if (this.config.maxReferenceBytes !== undefined) return this.config.maxReferenceBytes - // Options seed direct preparation; an assembled route owns model-step preparation. - const { provider, model } = this.assembledRoutes.get(agent) ?? agent.options + const { provider, model } = agent.currentRoute() const llm = this.ctx.get('llm') - if (provider === undefined || model === undefined || llm === undefined) return DEFAULT_MAX_REFERENCE_BYTES + if (llm === undefined) return DEFAULT_MAX_REFERENCE_BYTES let info: LlmResolvedModelInfo try { info = await settleWithCancellation(llm.resolveModelInfo(provider, model, signal), signal) diff --git a/packages/context/session-reference/tests/session-reference.spec.ts b/packages/context/session-reference/tests/session-reference.spec.ts index efc8447e90..b7af57466d 100644 --- a/packages/context/session-reference/tests/session-reference.spec.ts +++ b/packages/context/session-reference/tests/session-reference.spec.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { agentEvents, installModelSelection, type Agent, type ModelSelectionRef } from '@deepseek-ai/dsh-agent' +import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compaction' import LlmRuntime, { createDeveloperMessage, createMessage, createSystemMessage, createToolResultMessage, createUserMessage, LlmError, ToolCallId } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' @@ -86,7 +86,8 @@ function withProjectionCache( } function fakeAgent(session: Session): Agent { - return { id: session.id, session, options: {} } as Agent + const options: Agent['options'] = {} + return { id: session.id, session, options, currentRoute: () => ({ provider: options.provider ?? 'seed', model: options.model ?? 'seed' }) } as Agent } function expectCode(code: SessionReferenceErrorCode): Error { @@ -533,12 +534,10 @@ describe('model-relative reference budgets', () => { else expect(resolve).toHaveBeenCalledWith('seed', 'seed', undefined) }) - it('uses the assembled selection, not the header, seed, or next selected model', async () => { + it('uses the bound request route independently of stored and initial routes', async () => { const { ctx, agent, source, resolve } = await setup() - const selection: ModelSelectionRef = { current: { provider: 'selected', model: 'large' }, assembled: undefined } - installModelSelection(ctx, selection) - await ctx.systemPrompt.assemble({ agent, scope: agent }) - selection.current = { provider: 'selected', model: 'small' } + let route = { provider: 'selected', model: 'large' } + vi.spyOn(agent, 'currentRoute').mockImplementation(() => route) 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, signal }, @@ -551,6 +550,7 @@ describe('model-relative reference budgets', () => { expect(bytes({ content: [], additionalContext: firstContext })).toBe(160_000) expect(resolve).toHaveBeenLastCalledWith('selected', 'large', signal) await ctx.systemPrompt.assemble({ agent, scope: agent }) + route = { provider: 'selected', model: 'small' } resolve.mockResolvedValue({ provider: 'selected', id: 'small', name: 'small', context: { contextWindow: 8_000 } }) const second = await enter() if (second.kind !== 'enter' || second.messages[1] === undefined) throw new Error('expected reference context') @@ -558,23 +558,13 @@ describe('model-relative reference budgets', () => { expect(resolve).toHaveBeenLastCalledWith('selected', 'small', signal) }) - it('uses the floor for absent metadata, service, or assembled route and ignores diagnostic assemblies', async () => { + it('uses the floor for absent metadata or service', async () => { const { ctx, agent, resolve, prepare, llmFiber } = await setup() - await ctx.systemPrompt.assemble() resolve.mockResolvedValue({ provider: 'seed', id: 'seed', name: 'seed' }) expect(bytes(await prepare())).toBe(65_536) expect(resolve).toHaveBeenCalledOnce() - await ctx.systemPrompt.assemble({ agent, scope: agent }) - expect(bytes(await prepare())).toBe(65_536) - expect(resolve).toHaveBeenCalledOnce() - delete agent.options.model - const other = fakeAgent(agent.session) - other.options.provider = 'seed' - await ctx.sessionReferenceResolver.prepare(other, [], [{ sessionId: SessionId('source') }]) - expect(resolve).toHaveBeenCalledOnce() await llmFiber.dispose() - other.options.model = 'seed' - expect(bytes(await ctx.sessionReferenceResolver.prepare(other, [], [{ sessionId: SessionId('source') }]))).toBe(65_536) + expect(bytes(await ctx.sessionReferenceResolver.prepare(agent, [], [{ sessionId: SessionId('source') }]))).toBe(65_536) }) it('uses the floor when the real LLM runtime has no adapter for the route', async () => { @@ -619,7 +609,7 @@ describe('model-relative reference budgets', () => { expect(read).not.toHaveBeenCalled() }) - it('removes both listeners when the resolver fiber is disposed', async () => { + it('removes the preparation listener when the resolver fiber is disposed', async () => { const { ctx, agent, source, resolve, resolverFiber } = await setup() const resolver = ctx.sessionReferenceResolver await resolverFiber.dispose() diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts index 92499007d1..f5b28c96ef 100644 --- a/packages/context/time-context/tests/time-context.spec.ts +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -47,6 +47,7 @@ async function mount(config: Config = {}) { function sessionAgent(session: Session, id = 'agent'): Agent { const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: SessionId(id), options: {}, session, diff --git a/packages/context/tmux-context/tests/tmux-context.spec.ts b/packages/context/tmux-context/tests/tmux-context.spec.ts index ae15f20a46..b895f139db 100644 --- a/packages/context/tmux-context/tests/tmux-context.spec.ts +++ b/packages/context/tmux-context/tests/tmux-context.spec.ts @@ -106,6 +106,7 @@ async function mount( function sessionAgent(session: Session, id = 'agent'): Agent { const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: SessionId(id), options: {}, session, diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 0bbdb05d37..5c19f9b0b8 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: 28939261ccc7defe - zh: 9d157a321307ebd4 + en: 29c33bc92b4a8db6 + zh: d1152a6656bb772d /deepseek-ai-dsh-agent-loop/understand-the-implementation/failure-and-cancellation: en: eea56903c4ff82f5 zh: c0d5abb5767780ed @@ -63,8 +63,8 @@ en: ed0e841bed26a836 zh: f30d91bc5a64fa20 /deepseek-ai-dsh-agent-loop/model-experience/complete-conversation-request/token-effect: - en: ad93d8d61968a25d - zh: 268ff858b0fa96e8 + en: 3862d03b3b4d6ec4 + zh: 164c6ecc8a21fdb1 /deepseek-ai-dsh-agent-loop/model-experience/complete-conversation-request/kv-cache-effect: en: c002d27c825d04e8 zh: b0dbdd95d3b3b6b5 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 62dcd35a92..7ff895ef11 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 assembles the prompt and tools and runs `agent/pre-step`; the fallback projects runtime context after listeners run. A rejected decision or empty first batch opens no step. On the first attempt after acceptance, `step/start` precedes the `agent/request` waterfall and `prepareCall()`; neither async phase sees the pending system prompt or accepted users committed to history, and cancellation during either commits neither. 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 without reassembling the bound prompt or tools, 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 same rendered assembly without repeating assembly, `agent/pre-step`, or user admission. 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 binds the route through `agent/request` and `prepareCall()`, prepares variables and runtime context, then runs `agent/pre-step`; its fallback projects that context. A rejected decision or empty first batch opens no step. Cancellation during route preparation commits neither a system prompt nor users. After acceptance, `step/start` precedes asynchronous tool activation and final prompt assembly. 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 resolve tool conditions against their bound route without repeating `agent/pre-step` or user admission. An unchanged route and tool schema set reuse the accepted prompt; a changed route or tool set assembles the corresponding prompt again. The request-owned tool set is closed after dispatch settles, and the bound Agent route is cleared when the step ends. 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. @@ -157,7 +157,7 @@ For each step, the loop sends the session's derived messages and visible tool sc #### Token effect -System text and schemas are paid again on every step, and on an `in-history` route every retained prompt version is paid until compaction shadows it or prompt reconciliation empties it. Per-agent scoping chooses the contributions, while the authoritative assembly waterfall can alter the final request and makes its listener responsible for protocol coherence. +System text and schemas are paid again on every step, and on an `in-history` route every retained prompt version is paid until compaction shadows it or prompt reconciliation empties it. Per-agent scoping chooses the contributions, while the presentation waterfall can alter prompt text but cannot replace provider-owned tool declarations. #### KV Cache effect diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index ee355a328d..daf4663ac9 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/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/request` 与 `prepareCall()` 绑定路由,准备变量和运行时上下文,然后运行 `agent/pre-step`;fallback 投影该上下文。被拒绝的决定或空的首批输入不打开步骤。路由准备期间取消不会提交系统提示词或用户消息。接纳后,`step/start` 先于异步工具激活与最终提示词组装。每次尝试时,循环随后依据已准备调用的能力,同步将渲染后的提示词与存活的 `system/message` 节点协调一致、刷新已注册运行时事实、仅在首次尝试追加已接纳的 `user/message` 批次并原位替换循环自有上下文候选、恢复被移除的运行时上下文、按需记录 header 与 context,再派生并冻结请求,通过该绑定的已准备调用发起流式请求。重试按其绑定路由重新解析工具条件,不重复 `agent/pre-step` 或用户消息接纳。路由与工具 schema 集合未变时复用已接纳提示词;任一变化时重新组装对应提示词。请求拥有的工具集合在分派结束后关闭,Agent 的绑定路由在步骤结束时清除。协调过程可见 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 删除操作。 @@ -157,7 +157,7 @@ const handle = await ctx.agents.create({ #### Token 影响 -系统文本与 schema 在每个步骤都会再次计入,在 `in-history` 路由上,每个保留的提示词版本都会持续计入,直到压缩将其遮蔽或提示词协调将其清空。逐 agent 作用域决定贡献,而权威组装 waterfall 可以改变最终请求,并使其监听器负责保持协议连贯。 +系统文本与 schema 在每个步骤都会再次计入,在 `in-history` 路由上,每个保留的提示词版本都会持续计入,直到压缩将其遮蔽或提示词协调将其清空。逐 agent 作用域决定贡献;展示 waterfall 可以修改提示词文本,但不能替换提供方拥有的工具声明。 #### KV Cache 影响 diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index adf2e33ddc..d473dc5d23 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -30,7 +30,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 } from '@deepseek-ai/dsh-system-prompt' +import type { PromptAssembly, PromptPreparation } 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' @@ -38,6 +38,7 @@ import { RuntimeContextProjection } from './runtime-context.ts' import { AssistantStreamAttempt } from './assistant-stream.ts' import { SystemPromptProjection } from './runtime-context.ts' import { executeToolCalls } from './tool-calls.ts' +import { ToolActivationError } from '@deepseek-ai/dsh-tools' type Phase = | { kind: 'idle'; lastTurn: number } @@ -51,13 +52,19 @@ type Phase = type StepEndReason = Extract +interface PreparedRequest { + config: LlmCallConfig + preparedCall?: PreparedLlmCall +} + type PreparedStep = | { kind: 'reject' } | { kind: 'enter' messages: UserMessage[] startsRequestSeries?: true - assembly: PromptAssembly + preparation: PromptPreparation + request: PreparedRequest } /** Remove adapter-derived values before plugins propose the next request config. */ @@ -97,6 +104,7 @@ function abortedCancelCause(signal: AbortSignal): AgentCancelCause | undefined { /** Drives one session through turn and step boundaries. */ export class ReactLoopAgent implements Agent { readonly inbox: ReactLoopInbox + private requestRoute: Readonly> | undefined private phase: Phase private activityDone: Promise = Promise.resolve() @@ -137,6 +145,11 @@ export class ReactLoopAgent implements Agent { this.systemPrompt = new SystemPromptProjection(session) } + currentRoute(): Readonly> { + if (this.requestRoute === undefined) throw new Error('agent has no prepared model request') + return this.requestRoute + } + get status(): AgentStatus { return this.phase.kind === 'idle' || this.phase.kind === 'maintenance' ? 'idle' : 'running' } @@ -269,12 +282,14 @@ 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 assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal)) + const request = await this.prepareRequest(position.turn, position.step, signal) + const preparation = await this.loopCtx.systemPrompt.prepare(assembleContextFor(this, signal)) + preparation.variables = { ...preparation.variables, ...this.currentRoute() } signal.throwIfAborted() const decision = await this.dispatch.waterfall( 'agent/pre-step', { messages: claimed, ...position, signal }, (): Promise => { - const sections = renderContextSections(assembly) + const sections = renderContextSections(preparation) const context = this.runtimeContext.project(joinContextSections(sections), sections) return Promise.resolve({ kind: 'enter', @@ -284,7 +299,7 @@ export class ReactLoopAgent implements Agent { ) signal.throwIfAborted() if (decision.kind === 'reject') return decision - return { ...decision, assembly } + return { ...decision, preparation, request } } /** Whether the assembled tool schemas differ from the logged request header's. */ @@ -313,6 +328,7 @@ export class ReactLoopAgent implements Agent { let target: InboxTarget = 'next-turn' try { while (true) { + this.requestRoute = undefined signal.throwIfAborted() const step = phase.step + 1 const decision = await this.preStep(target, { turn, step }) @@ -356,6 +372,7 @@ export class ReactLoopAgent implements Agent { } finally { stopRecovery() this.session.append('step/end', { turn, step }) + this.requestRoute = undefined } signal.throwIfAborted() if (turnEnds && this.inbox.nextStep.length === 0) { @@ -372,16 +389,15 @@ export class ReactLoopAgent implements Agent { turnEnds = { kind: 'aborted', reason: cause } throw error } - // Every failure is structured: an `LlmError` keeps its facts, anything - // else flattens to `errorChain` text under the `UNKNOWN` code. turnEnds = { kind: 'error', error: error instanceof LlmError ? error.failure - : { message: errorChain(error), code: 'UNKNOWN' }, + : { message: errorChain(error), code: error instanceof ToolActivationError ? error.code : 'UNKNOWN' }, } this.throwError(error) } finally { + this.requestRoute = undefined try { // oxlint-disable-next-line typescript/no-non-null-assertion -- every exit assigns a turn ending this.session.append('turn/end', { turn, reason: turnEnds! }) @@ -403,159 +419,175 @@ export class ReactLoopAgent implements Agent { const { turn, step, abort: { signal } } = this.phase signal.throwIfAborted() - const { assembly } = decision - const renderedPrompt = renderPrompt(assembly) let firstAttempt = true + let acceptedAssembly: { provider: string; model: string; schemas: string; assembly: PromptAssembly } | undefined while (true) { - const { config, preparedCall } = await this.prepareRequest(turn, step, signal) - const currentContext = this.loopCtx.systemPrompt.refreshContext(assembly, assembleContextFor(this, signal)) - const sections = renderContextSections(currentContext) - const context = this.runtimeContext.project(joinContextSections(sections), sections) - signal.throwIfAborted() - const startsRequestSeries = firstAttempt && decision.startsRequestSeries === true - const commits = this.systemPrompt.project(renderedPrompt, { - inHistory: preparedCall?.systemPromptUpdate === 'in-history', - startsSeries: startsRequestSeries - || this.requestSurfaceGeneration !== this.session.surface.contentGeneration - || (preparedCall?.toolUpdate === undefined && this.toolsChanged(assembly.tools)), - }) - 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 + const { config, preparedCall } = firstAttempt ? decision.request : await this.prepareRequest(turn, step, signal) + const toolSet = await this.loopCtx.tools.resolve({ agent: this, signal }) + try { + const schemas = JSON.stringify(this.loopCtx.tools.schemas(this, toolSet)) + if (acceptedAssembly === undefined || acceptedAssembly.provider !== config.provider + || acceptedAssembly.model !== config.model || acceptedAssembly.schemas !== schemas) { + const assembly = await this.loopCtx.systemPrompt.assemble( + { ...assembleContextFor(this, signal), toolSet }, + { ...decision.preparation, variables: { ...decision.preparation.variables, ...this.currentRoute() } }, + ) + acceptedAssembly = { provider: config.provider, model: config.model, schemas, assembly } + } + const { assembly } = acceptedAssembly + const renderedPrompt = renderPrompt(assembly) + const currentContext = this.loopCtx.systemPrompt.refreshContext(assembly, assembleContextFor(this, signal)) + const sections = renderContextSections(currentContext) + const context = this.runtimeContext.project(joinContextSections(sections), sections) + signal.throwIfAborted() + const startsRequestSeries = firstAttempt && decision.startsRequestSeries === true + const commits = this.systemPrompt.project(renderedPrompt, { + inHistory: preparedCall?.systemPromptUpdate === 'in-history', + startsSeries: startsRequestSeries + || this.requestSurfaceGeneration !== this.session.surface.contentGeneration + || (preparedCall?.toolUpdate === undefined && this.toolsChanged(assembly.tools)), + }) + 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' }) } - } else { - this.session.append('user/message', message, { surfaceOp: 'append' }) } } - } - if (context !== undefined && !contextAdmitted) { - 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( - this.session.id, - ++this.assistantAttemptCounter, - () => ++this.assistantStreamRevision, - turn, - step, - (frame) => { this.dispatch.emit('agent/assistant-stream', { frame }) }, - ) - let started = false - try { - const stream = preparedCall?.stream(request) ?? this.loopCtx.llm.stream(request) - signal.throwIfAborted() - live.start() - started = true - for await (const chunk of stream) { - signal.throwIfAborted() - live.push(chunk) + if (context !== undefined && !contextAdmitted) { + this.session.append('user/message', context, { surfaceOp: 'append' }) } - signal.throwIfAborted() - } catch (error: unknown) { - if (!started) throw error + firstAttempt = false + const request = this.buildRequest(config, preparedCall, assembly.tools, { turn, step }, startsRequestSeries, signal) + const live = new AssistantStreamAttempt( + this.session.id, + ++this.assistantAttemptCounter, + () => ++this.assistantStreamRevision, + turn, + step, + (frame) => { this.dispatch.emit('agent/assistant-stream', { frame }) }, + ) + let started = false try { - if (signal.aborted) { - const content = live.interruptedBlocks() - if (content.length > 0) { - live.settle('assistant/message', () => this.session.append('assistant/message', { - turn, - step, - message: createAssistantMessage({ - content, - source: { - provider: request.provider, - model: request.model, - ...live.replayState === undefined ? {} : { replayState: live.replayState }, - }, - }), - interrupted: true, - ...live.usage === undefined ? {} : { usage: live.usage }, - stream: live.stream, - }, { surfaceOp: 'append' }).seq) + const stream = preparedCall?.stream(request) ?? this.loopCtx.llm.stream(request) + signal.throwIfAborted() + live.start() + started = true + for await (const chunk of stream) { + signal.throwIfAborted() + live.push(chunk) + } + signal.throwIfAborted() + } catch (error: unknown) { + if (!started) throw error + try { + if (signal.aborted) { + const content = live.interruptedBlocks() + if (content.length > 0) { + live.settle('assistant/message', () => this.session.append('assistant/message', { + turn, + step, + message: createAssistantMessage({ + content, + source: { + provider: request.provider, + model: request.model, + ...live.replayState === undefined ? {} : { replayState: live.replayState }, + }, + }), + interrupted: true, + ...live.usage === undefined ? {} : { usage: live.usage }, + stream: live.stream, + }, { surfaceOp: 'append' }).seq) + } else { + live.settle( + 'assistant/attempt', + () => this.session.append('assistant/attempt', { turn, step, stream: live.stream }).seq, + ) + } } else { live.settle( 'assistant/attempt', () => this.session.append('assistant/attempt', { turn, step, stream: live.stream }).seq, ) } - } else { + } catch (settlementError: unknown) { + throw new AggregateError( + [error, settlementError], + 'Assistant stream failed and its durable settlement was rejected', + { cause: error }, + ) + } + throw error + } + try { + const finish = live.finish + if (finish.kind === 'error' || finish.kind === 'aborted') { live.settle( 'assistant/attempt', () => this.session.append('assistant/attempt', { turn, step, stream: live.stream }).seq, ) + const action = await this.dispatch.waterfall( + 'agent/request-error', { + turn, + step, + provider: request.provider, + failure: finish.failure, + retryPolicy: preparedCall?.retryPolicy, + signal, + }, + () => Promise.resolve(undefined), + ) + signal.throwIfAborted() + if (action?.kind !== 'retry') { + throw new LlmError(finish.failure.message, finish.failure.code, finish.failure) + } + continue } - } catch (settlementError: unknown) { - throw new AggregateError( - [error, settlementError], - 'Assistant stream failed and its durable settlement was rejected', - { cause: error }, - ) - } - throw error - } - try { - const finish = live.finish - if (finish.kind === 'error' || finish.kind === 'aborted') { + + const message = createAssistantMessage({ + content: live.blocks(), + source: { + provider: request.provider, + model: request.model, + ...live.replayState !== undefined ? { replayState: live.replayState } : {}, + }, + }) live.settle( - 'assistant/attempt', - () => this.session.append('assistant/attempt', { turn, step, stream: live.stream }).seq, - ) - const action = await this.dispatch.waterfall( - 'agent/request-error', { + 'assistant/message', + () => this.session.append('assistant/message', { turn, step, - provider: request.provider, - failure: finish.failure, - retryPolicy: preparedCall?.retryPolicy, - signal, - }, - () => Promise.resolve(undefined), + message, + ...live.usage === undefined ? {} : { usage: live.usage }, + stream: live.stream, + }, { surfaceOp: 'append' }).seq, ) - signal.throwIfAborted() - if (action?.kind !== 'retry') { - throw new LlmError(finish.failure.message, finish.failure.code, finish.failure) - } - continue + if (finish.kind === 'max-tokens') return { kind: 'max-tokens' } + + const toolCalls = message.content.filter(block => block.type === 'tool-call') + if (toolCalls.length === 0) return { kind: 'completed' } + const { concluded } = await executeToolCalls( + this.loopCtx, turn, step, toolCalls, signal, + context => this.inbox.splice('next-step', this.inbox.nextStep.length, 0, [context]), + toolSet, + ) + return concluded ? { kind: 'completed' } : null + } catch (error: unknown) { + if (!live.ended) live.abandon() + throw error } - - const message = createAssistantMessage({ - content: live.blocks(), - source: { - provider: request.provider, - model: request.model, - ...live.replayState !== undefined ? { replayState: live.replayState } : {}, - }, - }) - live.settle( - 'assistant/message', - () => this.session.append('assistant/message', { - turn, - step, - message, - ...live.usage === undefined ? {} : { usage: live.usage }, - stream: live.stream, - }, { surfaceOp: 'append' }).seq, - ) - if (finish.kind === 'max-tokens') return { kind: 'max-tokens' } - - const toolCalls = message.content.filter(block => block.type === 'tool-call') - if (toolCalls.length === 0) return { kind: 'completed' } - const { concluded } = await executeToolCalls( - this.loopCtx, turn, step, toolCalls, signal, - context => this.inbox.splice('next-step', this.inbox.nextStep.length, 0, [context]), - ) - return concluded ? { kind: 'completed' } : null - } catch (error: unknown) { - if (!live.ended) live.abandon() - throw error + } finally { + toolSet.dispose() } } } @@ -565,7 +597,8 @@ export class ReactLoopAgent implements Agent { turn: number, step: number, signal: AbortSignal, - ): Promise<{ config: LlmCallConfig; preparedCall?: PreparedLlmCall }> { + ): Promise { + this.requestRoute = undefined const { session } = this // A loop instance starts from its declared route, restoring only an explicit @@ -609,6 +642,7 @@ export class ReactLoopAgent implements Agent { config = proposedConfig } signal.throwIfAborted() + this.requestRoute = Object.freeze({ provider: config.provider, model: config.model }) return { config, ...preparedCall === undefined ? {} : { preparedCall } } } diff --git a/packages/core/agent-loop/src/tool-calls.ts b/packages/core/agent-loop/src/tool-calls.ts index c12de68d19..7424a57931 100644 --- a/packages/core/agent-loop/src/tool-calls.ts +++ b/packages/core/agent-loop/src/tool-calls.ts @@ -11,6 +11,7 @@ * @module dsh-agent-loop/tool-calls */ +import type { ResolvedToolSet } from '@deepseek-ai/dsh-tools' import type { Context } from '@deepseek-ai/cordis' import { createToolResultMessage, type ToolCallBlock } from '@deepseek-ai/dsh-llm' import type { Session, SessionSeq, UserMessage } from '@deepseek-ai/dsh-session' @@ -56,6 +57,7 @@ interface GroupOutcome { * @param toolCalls - assistant calls in model order. * @param signal - abort signal shared by the step. * @param acceptContext - accepts committed result context for the next step boundary. + * @param toolSet - request-owned admission propagated into every Native dispatch. */ export async function executeToolCalls( ctx: Context, @@ -64,6 +66,7 @@ export async function executeToolCalls( toolCalls: ToolCallBlock[], signal: AbortSignal, acceptContext: (context: UserMessage) => void, + toolSet?: ResolvedToolSet, ): Promise<{ concluded: boolean }> { const agent = ctx.agents.requireInitiator() const { session } = agent @@ -77,6 +80,7 @@ export async function executeToolCalls( arguments: parseArguments(block.arguments), agent, signal, + ...toolSet === undefined ? {} : { toolSet }, }, })) diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts index 0213c38060..f6a887eea3 100644 --- a/packages/core/agent-loop/tests/contract-regressions.spec.ts +++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts @@ -1280,8 +1280,8 @@ describe('tool result call identity', () => { }) }) -describe('disposal and cancellation during pre-step assembly', () => { - it('disposal during system-prompt assembly closes a no-step turn', { timeout: 30000 }, async () => { +describe('disposal and cancellation during prompt assembly', () => { + it('disposal during system-prompt assembly closes an admitted step without model output', { timeout: 30000 }, async () => { // Start disposal, then release assembly. Do not await disposal first: it // waits for the blocked driver to exit. const adapter = new MockAdapter(['hang']) @@ -1327,13 +1327,13 @@ describe('disposal and cancellation during pre-step assembly', () => { const e = agent.session.snapshotEvents() expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type)) .toEqual(['turn/start', 'turn/end']) - expect(e.some(x => x.type === 'step/start')).toBe(false) - expect(e.some(x => x.type === 'step/end')).toBe(false) + expect(e.some(x => x.type === 'step/start')).toBe(true) + expect(e.some(x => x.type === 'step/end')).toBe(true) expect(e.some(x => x.type === 'assistant/attempt')).toBe(false) expect(reasons).toEqual([{ kind: 'aborted', reason: { kind: 'disposed' } }]) }) - it('cancel during system-prompt assembly closes a no-step turn', { timeout: 30000 }, async () => { + it('cancel during system-prompt assembly closes an admitted step without model output', { timeout: 30000 }, async () => { const adapter = new MockAdapter([textResponse('should not appear')]) let releaseAssemble!: () => void const blocker = new Promise(r => void (releaseAssemble = r)) @@ -1374,8 +1374,8 @@ describe('disposal and cancellation during pre-step assembly', () => { const e = agent.session.snapshotEvents() expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type)) .toEqual(['turn/start', 'turn/end']) - expect(e.some(x => x.type === 'step/start')).toBe(false) - expect(e.some(x => x.type === 'step/end')).toBe(false) + expect(e.some(x => x.type === 'step/start')).toBe(true) + expect(e.some(x => x.type === 'step/end')).toBe(true) expect(e.some(x => x.type === 'assistant/attempt')).toBe(false) expect(e.some(x => x.type === 'assistant/message')).toBe(false) expect(adapter.requests).toHaveLength(0) diff --git a/packages/core/agent-loop/tests/inbox.spec.ts b/packages/core/agent-loop/tests/inbox.spec.ts index 739d18b990..8f1ac7eeef 100644 --- a/packages/core/agent-loop/tests/inbox.spec.ts +++ b/packages/core/agent-loop/tests/inbox.spec.ts @@ -22,6 +22,7 @@ function stubAgent(rawId: string, overrides: Partial = {}): Agent { const session = overrides.session ?? Session.create(id) const ctx = overrides.ctx ?? new Context() return { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/core/agent-loop/tests/request-freeze.spec.ts b/packages/core/agent-loop/tests/request-freeze.spec.ts index 560be88627..a74891ca45 100644 --- a/packages/core/agent-loop/tests/request-freeze.spec.ts +++ b/packages/core/agent-loop/tests/request-freeze.spec.ts @@ -164,13 +164,12 @@ describe('loop-owned request freezing', () => { const { ctx } = await harness(adapter) const schemas: ToolSchema[][] = [] const stops: string[][] = [] - ctx.on('system-prompt/assemble', async (_assembly, _context, next) => { - const assembly = await next() + ctx.systemPrompt.tools(() => { const tools: ToolSchema[] = [{ name: 'nested', description: 'test', parameters: { type: 'object', properties: { value: { type: 'array', items: { type: 'string', enum: ['a', 'b'] } } }, } }] schemas.push(tools) - return { ...assembly, tools } + return { schemas: tools } }) ctx.on('agent/request', async (_payload, next) => { const config = await next() @@ -193,8 +192,8 @@ describe('loop-owned request freezing', () => { expect(errors).toEqual([]) expect(requests).toHaveLength(2) for (const [index, request] of requests.entries()) { - expect(request.tools).toBe(schemas[index]) - expectFrozen(schemas[index]) + expect(request.tools).toEqual(schemas[index]) + expectFrozen(request.tools) expect(() => request.stop!.push('mutate')).toThrow(TypeError) if (!registered) expect(request.stop).toBe(stops[index]) } diff --git a/packages/core/agent-loop/tests/system-prompt-admission.spec.ts b/packages/core/agent-loop/tests/system-prompt-admission.spec.ts index 01543b64df..47390d938b 100644 --- a/packages/core/agent-loop/tests/system-prompt-admission.spec.ts +++ b/packages/core/agent-loop/tests/system-prompt-admission.spec.ts @@ -369,13 +369,13 @@ describe('prepared-route prompt admission', () => { expect(resumed.session.snapshotEvents().filter(event => event.type === 'request/header').at(-1)?.data.reason).toBe('resume') }) - it.each(['request', 'prepare'] as const)('cancels during %s with a balanced empty step before input admission', async (stage) => { + it.each(['request', 'prepare'] as const)('cancels during %s before step and input admission', async (stage) => { const h = await harness() const entered = Promise.withResolvers() const release = Promise.withResolvers() const observe = async () => { const types = h.agent.session.snapshotEvents().map(event => event.type) - expect(types).toContain('step/start') + expect(types).not.toContain('step/start') expect(types).not.toContain('system/message') expect(types).not.toContain('user/message') expect(types).not.toContain('request/header') @@ -394,7 +394,7 @@ describe('prepared-route prompt admission', () => { await h.agent.whenIdle() expect(h.capable.requests).toHaveLength(0) const types = h.agent.session.snapshotEvents().map(event => event.type) - expect(types.filter(type => type === 'step/start' || type === 'step/end')).toEqual(['step/start', 'step/end']) + expect(types.filter(type => type === 'step/start' || type === 'step/end')).toEqual([]) expect(types.filter(type => ['system/message', 'user/message', 'request/header'].includes(type))).toEqual([]) }) diff --git a/packages/core/agent-loop/tests/tool-calls.spec.ts b/packages/core/agent-loop/tests/tool-calls.spec.ts index 8aa74cec2d..c84a10e06e 100644 --- a/packages/core/agent-loop/tests/tool-calls.spec.ts +++ b/packages/core/agent-loop/tests/tool-calls.spec.ts @@ -150,7 +150,7 @@ describe('tool-call scheduler: grouping and barriers', () => { expect(order).toEqual(['r-start-A1', 'r-end-A1', 'w-A2', 'r-start-A3', 'r-end-A3']) }) - it('reclassifies pending calls after an exclusive barrier replaces their tool', async () => { + it('rejects pending calls whose original registration was replaced', async () => { const adapter = new MockAdapter([ multiCall([ { id: 'c1', name: 'replace', args: { id: '0' } }, @@ -181,17 +181,13 @@ describe('tool-call scheduler: grouping and barriers', () => { const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })) - await until(() => replacement.started.length === 1) - await new Promise(r => setTimeout(r, 5)) - expect(replacement.started).toEqual(['1']) - replacement.release('1') - await until(() => replacement.started.length === 2) - expect(replacement.started).toEqual(['1', '2']) - replacement.release('2') await waitForIdle(ctx, agent) + expect(replacement.started).toEqual([]) + expect(events(agent).filter(event => event.type === 'tool/result').slice(1).map(event => event.data.error?.code)) + .toEqual(['UNKNOWN_TOOL', 'UNKNOWN_TOOL']) }) - it('stops replenishing when a result observer makes the next call exclusive', async () => { + it('drains started calls and rejects unstarted calls after replacement', async () => { const adapter = new MockAdapter([ multiCall([ { id: 'c1', name: 'x', args: { id: '1' } }, @@ -219,10 +215,9 @@ describe('tool-call scheduler: grouping and barriers', () => { await new Promise(r => setTimeout(r, 5)) expect(replacement.started).toEqual([]) initial.release('2') - await until(() => replacement.started.length === 1) - expect(replacement.started).toEqual(['3']) - replacement.release('3') await waitForIdle(ctx, agent) + expect(replacement.started).toEqual([]) + expect(events(agent).filter(event => event.type === 'tool/result').at(-1)?.data.error?.code).toBe('UNKNOWN_TOOL') }) }) diff --git a/packages/core/agent-loop/tests/tool-order.spec.ts b/packages/core/agent-loop/tests/tool-order.spec.ts index 287f5fe761..688be207cd 100644 --- a/packages/core/agent-loop/tests/tool-order.spec.ts +++ b/packages/core/agent-loop/tests/tool-order.spec.ts @@ -95,7 +95,7 @@ describe('loop-level canonical tool order', () => { expect(Object.isFrozen(adapter.requests[0])).toBe(true) }) - it('closes a no-step turn when toolOrder names an unregistered tool', async () => { + it('closes the admitted step without a request when toolOrder names an unregistered tool', async () => { const adapter = new MockAdapter([textResponse('never sent')]) const ctx = await harness(adapter, ['ghost', TOOL_ORDER_REST]) registerNamed(ctx, 'alpha') @@ -106,7 +106,7 @@ describe('loop-level canonical tool order', () => { expect(foldRequestHeader(agent.session.snapshotEvents())).toBeUndefined() expect(agent.session.snapshotEvents().some(e => e.type === 'turn/start')).toBe(true) expect(agent.session.snapshotEvents().some(e => e.type === 'turn/end')).toBe(true) - expect(agent.session.snapshotEvents().some(e => e.type === 'step/start')).toBe(false) - expect(agent.session.snapshotEvents().some(e => e.type === 'step/end')).toBe(false) + expect(agent.session.snapshotEvents().some(e => e.type === 'step/start')).toBe(true) + expect(agent.session.snapshotEvents().some(e => e.type === 'step/end')).toBe(true) }) }) diff --git a/packages/core/agent-loop/tests/tool-updates.spec.ts b/packages/core/agent-loop/tests/tool-updates.spec.ts index 6eca806085..bd0542cc74 100644 --- a/packages/core/agent-loop/tests/tool-updates.spec.ts +++ b/packages/core/agent-loop/tests/tool-updates.spec.ts @@ -9,7 +9,7 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools' -import { MockAdapter, textResponse } from './mock-adapter.ts' +import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' const contexts: Context[] = [] afterEach(async () => { @@ -112,3 +112,127 @@ describe('tool update emission', () => { expect(agent.session.requestHeader()?.tools?.map(schema => schema.name)).toEqual(['search']) }) }) + + +describe('request-time tool activation', () => { + it('uses the bound route for asynchronous conditions and rejects a historical name after removal', async () => { + const adapter = new MockAdapter([ + toolCallResponse('allowed', 'conditional', {}), textResponse('done'), + toolCallResponse('removed', 'conditional', {}), textResponse('done'), + ]) + const { ctx, agent } = await harness(adapter) + const routed: string[] = [] + let calls = 0 + let model = 'enabled' + ctx.on('agent/request', async (_payload, next) => ({ ...await next(), model })) + ctx.tools.register(defineContentToolFixture({ + name: 'conditional', description: 'Available on one route.', parameters: {}, + async execute(_args, exec) { + expect(exec.agent?.currentRoute()).toEqual({ provider: 'mock', model: 'enabled' }) + calls++ + return [{ type: 'text', text: 'done' }] + }, + }), { when: async ({ agent: subject }) => { + const route = subject.currentRoute() + expect(Object.isFrozen(route)).toBe(true) + routed.push(route.model) + await Promise.resolve() + return route.model === 'enabled' + } }) + expect(() => agent.currentRoute()).toThrow('no prepared model request') + await send(agent, 'enabled') + model = 'disabled' + await send(agent, 'try the historical name') + expect(calls).toBe(1) + expect(routed).toEqual(['enabled', 'enabled', 'disabled', 'disabled']) + expect(developerEvents(agent).at(-1)?.data.message.content) + .toEqual([{ type: 'tool-removal', toolName: 'conditional' }]) + expect(agent.session.snapshotEvents().find(event => event.type === 'tool/result' && event.data.message.toolCallId === 'removed')?.data) + .toMatchObject({ error: { code: 'UNKNOWN_TOOL' } }) + expect(() => agent.currentRoute()).toThrow('no prepared model request') + }) + + it('cancels an asynchronous condition without committing the proposed request', async () => { + const adapter = new MockAdapter([textResponse('unused')]) + const { ctx, agent } = await harness(adapter) + const entered = Promise.withResolvers() + const settle = Promise.withResolvers() + ctx.tools.register(defineContentToolFixture({ + name: 'conditional', description: 'A pending condition.', parameters: {}, + execute: async () => [{ type: 'text', text: 'unused' }], + }), { when: () => { entered.resolve(undefined); return settle.promise } }) + const running = send(agent, 'cancel during activation') + await entered.promise + agent.cancel({ kind: 'user' }) + settle.resolve(true) + await running + expect(adapter.requests).toEqual([]) + expect(agent.session.snapshotEvents().filter(event => ['request/header', 'system/message', 'user/message'].includes(event.type))).toEqual([]) + expect(() => agent.currentRoute()).toThrow('no prepared model request') + }) + + it.each(['condition', 'route'])('reassembles on a retry after the %s changes without admitting users again', async (change) => { + const adapter = new MockAdapter([ + [{ type: 'finish', reason: { kind: 'error', failure: { code: 'SERVER', message: 'retry', status: 503 } } }], + textResponse('done'), + ]) + const { ctx, agent } = await harness(adapter) + let retried = false + let admissions = 0 + ctx.on('agent/pre-step', (_input, next) => { admissions++; return next() }) + ctx.on('agent/request', async (_input, next) => ({ + ...await next(), model: change === 'route' && retried ? 'second' : 'first', + })) + ctx.on('agent/request-error', async (_input, next) => { + await next() + retried = true + return { kind: 'retry' } + }) + ctx.tools.register(defineContentToolFixture({ + name: 'conditional', description: 'Retry-dependent availability.', parameters: {}, + execute: async () => [{ type: 'text', text: 'unused' }], + }), { when: () => change === 'route' || retried }) + ctx.systemPrompt.section({ + name: 'retry:guidance', order: 0, complete: true, + text: context => `${agent.currentRoute().model}:${ctx.tools.schemas(agent, context.toolSet).map(tool => tool.name).join(',')}`, + }) + await send(agent, 'retry once') + const prompts = adapter.requests.map(request => request.messages.find(message => message.role === 'system')?.content) + expect(prompts[0]).toEqual([{ type: 'text', text: change === 'route' ? 'first:conditional' : 'first:' }]) + expect(prompts[1]).toEqual([{ type: 'text', text: change === 'route' ? 'second:conditional' : 'first:conditional' }]) + expect(admissions).toBe(1) + expect(agent.session.snapshotEvents().filter(event => event.type === 'user/message' && event.data.source.kind === 'user')).toHaveLength(1) + expect(() => agent.currentRoute()).toThrow('no prepared model request') + }) + + it('reports condition failures without admitting a partial request', async () => { + const adapter = new MockAdapter([textResponse('unused')]) + const { ctx, agent } = await harness(adapter) + ctx.tools.register(defineContentToolFixture({ + name: 'conditional', description: 'An unavailable activation dependency.', parameters: {}, + execute: async () => [{ type: 'text', text: 'unused' }], + }), { when: async () => { throw new Error('credential storage unavailable') } }) + await send(agent, 'inspect') + expect(adapter.requests).toEqual([]) + expect(agent.session.snapshotEvents().filter(event => ['request/header', 'system/message', 'user/message'].includes(event.type))).toEqual([]) + expect(agent.session.snapshotEvents().find(event => event.type === 'turn/end')?.data) + .toMatchObject({ reason: { kind: 'error', error: { code: 'TOOL_ACTIVATION_FAILED' } } }) + expect(() => agent.currentRoute()).toThrow('no prepared model request') + }) + + it('shares the selected set with prompt guidance', async () => { + const adapter = new MockAdapter([textResponse('done')]) + const { ctx, agent } = await harness(adapter) + ctx.tools.register(defineContentToolFixture({ + name: 'conditional', description: 'Disabled.', parameters: {}, + execute: async () => [{ type: 'text', text: 'unused' }], + }), { when: () => false }) + ctx.systemPrompt.section({ + name: 'conditional:guidance', order: 0, + text: context => ctx.tools.get('conditional', context.scope, context.toolSet) === undefined ? '' : 'Conditional guidance', + }) + await send(agent, 'inspect') + expect(adapter.requests[0]?.tools ?? []).toEqual([]) + expect(JSON.stringify(adapter.requests[0]?.messages)).not.toContain('Conditional guidance') + }) +}) diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index befdc09642..1502bd30aa 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -36,14 +36,14 @@ en: 7c850051d2857bfa zh: 4cab817553114e15 /deepseek-ai-dsh-agent/understand-the-implementation/step-admission: - en: 348b9ecd0890c585 - zh: d0f73d28d0d2c6f1 + en: 5bba803d6c23370a + zh: 0e5e3c037a111141 /deepseek-ai-dsh-agent/understand-the-implementation/durable-inbox: en: f6802c7f94f623b3 zh: cc3885db80765970 /deepseek-ai-dsh-agent/understand-the-implementation/source-map: - en: 7e09b94c79dc97ca - zh: 3f9c5455c4dc563a + en: 8efa6e74d4dbe28e + zh: 6126170ac464703f /deepseek-ai-dsh-agent/understand-the-implementation/registry-and-lifecycle: en: df92e97cedd0d02e zh: f6040c145bffd933 diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 9da1f06f64..d2255afddb 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -82,7 +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. Acceptance does not commit it: after assembly and `step/start`, `agent/request` and `prepareCall()` resolve the route before the loop commits the system prompt and user batch. Cancellation during either async phase commits neither. `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. Route preparation runs before `agent/pre-step`. Acceptance does not commit the batch: tool conditions and prompt assembly must also succeed before system and user messages are logged. Cancellation during route preparation opens no step and 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. + +`agent.currentRoute()` returns the immutable provider/model bound by request preparation, through the corresponding tool batch. It throws before preparation and after the batch ends; initial options and previous Session headers are not fallbacks. Auxiliary model calls do not replace the main request route. ### Durable inbox @@ -99,7 +101,7 @@ The package is built on one separation: the public `Agent` surface and registry | [`src/types.ts`](src/types.ts) | `AgentOptions`, cancellation causes, and inbox projection vocabulary | | [`src/dispatch.ts`](src/dispatch.ts) | `agentEvents` fused dispatcher and `assembleContextFor(agent)` | | [`src/consumed-work.ts`](src/consumed-work.ts) | `foldConsumedWork(events)`: what the log's consumed work became | -| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`: coupling one selection to assembly and routing | +| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`: capturing a selection for step routing | | [`src/archive-admission.ts`](src/archive-admission.ts) | The `turn` family of the Workspace registry's archive admission: a running turn and its user-cause cancel | ### Registry and lifecycle diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index 0166f96a80..722b9edd45 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -82,7 +82,9 @@ await handle.agent.whenIdle() ### 步骤准入 -`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages, startsRequestSeries? }`。enter 分支包含完整、带标识且冻结的消息批次。接纳不等于提交:组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析路由,循环随后才提交系统提示词与用户批次。在任一异步阶段取消都不会提交这两者。`startsRequestSeries: true` 声明一个独立的模型消息序列;包装下游 enter 的监听器会保留该声明与批次,除非有意替换其中一项。领取会从 inbox 移除候选消息,领取后插入的消息则等待后续边界。 +`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages, startsRequestSeries? }`。enter 分支包含完整、带标识且冻结的消息批次。路由准备在 `agent/pre-step` 之前执行。接纳不等于提交:工具条件与提示词组装也必须成功,随后才记录系统提示词与用户批次。在路由准备期间取消不会打开步骤,也不会提交这两种消息。`startsRequestSeries: true` 声明一个独立的模型消息序列;包装下游 enter 的监听器会保留该声明与批次,除非有意替换其中一项。领取会从 inbox 移除候选消息,领取后插入的消息则等待后续边界。 + +`agent.currentRoute()` 返回请求准备绑定的不可变提供方与模型,持续到对应工具批次结束。准备前和批次结束后调用会抛错,不回退到初始选项或先前 Session 请求头。辅助模型调用不会替换主请求路由。 ### 持久 inbox @@ -99,7 +101,7 @@ await handle.agent.whenIdle() | [`src/types.ts`](src/types.ts) | `AgentOptions`、取消原因与收件箱投影词汇 | | [`src/dispatch.ts`](src/dispatch.ts) | `agentEvents` 融合分发器与 `assembleContextFor(agent)` | | [`src/consumed-work.ts`](src/consumed-work.ts) | `foldConsumedWork(events)`:日志消费掉的工作最终怎样了 | -| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`:把一个选择耦合到组装与路由 | +| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`:为步骤路由捕获模型选择 | | [`src/archive-admission.ts`](src/archive-admission.ts) | Workspace 注册表归档准入中的 `turn` 族:运行中的回合及其用户原因取消 | ### 注册表与生命周期 diff --git a/packages/core/agent/src/model-selection.ts b/packages/core/agent/src/model-selection.ts index 8921bc12a4..d5801e30b7 100644 --- a/packages/core/agent/src/model-selection.ts +++ b/packages/core/agent/src/model-selection.ts @@ -30,9 +30,9 @@ export interface ModelSelection { /** Mutable model selection plus the value captured for the current step. */ export interface ModelSelectionRef { - /** Model selected for the next step that enters prompt assembly. */ + /** Model selected for the next step entering route preparation. */ current: ModelSelection | undefined - /** Selection captured when the current step entered prompt assembly. */ + /** Selection captured by the first route preparation of the current step. */ assembled: ModelSelection | undefined } @@ -61,15 +61,13 @@ function modelSwitchNotice(previous: ModelSelection, selected: ModelSelection) { } /** - * Couple one mutable selection to Agent-scoped prompt assembly and request routing. - * Prompt assembly snapshots the selected model before delegating, then applies - * its provider/model pair and effort to request config so a - * concurrent switch takes effect on a later step instead of splitting the two - * surfaces. An absent selected effort clears any inherited effort, restoring - * the selected model's provider/default behavior. + * Couple one mutable selection to Agent-scoped request routing. The first + * request preparation in a step captures the selection before delegating; + * retries retain it, and concurrent selections apply to later steps. An absent + * selected effort clears inherited effort, restoring the model's provider default. * * A provider/model change appends a durable user-role notice to the next - * admitted request. It compares the assembled selection with the latest + * admitted request. It compares the bound request route 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. @@ -79,23 +77,14 @@ function modelSwitchNotice(previous: ModelSelection, selected: ModelSelection) { * @returns Disposer for all scoped waterfall listeners. */ export function installModelSelection(agentCtx: Context, selection: ModelSelectionRef): () => void { - const disposeAssembly = agentCtx.on('system-prompt/assemble', async (_assembly, _context, next) => { - const selected = selection.current - const assembled = await next() - selection.assembled = selected - if (selected === undefined) return assembled - return { - ...assembled, - variables: { - ...assembled.variables, - provider: selected.provider, - model: selected.model, - }, - } - }) + let capturedStep: { turn: number; step: number } | undefined const disposeRequest = agentCtx.on( 'agent/request', - async (_payload, next): Promise => { + async ({ turn, step }, next): Promise => { + if (capturedStep?.turn !== turn || capturedStep.step !== step) { + capturedStep = { turn, step } + selection.assembled = selection.current === undefined ? undefined : { ...selection.current } + } const resolved = await next() const selected = selection.assembled if (selected === undefined) return resolved @@ -117,15 +106,14 @@ export function installModelSelection(agentCtx: Context, selection: ModelSelecti 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 = selection.assembled + const selected = agent.currentRoute() const previous = agent.session.requestHeader()?.config - if (selected === undefined || previous === undefined || sameRoute(selected, previous)) return decision + if (previous === undefined || sameRoute(selected, previous)) return decision return { ...decision, messages: [...decision.messages, modelSwitchNotice(previous, selected)] } }, { prepend: true }, ) return () => { - disposeAssembly() disposeRequest() disposeNotice() } diff --git a/packages/core/agent/src/runtime-types.ts b/packages/core/agent/src/runtime-types.ts index 0fa89afc05..9501b45841 100644 --- a/packages/core/agent/src/runtime-types.ts +++ b/packages/core/agent/src/runtime-types.ts @@ -162,8 +162,14 @@ export type AssistantStreamFrame = declare module './types.ts' { interface Agent { - /** The provider route and model this agent's requests use. */ + /** Initial request options; currentRoute() reports the bound request route. */ readonly options: AgentOptions + /** + * Read the immutable route bound to this request and its tool batch. + * @returns the actual provider and model selected by prepareCall(). + * @throws when no main request is prepared; never falls back to prior requests. + */ + currentRoute(): Readonly> /** The live session this agent drives; its log is the durable source of truth. */ readonly session: Session /** Agent-owned access to durable pending work. */ @@ -322,9 +328,9 @@ declare module '@deepseek-ai/cordis' { * Replace the frozen call configuration. `await next()` yields the config * the machine would use (agent options on the first request, the logged * header afterwards); return a replacement to switch. On step admission, - * this runs after assembly and `step/start`, before the system prompt and - * accepted user batch are committed. Cancellation here or during subsequent - * `prepareCall()` resolution commits neither. The prepared call capability + * this runs before context preparation and `agent/pre-step`. Cancellation + * here or during subsequent `prepareCall()` resolution opens no step and + * commits no system prompt or user batch. The prepared call capability * governs prompt admission. Model-visible content must use logged channels; * this waterfall cannot mutate messages. * @param payload.agent - the agent making the model call. diff --git a/packages/core/agent/tests/agent.spec.ts b/packages/core/agent/tests/agent.spec.ts index c31acf5c66..282c942dd6 100644 --- a/packages/core/agent/tests/agent.spec.ts +++ b/packages/core/agent/tests/agent.spec.ts @@ -18,6 +18,7 @@ function stubAgent(rawId: string, overrides: Partial = {}): Agent { const session = overrides.session ?? Session.create(id) const ctx = overrides.ctx ?? new Context() const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/core/agent/tests/archive-admission.spec.ts b/packages/core/agent/tests/archive-admission.spec.ts index 0f84de4fe0..40556764c7 100644 --- a/packages/core/agent/tests/archive-admission.spec.ts +++ b/packages/core/agent/tests/archive-admission.spec.ts @@ -15,6 +15,7 @@ async function liveAgent(ctx: Context, rawId: string, initial: AgentStatus): Pro const cancels: CancelCall[] = [] let status = initial const agent: LiveAgent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session: Session.create(id), diff --git a/packages/core/agent/tests/model-selection.spec.ts b/packages/core/agent/tests/model-selection.spec.ts index 27b2a7bffd..7dddee1b9a 100644 --- a/packages/core/agent/tests/model-selection.spec.ts +++ b/packages/core/agent/tests/model-selection.spec.ts @@ -1,191 +1,88 @@ -import { describe, expect, it } from 'vitest' +/** Model selection is captured before route preparation and remains stable within a step. */ +import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import SystemPrompt from '@deepseek-ai/dsh-system-prompt' -import { - agentEvents, - installModelSelection, - type Agent, - type ModelSelection, - type ModelSelectionRef, -} from '../src/index.ts' -import { - createUserMessage, - ReasoningEffortId, - type LlmCallConfig, - type UserMessage, -} from '@deepseek-ai/dsh-llm' +import { agentEvents, installModelSelection, type Agent, type ModelSelection, type ModelSelectionRef } from '../src/index.ts' +import { createUserMessage, ReasoningEffortId, type LlmCallConfig } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' -const SIGNAL = new AbortController().signal -const INPUT = createUserMessage({ - content: [{ type: 'text', text: 'continue' }], - source: { kind: 'user' }, +const contexts: Context[] = [] +afterEach(async () => { + for (const ctx of contexts.splice(0)) await ctx.fiber.dispose() }) +const signal = new AbortController().signal +const input = createUserMessage({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } }) -function createAgent(): Agent { - return { session: Session.create(SessionId('model-selection')) } as Agent -} - -function expectedNotice(from: string, to: string) { - return { - content: [{ - type: 'text', - text: `[model changed: assistant turns above this point were generated by ${from}; the session continues with ${to}]`, - }], - source: { kind: 'model-selection', form: 'notice', summary: `${from} → ${to}` }, - } -} - -async function switchHarness(current: ModelSelection, previous?: ModelSelection) { +function harness(current?: ModelSelection, previous?: ModelSelection) { const ctx = new Context() - await ctx.plugin(SystemPrompt) + contexts.push(ctx) const selection: ModelSelectionRef = { current, assembled: undefined } const dispose = installModelSelection(ctx, selection) - const agent = createAgent() - if (previous !== undefined) { - agent.session.append('request/header', { header: { config: previous }, reason: 'initial' }) + let route: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 } + const agent = { session: Session.create(SessionId('model-selection')), currentRoute: () => route } as Agent + if (previous) agent.session.append('request/header', { header: { config: previous }, reason: 'initial' }) + const request = async (step: number, seed: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 }) => { + route = await agentEvents(ctx, agent).waterfall('agent/request', { turn: 1, step, signal }, () => Promise.resolve(seed)) + return route } - await ctx.systemPrompt.assemble() - return { agent, ctx, dispose, selection } + const preStep = (messages = [input], offered = [input], step = 1, requestSignal = signal) => + agentEvents(ctx, agent).waterfall('agent/pre-step', { turn: 1, step, messages: offered, signal: requestSignal }, + () => Promise.resolve({ kind: 'enter' as const, messages })) + return { ctx, agent, selection, dispose, request, preStep } } -async function preStep( - ctx: Context, - agent: Agent, - { - messages = [INPUT], - offered = [INPUT], - step = 1, - signal = SIGNAL, - }: { - messages?: UserMessage[] - offered?: UserMessage[] - step?: number - signal?: AbortSignal - } = {}, -) { - return agentEvents(ctx, agent).waterfall( - 'agent/pre-step', - { turn: 1, step, messages: offered, signal }, - () => Promise.resolve({ kind: 'enter' as const, messages }), - ) +function notice(from: string, to: string) { + return { content: [{ type: 'text', text: `[model changed: assistant turns above this point were generated by ${from}; the session continues with ${to}]` }] } } describe('installModelSelection()', () => { - it('snapshots prompt variables and request routing together, then disposes its listeners', async () => { - const ctx = new Context() - await ctx.plugin(SystemPrompt) - const selection: ModelSelectionRef = { current: undefined, assembled: undefined } - const dispose = installModelSelection(ctx, selection) - const agent = createAgent() + it('captures selection per step, preserves retries, and disposes routing', async () => { + const h = harness() const seed: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 } - const signal = new AbortController().signal - - expect((await ctx.systemPrompt.assemble()).variables).toEqual({}) - await expect(agentEvents(ctx, agent).waterfall( - 'agent/request', { turn: 1, step: 0, signal }, () => Promise.resolve(seed), - )).resolves.toBe(seed) - - selection.current = { - provider: 'alpha', - model: 'a1', - reasoningEffort: ReasoningEffortId('high'), - } - expect((await ctx.systemPrompt.assemble()).variables).toMatchObject({ provider: 'alpha', model: 'a1' }) - selection.current = { provider: 'beta', model: 'b1' } - await expect(agentEvents(ctx, agent).waterfall( - 'agent/request', { turn: 1, step: 0, signal }, () => Promise.resolve(seed), - )).resolves.toEqual({ - provider: 'alpha', - model: 'a1', - reasoningEffort: ReasoningEffortId('high'), - temperature: 0.2, - }) - - expect((await ctx.systemPrompt.assemble()).variables).toMatchObject({ provider: 'beta', model: 'b1' }) - const inherited: LlmCallConfig = { - provider: 'alpha', - model: 'a1', - reasoningEffort: ReasoningEffortId('max'), - temperature: 0.2, - } - await expect(agentEvents(ctx, agent).waterfall( - 'agent/request', { turn: 1, step: 1, signal }, () => Promise.resolve(inherited), - )).resolves.toEqual({ provider: 'beta', model: 'b1', temperature: 0.2 }) - - dispose() - expect((await ctx.systemPrompt.assemble()).variables).toEqual({}) - await expect(agentEvents(ctx, agent).waterfall( - 'agent/request', { turn: 2, step: 0, signal }, () => Promise.resolve(seed), - )).resolves.toBe(seed) - await ctx.fiber.dispose() + expect(await h.request(0, seed)).toBe(seed) + h.selection.current = { provider: 'alpha', model: 'a1', reasoningEffort: ReasoningEffortId('high') } + expect(await h.request(1)).toEqual({ ...seed, ...h.selection.current }) + h.selection.current = { provider: 'beta', model: 'b1' } + expect(await h.request(1)).toMatchObject({ provider: 'alpha', model: 'a1' }) + expect(await h.request(2, { ...seed, reasoningEffort: ReasoningEffortId('max') })).toEqual({ ...seed, ...h.selection.current }) + h.dispose() + expect(await h.request(3, seed)).toBe(seed) }) - it('announces same-provider and cross-provider route changes from the assembled selection', async () => { - const { agent, ctx, dispose, selection } = await switchHarness( - { provider: 'alpha', model: 'a1' }, - { provider: 'alpha', model: 'a0' }, - ) - await expect(preStep(ctx, agent)).resolves.toMatchObject({ - messages: [INPUT, expectedNotice('a0', 'a1')], - }) - - selection.current = { provider: 'beta', model: 'b1' } - await ctx.systemPrompt.assemble() - selection.current = { provider: 'alpha', model: 'a2' } - await expect(preStep(ctx, agent)).resolves.toMatchObject({ - messages: [INPUT, expectedNotice('alpha/a0', 'beta/b1')], - }) - await ctx.systemPrompt.assemble() - await expect(preStep(ctx, agent)).resolves.toMatchObject({ - messages: [INPUT, expectedNotice('a0', 'a2')], - }) - - dispose() - await ctx.fiber.dispose() + it('announces the bound same-provider and cross-provider routes', async () => { + const h = harness({ provider: 'alpha', model: 'a1' }, { provider: 'alpha', model: 'a0' }) + await h.request(1) + await expect(h.preStep()).resolves.toMatchObject({ messages: [input, notice('a0', 'a1')] }) + h.selection.current = { provider: 'beta', model: 'b1' } + await h.request(2) + h.selection.current = { provider: 'alpha', model: 'a2' } + await expect(h.preStep()).resolves.toMatchObject({ messages: [input, notice('alpha/a0', 'beta/b1')] }) + await h.request(3) + await expect(h.preStep()).resolves.toMatchObject({ messages: [input, notice('a0', 'a2')] }) }) - it('does not announce initial, same-route, effort-only, rejected, aborted, or disposed steps', async () => { - const { agent, ctx, dispose, selection } = await switchHarness({ provider: 'alpha', model: 'a0' }) - await expect(preStep(ctx, agent)).resolves.toMatchObject({ kind: 'enter', messages: [INPUT] }) - agent.session.append('request/header', { - header: { config: { provider: 'alpha', model: 'a0' } }, reason: 'initial', - }) - selection.current = { - provider: 'alpha', - model: 'a0', - reasoningEffort: ReasoningEffortId('high'), - } - await ctx.systemPrompt.assemble() - await expect(preStep(ctx, agent)).resolves.toMatchObject({ kind: 'enter', messages: [INPUT] }) - - selection.current = { provider: 'alpha', model: 'a1' } - await ctx.systemPrompt.assemble() - const rejected = await agentEvents(ctx, agent).waterfall( - 'agent/pre-step', - { turn: 1, step: 1, messages: [], signal: SIGNAL }, - () => Promise.resolve({ kind: 'reject' as const }), - ) - expect(rejected).toEqual({ kind: 'reject' }) - const aborted = new AbortController() - aborted.abort() - await expect(preStep(ctx, agent, { signal: aborted.signal })).resolves.toMatchObject({ messages: [INPUT] }) - - dispose() - await expect(preStep(ctx, agent)).resolves.toMatchObject({ kind: 'enter', messages: [INPUT] }) - await ctx.fiber.dispose() + it('preserves initial, unchanged, rejected, cancelled, and disposed decisions', async () => { + const h = harness({ provider: 'alpha', model: 'a0' }) + await h.request(1) + await expect(h.preStep()).resolves.toEqual({ kind: 'enter', messages: [input] }) + h.agent.session.append('request/header', { header: { config: h.agent.currentRoute() }, reason: 'initial' }) + h.selection.current = { provider: 'alpha', model: 'a0', reasoningEffort: ReasoningEffortId('high') } + await h.request(2) + await expect(h.preStep()).resolves.toEqual({ kind: 'enter', messages: [input] }) + h.selection.current = { provider: 'alpha', model: 'a1' } + await h.request(3) + await expect(agentEvents(h.ctx, h.agent).waterfall('agent/pre-step', { turn: 1, step: 3, messages: [], signal }, + () => Promise.resolve({ kind: 'reject' as const }))).resolves.toEqual({ kind: 'reject' }) + const aborted = AbortSignal.abort() + await expect(h.preStep([input], [input], 3, aborted)).resolves.toEqual({ kind: 'enter', messages: [input] }) + h.dispose() + await expect(h.preStep()).resolves.toEqual({ kind: 'enter', messages: [input] }) }) - it('preserves empty no-call decisions and announces an empty tool continuation', async () => { - const { agent, ctx } = await switchHarness( - { provider: 'alpha', model: 'a1' }, - { provider: 'alpha', model: 'a0' }, - ) - await expect(preStep(ctx, agent, { messages: [] })).resolves.toEqual({ kind: 'enter', messages: [] }) - await expect(preStep(ctx, agent, { messages: [], step: 2 })).resolves.toEqual({ kind: 'enter', messages: [] }) - await expect(preStep(ctx, agent, { messages: [], offered: [], step: 2 })).resolves.toMatchObject({ - messages: [{ source: { summary: 'a0 → a1' } }], - }) - await ctx.fiber.dispose() + it('preserves empty claims and announces an empty tool continuation', async () => { + const h = harness({ provider: 'alpha', model: 'a1' }, { provider: 'alpha', model: 'a0' }) + await h.request(1) + await expect(h.preStep([])).resolves.toEqual({ kind: 'enter', messages: [] }) + await expect(h.preStep([], [input], 2)).resolves.toEqual({ kind: 'enter', messages: [] }) + await expect(h.preStep([], [], 2)).resolves.toMatchObject({ messages: [notice('a0', 'a1')] }) }) }) diff --git a/packages/core/system-prompt/README.i18n.yaml b/packages/core/system-prompt/README.i18n.yaml index a5c3f5913c..18bb14f61c 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: 165fecb5329b00b6 - zh: a7c595d1787916c9 + en: db364cc533949015 + zh: a6e8a12d9a661b60 /deepseek-ai-dsh-system-prompt/use-this-package/contribute-a-prompt-variable: en: b3e4c9b4ced59488 zh: d80c791e72bd5821 @@ -42,8 +42,8 @@ en: d9575bcb07a1145b zh: 0f1a388780d055b1 /deepseek-ai-dsh-system-prompt/understand-the-implementation/assembly-and-rendering: - en: 54977e6c431f851e - zh: ef60007054b97ff0 + en: d51940213af9b87f + zh: f3a2540010624a79 /deepseek-ai-dsh-system-prompt/understand-the-implementation/scoping: en: 1100ab1dfe961631 zh: c510275c893951b4 @@ -57,8 +57,8 @@ en: cf78b9dd5596495f zh: bd44560f8c2435e8 /deepseek-ai-dsh-system-prompt/model-experience/system-prompt/what-the-model-sees: - en: 0aaabee3a9883eea - zh: 13e6ead5b455fa30 + en: 622e83de7772dc1f + zh: 7b3c54f08a317e93 /deepseek-ai-dsh-system-prompt/model-experience/system-prompt/what-the-model-sees/harness-identity: en: 40effd71cb503319 zh: 9969356237a4f398 @@ -72,8 +72,8 @@ en: 3039bc5e7a796b21 zh: 6b7a9eaf69c9ea53 /deepseek-ai-dsh-system-prompt/model-experience/tool-schemas/what-the-model-sees: - en: a983ef220593cf73 - zh: 28770612ffc1be60 + en: b3873ff844d91e34 + zh: 5cccd2afda37af9a /deepseek-ai-dsh-system-prompt/model-experience/tool-schemas/token-effect: en: f3a7d374c55e1720 zh: 0ee48af678cb875f diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index 6c9d7338ec..d6a37baa85 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -67,6 +67,8 @@ 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 the loop binds the route and commits step 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 request selection as execution. + ### Contribute a prompt variable Variables are referenced from section or context text as `{{name}}` and resolved at each assembly; scoped variables shadow a same-named global for that agent. The loop supplies `provider` and `model`; plugins can register the facts they own. The working-directory service supplies directory text as literal user context. @@ -107,7 +109,7 @@ The package is a registry plus a cooperative assembly pipeline. One `assemble()` ### Assembly and rendering -Assembly resolves and renders in two stages: `assemble()` returns sections with resolved-but-uninterpolated text, the ordered tool schemas, and every registered variable resolved against the context, while `renderPrompt()` interpolates `{{variable}}` references unless a section sets `interpolate: false`, drops empty sections, and joins with blank lines — strictly, an unknown reference, a registered-but-valueless reference, or a malformed complete group throws, because a malformed prompt is worse than a loud failure. `toolOrder` canonicalizes the collected tools before the waterfall (registration order is a plugin-load artifact); a waterfall listener that mutates the list owns the determinism of what it emits. +Assembly resolves and renders in two stages: `assemble()` returns sections with resolved-but-uninterpolated text, the ordered tool schemas, and every registered variable resolved against the context, while `renderPrompt()` interpolates `{{variable}}` references unless a section sets `interpolate: false`, drops empty sections, and joins with blank lines — strictly, an unknown reference, a registered-but-valueless reference, or a malformed complete group throws, because a malformed prompt is worse than a loud failure. `toolOrder` canonicalizes the collected tools before the waterfall (registration order is a plugin-load artifact); the presentation waterfall receives no tool array and cannot replace provider-owned declarations. ### Scoping @@ -137,7 +139,7 @@ The package-level contract is enough for most consumers; read these when you nee #### What the model sees -First-party sections render the harness identity, deployment persona prefix (including the model-name introduction), reusable instructions (including the generated tools SDK and structured-output guidance), then the environment-bearing suffix: harness source (`10000`), Web surface (`10100`), and deployment persona suffix (`10200`). External section orders and assembly listeners remain authoritative. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete — that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain. The rendered prompt reaches the model as a system-role message of derived history — surface node 0, or the latest system node after an in-history update — neither the loop request nor `request/header` carries a separate `system` field. If the complete rendering is empty, the loop clears every active system node through logged empty replacements, so no older prompt remains in model history. Ordered dynamic contexts are separate from sections and become sourced user-role snapshots only when present; `includeRuntimeContext: false` or a scoped suppressor removes optional contributions while required operational context remains. +First-party sections render the harness identity, deployment persona prefix (including the model-name introduction), reusable instructions (including the generated tools SDK and structured-output guidance), then the environment-bearing suffix: harness source (`10000`), Web surface (`10100`), and deployment persona suffix (`10200`). External section orders and assembly listeners remain authoritative. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt unless one effective section declares itself complete — that exact section then becomes the whole system prompt while the waterfall's contexts and variables remain. Tool declarations stay provider-owned. The rendered prompt reaches the model as a system-role message of derived history — surface node 0, or the latest system node after an in-history update — neither the loop request nor `request/header` carries a separate `system` field. If the complete rendering is empty, the loop clears every active system node through logged empty replacements, so no older prompt remains in model history. Ordered dynamic contexts are separate from sections and become sourced user-role snapshots only when present; `includeRuntimeContext: false` or a scoped suppressor removes optional contributions while required operational context remains. ##### Harness identity @@ -157,7 +159,7 @@ Prefix-stable while identity, persona, variables, section text, and order render #### What the model sees -For shipped tools, the model receives the per-agent-visible subset of the [generated tool schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tools), ordered by configuration or lexicographically after restrictions and assembly interception. Extensions can contribute additional definitions through the same registry. Sections and schema providers are separate assembly inputs. A restriction does not remove a section registration: tool-guidance plugins use `text({ scope })` and `ctx.tools.get(name, scope)` to return empty text or select applicable fragments. Arbitrary static sections are not automatically rewritten. +For shipped tools, the model receives the per-agent-visible subset of the [generated tool schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tools), ordered by configuration or lexicographically after restrictions and request-time activation. Extensions can contribute additional definitions through the same registry. Sections and schema providers are separate assembly inputs. A restriction does not remove a section registration: tool-guidance plugins use `text({ scope, toolSet })` and `ctx.tools.get(name, scope, toolSet)` to return empty text or select applicable fragments. Arbitrary static sections are not automatically rewritten. #### Token effect diff --git a/packages/core/system-prompt/README.zh.md b/packages/core/system-prompt/README.zh.md index 1807bf0411..117ba86ffc 100644 --- a/packages/core/system-prompt/README.zh.md +++ b/packages/core/system-prompt/README.zh.md @@ -67,6 +67,8 @@ ctx.systemPrompt.section({ `refreshContext(assembly, context)` 在 pre-step 与路由准备之后刷新已注册的运行时事实。它保留已接纳的段、工具、变量及仅由组装 waterfall 添加的上下文,应用当前的可选上下文抑制,并恢复缺失的必需条目。将变化的事实放在上下文 provider 中;已注册上下文的文本会在准入前再次读取。 +`prepare(context)` 在步骤接纳前通过 `system-prompt/prepare` 解析变量与运行时事实。循环绑定路由并提交步骤选择后,`assemble(context, preparation)` 解析段落与工具声明。展示监听器接收段落、上下文与变量;工具声明仍由提供方持有。依赖工具的段落回调通过 `context.toolSet` 读取与执行相同的请求集合。 + ### 贡献提示词变量 变量从段或上下文文本中以 `{{name}}` 引用,每次组装时解析;带作用域的变量会为该 agent 遮蔽同名全局变量。循环提供 `provider` 和 `model`;插件可注册自己拥有的事实。工作目录服务通过不执行插值的用户上下文提供目录文本。 @@ -107,7 +109,7 @@ ctx.systemPrompt.variable('response_language', () => 'English') ### 组装与渲染 -组装分两阶段完成求值与渲染:`assemble()` 返回文本已求值但尚未插值的段、有序工具 schema,以及每个已注册变量按当前上下文求得的值;`renderPrompt()` 对未设置 `interpolate: false` 的段插值 `{{variable}}` 引用、删除空段并用空行连接——严格规则:未知引用、已注册但无值的引用或格式错误的完整组都会抛出,因为格式错误的提示词比明确失败更糟。`toolOrder` 在 waterfall 分发前规范化收集到的工具(注册顺序只是插件加载产物);修改列表的 waterfall 监听器对其输出的确定性负责。 +组装分两阶段完成求值与渲染:`assemble()` 返回文本已求值但尚未插值的段、有序工具 schema,以及每个已注册变量按当前上下文求得的值;`renderPrompt()` 对未设置 `interpolate: false` 的段插值 `{{variable}}` 引用、删除空段并用空行连接——严格规则:未知引用、已注册但无值的引用或格式错误的完整组都会抛出,因为格式错误的提示词比明确失败更糟。`toolOrder` 在 waterfall 分发前规范化收集到的工具(注册顺序只是插件加载产物);展示 waterfall 不接收工具数组,也不能替换提供方持有的声明。 ### 作用域 @@ -137,7 +139,7 @@ ctx.systemPrompt.variable('response_language', () => 'English') #### 模型看到什么 -第一方段落依次渲染 harness 身份、部署 persona 前缀(含模型名称介绍)、可复用指令(包括生成的工具 SDK 和结构化输出指导),最后是携带环境信息的后缀:harness 源码(`10000`)、Web 表层(`10100`)和部署 persona 后缀(`10200`)。外部段落的顺序与组装监听器仍决定其最终结果。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段与变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema,除非一个有效段声明自身为 complete——此时该确切段会成为完整的系统提示词,而 waterfall 得到的上下文、工具与变量保持不变。渲染后的提示词作为派生历史中的 system 角色消息——surface 第 0 号节点,或历史内更新之后最新的系统节点——到达模型;循环请求与 `request/header` 均不含单独的 `system` 字段。完整渲染结果为空时,循环通过有日志记录的空内容替换清除所有生效的系统节点,模型历史不再保留任何旧提示词。有序动态上下文与段分离,只在存在时才会成为带来源的 user 角色快照;`includeRuntimeContext: false` 或带作用域的抑制器会移除可选贡献;必需的操作上下文仍然保留。 +第一方段落依次渲染 harness 身份、部署 persona 前缀(含模型名称介绍)、可复用指令(包括生成的工具 SDK 和结构化输出指导),最后是携带环境信息的后缀:harness 源码(`10000`)、Web 表层(`10100`)和部署 persona 后缀(`10200`)。外部段落的顺序与组装监听器仍决定其最终结果。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段与变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词,除非一个有效段声明自身为 complete——此时该确切段会成为完整的系统提示词,而 waterfall 得到的上下文与变量保持不变。工具声明仍由提供方持有。渲染后的提示词作为派生历史中的 system 角色消息——surface 第 0 号节点,或历史内更新之后最新的系统节点——到达模型;循环请求与 `request/header` 均不含单独的 `system` 字段。完整渲染结果为空时,循环通过有日志记录的空内容替换清除所有生效的系统节点,模型历史不再保留任何旧提示词。有序动态上下文与段分离,只在存在时才会成为带来源的 user 角色快照;`includeRuntimeContext: false` 或带作用域的抑制器会移除可选贡献;必需的操作上下文仍然保留。 ##### harness 身份 @@ -157,7 +159,7 @@ You are an AI agent powered by DeepSeek Harness. #### 模型看到什么 -对于已交付工具,模型会收到[生成工具 schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tools)中对每个 agent 可见的子集;限制与组装拦截完成后,按配置或字典序排列。扩展可以通过同一注册表贡献其他定义。段与 schema 提供方是独立的组装输入。限制不会移除段落注册:工具指导插件通过 `text({ scope })` 与 `ctx.tools.get(name, scope)` 返回空文本或选择适用片段。任意静态段落不会被自动改写。 +对于已交付工具,模型会收到[生成工具 schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tools)中对每个 agent 可见的子集;限制与请求时激活完成后,按配置或字典序排列。扩展可以通过同一注册表贡献其他定义。段与 schema 提供方是独立的组装输入。限制不会移除段落注册:工具指导插件通过 `text({ scope, toolSet })` 与 `ctx.tools.get(name, scope, toolSet)` 返回空文本或选择适用片段。任意静态段落不会被自动改写。 #### Token 影响 diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts index 2f027dfaf9..57eb475eab 100644 --- a/packages/core/system-prompt/src/index.ts +++ b/packages/core/system-prompt/src/index.ts @@ -17,9 +17,9 @@ declare module '@deepseek-ai/cordis' { interface Events { /** - * Expert waterfall over the assembled sections, contexts, tools, and variables. + * Presentation waterfall over assembled sections, contexts, and variables. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners - * receive only that scope's assemblies. The returned value is authoritative. + * receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned. * A supplied signal controls only this explicit assembly request and must not * be retained to control later turns. A registered complete section is * restored after this waterfall, so listeners cannot add to or replace @@ -28,7 +28,14 @@ declare module '@deepseek-ai/cordis' { * @param context - the caller's per-assembly context. * @mode waterfall */ - 'system-prompt/assemble'(this: Scoped, assembly: PromptAssembly, context: AssembleContext, next: () => Promise): Promise + '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. @@ -117,17 +124,22 @@ export interface ToolProviderResult { readonly knownNames?: readonly string[] } -/** - * Merge-extensible assembled model input. Sections and contexts remain - * uninterpolated until rendered; tools are already in canonical order. - */ -export interface PromptAssembly { - sections: AssembledSection[] +/** Prepared runtime facts and variables; context text remains uninterpolated. */ +export interface PromptPreparation { contexts: AssembledContext[] - tools: ToolSchema[] 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. */ +export interface PromptAssembly extends PromptPresentation { + tools: ToolSchema[] +} + const SECTION_ORDERS = { HARNESS_IDENTITY: -1000, DEPLOYMENT_PERSONA_PREFIX: 0, @@ -323,7 +335,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: PromptAssembly): ContextSnapshotSection[] { +export function renderContextSections(assembly: PromptPreparation): ContextSnapshotSection[] { return assembly.contexts .map(context => ({ name: context.name, @@ -570,12 +582,12 @@ export class SystemPrompt extends Service { * 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 the accepted assembly; their providers and waterfall do not rerun. + * retain their accepted values during this refresh. * @param assembly - accepted assembly for this scope and step. * @param context - the same scope and current plugin-defined assembly fields. * @returns the accepted assembly with current runtime-context provider text. */ - refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly { + refreshContext(assembly: T, context: AssembleContext = {}): T { 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()) @@ -598,16 +610,12 @@ export class SystemPrompt extends Service { } /** - * Assemble global and scoped providers, detach tool parameters, apply - * canonical ordering, then run the assembly waterfall. Scoped sections and - * variables shadow globals. The returned waterfall value is authoritative - * except that an effective complete section is restored afterwards as the - * sole prompt section. - * @param context - the optional scope and plugin-defined assembly fields. - * @returns the post-waterfall assembly with any complete prompt enforced. + * 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. */ - // Keep configuration failures on the declared asynchronous error path. - async assemble(context: AssembleContext = {}): Promise { + async prepare(context: AssembleContext = {}): Promise { const scope = context.scope const scopeLayers = this.layers.chainLayers(scope) const runtimeContextSuppressed = !this.layers.global.runtimeContextSuppressors.isEmpty() @@ -623,6 +631,28 @@ 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) @@ -661,25 +691,28 @@ export class SystemPrompt extends Service { if (section.complete === true) completeSection = { ...assembled } return assembled }) - const assembly: PromptAssembly = { - sections, - contexts: [...contextByName.values()] - .filter(entry => !runtimeContextSuppressed || entry.required === true) - .sort((a, b) => a.order - b.order) - .map(entry => resolveContext(entry, context)), - tools: orderTools(collected, this.toolOrder, knownNames), - variables, - } + const tools = orderTools(collected, this.toolOrder, knownNames) + const assembly: PromptPresentation = { ...prepared, sections } const transformed = await this.ctx.waterfall( scopeTarget(this, scope), 'system-prompt/assemble', assembly, context, () => Promise.resolve(assembly), ) - if (completeSection === undefined && !runtimeContextSuppressed) return transformed const requiredContexts = new Set([...contextByName.values()].filter(entry => entry.required === true).map(entry => entry.name)) + const contexts = runtimeContextSuppressed + ? transformed.contexts.filter(entry => requiredContexts.has(entry.name)) : [...transformed.contexts] + for (const provider of contextByName.values()) { + if (provider.required !== true) continue + const name = provider.name + const value = resolveContext(provider, context) + const existing = contexts.findIndex(entry => entry.name === name) + if (existing >= 0) contexts[existing] = value + else contexts.push(value) + } return { ...transformed, + tools, sections: completeSection === undefined ? transformed.sections : [completeSection], - contexts: runtimeContextSuppressed ? transformed.contexts.filter(entry => requiredContexts.has(entry.name)) : transformed.contexts, + contexts, } } } diff --git a/packages/core/system-prompt/tests/tool-order.spec.ts b/packages/core/system-prompt/tests/tool-order.spec.ts index b716feae16..9218dab72a 100644 --- a/packages/core/system-prompt/tests/tool-order.spec.ts +++ b/packages/core/system-prompt/tests/tool-order.spec.ts @@ -78,21 +78,14 @@ describe('SystemPrompt tool order', () => { expect(assembly.tools.map(t => t.description)).toEqual(['anchor', 'first', 'second']) }) - it('canonicalizes BEFORE the assemble waterfall: listeners see the ordered list and own their own edits', async () => { + it('keeps ordered provider declarations outside presentation transforms', async () => { const ctx = await mount() ctx.systemPrompt.tools(() => ({ schemas: [tool('zulu'), tool('alpha')] })) - let seen: string[] | undefined - ctx.on('system-prompt/assemble', function (assembly, _context, next) { - seen = assembly.tools.map(t => t.name) - // A listener-appended tool is NOT re-sorted — same contract as sections: - // canonicalization applies to what the registry contributed, and a - // listener owns the determinism of what it emits. - assembly.tools.push(tool('aardvark')) - return next() + ctx.on('system-prompt/assemble', async (assembly, _context, next) => { + expect(assembly).not.toHaveProperty('tools') + return { ...await next(), tools: [tool('aardvark')] } }) - const assembly = await ctx.systemPrompt.assemble() - expect(seen).toEqual(['alpha', 'zulu']) - expect(names(assembly)).toEqual(['alpha', 'zulu', 'aardvark']) + expect(names(await ctx.systemPrompt.assemble())).toEqual(['alpha', 'zulu']) }) it.each([ diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml index 739e42cb05..0103da13ae 100644 --- a/packages/core/tools/README.i18n.yaml +++ b/packages/core/tools/README.i18n.yaml @@ -20,6 +20,9 @@ /deepseek-ai-dsh-tools/use-this-package/register-a-tool: en: 9d2e6caa39c53d62 zh: 096a777f828c7565 +/deepseek-ai-dsh-tools/use-this-package/conditional-tools: + en: 93008deced28d5fe + zh: 09fe2f8693c5f147 /deepseek-ai-dsh-tools/use-this-package/configure-the-presentation-mode: en: 19a752864651fc41 zh: 48e20c3cdc867428 diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index bbf221031b..8360559504 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -63,6 +63,14 @@ ctx.tools.register(defineTool({ The unified schema DSL supports `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`, author-only `json`, and exact-one `oneOf`; `InferValue` preserves exact types through 16 container levels before widening to `JsonValue`. A raw JSON Schema (`JsonSchemaNode`) is the wire-level counterpart shared with subagents, workflows, and MCP. +### Conditional tools + +`register(definition, { when })` evaluates a synchronous or asynchronous condition for each model request. The condition receives `{ agent, signal }`; `agent.currentRoute()` reports the resolved provider/model. False omits the tool from Native declarations, PTC bindings, and execution. A thrown condition fails request 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, signal })` returns a request-owned `ResolvedToolSet`. Pass it as `toolSet` to prompt assembly and every Native or PTC execution, then dispose it after the batch settles. 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. + ### Configure the presentation mode The `mode` config decides what the model sees: `native` (every visible schema) or `ptc` (only `run_code` plus a generated SDK). diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md index a3e9989874..8ea7f82f5e 100644 --- a/packages/core/tools/README.zh.md +++ b/packages/core/tools/README.zh.md @@ -63,6 +63,14 @@ ctx.tools.register(defineTool({ 统一 schema DSL 支持 `string`、`number`、`integer`、`boolean`、`null`、`array`、`object`、仅供作者使用的 `json` 与恰好匹配一个分支的 `oneOf`;`InferValue` 在 16 层容器内保留精确类型,之后加宽为 `JsonValue`。原始 JSON Schema(`JsonSchemaNode`)是与 subagent、工作流和 MCP 共享的协议级对应类型。 +### 条件工具 + +`register(definition, { when })` 为每次模型请求计算同步或异步条件。条件接收 `{ agent, signal }`;`agent.currentRoute()` 返回已解析的提供方与模型。返回 false 会从原生声明、PTC 绑定和执行集合中排除工具。条件抛错会以 `TOOL_ACTIVATION_FAILED` 中止请求准备,不会被当成正常不可用。条件必须响应取消,不得改变注册或执行工具操作。 + +`resolve({ agent, signal })` 返回请求拥有的 `ResolvedToolSet`。将它作为 `toolSet` 传给提示词组装以及每次原生或 PTC 执行,批次结束后释放。注册仍由 effect 持有;每份集合只准入原 agent scope 中的原注册。注销、新的限制、取消或同名替换会阻止尚未开始的调用继续使用它。已经开始的操作保留协作取消义务。没有已解析集合的条件工具调用返回 `UNKNOWN_TOOL`。 + +`get(name, scope, toolSet)` 和 `schemas(scope, toolSet)` 读取已准入集合;省略 `toolSet` 时只查看已注册定义,不计算条件。工具指导文本通过 `AssembleContext.toolSet` 接收同一集合。展示变换不能增删可调用工具。 + ### 配置呈现模式 `mode` 配置决定模型看到什么:`native`(每个可见 schema)或 `ptc`(只有 `run_code` 加一份生成 SDK)。 diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index e506d99da0..b79fa6daf5 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -29,6 +29,13 @@ import { renderToolsSdk } from './ts-types.ts' import type { ToolSdkSchema } from './ts-types.ts' import { renderToolsSdkPy } from './py-types.ts' +declare module '@deepseek-ai/dsh-system-prompt' { + interface AssembleContext { + /** Request-owned tool selection; absent when inspecting registered definitions. */ + toolSet?: ResolvedToolSet + } +} + declare module '@deepseek-ai/dsh-llm' { interface MessageSourceMap { /** Tool availability changes supplied by the tool registry. */ @@ -220,6 +227,55 @@ export interface ToolOutputDefinition { presentationMeta?(args: unknown, value: JsonValue): JsonValue } +/** Inputs for a tool's request-time activation condition. */ +export interface ToolActivationContext { + /** Agent whose currentRoute() is bound to the request being prepared. */ + readonly agent: Agent + /** Cancellation of request preparation; asynchronous conditions must observe it. */ + readonly signal: AbortSignal +} + +/** Lifecycle-owned conditions for a registered tool. */ +export interface ToolRegistrationOptions { + /** + * Decide whether this request may call the tool. Omission enables it. False + * excludes it from both declarations and dispatch; rejection fails request + * preparation. Conditions must not mutate registrations or start tool work. + * @param context - requesting agent and cancellation signal. + * @returns whether the tool participates in this request. + */ + readonly when?: (context: ToolActivationContext) => boolean | Promise +} + +/** One request's admitted registrations, also used by nested PTC calls. */ +export interface ResolvedToolSet { + /** Close request admission after its tool batch settles. */ + dispose(): void + /** + * Read admitted registrations that remain visible in their original scope. + * @param scope - exact requesting agent; another scope is never admitted. + * @returns original definitions excluding removed or restricted registrations. + */ + view(scope: ScopeKey | undefined): ReadonlyMap +} + +/** Failure to determine a tool's request-time availability. */ +export class ToolActivationError extends HarnessError { + /** + * Preserve a condition failure without treating it as normal unavailability. + * @param name - tool whose condition rejected. + * @param cause - underlying condition failure. + */ + constructor(name: string, cause: unknown) { + super(`cannot resolve availability of tool "${name}": ${errorMessage(cause)}`, 'TOOL_ACTIVATION_FAILED', { cause }) + } +} + +interface ToolRegistration { + readonly definition: ToolDefinition + readonly when: ToolRegistrationOptions['when'] +} + /** A registered tool: its schema plus the execution function. */ export interface ToolDefinition extends ToolSchema { /** Mandatory canonical output declaration. */ @@ -338,6 +394,8 @@ export interface ToolExecutionInput { readonly arguments: unknown /** The agent on whose behalf the call runs (set by the agent loop). */ readonly agent?: Agent + /** Request admission shared by Native and PTC dispatch. Conditional tools require it. */ + readonly toolSet?: ResolvedToolSet /** * Opaque token of the enclosing transport execution, when one exists. PTC * mode sets this on SDK sub-dispatches so commit-style observers can wait for @@ -715,6 +773,7 @@ interface CompiledToolRestriction { interface ToolView { /** Visible definitions after restrictions, scoped shadowing, and transport insertion. */ readonly visible: ReadonlyMap + readonly registrations: ReadonlyMap /** Pre-restriction capability names used by prompt-order validation. */ readonly knownNames: ReadonlySet /** Current global names that a scoped restriction may name. */ @@ -733,7 +792,7 @@ export type ToolGuard = (execution: Readonly) => string | undefin /** One scope's complete tool-registry contribution. */ class ToolLayer implements ScopeLayer { - readonly tools: NamedEntries + readonly tools: NamedEntries readonly restrictions = new AnonymousEntries() readonly guards = new AnonymousEntries() /** @@ -852,7 +911,7 @@ export class ToolRuntime extends Service { // optional-input type for direct (non-Loader) construction in tests. this.defaultMode = config.mode ?? 'native' this.maxParallelSubCalls = resolveMaxParallelSubCalls(config.maxParallelSubCalls) - ctx.systemPrompt.tools(context => this.wireSchemas(context.scope)) + ctx.systemPrompt.tools(context => this.wireSchemas(context.scope, context.toolSet)) if (this.defaultMode !== 'native') { ctx.systemPrompt.section(this.collapseSection()) ctx.systemPrompt.section(this.sdkSection()) @@ -909,7 +968,7 @@ export class ToolRuntime extends Service { const render = SDK_RENDERERS[runtime.language] /* v8 ignore next -- requirePtcRuntime rejects an unknown language before this runs. */ if (render === undefined) throw new Error(`dsh-tools: no SDK renderer for ${runtime.language}`) - return render(this.sdkSchemas(context.scope)) + return render(this.sdkSchemas(context.scope, context.toolSet)) }, } } @@ -1014,11 +1073,11 @@ export class ToolRuntime extends Service { * Build one scope's wire schemas and names for prompt-order validation. * Restrictions do not make known tools invalid, but a mode collapse does. */ - private wireSchemas(scope?: ScopeKey): ToolProviderResult { + private wireSchemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolProviderResult { const view = this.view(scope) const mode = this.modeFor(scope) if (mode === 'native') { - const schemas = [...view.visible.values()].map(definition => this.schemaOf(definition, false)) + const schemas = this.definitions(scope, toolSet).map(definition => this.schemaOf(definition, false)) return { schemas, knownNames: [...view.knownNames] } } // Validate the runtime language BEFORE projecting schemas: schemaOf reads @@ -1027,7 +1086,7 @@ export class ToolRuntime extends Service { // renderer-table rejection the canonical assembly-time error for a // language with no SDK renderer. this.requirePtcRuntime(mode) - const schemas = [...view.visible.values()].map(definition => this.schemaOf(definition, false)) + const schemas = this.definitions(scope, toolSet).map(definition => this.schemaOf(definition, false)) return { schemas: schemas.filter(schema => schema.name === RUN_CODE_NAME), knownNames: [RUN_CODE_NAME], @@ -1064,9 +1123,10 @@ export class ToolRuntime extends Service { * Register globally or in the calling agent scope. Scoped tools shadow * globals; duplicates within one layer and the reserved `run_code` name fail. * @param definition - tool schema, execution, and optional finalization/presentation callbacks. + * @param options - optional request-time activation condition. * @returns the exact disposer that unregisters the tool. */ - register(definition: ToolDefinition): () => void { + register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void { const name = definition.name const output = (definition as Partial).output if (output === undefined || typeof output !== 'object' @@ -1088,7 +1148,7 @@ export class ToolRuntime extends Service { } return this.layers.effect( this.ctx, - layer => layer.tools.insert(name, definition), + layer => layer.tools.insert(name, { definition, when: options.when }), { label: 'tools.register()' }, ) } @@ -1190,27 +1250,32 @@ export class ToolRuntime extends Service { const own = this.layers.peek(scope) // Inherited surface, nearest ancestor last: a nearer scope's same-name // entry shadows a farther one, and the global layer is the farthest. - const inherited = new Map(this.layers.global.tools.entries()) + const inherited = new Map(this.layers.global.tools.entries()) for (const layer of layers) { if (layer === own) continue for (const [name, definition] of layer.tools.entries()) inherited.set(name, definition) } const visible = new Map() + const registrations = new Map() const knownNames = new Set() const restrictableNames = new Set() - for (const [name, definition] of inherited) { + for (const [name, registration] of inherited) { knownNames.add(name) restrictableNames.add(name) // Restrictions intersect across the whole chain: any scope on it may // mask an inherited name for everything nested inside it. - if (layers.every(layer => layer.admits(name))) visible.set(name, definition) + if (layers.every(layer => layer.admits(name))) { + visible.set(name, registration.definition) + registrations.set(name, registration) + } } // The scope's own registrations last, shadowing an inherited name and // outside the filter above. if (own !== undefined) { - for (const [name, definition] of own.tools.entries()) { + for (const [name, registration] of own.tools.entries()) { knownNames.add(name) - visible.set(name, definition) + visible.set(name, registration.definition) + registrations.set(name, registration) } } // Presentation infrastructure is resolved last and outside capability @@ -1221,7 +1286,7 @@ export class ToolRuntime extends Service { if (this.modeFor(scope) !== 'native') { visible.set(RUN_CODE_NAME, this.requirePtcTransport()) } - return { visible, knownNames, restrictableNames } + return { visible, registrations, knownNames, restrictableNames } } /** @@ -1231,10 +1296,58 @@ export class ToolRuntime extends Service { * actually executed. * @param name - the tool name as registered. * @param scope - the viewing scope (the agent); omitted = the global view. + * @param toolSet - request admission; omit only to inspect registered definitions. * @returns the definition the scope resolves, or undefined when none is visible. */ - get(name: string, scope?: ScopeKey): ToolDefinition | undefined { - return this.view(scope).visible.get(name) + get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined { + return toolSet === undefined ? this.view(scope).visible.get(name) : toolSet.view(scope).get(name) + } + + /** + * Resolve conditions once for this request 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 - agent with a bound route and request cancellation. + * @returns the complete selection; the caller disposes it after its tool batch settles. + */ + async resolve(context: ToolActivationContext): Promise { + const { agent, signal } = context + signal.throwIfAborted() + const view = this.view(agent) + const admitted = new Map() + for (const [name, definition] of view.visible) { + const registration = view.registrations.get(name) + let enabled: boolean + try { + enabled = registration?.when === undefined || await registration.when(context) + } catch (cause: unknown) { + signal.throwIfAborted() + throw new ToolActivationError(name, cause) + } + signal.throwIfAborted() + if (enabled) admitted.set(name, definition) + } + let closed = false + return Object.freeze({ + dispose: () => { closed = true }, + view: (scope: ScopeKey | undefined): ReadonlyMap => { + const visible = new Map() + if (closed || scope !== agent || signal.aborted) return visible + const current = this.view(agent) + for (const [name, definition] of admitted) { + if (current.visible.get(name) === definition + && current.registrations.get(name) === view.registrations.get(name)) { + visible.set(name, definition) + } + } + return visible + }, + }) + } + + private definitions(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition[] { + if (toolSet === undefined) return [...this.view(scope).visible.values()] + return [...toolSet.view(scope).values()] } /** @@ -1250,8 +1363,11 @@ export class ToolRuntime extends Service { * @param nested - whether the call is a transport sub-dispatch, not a model-direct call. * @returns the definition that may run, or undefined when the call must be rejected. */ - private resolveExecution(name: string, scope: ScopeKey | undefined, nested: boolean): ToolDefinition | undefined { - const tool = this.get(name, scope) + private resolveExecution( + name: string, scope: ScopeKey | undefined, nested: boolean, toolSet?: ResolvedToolSet, + ): ToolDefinition | undefined { + if (toolSet === undefined && this.view(scope).registrations.get(name)?.when !== undefined) return undefined + const tool = this.get(name, scope, toolSet) if (tool === undefined) return undefined if (this.collapses(name, scope, nested)) return undefined return tool @@ -1261,15 +1377,16 @@ export class ToolRuntime extends Service { * Project visible definitions onto the allowlisted model-facing schema fields, * excluding execution and presentation callbacks. * @param scope - the viewing scope (the agent); omitted = the global view. + * @param toolSet - request admission, or omitted for registered-definition inspection. * @returns one deep-cloned schema per visible tool. */ - schemas(scope?: ScopeKey): ToolSchema[] { - return [...this.view(scope).visible.values()].map(definition => this.schemaOf(definition, true)) + schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[] { + return this.definitions(scope, toolSet).map(definition => this.schemaOf(definition, true)) } /** Project visible callable tools onto the generated PTC mode SDK contract. */ - private sdkSchemas(scope?: ScopeKey): ToolSdkSchema[] { - return [...this.view(scope).visible.values()] + private sdkSchemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSdkSchema[] { + return this.definitions(scope, toolSet) .filter(definition => definition.name !== RUN_CODE_NAME) .map((definition): ToolSdkSchema => { const output = snapshotJsonValue(definition.output.schema) @@ -1307,7 +1424,7 @@ export class ToolRuntime extends Service { * @returns the fail-closed scheduling mode. */ executionMode(exec: ToolExecutionInput): ToolExecutionMode { - const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined) + const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined, exec.toolSet) if (!tool?.isConcurrencySafe) return { kind: 'exclusive' } try { const concurrencySafe: unknown = tool.isConcurrencySafe(exec.arguments) @@ -1410,7 +1527,7 @@ export class ToolRuntime extends Service { // observe — or worse, approve — a call that can only fail. An unknown tool // keeps the historical dispatch-stage `UNKNOWN_TOOL` path so policy // listeners still see every name that reaches the registry. - const visible = this.get(name, agent) + const visible = this.get(name, agent, exec.toolSet) const collapsed = visible !== undefined && this.collapses(name, agent, parent !== undefined) const concludingExecutions = this.concludingExecutions const base = { @@ -1420,6 +1537,7 @@ export class ToolRuntime extends Service { name, signal, ...agent !== undefined ? { agent } : {}, + ...exec.toolSet !== undefined ? { toolSet: exec.toolSet } : {}, ...parent !== undefined ? { parent } : {}, ...exec.schema !== undefined ? { schema: exec.schema } : {}, deferContext(context: UserMessage): void { @@ -1581,7 +1699,7 @@ export class ToolRuntime extends Service { } exec.signal = signal try { - const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined) + const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined, exec.toolSet) if (!tool) throw new ToolNotFoundError(exec.name) state.bodyInvoked = true const returned = await tool.execute(exec.arguments, exec) @@ -1810,7 +1928,7 @@ export class ToolRuntime extends Service { if (result.isError) { throw new TypeError('tools/post-execute cannot replace the value of a failed result') } - const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined) + const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined, exec.toolSet) if (tool === undefined) throw new ToolNotFoundError(exec.name) const replaced = this.createSuccessResult(exec, tool, decision.value) return this.markCanonical(exec, { @@ -1879,7 +1997,7 @@ export class ToolRuntime extends Service { ...result.additionalContexts !== undefined ? { additionalContexts: result.additionalContexts } : {}, }) } - const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined) + const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined, exec.toolSet) if (tool === undefined) throw new ToolNotFoundError(exec.name) const normalized = this.createSuccessResult(exec, tool, result.value) return this.markCanonical(exec, { diff --git a/packages/core/tools/src/ptc.ts b/packages/core/tools/src/ptc.ts index 9d3bea545b..815af25971 100644 --- a/packages/core/tools/src/ptc.ts +++ b/packages/core/tools/src/ptc.ts @@ -554,6 +554,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO schema, arguments: normalized.dispatched, ...exec.agent ? { agent: exec.agent } : {}, + ...exec.toolSet !== undefined ? { toolSet: exec.toolSet } : {}, parent: exec.token, signal: runController.signal, } @@ -691,7 +692,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO // restricted globals vanish) — the same view the SDK section declared, // so a program can bind exactly what its prompt promised; sub-dispatch // re-resolves per call through the same view (exec.agent threads down). - for (const schema of registry.schemas(exec.agent)) { + for (const schema of registry.schemas(exec.agent, exec.toolSet)) { if (schema.name === RUN_CODE_NAME) continue Object.defineProperty(functions, schema.name, { enumerable: true, value: binding(deepFreeze(schema)) }) } diff --git a/packages/core/tools/tests/activation.spec.ts b/packages/core/tools/tests/activation.spec.ts new file mode 100644 index 0000000000..5b052468d0 --- /dev/null +++ b/packages/core/tools/tests/activation.spec.ts @@ -0,0 +1,123 @@ +/** Conditional registrations share request admission with direct and nested execution. */ +import { afterEach, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import { ToolCallId } from '@deepseek-ai/dsh-llm' +import { createScope, type Scope } from '@deepseek-ai/dsh-scope' +import { SessionId } from '@deepseek-ai/dsh-session' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import ToolRuntime, { defineTool, ToolActivationError } from '@deepseek-ai/dsh-tools' + +const contexts: Context[] = [] +afterEach(async () => { + for (const ctx of contexts.splice(0)) await ctx.fiber.dispose() +}) + +async function setup() { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + const agent = { id: SessionId('activation'), currentRoute: () => ({ provider: 'test', model: 'test' }) } as Agent + let scope!: Scope + await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, agent) }, { inject: ['tools'] })) + const controller = new AbortController() + let calls = 0 + const tool = defineTool({ + name: 'conditional', description: 'A conditional operation.', parameters: {}, + output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] }, + async execute() { calls++; return 'done' }, + }) + const input = { agent, name: tool.name, arguments: {}, callId: ToolCallId('call'), signal: controller.signal } + 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 () => { + const h = await setup() + let enabled = false + h.ctx.tools.register(h.tool, { when: () => enabled }) + const unavailable = await h.resolve() + expect(h.ctx.tools.schemas(h.agent, unavailable)).toEqual([]) + enabled = true + expect((await h.ctx.tools.execute({ ...h.input, toolSet: unavailable })).error?.info?.code).toBe('UNKNOWN_TOOL') + expect((await h.ctx.tools.execute(h.input)).error?.info?.code).toBe('UNKNOWN_TOOL') + const available = await h.resolve() + expect(h.ctx.tools.schemas(h.agent, available).map(tool => tool.name)).toEqual(['conditional']) + expect((await h.ctx.tools.execute({ ...h.input, toolSet: available })).isError).toBe(false) + enabled = false + const disabled = await h.resolve() + expect((await h.ctx.tools.execute({ ...h.input, toolSet: disabled })).error?.info?.code).toBe('UNKNOWN_TOOL') + expect(h.calls()).toBe(1) +}) + +it('keeps admissions specific to the requesting agent', async () => { + const h = await setup() + h.ctx.tools.register(h.tool, { when: () => true }) + const toolSet = await h.resolve() + const other = { id: SessionId('other') } as Agent + expect((await h.ctx.tools.execute({ ...h.input, agent: other, toolSet })).error?.info?.code).toBe('UNKNOWN_TOOL') + expect(h.calls()).toBe(0) +}) + +it('applies live restrictions and closes request admission permanently', async () => { + const h = await setup() + h.ctx.tools.register(h.tool, { when: () => true }) + const toolSet = await h.resolve() + const lift = h.scope.ctx.tools.restrict({ deny: ['conditional'] }) + expect((await h.ctx.tools.execute({ ...h.input, toolSet })).error?.info?.code).toBe('UNKNOWN_TOOL') + lift() + expect((await h.ctx.tools.execute({ ...h.input, toolSet })).isError).toBe(false) + toolSet.dispose() + expect(h.ctx.tools.schemas(h.agent, toolSet)).toEqual([]) + expect((await h.ctx.tools.execute({ ...h.input, toolSet })).error?.info?.code).toBe('UNKNOWN_TOOL') + expect(h.calls()).toBe(1) +}) + +it('does not substitute a same-name registration after admission', async () => { + const h = await setup() + const unregister = h.ctx.tools.register(h.tool, { when: () => true }) + const old = await h.resolve() + unregister() + h.ctx.tools.register(h.tool, { when: () => true }) + expect((await h.ctx.tools.execute({ ...h.input, toolSet: old })).error?.info?.code).toBe('UNKNOWN_TOOL') + expect((await h.ctx.tools.execute({ ...h.input, toolSet: await h.resolve() })).isError).toBe(false) + expect(h.calls()).toBe(1) +}) + +it('does not restore a registration removed during its asynchronous condition', async () => { + const h = await setup() + const condition = Promise.withResolvers() + const entered = Promise.withResolvers() + const unregister = h.ctx.tools.register(h.tool, { when: () => { entered.resolve(undefined); return condition.promise } }) + const pending = h.resolve() + await entered.promise + unregister() + condition.resolve(true) + const toolSet = await pending + expect(h.ctx.tools.schemas(h.agent, toolSet)).toEqual([]) + expect((await h.ctx.tools.execute({ ...h.input, toolSet })).error?.info?.code).toBe('UNKNOWN_TOOL') + expect(h.calls()).toBe(0) +}) + +it('propagates cancellation after an asynchronous condition settles', async () => { + const h = await setup() + const condition = Promise.withResolvers() + h.ctx.tools.register(h.tool, { when: ({ signal }) => { expect(signal).toBe(h.controller.signal); return condition.promise } }) + const pending = h.resolve() + const rejection = expect(pending).rejects.toMatchObject({ name: 'AbortError' }) + h.controller.abort() + condition.resolve(true) + await rejection + expect(h.calls()).toBe(0) +}) + +it('reports condition failures without treating them as disabled tools', async () => { + const h = await setup() + const cause = new Error('credential storage unavailable') + h.ctx.tools.register(h.tool, { when: () => { throw cause } }) + await expect(h.resolve()).rejects.toMatchObject({ + name: ToolActivationError.name, code: 'TOOL_ACTIVATION_FAILED', cause, + }) + expect(h.calls()).toBe(0) +}) diff --git a/packages/core/tools/tests/ptc.spec.ts b/packages/core/tools/tests/ptc.spec.ts index aa18a99f0f..a968ea0be8 100644 --- a/packages/core/tools/tests/ptc.spec.ts +++ b/packages/core/tools/tests/ptc.spec.ts @@ -169,6 +169,51 @@ it('logs detached frozen nested metadata without changing the binding value or l }) describe('mode-aware wire contribution', () => { + it('shares conditional admission with SDK bindings and rejects a replaced cached binding', async () => { + const { ctx, runtime, systemPrompt } = await setup({ mode: 'ptc' }) + try { + const agent = { session: Session.create(SessionId('conditional-ptc')) } as Agent + let active = false + const execute = vi.fn(async () => 'done') + const definition = defineTool({ + name: 'conditional', description: 'Available on selected requests.', parameters: {}, + output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] }, + execute, + }) + const unregister = ctx.tools.register(definition, { when: () => active }) + for (const enabled of [false, true]) { + active = enabled + 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 ?? '' + expect(sdk.includes('conditional:')).toBe(enabled) + runtime.behavior = async (request) => { + const functions = request.bindings[0]!.functions + expect(Object.keys(functions)).toEqual(enabled ? ['conditional'] : []) + if (enabled) { + expect(await functions.conditional!({})).toBe('done') + unregister() + ctx.tools.register(definition, { when: () => true }) + await expect(functions.conditional!({})).rejects.toThrow('unknown tool "conditional"') + } + return { logs: [] } + } + const result = await ctx.tools.execute({ + agent, toolSet, signal: testToolSignal, callId: ToolCallId('conditional-program'), + name: RUN_CODE_NAME, arguments: { code: 'program', description: 'Check conditional bindings' }, + }) + expect(result.isError).toBe(false) + } finally { + toolSet.dispose() + } + } + expect(execute).toHaveBeenCalledTimes(1) + } finally { + await ctx.fiber.dispose() + } + }) + it('rejects an unsupported presentation mode in configuration', () => { expect(() => ToolRuntime.Config({ mode: 'both' } as never)).toThrow() }) @@ -267,7 +312,7 @@ describe('mode-aware wire contribution', () => { expect(sdk).toContain('deep_output: string | null') }) - it('treats expert assembly output as authoritative in mode ptc', async () => { + it('keeps PTC declarations owned by the registry during presentation transforms', async () => { const mode = 'ptc' const { ctx, systemPrompt } = await setup({ mode }) registerEcho(ctx) @@ -276,13 +321,13 @@ describe('mode-aware wire contribution', () => { return { ...assembly, sections: assembly.sections.filter(section => section.name !== 'tools:sdk'), - tools: assembly.tools.filter(tool => tool.name !== RUN_CODE_NAME), + tools: [], } }, { prepend: true }) const assembly = await systemPrompt.assemble() expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false) - expect(assembly.tools.some(tool => tool.name === RUN_CODE_NAME)).toBe(false) + expect(assembly.tools.some(tool => tool.name === RUN_CODE_NAME)).toBe(true) }) it('lets one scope shadow the default SDK section in mode ptc', async () => { diff --git a/packages/deliverables/tool-present/tests/present.spec.ts b/packages/deliverables/tool-present/tests/present.spec.ts index afbbf5958a..0583f21f1d 100644 --- a/packages/deliverables/tool-present/tests/present.spec.ts +++ b/packages/deliverables/tool-present/tests/present.spec.ts @@ -31,6 +31,7 @@ async function agent(ctx: Context, cwd: string | undefined): Promise { version: SESSION_FORMAT_VERSION, id, createdAt: 0, ...cwd === undefined ? {} : { cwd }, isSeeded: false, }) const value: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/experimental/browser-use-runtime/tests/resources.spec.ts b/packages/experimental/browser-use-runtime/tests/resources.spec.ts index f838191d44..285394c448 100644 --- a/packages/experimental/browser-use-runtime/tests/resources.spec.ts +++ b/packages/experimental/browser-use-runtime/tests/resources.spec.ts @@ -16,6 +16,7 @@ async function fixture() { const fiber = ctx.plugin(() => {}) const session = Session.create(SessionId(id)) const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, session, ctx: fiber.ctx, options: {}, status: 'idle', inbox: unsupportedInbox(), send() {}, followup() {}, inject() {}, cancel() {}, steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }), diff --git a/packages/experimental/tool-terminal/tests/loader-composition.spec.ts b/packages/experimental/tool-terminal/tests/loader-composition.spec.ts index 9d2db58481..1a624d6d04 100644 --- a/packages/experimental/tool-terminal/tests/loader-composition.spec.ts +++ b/packages/experimental/tool-terminal/tests/loader-composition.spec.ts @@ -43,6 +43,7 @@ async function agent(ctx: Context): Promise { const id = SessionId('pty-loader-agent') const session = Session.create(id) const value: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, inbox: unsupportedInbox(), status: 'idle', ctx: scope.ctx, diff --git a/packages/experimental/tool-terminal/tests/tools.spec.ts b/packages/experimental/tool-terminal/tests/tools.spec.ts index 8f6f3a5897..e00ba3542e 100644 --- a/packages/experimental/tool-terminal/tests/tools.spec.ts +++ b/packages/experimental/tool-terminal/tests/tools.spec.ts @@ -20,6 +20,7 @@ async function fakeAgent(ctx: Context, rawId: string): Promise { const id = SessionId(rawId) const session = Session.create(id) const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, inbox: unsupportedInbox(), status: 'idle', ctx: scope.ctx, diff --git a/packages/experimental/worktree/tests/harness.ts b/packages/experimental/worktree/tests/harness.ts index c38be4d57c..f425987975 100644 --- a/packages/experimental/worktree/tests/harness.ts +++ b/packages/experimental/worktree/tests/harness.ts @@ -42,6 +42,7 @@ export function testAgent(ctx: Context, cwd: string, name = 'worktree-test'): Ag const id = SessionId(name) const session = Session.create(id, [], { version: SESSION_FORMAT_VERSION, id, createdAt: 0, isSeeded: false, cwd }) const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, session, options: {}, status: 'idle', ctx: scope.ctx, inbox: unsupportedInbox(), send() {}, followup() {}, steer() {}, inject() {}, cancel() {}, whenIdle: async () => {}, runMaintenance: task => task(new AbortController().signal), diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index cac34cc692..c2f549d957 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1655,8 +1655,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { key: 'planMode', - summary: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool.', - description: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool. Client carriers expose the projection\'s cropped `{ active, pending }` view.', + summary: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the conditional exit tool.', + description: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the conditional exit tool. Client carriers expose the projection\'s cropped `{ active, pending }` view.', methods: [ { signature: 'get(agent: Agent): { active: boolean; pending?: boolean }', @@ -3088,16 +3088,22 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'the exact Cordis effect disposer.', }, { - 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 the accepted assembly; their providers and waterfall do not rerun.', + signature: 'refreshContext(assembly: T, context: AssembleContext = {}): T', + 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 assemble(context: AssembleContext = {}): Promise', - description: 'Assemble global and scoped providers, detach tool parameters, apply canonical ordering, then run the assembly waterfall. Scoped sections and variables shadow globals. The returned waterfall value is authoritative except that an effective complete section is restored afterwards as the sole prompt section.', - parameters: [{ name: 'context', description: 'the optional scope and plugin-defined assembly fields.' }], - returns: 'the post-waterfall assembly with any complete prompt enforced.', + 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.', }, ], }, @@ -3327,9 +3333,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'the exact disposer that restores the deployment default.', }, { - signature: 'register(definition: ToolDefinition): () => void', + signature: 'register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void', description: 'Register globally or in the calling agent scope. Scoped tools shadow globals; duplicates within one layer and the reserved `run_code` name fail.', - parameters: [{ name: 'definition', description: 'tool schema, execution, and optional finalization/presentation callbacks.' }], + parameters: [{ name: 'definition', description: 'tool schema, execution, and optional finalization/presentation callbacks.' }, { name: 'options', description: 'optional request-time activation condition.' }], returns: 'the exact disposer that unregisters the tool.', }, { @@ -3345,15 +3351,21 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'the exact disposer that unregisters the guard.', }, { - signature: 'get(name: string, scope?: ScopeKey): ToolDefinition | undefined', + signature: 'get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined', description: 'Look up a tool as one scope sees it (scoped shadows global; a restricted-away global reads as absent). Presenters pass the calling agent so the rendered card matches the definition that actually executed.', - parameters: [{ name: 'name', description: 'the tool name as registered.' }, { name: 'scope', description: 'the viewing scope (the agent); omitted = the global view.' }], + parameters: [{ name: 'name', description: 'the tool name as registered.' }, { name: 'scope', description: 'the viewing scope (the agent); omitted = the global view.' }, { name: 'toolSet', description: 'request admission; omit only to inspect registered definitions.' }], returns: 'the definition the scope resolves, or undefined when none is visible.', }, { - signature: 'schemas(scope?: ScopeKey): ToolSchema[]', + signature: 'async resolve(context: ToolActivationContext): Promise', + description: 'Resolve conditions once for this request 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: 'agent with a bound route and request cancellation.' }], + returns: 'the complete selection; the caller disposes it after its tool batch settles.', + }, + { + signature: 'schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[]', description: 'Project visible definitions onto the allowlisted model-facing schema fields, excluding execution and presentation callbacks.', - parameters: [{ name: 'scope', description: 'the viewing scope (the agent); omitted = the global view.' }], + parameters: [{ name: 'scope', description: 'the viewing scope (the agent); omitted = the global view.' }, { name: 'toolSet', description: 'request admission, or omitted for registered-definition inspection.' }], returns: 'one deep-cloned schema per visible tool.', }, { @@ -3966,7 +3978,7 @@ export const EVENT_API: readonly EventApiEntry[] = [ mode: 'waterfall', signature: '\'agent/request\'(this: Scoped, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise): Promise', summary: 'Replace the frozen call configuration.', - description: 'Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. On step admission, this runs after assembly and `step/start`, before the system prompt and accepted user batch are committed. Cancellation here or during subsequent `prepareCall()` resolution commits neither. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages.', + description: 'Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. On step admission, this runs before context preparation and `agent/pre-step`. Cancellation here or during subsequent `prepareCall()` resolution opens no step and commits no system prompt or user batch. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages.', 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.' }], }, { @@ -4388,9 +4400,9 @@ export const EVENT_API: readonly EventApiEntry[] = [ { name: 'system-prompt/assemble', mode: 'waterfall', - signature: '\'system-prompt/assemble\'(this: Scoped, assembly: PromptAssembly, context: AssembleContext, next: () => Promise): Promise', - summary: 'Expert waterfall over the assembled sections, contexts, tools, and variables.', - description: 'Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope\'s assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope\'s system prompt.', + signature: '\'system-prompt/assemble\'(this: Scoped, assembly: PromptPresentation, context: AssembleContext, next: () => Promise): Promise', + summary: 'Presentation waterfall over assembled sections, contexts, and variables.', + description: 'Presentation waterfall over assembled sections, contexts, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope\'s assemblies. The returned text is authoritative; tool declarations remain provider-owned. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope\'s system prompt.', parameters: [{ name: 'assembly', description: 'the mutable assembly built from registered providers.' }, { name: 'context', description: 'the caller\'s per-assembly context.' }], }, { @@ -4401,6 +4413,14 @@ 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', @@ -6315,7 +6335,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'PromptAssembly', - declaration: 'export interface PromptAssembly {\n sections: AssembledSection[];\n contexts: AssembledContext[];\n tools: ToolSchema[];\n variables: Record;\n}', + declaration: 'export interface PromptAssembly extends PromptPresentation {\n tools: ToolSchema[];\n}', }, { name: 'PromptContext', @@ -6333,6 +6353,14 @@ 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}', + }, { name: 'PromptSection', declaration: 'export interface PromptSection {\n readonly name: string;\n readonly order: number;\n readonly text: string | ((context: AssembleContext) => string);\n readonly interpolate?: boolean;\n readonly complete?: boolean;\n}', @@ -6521,6 +6549,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'ResolvedSubagentStartRequest', declaration: 'export interface ResolvedSubagentStartRequest extends SubagentStartRequest {\n readonly cwd: string;\n}', }, + { + name: 'ResolvedToolSet', + declaration: 'export interface ResolvedToolSet {\n dispose(): void;\n view(scope: ScopeKey | undefined): ReadonlyMap;\n}', + }, { name: 'RestoredSessionOptions', declaration: 'export interface RestoredSessionOptions {\n readonly seed: SessionEvent[];\n readonly meta: SessionHeader;\n readonly inheritedEventCount: SessionLogOffset;\n readonly eventState: SessionSeedEventState;\n}', @@ -7707,7 +7739,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: PromptAssembly, context: AssembleContext = {}): PromptAssembly;\n async assemble(context: AssembleContext = {}): 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: T, context: AssembleContext = {}): T;\n async prepare(context: AssembleContext = {}): Promise;\n async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise;\n}', }, { name: 'SystemPromptMessageSource', @@ -7893,6 +7925,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'TokenUsage', declaration: 'export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n totalTokens?: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n}', }, + { + name: 'ToolActivationContext', + declaration: 'export interface ToolActivationContext {\n readonly agent: Agent;\n readonly signal: AbortSignal;\n}', + }, { name: 'ToolAdditionBlock', declaration: 'export interface ToolAdditionBlock {\n type: \'tool-addition\';\n toolName: string;\n tool?: never;\n}', @@ -7943,7 +7979,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolExecutionInput', - declaration: 'export interface ToolExecutionInput {\n readonly callId: ToolCallId;\n readonly rootCallId?: ToolCallId;\n readonly name: string;\n readonly schema?: ToolSchema;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n}', + declaration: 'export interface ToolExecutionInput {\n readonly callId: ToolCallId;\n readonly rootCallId?: ToolCallId;\n readonly name: string;\n readonly schema?: ToolSchema;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly toolSet?: ResolvedToolSet;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n}', }, { name: 'ToolExecutionMode', @@ -7993,6 +8029,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'ToolProviderResult', declaration: 'export interface ToolProviderResult {\n readonly schemas: readonly ToolSchema[];\n readonly knownNames?: readonly string[];\n}', }, + { + name: 'ToolRegistrationOptions', + declaration: 'export interface ToolRegistrationOptions {\n readonly when?: (context: ToolActivationContext) => boolean | Promise;\n}', + }, { name: 'ToolRemovalBlock', declaration: 'export interface ToolRemovalBlock {\n type: \'tool-removal\';\n toolName: string;\n}', @@ -8019,7 +8059,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolRuntime', - declaration: 'export class ToolRuntime extends Service {\n static inject;\n static Config: z;\n readonly [TOOL_RUNTIME_SCHEDULER]: ToolRuntimeScheduler;\n constructor(ctx: Context, config: Config = {});\n presentAs(mode: ToolPresentationMode): () => void;\n register(definition: ToolDefinition): () => void;\n restrict(filter: ToolRestriction): () => void;\n guard(guard: ToolGuard): () => void;\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined;\n schemas(scope?: ScopeKey): ToolSchema[];\n executionMode(exec: ToolExecutionInput): ToolExecutionMode;\n async execute(exec: ToolExecutionInput): Promise;\n}', + declaration: 'export class ToolRuntime extends Service {\n static inject;\n static Config: z;\n readonly [TOOL_RUNTIME_SCHEDULER]: ToolRuntimeScheduler;\n constructor(ctx: Context, config: Config = {});\n presentAs(mode: ToolPresentationMode): () => void;\n register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void;\n restrict(filter: ToolRestriction): () => void;\n guard(guard: ToolGuard): () => void;\n get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined;\n async resolve(context: ToolActivationContext): Promise;\n schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[];\n executionMode(exec: ToolExecutionInput): ToolExecutionMode;\n async execute(exec: ToolExecutionInput): Promise;\n}', }, { name: 'ToolRuntimeScheduler', diff --git a/packages/feedback/command-feedback/tests/command-feedback.spec.ts b/packages/feedback/command-feedback/tests/command-feedback.spec.ts index 4e5401c09e..680a1e87f0 100644 --- a/packages/feedback/command-feedback/tests/command-feedback.spec.ts +++ b/packages/feedback/command-feedback/tests/command-feedback.spec.ts @@ -33,6 +33,7 @@ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session } const session = ctx.sessions.create(SessionId(id)) let status: AgentStatus = 'idle' const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, diff --git a/packages/feedback/command-feedback/tests/loader-composition.spec.ts b/packages/feedback/command-feedback/tests/loader-composition.spec.ts index 273657e579..bf2090a66c 100644 --- a/packages/feedback/command-feedback/tests/loader-composition.spec.ts +++ b/packages/feedback/command-feedback/tests/loader-composition.spec.ts @@ -32,6 +32,7 @@ async function agent(ctx: Context): Promise { const session = ctx.sessions.create(id) let status: AgentStatus = 'idle' const value: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts index 74b8ce5dd6..8e2855bd45 100644 --- a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts +++ b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts @@ -34,6 +34,7 @@ async function agent(ctx: Context, cwd: string): Promise { version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd, isSeeded: false, }) const value: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/goal/command-goal/tests/command-goal.spec.ts b/packages/goal/command-goal/tests/command-goal.spec.ts index 1e4e65c7a1..0329d2eea1 100644 --- a/packages/goal/command-goal/tests/command-goal.spec.ts +++ b/packages/goal/command-goal/tests/command-goal.spec.ts @@ -25,6 +25,7 @@ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session } const inbox = createInboxStub() let status: AgentStatus = 'idle' const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, diff --git a/packages/goal/goal/tests/goal.spec.ts b/packages/goal/goal/tests/goal.spec.ts index fe9c2d0030..9f98194d2a 100644 --- a/packages/goal/goal/tests/goal.spec.ts +++ b/packages/goal/goal/tests/goal.spec.ts @@ -54,6 +54,7 @@ function stubAgentForSession(session: Session, suppliedCtx?: Context): StubAgent } const inbox = createInboxStub() const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/goal/goal/tests/projection.spec.ts b/packages/goal/goal/tests/projection.spec.ts index 227da38ad8..b2f6491192 100644 --- a/packages/goal/goal/tests/projection.spec.ts +++ b/packages/goal/goal/tests/projection.spec.ts @@ -39,6 +39,7 @@ interface Bench { async function liveAgent(ctx: Context, session: Session): Promise { const status: AgentStatus = 'idle' const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, diff --git a/packages/goal/tool-goal/tests/tool-goal.spec.ts b/packages/goal/tool-goal/tests/tool-goal.spec.ts index ecc7822bce..65fcdc64fc 100644 --- a/packages/goal/tool-goal/tests/tool-goal.spec.ts +++ b/packages/goal/tool-goal/tests/tool-goal.spec.ts @@ -54,6 +54,7 @@ function stubAgent(rawId: string, supplied?: Session, suppliedCtx?: Context): St const inbox = createInboxStub() let status: AgentStatus = 'running' const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, diff --git a/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts b/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts index 5ba9c7f544..c6af01c97b 100644 --- a/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts +++ b/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts @@ -80,6 +80,7 @@ async function setup(config: toolAskUser.Config = { mode: 'timed', timeout: -1 } function stubAgent(id: string, delegationDepth = 0): Agent { const agentId = SessionId(id) const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: agentId, options: {}, session: Session.create(agentId, [], { diff --git a/packages/jobs/jobs-local/tests/events.spec.ts b/packages/jobs/jobs-local/tests/events.spec.ts index 5a653569b9..89e629dd65 100644 --- a/packages/jobs/jobs-local/tests/events.spec.ts +++ b/packages/jobs/jobs-local/tests/events.spec.ts @@ -19,6 +19,7 @@ function agentIn(ctx: Context, rawId: string, presetScope?: ScopeKey): Agent { } const session = Session.create(id) return { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/jobs/jobs-local/tests/jobs.spec.ts b/packages/jobs/jobs-local/tests/jobs.spec.ts index 0c409049b9..3d24152a0f 100644 --- a/packages/jobs/jobs-local/tests/jobs.spec.ts +++ b/packages/jobs/jobs-local/tests/jobs.spec.ts @@ -35,6 +35,7 @@ function stubAgent(ctx: Context, rawId: string, presetScope?: ScopeKey): Agent { } const session = Session.create(id) const agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/plan/plan-mode/README.i18n.yaml b/packages/plan/plan-mode/README.i18n.yaml index 534d4abb49..e4c32250f2 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: ff946ab59a7b4a0f - zh: b1c79937770f57ad + en: 6d0adadc354f211e + zh: 86fa49a9f4377368 /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: 56798d0f45fb5775 - zh: 282e4a522ce6dae0 + en: 86b26413acb2e835 + zh: a84d876efefa5bea /deepseek-ai-dsh-plan-mode/understand-the-implementation/session-projection-unit: en: 74cd6bdf50af51f5 zh: 67609cf7202ececb @@ -90,14 +90,14 @@ en: 9622e96bdc01276d zh: 53801e68a49f3d1d /deepseek-ai-dsh-plan-mode/model-experience/exit-tool-schema-and-review-exchange/what-the-model-sees: - en: 3768d0189f9f13af - zh: 2168a9b24551b7c6 + en: 76ba584f0225cc7f + zh: f8b010dbc909e4ac /deepseek-ai-dsh-plan-mode/model-experience/exit-tool-schema-and-review-exchange/token-effect: - en: d1b1418f20ee119d - zh: c2b89ea3cae57175 + en: a1a020eb7ba26e7c + zh: a99208693d331a31 /deepseek-ai-dsh-plan-mode/model-experience/exit-tool-schema-and-review-exchange/kv-cache-effect: - en: 6b1404ea11f1e60d - zh: 348d0579de218652 + en: f640fc33aa30602f + zh: e0925a15d01141e9 /deepseek-ai-dsh-plan-mode/known-limitations-and-deferred-work: en: a87aabfede0087fd zh: 201e9b3766980005 diff --git a/packages/plan/plan-mode/README.md b/packages/plan/plan-mode/README.md index 6cbcbf2632..428c53d7f7 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 cannot block the turn. 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 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 `/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` stays registered while plan mode is inactive, so entering or leaving changes only the prompt section, never the request tool catalog. 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 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. ### Session projection unit @@ -164,15 +164,15 @@ The user block is append-only conversation growth. Entering or leaving plan mode #### What the model sees -The [`exit_plan_mode` schema](../../../docs/tool-catalog.md#deepseek-aidsh-plan-mode) remains available in both states; execution outside plan mode fails, while an approved in-mode review returns the canonical `{ approved: true }` value and renders the existing confirmation text. Rejection remains a failed call carrying review feedback, and a dismissed review a failed call naming the user's takeover. +The [`exit_plan_mode` schema](../../../docs/tool-catalog.md#deepseek-aidsh-plan-mode) is offered only in plan mode; calls outside its admitted set return `UNKNOWN_TOOL`, while an approved in-mode review returns the canonical `{ approved: true }` value and renders the existing confirmation text. Rejection remains a failed call carrying review feedback, and a dismissed review a failed call naming the user's takeover. #### Token effect -The stable schema is paid according to ToolRuntime mode, and each plan argument and review result remains in conversation history. +The schema is paid only while active, according to ToolRuntime mode, and each plan argument and review result remains in conversation history. #### KV Cache effect -Mode transitions do not change the tool catalog; plan arguments and review results extend the conversation normally. +Mode transitions add or remove the exit tool through the route’s tool-update protocol; plan arguments and review results extend the conversation normally. ## Known Limitations and Deferred Work diff --git a/packages/plan/plan-mode/README.zh.md b/packages/plan/plan-mode/README.zh.md index 8ecb60b1db..40cb116121 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——agent 运行时唯一的追加点——且追加失败不能阻塞轮次。`set`/`get` 服务方法及其确切返回状态见 [`src/index.ts`](src/index.ts),并读取已注册的 `plan` 投影;注册表或 key 缺失时,第一次依赖它们的访问会显式失败。 +本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有打开的轮次时立即记录模式变化;轮次内则等待下一个被接纳的 pre-step。写入失败会停止步骤接纳并保留待提交选择。`set`/`get` 的精确返回状态见 [`src/index.ts`](src/index.ts);它们读取已注册的 `plan` 投影,缺少注册表或投影键时在首次依赖访问处明确失败。 ### `/plan` 命令 @@ -90,7 +90,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并 ### 退出工具 -`exit_plan_mode` 在计划模式未激活时仍保持注册,因此进入或离开只改变提示词段落,绝不改变请求的工具目录。经批准的评审会记录一个静默的待生效退出,由下一个被接受的轮内 pre-step 追加,当前这批工具调用剩余部分仍保留计划引导。缺少用户交互通道,或评审等待期间服务重载,调用都会以拒绝方式失败,`/plan off` 仍是手动退路。 +`exit_plan_mode` 只在已提交的计划状态为 active 时准入。进入或离开会同时改变工具声明与计划指导;原生和 PTC 执行使用同一准入集合。批准评审后记录静默的待退出选择,由下一个被接纳的轮次内 pre-step 提交,当前工具批次的剩余调用保留计划指导。没有用户问答通道,或评审期间服务重载时,调用失败;`/plan off` 仍可手动退出。 ### 会话投影单元 @@ -164,15 +164,15 @@ You are in plan mode. Explore and design before presenting the complete plan thr #### 模型看到什么 -[`exit_plan_mode` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-plan-mode) 在两种状态下均可用;在计划模式之外执行会失败,而计划模式内经批准的评审返回规范的 `{ approved: true }` 值,并渲染既有的确认文本。拒绝仍是携带评审反馈的失败调用,放弃评审则是一次指明用户接手的失败调用。 +[`exit_plan_mode` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-plan-mode) 只在计划模式中提供;不在准入集合中的调用返回 `UNKNOWN_TOOL`。模式内获批的评审返回规范值 `{ approved: true }` 并渲染现有确认文本。拒绝仍是携带评审反馈的失败调用;关闭评审则报告用户接管对话。 #### Token 影响 -稳定 schema 的成本取决于 ToolRuntime 模式,每次传入的 plan 参数与评审结果都会保留在对话历史中。 +schema 仅在启用时按 ToolRuntime 模式计入开销;每份计划参数与评审结果保留在对话历史中。 #### KV Cache 影响 -模式转换不改变工具目录;plan 参数与评审结果按常规方式扩展对话。 +模式转换通过路由的工具更新协议新增或移除退出工具;计划参数与评审结果正常扩展对话。 ## 已知限制与延期工作 diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts index e711607da8..a4fa5cf12e 100644 --- a/packages/plan/plan-mode/src/index.ts +++ b/packages/plan/plan-mode/src/index.ts @@ -8,13 +8,12 @@ * * 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 includes the selected state in the proposed step - * assembly, then appends `plan/mode` from `agent/pre-step` only when the step - * is accepted. Same-step request retries reuse their assembly. + * pre-step. The service appends `plan/mode` from `agent/pre-step` before + * request-time tool conditions and plan guidance read that committed state. + * Same-step retries do not commit pending selections. * - * The exit tool remains registered while plan mode is inactive, so entering - * or leaving plan mode changes only the prompt section, not the request tool - * catalog. + * The exit tool is admitted only for requests whose committed plan state is + * active. Native declarations, PTC bindings, and execution share that selection. * * See packages/plan/plan-mode/README.md. * @@ -60,8 +59,7 @@ declare module '@deepseek-ai/cordis' { } /** - * The model-facing exit tool's name. It stays registered while plan mode is - * inactive so the request tool catalog is stable across transitions. + * The model-facing exit tool's name, admitted only while plan mode is active. */ export const EXIT_PLAN_MODE = 'exit_plan_mode' @@ -169,7 +167,7 @@ export const planProjectionDefinition = { /** * `ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, - * the `plan:policy` section, the `/plan` command, and the stable exit tool. + * the `plan:policy` section, the `/plan` command, and the conditional exit tool. * Client carriers expose the projection's cropped `{ active, pending }` view. */ export class PlanModeController extends Service { @@ -191,8 +189,8 @@ export class PlanModeController extends Service { 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 for a later accepted in-turn pre-step, - // and policy cannot block the step. + // A failed append remains pending and stops admission before guidance or + // tool availability could describe an uncommitted state. ctx.on('agent/pre-step', async ( { agent, signal }, next, @@ -201,12 +199,7 @@ export class PlanModeController extends Service { 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) - try { - this.onBoundary(agent.session) - } catch (error) { - ctx.logger.warn('dsh-plan-mode: failed to append selected plan mode at step start: %o', error) - return decision - } + this.onBoundary(agent.session) return !pending.narrate || narration === undefined ? decision : { ...decision, messages: [...decision.messages, narration] } @@ -218,8 +211,7 @@ export class PlanModeController extends Service { order: ctx.systemPrompt.getSectionOrder('PLAN_POLICY'), text: (context) => { if (context.agent === undefined) return '' - const pending = this.pendingIntents.get(context.agent.session) - return (pending?.active ?? this.loggedActive(context.agent.session)) ? this.section : '' + return this.loggedActive(context.agent.session) ? this.section : '' }, }) @@ -347,7 +339,7 @@ export class PlanModeController extends Service { } // 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 request assembly. + // before its tool and prompt assembly. this.pendingIntents.set(agent.session, { active: false, narrate: false }) return { approved: true } }, @@ -362,7 +354,7 @@ export class PlanModeController extends Service { title: 'Plan review', content: result.content, }), - })) + }), { when: ({ agent }) => this.loggedActive(agent.session) }) } private loggedActive(session: Session): boolean { diff --git a/packages/plan/plan-mode/tests/integration.spec.ts b/packages/plan/plan-mode/tests/integration.spec.ts index b1742a1ba2..80d843c577 100644 --- a/packages/plan/plan-mode/tests/integration.spec.ts +++ b/packages/plan/plan-mode/tests/integration.spec.ts @@ -127,7 +127,7 @@ describe('plan mode through the agent loop', () => { expect(log.some(event => event.type === 'user/message' && event.data.source.kind !== 'user')).toBe(false) }) - it('a user flip between turns lands at the boundary: one notice and a replaced system node with stable tool schemas', async () => { + it('a user flip between turns lands at the boundary: one notice and an added exit tool', async () => { const adapter = new MockAdapter([ textResponse('First turn, default mode.'), textResponse('Second turn, plan mode.'), @@ -139,7 +139,7 @@ describe('plan mode through the agent loop', () => { await waitForIdle(ctx, agent) expect(planActive(ctx, agent)).toBe(false) const first = findEvent(agent.session.snapshotEvents(), 'request/header') - expect(first.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write']) + expect(first.data.header.tools?.map(tool => tool.name)).toEqual(['read', 'write']) const firstSystem = findEvent(agent.session.snapshotEvents(), 'system/message') expect(systemText(agent)).not.toContain(PLAN_CONFIG.section) @@ -163,10 +163,10 @@ describe('plan mode through the agent loop', () => { expect(second?.sourceEventSeqs).toEqual([firstSystem.seq]) expect(agent.session.surface.nodes[0]).toBe(second?.seq) expect(systemText(agent)).toContain('plan mode') - expect(log.filter(event => event.type === 'request/header').map(event => event.data.reason)).toEqual(['initial', 'series']) + expect(log.filter(event => event.type === 'request/header').map(event => event.data.reason)).toEqual(['initial', 'change']) const secondHeader = findEvent(log, 'request/header', 'last') expect(secondHeader.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write']) - expect(secondHeader.data.header.tools).toEqual(first.data.header.tools) + expect(secondHeader.data.header.tools).not.toEqual(first.data.header.tools) }) it('a mode flip at error settlement waits until the step after a same-step retry', async () => { @@ -204,7 +204,7 @@ describe('plan mode through the agent loop', () => { expect(adapter.requests).toHaveLength(3) expect(requestSystem(adapter.requests[2])).toContain(PLAN_CONFIG.section) - expect(adapter.requests[2]?.tools).toEqual(adapter.requests[0]?.tools) + expect(adapter.requests[2]?.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write']) const log = agent.session.snapshotEvents() const planMode = findEvent(log, 'plan/mode') const firstEnd = log.find(event => event.type === 'step/end' diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts index bf97cc789c..7c58740292 100644 --- a/packages/plan/plan-mode/tests/plan-mode.spec.ts +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -1,4 +1,6 @@ import { describe, expect, it, vi } from 'vitest' + +const testToolSignal = new AbortController().signal import { Context } from '@deepseek-ai/cordis' import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' @@ -72,8 +74,9 @@ async function agentWithSession( return agent } -function assembleFor(ctx: Context, agent: Agent) { - return ctx.systemPrompt.assemble({ agent, scope: agent }) +async function assembleFor(ctx: Context, agent: Agent) { + const toolSet = await ctx.tools.resolve({ agent, signal: testToolSignal }) + return ctx.systemPrompt.assemble({ agent, scope: agent, toolSet }) } function foldPlanMode(events: readonly SessionEvent[], end = events.length): boolean { @@ -159,7 +162,7 @@ function registerNamedTools(ctx: Context, names: string[]): void { } } -/** Assert the mapped PTC mode SDK includes the stable plan exit binding and test tools. */ +/** Assert the mapped PTC mode SDK includes the active plan exit binding and test tools. */ function expectPlanPtcSdkBindings(sdk: string): void { expect(sdk).toContain('interface ToolArgsMap {') expect(sdk).toContain('read: Record;') @@ -409,7 +412,7 @@ describe('the boundary flush', () => { }) - it('contains an append failure instead of blocking the prompt or the turn', async () => { + it('stops admission on append failure and retries the pending selection', async () => { const ctx = await setup() const warn = vi.fn() ctx.logger.warn = warn as never @@ -423,8 +426,8 @@ describe('the boundary flush', () => { if (type === 'plan/mode') throw new Error('backend gone') return (original as (...args: unknown[]) => unknown)(type, ...rest) }) as unknown) as typeof agent.session.append - await boundary(ctx, agent, 'step-start') - expect(warn).toHaveBeenCalledOnce() + await expect(boundary(ctx, agent, 'step-start')).rejects.toThrow('backend gone') + expect(warn).not.toHaveBeenCalled() // The failed flush re-parks the intent (cleared only after a landed // append), so the next healthy boundary converges the log with the // picker's optimistic state instead of dropping the switch forever. @@ -435,7 +438,7 @@ describe('the boundary flush', () => { expect(ctx.planMode.get(agent).pending).toBeUndefined() }) - it('contains a pre-step append failure and keeps the intent pending', async () => { + it('rejects a pre-step append failure and keeps the intent pending', async () => { const ctx = await setup() const warn = vi.fn() ctx.logger.warn = warn as never @@ -447,24 +450,24 @@ describe('the boundary flush', () => { if (type === 'plan/mode') throw new Error('backend gone') return (original as (...args: unknown[]) => unknown)(type, ...rest) }) as unknown) as typeof agent.session.append - await boundary(ctx, agent, 'pre-step') - expect(warn).toHaveBeenCalledOnce() + await expect(boundary(ctx, agent, 'pre-step')).rejects.toThrow('backend gone') + expect(warn).not.toHaveBeenCalled() expect(ctx.planMode.get(agent)).toEqual({ active: false, pending: true }) }) }) describe('the soft layer', () => { - it('keeps the tool schemas identical across default and plan mode', async () => { + it('adds the exit schema together with plan guidance', async () => { const ctx = await setup() registerNamedTools(ctx, ['read', 'write']) const agent = await agentWithSession(ctx) const defaultAssembly = await assembleFor(ctx, agent) - expect(defaultAssembly.tools.map(tool => tool.name)).toEqual([EXIT_PLAN_MODE, 'read', 'write']) + expect(defaultAssembly.tools.map(tool => tool.name)).toEqual(['read', 'write']) expect(defaultAssembly.sections.find(section => section.name === 'plan:policy')?.text).toBe('') agent.session.append('plan/mode', { active: true }) const planAssembly = await assembleFor(ctx, agent) - expect(planAssembly.tools).toEqual(defaultAssembly.tools) + expect(planAssembly.tools.map(tool => tool.name)).toEqual([EXIT_PLAN_MODE, 'read', 'write']) expect(planAssembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION) }) @@ -485,25 +488,15 @@ describe('the soft layer', () => { expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION) }) - it('leaves foreign assemble additions alone (no assemble-layer filtering)', async () => { - // Plan guidance does not filter the registry or later assembly additions. - const ctx = new Context() - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRuntime) - await mountProjectionSeam(ctx) - ctx.on('system-prompt/assemble', async (_assembly, _context, next) => { - const final = await next() - final.tools = [...final.tools, { name: 'added-later', description: 'added after next()', parameters: {} }] - return final - }) - await ctx.plugin(PlanModeController, PLAN_CONFIG) - registerNamedTools(ctx, ['read']) + it('keeps independently registered tools available across plan changes', async () => { + const ctx = await setup() + registerNamedTools(ctx, ['read', 'added-later']) const planning = await agentWithSession(ctx, 'planning', { active: true }) expect((await assembleFor(ctx, planning)).tools.map(tool => tool.name)) - .toEqual(['exit_plan_mode', 'read', 'added-later']) + .toEqual(['added-later', 'exit_plan_mode', 'read']) const defaulted = await agentWithSession(ctx, 'defaulted') expect((await assembleFor(ctx, defaulted)).tools.map(tool => tool.name)) - .toEqual(['exit_plan_mode', 'read', 'added-later']) + .toEqual(['added-later', 'read']) }) it('keeps run_code the only wire tool in plan mode under the registry PTC mode; the SDK gains the exit binding', async () => { @@ -532,7 +525,7 @@ describe('the soft layer', () => { expectPlanPtcSdkBindings(sdk) }) - it('keeps the PTC mode SDK byte-identical across mode switches', async () => { + it('includes the PTC exit binding only while plan mode is active', async () => { class FakeRuntime extends PtcRuntime { resolve(request: import('@deepseek-ai/dsh-ptc-runtime').PtcRunRequest): import('@deepseek-ai/dsh-ptc-runtime').PtcRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } } @@ -549,13 +542,11 @@ describe('the soft layer', () => { registerNamedTools(withPlanMode, ['read', 'write']) const agent = await agentWithSession(withPlanMode) const defaultSdk = (await assembleFor(withPlanMode, agent)).sections.find(section => section.name === 'tools:sdk')?.text ?? '' - expectPlanPtcSdkBindings(defaultSdk) + expect(defaultSdk).not.toContain('exit_plan_mode:') agent.session.append('plan/mode', { active: true }) const planSdk = (await assembleFor(withPlanMode, agent)).sections.find(section => section.name === 'tools:sdk')?.text ?? '' - expect(planSdk).toBe(defaultSdk) + expectPlanPtcSdkBindings(planSdk) - // Loading the plan-mode plugin deliberately adds one stable binding compared - // with a deployment that does not compose plan mode at all. const bare = new Context() await bare.plugin(SystemPrompt) await bare.plugin(ToolRuntime, { mode: 'ptc' }) @@ -563,7 +554,7 @@ describe('the soft layer', () => { registerNamedTools(bare, ['read', 'write']) const bareSdk = (await bare.systemPrompt.assemble({ agent })).sections.find(section => section.name === 'tools:sdk')?.text ?? '' expect(bareSdk).not.toContain('exit_plan_mode:') - expect(defaultSdk).not.toBe(bareSdk) + expect(defaultSdk).toBe(bareSdk) }) }) @@ -804,8 +795,11 @@ describe('exit_plan_mode', () => { return { ctx, agent, asked } } - function callExit(ctx: Context, agent: Agent | undefined, plan = '# The plan\n\ndo things') { + 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, signal }) return ctx.tools.execute({ + ...toolSet === undefined ? {} : { toolSet }, callId: ToolCallId(`call-exit-${++callCounter}`), name: EXIT_PLAN_MODE, arguments: { plan }, @@ -827,16 +821,16 @@ describe('exit_plan_mode', () => { const ctx = await setup() const result = await callExit(ctx, undefined) expect(result.isError).toBe(true) - expect(result.content).toEqual([{ type: 'text', text: 'Error: exit_plan_mode requires a calling agent (no session to switch)' }]) + expect(result.content).toEqual([{ type: 'text', text: 'Error: unknown tool "exit_plan_mode"' }]) }) - it('rejects a call outside plan mode while remaining advertised', async () => { + it('omits and rejects the exit tool outside plan mode', async () => { const ctx = await setup() const agent = await agentWithSession(ctx) - expect(ctx.tools.schemas().map(tool => tool.name)).toContain(EXIT_PLAN_MODE) + expect((await assembleFor(ctx, agent)).tools.map(tool => tool.name)).not.toContain(EXIT_PLAN_MODE) const result = await callExit(ctx, agent) expect(result.isError).toBe(true) - expect(result.content).toEqual([{ type: 'text', text: 'Error: exit_plan_mode is only available in plan mode' }]) + expect(result.content).toEqual([{ type: 'text', text: 'Error: unknown tool "exit_plan_mode"' }]) }) it('rejects an empty or heading-less plan before asking the reviewer', async () => { @@ -938,6 +932,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, 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' }, @@ -960,21 +955,19 @@ describe('exit_plan_mode', () => { expect(ctx.planMode.get(agent)).toEqual({ active: true, pending: false }) }) - it('an approved exit projects the next assembly before the boundary and never removes the tool', async () => { + it('removes the exit tool and plan guidance together after the accepted boundary', async () => { const { ctx, agent } = await setupWithReview({ selected: ['Approve'] }) const approved = await callExit(ctx, agent) expect(approved.isError).toBe(false) - // Calls of the SAME assistant response were requested under the existing - // plan-shaped header. Pending state shapes only the proposed next - // assembly; the accepted boundary then commits the matching durable fold. + // Calls from the same assistant response retain the committed plan state. expect(foldPlanMode(agent.session.snapshotEvents())).toBe(true) - const assembly = await ctx.systemPrompt.assemble({ agent }) + const assembly = await assembleFor(ctx, agent) expect(assembly.tools.some(tool => tool.name === EXIT_PLAN_MODE)).toBe(true) - expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe('') + expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION) await boundary(ctx, agent, 'step-start') expect(foldPlanMode(agent.session.snapshotEvents())).toBe(false) - const afterExit = await ctx.systemPrompt.assemble({ agent }) - expect(afterExit.tools).toEqual(assembly.tools) + const afterExit = await assembleFor(ctx, agent) + expect(afterExit.tools.some(tool => tool.name === EXIT_PLAN_MODE)).toBe(false) expect(afterExit.sections.find(section => section.name === 'plan:policy')?.text).toBe('') }) @@ -1088,6 +1081,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, signal: testToolSignal }), callId: ToolCallId(`call-exit-${++callCounter}`), name: EXIT_PLAN_MODE, arguments: { plan: '# P' }, @@ -1163,7 +1157,7 @@ describe('exit_plan_mode', () => { }) describe('HMR disposal', () => { - it('unregisters the service, listeners, prompt section, and stable exit tool with the plugin fiber', async () => { + it('unregisters the service, listeners, prompt section, and exit tool with the plugin fiber', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) diff --git a/packages/schedule/schedule/tests/harness.ts b/packages/schedule/schedule/tests/harness.ts index bf152ad187..f6c9794a32 100644 --- a/packages/schedule/schedule/tests/harness.ts +++ b/packages/schedule/schedule/tests/harness.ts @@ -125,6 +125,7 @@ export interface HarnessAgent extends Agent { export function agentFor(ctx: Context, id = 'original', meta?: CreateSessionOptions['meta']): HarnessAgent { const session = ctx.sessions.create(SessionId(id), meta === undefined ? undefined : { meta }) return { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, session, ctx: ctx.extend(), options: {}, status: 'idle', inbox: unsupportedInbox(), send() {}, followup: vi.fn<(message: UserMessage) => void>(), steer() {}, inject() {}, cancel() {}, whenIdle: async () => {}, runMaintenance: operation => operation(new AbortController().signal), diff --git a/packages/session/working-directory/README.i18n.yaml b/packages/session/working-directory/README.i18n.yaml index a593c37b80..cee7dbc5ed 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: f0458147d29631b1 - zh: 6a288670dd084ffb + en: 6f7b264aef175243 + zh: e2798c0aab0eb21b /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: a0f7bcd972f4a4fb - zh: 051ec0eb0fa92bea + en: 7f93a8dd08c52ac5 + zh: 38f35141d2e7eb6d /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 1f54ad8c92..19f00941aa 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. 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. +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. 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, 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. +- **Missing original project** — when the current directory and the original project are both unavailable, request 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. ### Dev Note diff --git a/packages/session/working-directory/README.zh.md b/packages/session/working-directory/README.zh.md index ad92f21b59..b656f1e835 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 生命周期,提交事件后排入用户上下文通知。若提交后通知入队失败,操作仍返回已提交的目录并记录警告;下一次请求或恢复仍会收到必需的目录上下文。提示词组装在发布必需的上下文条目前验证当前目录。已有进程与沙箱授权继续由各自的服务拥有。[服务实现](src/index.ts) 定义 API。 +Session 投影拥有当前目录,并通过 Session observation 暴露它,因此冷态读取方无需激活 Agent 即可使用;header 继续标识原始项目。每个 Session 的变更串行执行,通过文件系统 provider 验证,并检查取消状态与 Agent 生命周期,提交事件后排入用户上下文通知。若提交后通知入队失败,操作仍返回已提交的目录并记录警告;下一次请求或恢复仍会收到必需的目录上下文。`system-prompt/prepare` 在步骤接纳以及发布必需的上下文条目前验证当前目录。已有进程与沙箱授权继续由各自的服务拥有。[服务实现](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 7346feb274..90156169ed 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/assemble', async (_assembly, context, next) => { + ctx.on('system-prompt/prepare', 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/shell/tool-bash-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts index f30e87b978..d8644dbc62 100644 --- a/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts +++ b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts @@ -45,6 +45,7 @@ async function agent(ctx: Context, cwd: string): Promise { version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd, isSeeded: false, }) const value: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/shell/tool-bash-persistent/tests/tools.spec.ts b/packages/shell/tool-bash-persistent/tests/tools.spec.ts index d3507373c7..ab9967bfd9 100644 --- a/packages/shell/tool-bash-persistent/tests/tools.spec.ts +++ b/packages/shell/tool-bash-persistent/tests/tools.spec.ts @@ -39,6 +39,7 @@ async function agent(ctx: Context, cwd: string | undefined): Promise { ...cwd === undefined ? {} : { cwd }, }) const value: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/shell/tool-bash/tests/background.spec.ts b/packages/shell/tool-bash/tests/background.spec.ts index 3a1fbff9bb..35e6b566ba 100644 --- a/packages/shell/tool-bash/tests/background.spec.ts +++ b/packages/shell/tool-bash/tests/background.spec.ts @@ -352,6 +352,7 @@ describe('foreground commands as jobs', () => { const ctx = await setup() const ownerId = SessionId('promote-owner') const owner: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: ownerId, options: {}, session: Session.create(ownerId, undefined, { @@ -468,6 +469,7 @@ describe('owned background output', () => { const ctx = await setup() const ownerId = SessionId('background-owner') const owner: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: ownerId, options: {}, session: Session.create(ownerId, undefined, { diff --git a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts index 90c29923a0..f48241d964 100644 --- a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts +++ b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts @@ -80,6 +80,7 @@ async function agent(ctx: Context, cwd: string): Promise { version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd, isSeeded: false, }) const value: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts index b2b6d96137..6c0f41fe57 100644 --- a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts +++ b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts @@ -39,6 +39,7 @@ async function agent(ctx: Context, cwd: string | undefined): Promise { ...cwd === undefined ? {} : { cwd }, }) const value: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/shell/tool-pwsh/tests/background.spec.ts b/packages/shell/tool-pwsh/tests/background.spec.ts index 8a1d46addc..3382c91132 100644 --- a/packages/shell/tool-pwsh/tests/background.spec.ts +++ b/packages/shell/tool-pwsh/tests/background.spec.ts @@ -204,6 +204,7 @@ describe('owned background output (pwsh)', () => { const { ctx, pwsh } = await setup() const ownerId = SessionId('pwsh-background-owner') const owner: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: ownerId, options: {}, session: Session.create(ownerId, undefined, { @@ -404,6 +405,7 @@ describe('foreground commands as jobs (pwsh)', () => { const { ctx, pwsh } = await setup() const ownerId = SessionId('pwsh-promote-owner') const owner: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: ownerId, options: {}, session: Session.create(ownerId, undefined, { diff --git a/packages/skill/tool-skill/tests/tool-skill.spec.ts b/packages/skill/tool-skill/tests/tool-skill.spec.ts index e3362cda0a..21253e2cac 100644 --- a/packages/skill/tool-skill/tests/tool-skill.spec.ts +++ b/packages/skill/tool-skill/tests/tool-skill.spec.ts @@ -69,6 +69,7 @@ function agentForCwd(cwd: string): Agent { version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd, isSeeded: false, }) return { + currentRoute() { throw new Error('no prepared model request in this fixture') }, ctx: new Context(), id, options: {}, @@ -87,6 +88,7 @@ function agentForCwd(cwd: string): Agent { function sessionAgent(session: Session, id = 'tool-skill-agent'): Agent { const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: SessionId(id), options: {}, session, diff --git a/packages/subagent/subagent/tests/archive-admission.spec.ts b/packages/subagent/subagent/tests/archive-admission.spec.ts index 9e8c1b2b12..9aba8f2235 100644 --- a/packages/subagent/subagent/tests/archive-admission.spec.ts +++ b/packages/subagent/subagent/tests/archive-admission.spec.ts @@ -31,6 +31,7 @@ async function liveAgent(ctx: Context, session: Session, initial: AgentStatus): const cancels: CancelCall[] = [] let status = initial const agent: LiveAgent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, diff --git a/packages/subagent/subagent/tests/external-activation-helpers.ts b/packages/subagent/subagent/tests/external-activation-helpers.ts index 389ce804d6..21f53b0e22 100644 --- a/packages/subagent/subagent/tests/external-activation-helpers.ts +++ b/packages/subagent/subagent/tests/external-activation-helpers.ts @@ -33,6 +33,7 @@ export async function externalTestParent(ctx: Context, cwd?: string): Promise { const session = ctx.sessions.create(SessionId('mode-owner')) const ownerFiber = await ctx.plugin(() => {}) const owner: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, inbox: unsupportedInbox(), status: 'idle', ctx: ownerFiber.ctx, @@ -726,6 +728,7 @@ describe('terminal-bash plugin shape', () => { const session = ctx.sessions.create(SessionId('pending-mode-owner')) const ownerFiber = await ctx.plugin(() => {}) const owner: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, inbox: unsupportedInbox(), status: 'idle', ctx: ownerFiber.ctx, diff --git a/packages/terminal/terminal-bash/tests/local.spec.ts b/packages/terminal/terminal-bash/tests/local.spec.ts index e5a53123d0..0d11094467 100644 --- a/packages/terminal/terminal-bash/tests/local.spec.ts +++ b/packages/terminal/terminal-bash/tests/local.spec.ts @@ -40,6 +40,7 @@ function stubAgent(ctx: Context, rawId: string): Agent { const scope = ctx.plugin(() => {}) const session = Session.create(id) const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, inbox: unsupportedInbox(), status: 'idle', ctx: scope.ctx, diff --git a/packages/terminal/terminal/tests/service.spec.ts b/packages/terminal/terminal/tests/service.spec.ts index 11da341945..b14e47b3ab 100644 --- a/packages/terminal/terminal/tests/service.spec.ts +++ b/packages/terminal/terminal/tests/service.spec.ts @@ -24,6 +24,7 @@ function stubAgent(ctx: Context, rawId: string): Agent { const scopeFiber = ctx.plugin(() => {}) const session = Session.create(id) const agent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, diff --git a/packages/test-support/session-snapshot/tests/fixtures/dynamic-tool-updates.ts b/packages/test-support/session-snapshot/tests/fixtures/dynamic-tool-updates.ts index 5ff044af6b..f85e947ad9 100644 --- a/packages/test-support/session-snapshot/tests/fixtures/dynamic-tool-updates.ts +++ b/packages/test-support/session-snapshot/tests/fixtures/dynamic-tool-updates.ts @@ -6,24 +6,23 @@ import { defineContentToolFixture } from '@deepseek-ai/dsh-tools' export const name = 'snapshot-dynamic-tool-updates' export const inject = ['tools'] -/** Register a tool after reading, then remove it after use without changing prompt text. */ +/** Activate a tool after reading, then disable it after use without changing prompt text. */ export function apply(ctx: Context): void { - let removeTool: (() => void) | undefined + let active = false + ctx.tools.register(defineContentToolFixture({ + name: 'snapshot_ping', description: 'Return pong once.', parameters: {}, + async execute() { return [{ type: 'text', text: 'pong' }] }, + }), { when: async () => active }) let requests = 0 ctx.on('tools/post-execute', async (exec, result, next) => { const downstream = await next() if (result.isError) return downstream switch (exec.name) { case 'read': - removeTool = ctx.tools.register(defineContentToolFixture({ - name: 'snapshot_ping', - description: 'Return pong once.', - parameters: {}, - async execute() { return [{ type: 'text', text: 'pong' }] }, - })) + active = true break case 'snapshot_ping': - removeTool?.() + active = false break } return downstream diff --git a/packages/todo/tool-todo/tests/loader-composition.spec.ts b/packages/todo/tool-todo/tests/loader-composition.spec.ts index 3642d8485f..11a8cdbc9e 100644 --- a/packages/todo/tool-todo/tests/loader-composition.spec.ts +++ b/packages/todo/tool-todo/tests/loader-composition.spec.ts @@ -34,6 +34,7 @@ async function agent(ctx: Context): Promise { const id = SessionId('todo-loader-agent') const session = Session.create(id) const value: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id, options: {}, session, inbox: unsupportedInbox(), status: 'idle', ctx: scope.ctx, followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, diff --git a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts index 7c7f819072..bc39c173eb 100644 --- a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts +++ b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts @@ -457,6 +457,7 @@ describe('dsh-tool-workflow', () => { const engine = ctx.workflowEngine as StubEngine const session = Session.create(SessionId('caller')) const parent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, inbox: unsupportedInbox(), status: 'idle', ctx, send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel: () => {}, runMaintenance: task => task(new AbortController().signal), whenIdle: () => Promise.resolve(), @@ -670,6 +671,7 @@ describe('dsh-tool-workflow', () => { await ctx.plugin(toolWorkflow, {}) const session = Session.create(SessionId('caller')) const parent: Agent = { + currentRoute() { throw new Error('no prepared model request in this fixture') }, id: session.id, options: {}, session, inbox: unsupportedInbox(), status: 'idle', ctx, send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel: () => {}, runMaintenance: task => task(new AbortController().signal), whenIdle: () => Promise.resolve(), diff --git a/scripts/fixtures/python-sdk-dynamic-tools.mjs b/scripts/fixtures/python-sdk-dynamic-tools.mjs index e21829c33d..f64930c327 100644 --- a/scripts/fixtures/python-sdk-dynamic-tools.mjs +++ b/scripts/fixtures/python-sdk-dynamic-tools.mjs @@ -1,27 +1,27 @@ -/** Add a native tool after a file read and unregister it after its first execution. */ +/** Activate a native tool after a file read and disable it after its first execution. */ export const name = 'python-sdk-dynamic-tools' export const inject = ['tools'] /** @param {import('@deepseek-ai/cordis').Context} ctx - Scenario-owned tool registry. */ export function apply(ctx) { - let removeTool + let active = false + ctx.tools.register({ + name: 'snapshot_ping', + description: 'Return pong once.', + parameters: { type: 'object', properties: {}, additionalProperties: false }, + output: { + schema: { type: 'string' }, + render(_args, value) { return [{ type: 'text', text: value }] }, + }, + async execute() { return 'pong' }, + }, { when: async ({ agent }) => active && agent.currentRoute().provider === 'deepseek-official' }) ctx.on('tools/post-execute', async (exec, result, next) => { const downstream = await next() if (result.isError) return downstream if (exec.name === 'read' && exec.arguments.file_path === 'dynamic-tool-task.txt') { - removeTool = ctx.effect(() => ctx.tools.register({ - name: 'snapshot_ping', - description: 'Return pong once.', - parameters: { type: 'object', properties: {}, additionalProperties: false }, - output: { - schema: { type: 'string' }, - render(_args, value) { return [{ type: 'text', text: value }] }, - }, - async execute() { return 'pong' }, - })) + active = true } else if (exec.name === 'snapshot_ping') { - removeTool?.() - removeTool = undefined + active = false } return downstream }) diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 2c012bde12..a08fbcb5a9 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -933,7 +933,12 @@ export const TYPE_LINK_EXEMPTIONS: Readonly> = { 'upgrade route registration contract is owned by packages/host/webserver/src/index.ts', JsonValue: 'JSON value union is owned by packages/core/session/src/json.ts', KnobState: 'projection unit state fields are owned by packages/interaction/permission-presets/README.md', + ToolActivationContext: 'conditional activation inputs are owned by packages/core/tools/README.md', + ToolRegistrationOptions: 'conditional registration options are owned by packages/core/tools/README.md', + ResolvedToolSet: 'request-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', SessionForkSource: 'service-local fork input is owned by packages/core/session/src/index.ts', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index e161b0452c..8df91243ce 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -1540,16 +1540,18 @@ 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->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`, + ` Driver->>Hooks: ${mermaidCode('agent/request')} waterfall`, + ' Driver->>LLM: prepareCall(config, signal), bind Agent.currentRoute()', + ` Driver->>Prompt: ${mermaidCode('system-prompt/prepare')} waterfall`, ` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`, ' 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->>Hooks: ${mermaidCode('agent/request')} waterfall`, - ' Driver->>LLM: prepareCall(config, signal)', - ' Note over Driver,LLM: cancellation during either async phase commits neither system nor users', + ' Driver->>Tools: resolve({ agent, signal })', + ` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} with resolved tool set`, + ' 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`, @@ -1563,7 +1565,7 @@ function renderLifecycle(): string { ` Driver-->>SDK: ${mermaidCode('agent/assistant-stream')} committed end`, ` Driver->>Hooks: ${mermaidCode('agent/request-error')} waterfall`, ' Hooks-->>Driver: return retry action or preserve the original error', - ' Note over Driver,LLM: retry in the open step: prepare and reconcile the same rendered assembly without repeating pre-step or users', + ' Note over Driver,LLM: retry resolves route and tools. Unchanged inputs reuse the prompt without repeating pre-step or users', ' else model request succeeded', ` Driver->>Session: ${mermaidCode('assistant/message')}`, ` Driver-->>SDK: ${mermaidCode('agent/assistant-stream')} committed end`, @@ -1571,7 +1573,7 @@ function renderLifecycle(): string { ' loop barriers and bounded rolling pool, reclassify before start', ' opt call starts', ` Driver->>Session: ${mermaidCode('tool/call')}`, - ' Driver->>Tools: ordered pre, concurrent execute', + ' Driver->>Tools: ordered pre, concurrent execute with request tool set', ' Tools-->>Session: tool-owned events when applicable', ' end', ' opt next model-order result ready', @@ -1579,13 +1581,16 @@ function renderLifecycle(): string { ` Driver->>Session: ${mermaidCode('tool/result')}`, ' end', ' end', + ' Driver->>Tools: dispose request tool set', ` Driver->>Session: ${mermaidCode('step/end')}`, + ' Driver->>Driver: clear bound route', ' opt natural stop and next-step inbox empty', ` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`, ' end', ' opt next-step input is pending', ' Driver-->>Driver: claim pending next-step input', ` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`, + ' Driver->>Driver: prepare route and runtime context for the next step', ` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`, ' Hooks-->>Driver: authoritative reject or enter(messages)', ' end', @@ -1597,7 +1602,7 @@ function renderLifecycle(): string { '', 'The `assistant/message` event records every successful provider call, including content-less and `max-tokens` finishes, and embeds the exact compact timed stream. Empty content stays out of derived history. A failed, retried, cancelled, or stream-error attempt that reaches settlement without a surface message records its stream as `assistant/attempt`. Live `agent/assistant-stream` chunk frames are transient; replay reads either durable settlement, and a hard process loss before settlement leaves no durable attempt stream.', '', - '`dsh-compaction-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery runs within the open step and retries only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative. Each retry prepares its call and reconciles the retained rendered assembly before request derivation, without repeating assembly, pre-step, or user admission.', + '`dsh-compaction-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery runs within the open step and retries only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative. Each retry prepares its call and resolves tool conditions before request derivation, without repeating pre-step or user admission. Prompt assembly repeats only when the bound route or tool schemas change.', '', 'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages and `startsRequestSeries` unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.', '', diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index d79a0a3f88..2a6235b233 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -1334,6 +1334,8 @@ def smoke_sdk_dynamic_tools(base_url: str, executable: Path, update_snapshots: b dsh_bin=str(executable), dsh_home=str(dsh_home), patches=(str(patch),), env={ "DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1", + # This scenario owns tool changes independently of bundled office assets. + "DSH_PRIMARY_RUNTIME": "", "HOME": str(user_home), "USERPROFILE": str(user_home), "DSH_AGENTS_HOME": str(user_home / ".agents"), }, diff --git a/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json b/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json index 7a8a75aa85..4c56b5a792 100644 --- a/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json +++ b/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json @@ -47,7 +47,6 @@ "tools": [ "create_goal", "edit", - "exit_plan_mode", "get_goal", "glob", "grep", @@ -56,7 +55,6 @@ "job_list", "job_output", "list_agents", - "load_workspace_dependencies", "read", "read_image", "send_message", @@ -87,7 +85,7 @@ }, { "type": "request/header", - "seq": 20, + "seq": 19, "time": 0, "data": { "header": { @@ -104,7 +102,6 @@ "tools": [ "create_goal", "edit", - "exit_plan_mode", "get_goal", "glob", "grep", @@ -113,7 +110,6 @@ "job_list", "job_output", "list_agents", - "load_workspace_dependencies", "read", "read_image", "send_message", @@ -135,7 +131,7 @@ }, { "type": "developer/message", - "seq": 21, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -153,13 +149,13 @@ "role": "developer", "id": "{{messageId}}" }, - "headerSeq": 20 + "headerSeq": 19 }, "surfaceOp": "append" }, { "type": "request/header", - "seq": 28, + "seq": 26, "time": 0, "data": { "header": { @@ -176,7 +172,6 @@ "tools": [ "create_goal", "edit", - "exit_plan_mode", "get_goal", "glob", "grep", @@ -185,7 +180,6 @@ "job_list", "job_output", "list_agents", - "load_workspace_dependencies", "read", "read_image", "send_message", @@ -206,7 +200,7 @@ }, { "type": "developer/message", - "seq": 29, + "seq": 27, "time": 0, "data": { "turn": 1, diff --git a/snapshots/sdk/bash-tool/tool-schemas.expected.json b/snapshots/sdk/bash-tool/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/sdk/bash-tool/tool-schemas.expected.json +++ b/snapshots/sdk/bash-tool/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl b/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl index bfec6f1e36..cca4e59344 100644 --- a/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl +++ b/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl @@ -6,7 +6,7 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} @@ -14,14 +14,14 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_task_read"},"toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false,"id":"{{sessionId}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":18,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":18,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":19,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":20,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":21,"time":0,"data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":22,"time":0,"data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":24,"time":0,"data":{"turn":1,"step":3}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":25,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":25,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":26,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{sessionId}}"}},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":28,"time":0,"data":{"turn":1,"step":3}}}} diff --git a/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl b/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl index aee79405da..03befefbea 100644 --- a/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl +++ b/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790049391235,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391235,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790049391235,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -17,14 +17,14 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_task_read"},"toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false,"id":"{{message:5}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:6}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1790049391255,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391255,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790049391255,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790049391255,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:8}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:9}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790049391270,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790049391270,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790049391270,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json index ed6f50a025..e21bdfc94e 100644 --- a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json index ce1cf8dfd5..f19dc369b8 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/text-turn/tool-schemas.expected.json b/snapshots/sdk/text-turn/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/sdk/text-turn/tool-schemas.expected.json +++ b/snapshots/sdk/text-turn/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/sdk/tool-error-details/notifications.expected.jsonl b/snapshots/sdk/tool-error-details/notifications.expected.jsonl index e6d6e7fde2..255a064363 100644 --- a/snapshots/sdk/tool-error-details/notifications.expected.jsonl +++ b/snapshots/sdk/tool-error-details/notifications.expected.jsonl @@ -6,7 +6,7 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Exercise a native denial and a caught PTC inner denial, then reply ERROR_DETAILS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Exercise a native denial and","messageSeqs":[8],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"denied-native","name":"bash","args":["{\"command\":\"echo AUTO_DENIED\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} @@ -16,7 +16,7 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"system/message","seq":18,"time":0,"data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":19,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["run_code"]},"reason":"change","startsSeries":true}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":20,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":19},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":20,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":19},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"denied-code","name":"run_code","args":["{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":22,"time":0,"data":{"turn":1,"step":2,"callId":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/ptc-dispatch-start","seq":23,"time":0,"data":{"rootCallId":"denied-code","parentCallId":"denied-code","subCallId":"denied-code:ptc:1","name":"bash","arguments":{"command":"echo AUTO_DENIED"}}}}} diff --git a/snapshots/sdk/tool-error-details/session.v4.jsonl b/snapshots/sdk/tool-error-details/session.v4.jsonl index 871095019b..e88e55d31a 100644 --- a/snapshots/sdk/tool-error-details/session.v4.jsonl +++ b/snapshots/sdk/tool-error-details/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Exercise a native denial and a caught PTC inner denial, then reply ERROR_DETAILS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Exercise a native denial and","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"stream":[{"type":"chunk","time":1791315723125,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315723125,"index":0,"dt":[],"id":"denied-native","name":"bash","args":["{\"command\":\"echo AUTO_DENIED\"}"]},{"type":"chunk","time":1791315723125,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}}},{"type":"chunk","time":1791315723125,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -19,7 +19,7 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["run_code"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} +{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1791315723151,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315723151,"index":0,"dt":[],"id":"denied-code","name":"run_code","args":["{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"]},{"type":"chunk","time":1791315723151,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}}},{"type":"chunk","time":1791315723151,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"denied-code","parentCallId":"denied-code","subCallId":"denied-code:ptc:1","name":"bash","arguments":{"command":"echo AUTO_DENIED"}}} diff --git a/snapshots/sdk/tool-error-details/system-prompt.expected.md b/snapshots/sdk/tool-error-details/system-prompt.expected.md index 9070405e5b..ad38551639 100644 --- a/snapshots/sdk/tool-error-details/system-prompt.expected.md +++ b/snapshots/sdk/tool-error-details/system-prompt.expected.md @@ -122,11 +122,6 @@ interface ToolArgsMap { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ justification?: string; } & Record; - /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ - exit_plan_mode: { - /** The complete plan, as markdown, starting with a # heading that names it. */ - plan: string; - } & Record; /** Read the current session goal, including the id and revision that update_goal requires. */ get_goal: Record; /** Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 100 paths in modification-time order; a larger result keeps the first paths and reports where the complete list was saved. */ @@ -359,9 +354,6 @@ interface ToolOutputMap { before: string; after: string; }; - exit_plan_mode: { - approved: true; - }; get_goal: { goal: null; } | { diff --git a/snapshots/sdk/tool-error-details/tool-schemas.expected.json b/snapshots/sdk/tool-error-details/tool-schemas.expected.json index e150d82593..4d0a928cb7 100644 --- a/snapshots/sdk/tool-error-details/tool-schemas.expected.json +++ b/snapshots/sdk/tool-error-details/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/advanced-toolchain/session.v4.jsonl b/snapshots/session/advanced-toolchain/session.v4.jsonl index c0f1a22b8d..add1f65e1f 100644 --- a/snapshots/session/advanced-toolchain/session.v4.jsonl +++ b/snapshots/session/advanced-toolchain/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: list runtime inspect providers; discover providers through run_code when available; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; list providers again; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-inspect-before","name":"cordis_inspect_list","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791315715021,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315715022,"index":0,"dt":[],"id":"advanced-inspect-before","name":"cordis_inspect_list","args":["{}"]},{"type":"chunk","time":1791315715022,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-inspect-before","name":"cordis_inspect_list","arguments":"{}"}}},{"type":"chunk","time":1791315715022,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791315715022,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -19,7 +19,7 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["run_code"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"cordis_inspect_list"},{"type":"tool-removal","toolName":"cordis_inspect_query"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} +{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"cordis_inspect_list"},{"type":"tool-removal","toolName":"cordis_inspect_query"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791315715055,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315715055,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"]},{"type":"chunk","time":1791315715055,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"}}},{"type":"chunk","time":1791315715055,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791315715055,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"cordis_inspect_list","arguments":{}}} @@ -28,8 +28,8 @@ {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"system/message","data":{"turn":1,"step":3,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:10}}"}},"sourceEventSeqs":[18],"surfaceOp":{"op":"replace","startSeq":18,"endSeq":18}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"cordis_inspect_list"},{"type":"tool-addition","toolName":"cordis_inspect_query"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"},{"type":"tool-removal","toolName":"run_code"}],"role":"developer","id":"{{message:11}}"},"headerSeq":29},"surfaceOp":"append"} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} +{"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"cordis_inspect_list"},{"type":"tool-addition","toolName":"cordis_inspect_query"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"},{"type":"tool-removal","toolName":"run_code"}],"role":"developer","id":"{{message:11}}"},"headerSeq":29},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791315715200,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315715200,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1791315715200,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1791315715200,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791315715200,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} {"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1791315715221,"mode":"continuable","label":"Check direct child"}} diff --git a/snapshots/session/advanced-toolchain/system-prompt.expected.md b/snapshots/session/advanced-toolchain/system-prompt.expected.md index cf4dcd9c3f..1f8ae75e47 100644 --- a/snapshots/session/advanced-toolchain/system-prompt.expected.md +++ b/snapshots/session/advanced-toolchain/system-prompt.expected.md @@ -135,11 +135,6 @@ interface ToolArgsMap { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ justification?: string; } & Record; - /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ - exit_plan_mode: { - /** The complete plan, as markdown, starting with a # heading that names it. */ - plan: string; - } & Record; /** Read the current session goal, including the id and revision that update_goal requires. */ get_goal: Record; /** Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 100 paths in modification-time order; a larger result keeps the first paths and reports where the complete list was saved. */ @@ -374,9 +369,6 @@ interface ToolOutputMap { before: string; after: string; }; - exit_plan_mode: { - approved: true; - }; get_goal: { goal: null; } | { diff --git a/snapshots/session/advanced-toolchain/tool-schemas.expected.json b/snapshots/session/advanced-toolchain/tool-schemas.expected.json index cb349d3ca4..c1869ed807 100644 --- a/snapshots/session/advanced-toolchain/tool-schemas.expected.json +++ b/snapshots/session/advanced-toolchain/tool-schemas.expected.json @@ -148,22 +148,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", @@ -861,22 +845,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/agent-instructions/tool-schemas.expected.json b/snapshots/session/agent-instructions/tool-schemas.expected.json index 00e71002a7..f6aebf6daf 100644 --- a/snapshots/session/agent-instructions/tool-schemas.expected.json +++ b/snapshots/session/agent-instructions/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", @@ -740,22 +724,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/background-confinement-failure/tool-schemas.expected.json b/snapshots/session/background-confinement-failure/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/session/background-confinement-failure/tool-schemas.expected.json +++ b/snapshots/session/background-confinement-failure/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/both-mode-turn/system-prompt.expected.md b/snapshots/session/both-mode-turn/system-prompt.expected.md index 329e451ce8..e0bdbd4e54 100644 --- a/snapshots/session/both-mode-turn/system-prompt.expected.md +++ b/snapshots/session/both-mode-turn/system-prompt.expected.md @@ -89,11 +89,6 @@ interface ToolArgsMap { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ justification?: string; } & Record; - /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ - exit_plan_mode: { - /** The complete plan, as markdown, starting with a # heading that names it. */ - plan: string; - } & Record; /** Read the current session goal, including the id and revision that update_goal requires. */ get_goal: Record; /** Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 100 paths in modification-time order; a larger result keeps the first paths and reports where the complete list was saved. */ @@ -326,9 +321,6 @@ interface ToolOutputMap { before: string; after: string; }; - exit_plan_mode: { - approved: true; - }; get_goal: { goal: null; } | { diff --git a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json index cd473bea70..cc51623a5b 100644 --- a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json index 2d24d7c17d..43d2d085bd 100644 --- a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json index e1acbc8206..b4bd87c655 100644 --- a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json +++ b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/claude-code-mods/tool-schemas.expected.json b/snapshots/session/claude-code-mods/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/session/claude-code-mods/tool-schemas.expected.json +++ b/snapshots/session/claude-code-mods/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/compaction-recovery/tool-schemas.expected.json b/snapshots/session/compaction-recovery/tool-schemas.expected.json index 00e71002a7..f6aebf6daf 100644 --- a/snapshots/session/compaction-recovery/tool-schemas.expected.json +++ b/snapshots/session/compaction-recovery/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", @@ -740,22 +724,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json index 4b839adb31..e1d4a256e3 100644 --- a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json index 802ed2fe0d..078ba05d26 100644 --- a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json @@ -153,22 +153,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/cordis-inspect-jsdoc/session.v4.jsonl b/snapshots/session/cordis-inspect-jsdoc/session.v4.jsonl index a165bcdee1..ac1bcf8418 100644 --- a/snapshots/session/cordis-inspect-jsdoc/session.v4.jsonl +++ b/snapshots/session/cordis-inspect-jsdoc/session.v4.jsonl @@ -14,7 +14,7 @@ {"type":"session/title","data":{"title":"Inspect the exact tools service","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-tools-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778145981,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778145981,"index":0,"dt":[],"id":"inspect-tools-api","name":"cordis_inspect_query","args":["{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"]},{"type":"chunk","time":1788778145981,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"inspect-tools-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"}}},{"type":"chunk","time":1788778145981,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778145981,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"inspect-tools-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"inspect-tools-api"},"toolCallId":"inspect-tools-api","content":[{"type":"text","text":"{\n \"platform\": \"host\",\n \"provider\": \"Service\",\n \"method\": \"listService\",\n \"data\": {\n \"mode\": \"service\",\n \"service\": {\n \"key\": \"tools\",\n \"description\": \"Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch.\",\n \"access\": {\n \"optional\": {\n \"expression\": \"ctx.get(\\\"tools\\\")\",\n \"requiresUndefinedCheck\": true\n },\n \"hardDependency\": {\n \"inject\": [\n \"tools\"\n ],\n \"expression\": \"ctx.tools\"\n }\n },\n \"methods\": [\n {\n \"signature\": \"presentAs(mode: ToolPresentationMode): () => void\",\n \"description\": \"Present the calling scope's tools in `mode` instead of the deployment default. Nearest scope on the chain wins, so a preset's standing declaration covers every agent joined under it.\\n\\nScoped only, and one declaration per scope: this is how an agent preset composes PTC mode agents beside native ones in the same process, and a process-global override would be the `mode` config field instead.\",\n \"parameters\": [\n {\n \"name\": \"mode\",\n \"description\": \"the presentation the covered agents' models see.\"\n }\n ],\n \"returns\": \"the exact disposer that restores the deployment default.\"\n },\n {\n \"signature\": \"register(definition: ToolDefinition): () => void\",\n \"description\": \"Register globally or in the calling agent scope. Scoped tools shadow globals; duplicates within one layer and the reserved `run_code` name fail.\",\n \"parameters\": [\n {\n \"name\": \"definition\",\n \"description\": \"tool schema, execution, and optional finalization/presentation callbacks.\"\n }\n ],\n \"returns\": \"the exact disposer that unregisters the tool.\"\n },\n {\n \"signature\": \"restrict(filter: ToolRestriction): () => void\",\n \"description\": \"Restrict global tools for the calling agent scope. Empty filters, unknown names, scope-local names, and reserved transport names fail. Restrictions intersect; scoped registrations remain visible.\",\n \"parameters\": [\n {\n \"name\": \"filter\",\n \"description\": \"global-tool mask: `allow` (keep only) and/or `deny` (remove).\"\n }\n ],\n \"returns\": \"the exact disposer that lifts this restriction.\"\n },\n {\n \"signature\": \"guard(guard: ToolGuard): () => void\",\n \"description\": \"Register a monotonic guard after the extensible `tools/pre-execute` waterfall. A plain-context guard applies globally; one registered through `agent.ctx` applies only to that agent. Any matching guard may deny by returning a reason, while no guard can force-allow a call another guard denied. The exact effect disposer is returned for ordered ownership and HMR cleanup.\",\n \"parameters\": [\n {\n \"name\": \"guard\",\n \"description\": \"synchronous check; a returned string denies the execution.\"\n }\n ],\n \"returns\": \"the exact disposer that unregisters the guard.\"\n },\n {\n \"signature\": \"get(name: string, scope?: ScopeKey): ToolDefinition | undefined\",\n \"description\": \"Look up a tool as one scope sees it (scoped shadows global; a restricted-away global reads as absent). Presenters pass the calling agent so the rendered card matches the definition that actually executed.\",\n \"parameters\": [\n {\n \"name\": \"name\",\n \"description\": \"the tool name as registered.\"\n },\n {\n \"name\": \"scope\",\n \"description\": \"the viewing scope (the agent); omitted = the global view.\"\n }\n ],\n \"returns\": \"the definition the scope resolves, or undefined when none is visible.\"\n },\n {\n \"signature\": \"schemas(scope?: ScopeKey): ToolSchema[]\",\n \"description\": \"Project visible definitions onto the allowlisted model-facing schema fields, excluding execution and presentation callbacks.\",\n \"parameters\": [\n {\n \"name\": \"scope\",\n \"description\": \"the viewing scope (the agent); omitted = the global view.\"\n }\n ],\n \"returns\": \"one deep-cloned schema per visible tool.\"\n },\n {\n \"signature\": \"executionMode(exec: ToolExecutionInput): ToolExecutionMode\",\n \"description\": \"Classify a pending call through the caller's visible tool definition. Only an exact `true` is parallel; unknown, hidden, undeclared, invalid, or throwing classifiers are exclusive.\",\n \"parameters\": [\n {\n \"name\": \"exec\",\n \"description\": \"call name, parsed arguments, and optional agent scope.\"\n }\n ],\n \"returns\": \"the fail-closed scheduling mode.\"\n },\n {\n \"signature\": \"async execute(exec: ToolExecutionInput): Promise\",\n \"description\": \"Execute through pre-policy, guards, around-dispatch, post-policy, definition-owned content finalization, and final notification. Tool and listener failures resolve as materialized error results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen snapshot final observers receive. Cancellation arriving after entry and before final result materialization skips a not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a successful started outcome with `ABORTED`; already-started work is still drained and may retain a tool-owned structured error.\",\n \"parameters\": [\n {\n \"name\": \"exec\",\n \"description\": \"the typed same-process call input. The registry assigns its correlation token before policy begins.\"\n }\n ],\n \"returns\": \"the materialized final result.\"\n }\n ]\n },\n \"referencedTypes\": [\n {\n \"name\": \"Agent\",\n \"declaration\": \"export interface Agent {\\n readonly id: SessionId;\\n}\"\n },\n {\n \"name\": \"Branded\",\n \"declaration\": \"export type Branded = string & {\\n readonly [BRAND]: B;\\n};\"\n },\n {\n \"name\": \"DiffCallView\",\n \"declaration\": \"export interface DiffCallView {\\n card: 'diff';\\n title: string;\\n diffs: FileDiff[];\\n locations?: FileLocation[];\\n}\"\n },\n {\n \"name\": \"DiffResultView\",\n \"declaration\": \"export interface DiffResultView {\\n card: 'diff';\\n title?: string;\\n diffs: FileDiff[];\\n}\"\n },\n {\n \"name\": \"FileDiff\",\n \"declaration\": \"export interface FileDiff {\\n path: string;\\n oldText: string | null;\\n newText: string;\\n}\"\n },\n {\n \"name\": \"FileLocation\",\n \"declaration\": \"export interface FileLocation {\\n path: string;\\n line?: number;\\n}\"\n },\n {\n \"name\": \"GenericCallView\",\n \"declaration\": \"export interface GenericCallView {\\n card: 'generic';\\n title: string;\\n kind?: ToolCallKind;\\n rawInput?: unknown;\\n content?: ContentBlock[];\\n locations?: FileLocation[];\\n}\"\n },\n {\n \"name\": \"GenericResultView\",\n \"declaration\": \"export interface GenericResultView {\\n card: 'generic';\\n title?: string;\\n content?: ContentBlock[];\\n}\"\n },\n {\n \"name\": \"JsonSchemaNode\",\n \"declaration\": \"export interface JsonSchemaNode {\\n type?: JsonSchemaType;\\n oneOf?: JsonSchemaNode[];\\n properties?: Record;\\n required?: string[];\\n additionalProperties?: boolean;\\n items?: JsonSchemaNode;\\n enum?: JsonSchemaScalar[];\\n const?: JsonSchemaScalar;\\n description?: string;\\n title?: string;\\n default?: JsonValue;\\n examples?: JsonValue;\\n}\"\n },\n {\n \"name\": \"JsonSchemaScalar\",\n \"declaration\": \"export type JsonSchemaScalar = string | number | boolean | null;\"\n },\n {\n \"name\": \"JsonSchemaType\",\n \"declaration\": \"export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\"\n },\n {\n \"name\": \"JsonValue\",\n \"declaration\": \"export type JsonValue = null | boolean | number | string | JsonValue[] | {\\n [key: string]: JsonValue;\\n};\"\n },\n {\n \"name\": \"ReadFileLine\",\n \"declaration\": \"export interface ReadFileLine {\\n number: number;\\n text: string;\\n}\"\n },\n {\n \"name\": \"ReadResultView\",\n \"declaration\": \"export interface ReadResultView {\\n card: 'read';\\n title?: string;\\n path: string;\\n offset: number;\\n lines: ReadFileLine[];\\n totalLines: number;\\n lang?: string;\\n content?: ContentBlock[];\\n}\"\n },\n {\n \"name\": \"ScopeKey\",\n \"declaration\": \"export type ScopeKey = object;\"\n },\n {\n \"name\": \"SearchFileMatches\",\n \"declaration\": \"export interface SearchFileMatches {\\n path: string;\\n matches: SearchLineMatch[];\\n}\"\n },\n {\n \"name\": \"SearchLineMatch\",\n \"declaration\": \"export interface SearchLineMatch {\\n lineNumber: number;\\n line: string;\\n}\"\n },\n {\n \"name\": \"SearchMatchesResultView\",\n \"declaration\": \"export interface SearchMatchesResultView {\\n card: 'search';\\n shape: 'matches';\\n title?: string;\\n files: SearchFileMatches[];\\n truncated: boolean;\\n total: number;\\n}\"\n },\n {\n \"name\": \"SearchPathsResultView\",\n \"declaration\": \"export interface SearchPathsResultView {\\n card: 'search';\\n shape: 'paths';\\n title?: string;\\n paths: string[];\\n truncated: boolean;\\n total: number;\\n}\"\n },\n {\n \"name\": \"SearchResultView\",\n \"declaration\": \"export type SearchResultView = SearchMatchesResultView | SearchPathsResultView;\"\n },\n {\n \"name\": \"SessionId\",\n \"declaration\": \"export type SessionId = Branded<'SessionId'>;\"\n },\n {\n \"name\": \"TerminalCallView\",\n \"declaration\": \"export interface TerminalCallView {\\n card: 'terminal';\\n title: string;\\n description?: string;\\n cwd?: string;\\n}\"\n },\n {\n \"name\": \"TerminalResultView\",\n \"declaration\": \"export interface TerminalResultView {\\n card: 'terminal';\\n title?: string;\\n output?: string;\\n exitCode?: number;\\n signal?: string;\\n}\"\n },\n {\n \"name\": \"ToolCallId\",\n \"declaration\": \"export type ToolCallId = Branded<'ToolCallId'>;\"\n },\n {\n \"name\": \"ToolCallKind\",\n \"declaration\": \"export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\"\n },\n {\n \"name\": \"ToolCallView\",\n \"declaration\": \"export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\"\n },\n {\n \"name\": \"ToolDefinition\",\n \"declaration\": \"export interface ToolDefinition extends ToolSchema {\\n readonly output: ToolOutputDefinition;\\n execute(args: unknown, exec: ToolRunContext): Promise;\\n projectContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\\n timeoutMs?: number;\\n isConcurrencySafe?(args: unknown): boolean;\\n presentCall?(args: unknown): ToolCallView | undefined;\\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\\n}\"\n },\n {\n \"name\": \"ToolErrorInfo\",\n \"declaration\": \"export interface ToolErrorInfo {\\n name: string;\\n code: string;\\n reason?: string;\\n}\"\n },\n {\n \"name\": \"ToolExecution\",\n \"declaration\": \"export interface ToolExecution extends ToolExecutionInput {\\n readonly rootCallId: ToolCallId;\\n readonly token: ToolExecutionToken;\\n}\"\n },\n {\n \"name\": \"ToolExecutionFailure\",\n \"declaration\": \"export interface ToolExecutionFailure {\\n readonly isError: true;\\n readonly error: ToolFailure;\\n readonly value?: never;\\n readonly content: ContentBlock[];\\n readonly meta?: JsonValue;\\n readonly additionalContexts?: UserMessage[];\\n readonly concludesTurn?: never;\\n}\"\n },\n {\n \"name\": \"ToolExecutionInput\",\n \"declaration\": \"export interface ToolExecutionInput {\\n readonly callId: ToolCallId;\\n readonly rootCallId?: ToolCallId;\\n readonly name: string;\\n readonly schema?: ToolSchema;\\n readonly arguments: unknown;\\n readonly agent?: Agent;\\n readonly parent?: ToolExecutionToken;\\n readonly signal: AbortSignal;\\n}\"\n },\n {\n \"name\": \"ToolExecutionMode\",\n \"declaration\": \"export type ToolExecutionMode = {\\n kind: 'parallel';\\n} | {\\n kind: 'exclusive';\\n};\"\n },\n {\n \"name\": \"ToolExecutionResult\",\n \"declaration\": \"export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\"\n },\n {\n \"name\": \"ToolExecutionSuccess\",\n \"declaration\": \"export interface ToolExecutionSuccess {\\n readonly isError: false;\\n readonly value: JsonValue;\\n readonly content: ContentBlock[];\\n readonly error?: never;\\n readonly meta?: JsonValue;\\n readonly additionalContexts?: UserMessage[];\\n readonly concludesTurn?: true;\\n}\"\n },\n {\n \"name\": \"ToolExecutionToken\",\n \"declaration\": \"export type ToolExecutionToken = symbol & {\\n readonly [toolExecutionTokenBrand]: true;\\n};\"\n },\n {\n \"name\": \"ToolFailure\",\n \"declaration\": \"export interface ToolFailure {\\n message: string;\\n info?: ToolErrorInfo;\\n}\"\n },\n {\n \"name\": \"ToolGuard\",\n \"declaration\": \"export type ToolGuard = (execution: Readonly) => string | undefined;\"\n },\n {\n \"name\": \"ToolOutputDefinition\",\n \"declaration\": \"export interface ToolOutputDefinition {\\n readonly schema: JsonSchemaNode;\\n render(args: unknown, value: JsonValue): ContentBlock[];\\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\\n}\"\n },\n {\n \"name\": \"ToolPresentationMode\",\n \"declaration\": \"export type ToolPresentationMode = 'native' | 'ptc';\"\n },\n {\n \"name\": \"ToolRestriction\",\n \"declaration\": \"export interface ToolRestriction {\\n readonly allow?: readonly string[];\\n readonly deny?: readonly string[];\\n}\"\n },\n {\n \"name\": \"ToolResult\",\n \"declaration\": \"export interface ToolResult {\\n content: ContentBlock[];\\n isError: boolean;\\n meta?: JsonValue;\\n}\"\n },\n {\n \"name\": \"ToolResultView\",\n \"declaration\": \"export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | SearchResultView | ReadResultView | WebResultView;\"\n },\n {\n \"name\": \"ToolRunContext\",\n \"declaration\": \"export interface ToolRunContext extends ToolExecution {\\n deferContext(context: UserMessage): void;\\n concludeTurn(): void;\\n}\"\n },\n {\n \"name\": \"ToolSchema\",\n \"declaration\": \"export interface ToolSchema {\\n deferLoading?: true;\\n name: string;\\n description: string;\\n parameters: Record;\\n}\"\n },\n {\n \"name\": \"UserMessage\",\n \"declaration\": \"export interface UserMessage extends MessageBase {\\n readonly role: 'user';\\n}\"\n },\n {\n \"name\": \"WebFetchResultView\",\n \"declaration\": \"export interface WebFetchResultView {\\n card: 'web';\\n kind: 'fetch';\\n title?: string;\\n url: string;\\n statusCode: number;\\n truncated: boolean;\\n}\"\n },\n {\n \"name\": \"WebResultView\",\n \"declaration\": \"export type WebResultView = WebSearchResultView | WebFetchResultView;\"\n },\n {\n \"name\": \"WebSearchResultView\",\n \"declaration\": \"export interface WebSearchResultView {\\n card: 'web';\\n kind: 'search';\\n title?: string;\\n sources: WebSource[];\\n answer?: string;\\n truncated: boolean;\\n}\"\n },\n {\n \"name\": \"WebSource\",\n \"declaration\": \"export interface WebSource {\\n url: string;\\n title?: string;\\n snippet?: string;\\n publishedAt?: string;\\n}\"\n }\n ]\n }\n}"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"inspect-tools-api"},"toolCallId":"inspect-tools-api","content":[{"type":"text","text":"{\n \"platform\": \"host\",\n \"provider\": \"Service\",\n \"method\": \"listService\",\n \"data\": {\n \"mode\": \"service\",\n \"service\": {\n \"key\": \"tools\",\n \"description\": \"Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch.\",\n \"access\": {\n \"optional\": {\n \"expression\": \"ctx.get(\\\"tools\\\")\",\n \"requiresUndefinedCheck\": true\n },\n \"hardDependency\": {\n \"inject\": [\n \"tools\"\n ],\n \"expression\": \"ctx.tools\"\n }\n },\n \"methods\": [\n {\n \"signature\": \"presentAs(mode: ToolPresentationMode): () => void\",\n \"description\": \"Present the calling scope's tools in `mode` instead of the deployment default. Nearest scope on the chain wins, so a preset's standing declaration covers every agent joined under it.\\n\\nScoped only, and one declaration per scope: this is how an agent preset composes PTC mode agents beside native ones in the same process, and a process-global override would be the `mode` config field instead.\",\n \"parameters\": [\n {\n \"name\": \"mode\",\n \"description\": \"the presentation the covered agents' models see.\"\n }\n ],\n \"returns\": \"the exact disposer that restores the deployment default.\"\n },\n {\n \"signature\": \"register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void\",\n \"description\": \"Register globally or in the calling agent scope. Scoped tools shadow globals; duplicates within one layer and the reserved `run_code` name fail.\",\n \"parameters\": [\n {\n \"name\": \"definition\",\n \"description\": \"tool schema, execution, and optional finalization/presentation callbacks.\"\n },\n {\n \"name\": \"options\",\n \"description\": \"optional request-time activation condition.\"\n }\n ],\n \"returns\": \"the exact disposer that unregisters the tool.\"\n },\n {\n \"signature\": \"restrict(filter: ToolRestriction): () => void\",\n \"description\": \"Restrict global tools for the calling agent scope. Empty filters, unknown names, scope-local names, and reserved transport names fail. Restrictions intersect; scoped registrations remain visible.\",\n \"parameters\": [\n {\n \"name\": \"filter\",\n \"description\": \"global-tool mask: `allow` (keep only) and/or `deny` (remove).\"\n }\n ],\n \"returns\": \"the exact disposer that lifts this restriction.\"\n },\n {\n \"signature\": \"guard(guard: ToolGuard): () => void\",\n \"description\": \"Register a monotonic guard after the extensible `tools/pre-execute` waterfall. A plain-context guard applies globally; one registered through `agent.ctx` applies only to that agent. Any matching guard may deny by returning a reason, while no guard can force-allow a call another guard denied. The exact effect disposer is returned for ordered ownership and HMR cleanup.\",\n \"parameters\": [\n {\n \"name\": \"guard\",\n \"description\": \"synchronous check; a returned string denies the execution.\"\n }\n ],\n \"returns\": \"the exact disposer that unregisters the guard.\"\n },\n {\n \"signature\": \"get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined\",\n \"description\": \"Look up a tool as one scope sees it (scoped shadows global; a restricted-away global reads as absent). Presenters pass the calling agent so the rendered card matches the definition that actually executed.\",\n \"parameters\": [\n {\n \"name\": \"name\",\n \"description\": \"the tool name as registered.\"\n },\n {\n \"name\": \"scope\",\n \"description\": \"the viewing scope (the agent); omitted = the global view.\"\n },\n {\n \"name\": \"toolSet\",\n \"description\": \"request admission; omit only to inspect registered definitions.\"\n }\n ],\n \"returns\": \"the definition the scope resolves, or undefined when none is visible.\"\n },\n {\n \"signature\": \"async resolve(context: ToolActivationContext): Promise\",\n \"description\": \"Resolve conditions once for this request without changing shared registrations. Cancellation or a rejected condition publishes no partial selection. Removed registrations cannot be restored by a late condition result.\",\n \"parameters\": [\n {\n \"name\": \"context\",\n \"description\": \"agent with a bound route and request cancellation.\"\n }\n ],\n \"returns\": \"the complete selection; the caller disposes it after its tool batch settles.\"\n },\n {\n \"signature\": \"schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[]\",\n \"description\": \"Project visible definitions onto the allowlisted model-facing schema fields, excluding execution and presentation callbacks.\",\n \"parameters\": [\n {\n \"name\": \"scope\",\n \"description\": \"the viewing scope (the agent); omitted = the global view.\"\n },\n {\n \"name\": \"toolSet\",\n \"description\": \"request admission, or omitted for registered-definition inspection.\"\n }\n ],\n \"returns\": \"one deep-cloned schema per visible tool.\"\n },\n {\n \"signature\": \"executionMode(exec: ToolExecutionInput): ToolExecutionMode\",\n \"description\": \"Classify a pending call through the caller's visible tool definition. Only an exact `true` is parallel; unknown, hidden, undeclared, invalid, or throwing classifiers are exclusive.\",\n \"parameters\": [\n {\n \"name\": \"exec\",\n \"description\": \"call name, parsed arguments, and optional agent scope.\"\n }\n ],\n \"returns\": \"the fail-closed scheduling mode.\"\n },\n {\n \"signature\": \"async execute(exec: ToolExecutionInput): Promise\",\n \"description\": \"Execute through pre-policy, guards, around-dispatch, post-policy, definition-owned content finalization, and final notification. Tool and listener failures resolve as materialized error results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen snapshot final observers receive. Cancellation arriving after entry and before final result materialization skips a not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a successful started outcome with `ABORTED`; already-started work is still drained and may retain a tool-owned structured error.\",\n \"parameters\": [\n {\n \"name\": \"exec\",\n \"description\": \"the typed same-process call input. The registry assigns its correlation token before policy begins.\"\n }\n ],\n \"returns\": \"the materialized final result.\"\n }\n ]\n },\n \"referencedTypes\": [\n {\n \"name\": \"Agent\",\n \"declaration\": \"export interface Agent {\\n readonly id: SessionId;\\n}\"\n },\n {\n \"name\": \"Branded\",\n \"declaration\": \"export type Branded = string & {\\n readonly [BRAND]: B;\\n};\"\n },\n {\n \"name\": \"DiffCallView\",\n \"declaration\": \"export interface DiffCallView {\\n card: 'diff';\\n title: string;\\n diffs: FileDiff[];\\n locations?: FileLocation[];\\n}\"\n },\n {\n \"name\": \"DiffResultView\",\n \"declaration\": \"export interface DiffResultView {\\n card: 'diff';\\n title?: string;\\n diffs: FileDiff[];\\n}\"\n },\n {\n \"name\": \"FileDiff\",\n \"declaration\": \"export interface FileDiff {\\n path: string;\\n oldText: string | null;\\n newText: string;\\n}\"\n },\n {\n \"name\": \"FileLocation\",\n \"declaration\": \"export interface FileLocation {\\n path: string;\\n line?: number;\\n}\"\n },\n {\n \"name\": \"GenericCallView\",\n \"declaration\": \"export interface GenericCallView {\\n card: 'generic';\\n title: string;\\n kind?: ToolCallKind;\\n rawInput?: unknown;\\n content?: ContentBlock[];\\n locations?: FileLocation[];\\n}\"\n },\n {\n \"name\": \"GenericResultView\",\n \"declaration\": \"export interface GenericResultView {\\n card: 'generic';\\n title?: string;\\n content?: ContentBlock[];\\n}\"\n },\n {\n \"name\": \"JsonSchemaNode\",\n \"declaration\": \"export interface JsonSchemaNode {\\n type?: JsonSchemaType;\\n oneOf?: JsonSchemaNode[];\\n properties?: Record;\\n required?: string[];\\n additionalProperties?: boolean;\\n items?: JsonSchemaNode;\\n enum?: JsonSchemaScalar[];\\n const?: JsonSchemaScalar;\\n description?: string;\\n title?: string;\\n default?: JsonValue;\\n examples?: JsonValue;\\n}\"\n },\n {\n \"name\": \"JsonSchemaScalar\",\n \"declaration\": \"export type JsonSchemaScalar = string | number | boolean | null;\"\n },\n {\n \"name\": \"JsonSchemaType\",\n \"declaration\": \"export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\"\n },\n {\n \"name\": \"JsonValue\",\n \"declaration\": \"export type JsonValue = null | boolean | number | string | JsonValue[] | {\\n [key: string]: JsonValue;\\n};\"\n },\n {\n \"name\": \"ReadFileLine\",\n \"declaration\": \"export interface ReadFileLine {\\n number: number;\\n text: string;\\n}\"\n },\n {\n \"name\": \"ReadResultView\",\n \"declaration\": \"export interface ReadResultView {\\n card: 'read';\\n title?: string;\\n path: string;\\n offset: number;\\n lines: ReadFileLine[];\\n totalLines: number;\\n lang?: string;\\n content?: ContentBlock[];\\n}\"\n },\n {\n \"name\": \"ResolvedToolSet\",\n \"declaration\": \"export interface ResolvedToolSet {\\n dispose(): void;\\n view(scope: ScopeKey | undefined): ReadonlyMap;\\n}\"\n },\n {\n \"name\": \"ScopeKey\",\n \"declaration\": \"export type ScopeKey = object;\"\n },\n {\n \"name\": \"SearchFileMatches\",\n \"declaration\": \"export interface SearchFileMatches {\\n path: string;\\n matches: SearchLineMatch[];\\n}\"\n },\n {\n \"name\": \"SearchLineMatch\",\n \"declaration\": \"export interface SearchLineMatch {\\n lineNumber: number;\\n line: string;\\n}\"\n },\n {\n \"name\": \"SearchMatchesResultView\",\n \"declaration\": \"export interface SearchMatchesResultView {\\n card: 'search';\\n shape: 'matches';\\n title?: string;\\n files: SearchFileMatches[];\\n truncated: boolean;\\n total: number;\\n}\"\n },\n {\n \"name\": \"SearchPathsResultView\",\n \"declaration\": \"export interface SearchPathsResultView {\\n card: 'search';\\n shape: 'paths';\\n title?: string;\\n paths: string[];\\n truncated: boolean;\\n total: number;\\n}\"\n },\n {\n \"name\": \"SearchResultView\",\n \"declaration\": \"export type SearchResultView = SearchMatchesResultView | SearchPathsResultView;\"\n },\n {\n \"name\": \"SessionId\",\n \"declaration\": \"export type SessionId = Branded<'SessionId'>;\"\n },\n {\n \"name\": \"TerminalCallView\",\n \"declaration\": \"export interface TerminalCallView {\\n card: 'terminal';\\n title: string;\\n description?: string;\\n cwd?: string;\\n}\"\n },\n {\n \"name\": \"TerminalResultView\",\n \"declaration\": \"export interface TerminalResultView {\\n card: 'terminal';\\n title?: string;\\n output?: string;\\n exitCode?: number;\\n signal?: string;\\n}\"\n },\n {\n \"name\": \"ToolActivationContext\",\n \"declaration\": \"export interface ToolActivationContext {\\n readonly agent: Agent;\\n readonly signal: AbortSignal;\\n}\"\n },\n {\n \"name\": \"ToolCallId\",\n \"declaration\": \"export type ToolCallId = Branded<'ToolCallId'>;\"\n },\n {\n \"name\": \"ToolCallKind\",\n \"declaration\": \"export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\"\n },\n {\n \"name\": \"ToolCallView\",\n \"declaration\": \"export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\"\n },\n {\n \"name\": \"ToolDefinition\",\n \"declaration\": \"export interface ToolDefinition extends ToolSchema {\\n readonly output: ToolOutputDefinition;\\n execute(args: unknown, exec: ToolRunContext): Promise;\\n projectContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\\n timeoutMs?: number;\\n isConcurrencySafe?(args: unknown): boolean;\\n presentCall?(args: unknown): ToolCallView | undefined;\\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\\n}\"\n },\n {\n \"name\": \"ToolErrorInfo\",\n \"declaration\": \"export interface ToolErrorInfo {\\n name: string;\\n code: string;\\n reason?: string;\\n}\"\n },\n {\n \"name\": \"ToolExecution\",\n \"declaration\": \"export interface ToolExecution extends ToolExecutionInput {\\n readonly rootCallId: ToolCallId;\\n readonly token: ToolExecutionToken;\\n}\"\n },\n {\n \"name\": \"ToolExecutionFailure\",\n \"declaration\": \"export interface ToolExecutionFailure {\\n readonly isError: true;\\n readonly error: ToolFailure;\\n readonly value?: never;\\n readonly content: ContentBlock[];\\n readonly meta?: JsonValue;\\n readonly additionalContexts?: UserMessage[];\\n readonly concludesTurn?: never;\\n}\"\n },\n {\n \"name\": \"ToolExecutionInput\",\n \"declaration\": \"export interface ToolExecutionInput {\\n readonly callId: ToolCallId;\\n readonly rootCallId?: ToolCallId;\\n readonly name: string;\\n readonly schema?: ToolSchema;\\n readonly arguments: unknown;\\n readonly agent?: Agent;\\n readonly toolSet?: ResolvedToolSet;\\n readonly parent?: ToolExecutionToken;\\n readonly signal: AbortSignal;\\n}\"\n },\n {\n \"name\": \"ToolExecutionMode\",\n \"declaration\": \"export type ToolExecutionMode = {\\n kind: 'parallel';\\n} | {\\n kind: 'exclusive';\\n};\"\n },\n {\n \"name\": \"ToolExecutionResult\",\n \"declaration\": \"export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\"\n },\n {\n \"name\": \"ToolExecutionSuccess\",\n \"declaration\": \"export interface ToolExecutionSuccess {\\n readonly isError: false;\\n readonly value: JsonValue;\\n readonly content: ContentBlock[];\\n readonly error?: never;\\n readonly meta?: JsonValue;\\n readonly additionalContexts?: UserMessage[];\\n readonly concludesTurn?: true;\\n}\"\n },\n {\n \"name\": \"ToolExecutionToken\",\n \"declaration\": \"export type ToolExecutionToken = symbol & {\\n readonly [toolExecutionTokenBrand]: true;\\n};\"\n },\n {\n \"name\": \"ToolFailure\",\n \"declaration\": \"export interface ToolFailure {\\n message: string;\\n info?: ToolErrorInfo;\\n}\"\n },\n {\n \"name\": \"ToolGuard\",\n \"declaration\": \"export type ToolGuard = (execution: Readonly) => string | undefined;\"\n },\n {\n \"name\": \"ToolOutputDefinition\",\n \"declaration\": \"export interface ToolOutputDefinition {\\n readonly schema: JsonSchemaNode;\\n render(args: unknown, value: JsonValue): ContentBlock[];\\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\\n}\"\n },\n {\n \"name\": \"ToolPresentationMode\",\n \"declaration\": \"export type ToolPresentationMode = 'native' | 'ptc';\"\n },\n {\n \"name\": \"ToolRegistrationOptions\",\n \"declaration\": \"export interface ToolRegistrationOptions {\\n readonly when?: (context: ToolActivationContext) => boolean | Promise;\\n}\"\n },\n {\n \"name\": \"ToolRestriction\",\n \"declaration\": \"export interface ToolRestriction {\\n readonly allow?: readonly string[];\\n readonly deny?: readonly string[];\\n}\"\n },\n {\n \"name\": \"ToolResult\",\n \"declaration\": \"export interface ToolResult {\\n content: ContentBlock[];\\n isError: boolean;\\n meta?: JsonValue;\\n}\"\n },\n {\n \"name\": \"ToolResultView\",\n \"declaration\": \"export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView | SearchResultView | ReadResultView | WebResultView;\"\n },\n {\n \"name\": \"ToolRunContext\",\n \"declaration\": \"export interface ToolRunContext extends ToolExecution {\\n deferContext(context: UserMessage): void;\\n concludeTurn(): void;\\n}\"\n },\n {\n \"name\": \"ToolSchema\",\n \"declaration\": \"export interface ToolSchema {\\n deferLoading?: true;\\n name: string;\\n description: string;\\n parameters: Record;\\n}\"\n },\n {\n \"name\": \"UserMessage\",\n \"declaration\": \"export interface UserMessage extends MessageBase {\\n readonly role: 'user';\\n}\"\n },\n {\n \"name\": \"WebFetchResultView\",\n \"declaration\": \"export interface WebFetchResultView {\\n card: 'web';\\n kind: 'fetch';\\n title?: string;\\n url: string;\\n statusCode: number;\\n truncated: boolean;\\n}\"\n },\n {\n \"name\": \"WebResultView\",\n \"declaration\": \"export type WebResultView = WebSearchResultView | WebFetchResultView;\"\n },\n {\n \"name\": \"WebSearchResultView\",\n \"declaration\": \"export interface WebSearchResultView {\\n card: 'web';\\n kind: 'search';\\n title?: string;\\n sources: WebSource[];\\n answer?: string;\\n truncated: boolean;\\n}\"\n },\n {\n \"name\": \"WebSource\",\n \"declaration\": \"export interface WebSource {\\n url: string;\\n title?: string;\\n snippet?: string;\\n publishedAt?: string;\\n}\"\n }\n ]\n }\n}"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146017,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778146017,"index":0,"dt":[],"id":"inspect-subprocess-api","name":"cordis_inspect_query","args":["{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"]},{"type":"chunk","time":1788778146017,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} diff --git a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json index 5d880fdde7..f33d4e7c51 100644 --- a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json +++ b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json @@ -148,22 +148,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl b/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl index cff0e5a329..7532f7f79d 100644 --- a/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl +++ b/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","systemPromptUpdate":"in-history"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790149644504,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644504,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790149644504,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -18,14 +18,14 @@ {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1790149644534,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644534,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790149644534,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790149644535,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:9}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:10}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790149644557,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790149644557,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790149644557,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/session/dynamic-tool-updates/session.v4.jsonl b/snapshots/session/dynamic-tool-updates/session.v4.jsonl index aee79405da..03befefbea 100644 --- a/snapshots/session/dynamic-tool-updates/session.v4.jsonl +++ b/snapshots/session/dynamic-tool-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790049391235,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391235,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790049391235,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -17,14 +17,14 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_task_read"},"toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false,"id":"{{message:5}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:6}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1790049391255,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391255,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790049391255,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790049391255,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:8}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:9}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790049391270,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790049391270,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790049391270,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json index 87bccafb3f..6a6b432083 100644 --- a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json +++ b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", @@ -740,22 +724,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", @@ -1380,22 +1348,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json +++ b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json index 3fb3980c3b..5b4d780009 100644 --- a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json +++ b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json @@ -87,22 +87,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "glob", "description": "Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 4 paths in modification-time order; a larger result is sampled across top-level entries and reports where the complete list was saved.", diff --git a/snapshots/session/lsp-definition/tool-schemas.expected.json b/snapshots/session/lsp-definition/tool-schemas.expected.json index 76087bb96f..9b31b5eb2c 100644 --- a/snapshots/session/lsp-definition/tool-schemas.expected.json +++ b/snapshots/session/lsp-definition/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md index a6b0b07911..88d3f9beac 100644 --- a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md +++ b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md @@ -97,11 +97,6 @@ interface ToolArgsMap { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ justification?: string; } & Record; - /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ - exit_plan_mode: { - /** The complete plan, as markdown, starting with a # heading that names it. */ - plan: string; - } & Record; /** Read the current session goal, including the id and revision that update_goal requires. */ get_goal: Record; /** Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 100 paths in modification-time order; a larger result keeps the first paths and reports where the complete list was saved. */ @@ -355,9 +350,6 @@ interface ToolOutputMap { before: string; after: string; }; - exit_plan_mode: { - approved: true; - }; get_goal: { goal: null; } | { diff --git a/snapshots/session/mcp-resources/tool-schemas.expected.json b/snapshots/session/mcp-resources/tool-schemas.expected.json index 4e5e9d557c..f47e927b07 100644 --- a/snapshots/session/mcp-resources/tool-schemas.expected.json +++ b/snapshots/session/mcp-resources/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/model-switch-notice/model-switch-driver.mjs b/snapshots/session/model-switch-notice/model-switch-driver.mjs index 508e1ec8bf..072dc673af 100644 --- a/snapshots/session/model-switch-notice/model-switch-driver.mjs +++ b/snapshots/session/model-switch-notice/model-switch-driver.mjs @@ -1,46 +1,23 @@ /** Test-only driver that selects another model after the first step's tool call. */ -import { installModelSelection } from '@deepseek-ai/dsh-agent' - const SELECTED = { provider: 'deepseek-official', model: 'deepseek-v4-pro' } -const selections = new WeakMap() +const switched = new WeakSet() export const name = 'model-switch-driver' export const inject = ['agents'] /** - * Install the real selection helper and change its input after `todo_write`. + * Change the request route after `todo_write`; headless owns the selection notice. * @param {import('@deepseek-ai/cordis').Context} ctx - composition context. */ export function apply(ctx) { - ctx.on('agent/created', ({ agent }) => { - const selection = { current: undefined, assembled: undefined } - selections.set(agent.session, selection) - installModelSelection(agent.ctx, selection) - }) ctx.on('session/event', (session, event) => { - if (event.type !== 'todo/write') return - const selection = selections.get(session) - if (selection === undefined) throw new Error('model-switch driver requires an installed selection') - selection.current = SELECTED - }) - // Headless also fixes the original selection. These root waterfalls make the - // driver authoritative; ending after step two avoids a reverse notice. - ctx.on('system-prompt/assemble', async (_assembly, context, next) => { - const assembled = await next() - if (context.agent === undefined) return assembled - const selected = selections.get(context.agent.session)?.assembled - if (selected === undefined) return assembled - return { - ...assembled, - variables: { ...assembled.variables, provider: selected.provider, model: selected.model }, - } + if (event.type === 'todo/write') switched.add(session) }) ctx.on('agent/request', async ({ agent }, next) => { const resolved = await next() - const selected = selections.get(agent.session)?.assembled - if (selected === undefined) return resolved + if (!switched.has(agent.session)) return resolved const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved - return { ...withoutInheritedEffort, ...selected } + return { ...withoutInheritedEffort, ...SELECTED } }) } diff --git a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json index 637c069b74..3a003efa6c 100644 --- a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json +++ b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/office-skills/tool-schemas.expected.json b/snapshots/session/office-skills/tool-schemas.expected.json index 79d6891abc..b9c653bab9 100644 --- a/snapshots/session/office-skills/tool-schemas.expected.json +++ b/snapshots/session/office-skills/tool-schemas.expected.json @@ -62,22 +62,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json index d1b4eef213..925f644478 100644 --- a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json +++ b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json @@ -42,22 +42,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "glob", "description": "Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 100 paths in modification-time order; a larger result keeps the first paths and reports where the complete list was saved.", diff --git a/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl b/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl index 843a75e88d..3c238c7228 100644 --- a/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl +++ b/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"moonshotai","model":"kimi-k3","contextWindow":1048576,"systemPromptUpdate":"in-history"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"moonshotai","model":"kimi-k3"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790149644504,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644504,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790149644504,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -18,14 +18,14 @@ {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"moonshotai","model":"kimi-k3"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1790149644534,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644534,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790149644534,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790149644535,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:9}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:10}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"moonshotai","model":"kimi-k3"},"id":"{{message:11}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790149644557,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790149644557,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790149644557,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/session/plugin-manager-mcp/session.v4.jsonl b/snapshots/session/plugin-manager-mcp/session.v4.jsonl index 794d19b23f..af8d95fc88 100644 --- a/snapshots/session/plugin-manager-mcp/session.v4.jsonl +++ b/snapshots/session/plugin-manager-mcp/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Use plugin_manager with action set_plugin, target include:demo, enabled true to activate the configured MCP entry. Then call its newly discovered mcp__demo__ping tool and report the result. Do not use other tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","plugin_manager","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","plugin_manager","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}} {"type":"session/title","data":{"title":"Use plugin_manager with action set_plugi","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to activate a plugin via plugin_manager, then call an MCP tool. Let me do the plugin_manager call first.\n\nNote: they say \"Do not use other tools\" other than plugin_manager and the mcp tool. But I need to first enable it. Let's call plugin_manager set_plugin.\n\nLet me also list plugins first? The instruction says use plugin_manager with action set_plugin, target include:demo, enabled true. Let's just do that."},{"type":"tool-call","id":"call_00_xLhr9wia3A5m7wuSRWwD0436","name":"plugin_manager","arguments":"{\"action\": \"set_plugin\", \"target\": \"include:demo\", \"enabled\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"75d99e21-1d47-4a33-bb88-340a1e84b749"},{"type":"tool-call"}]}},"id":"{{message:4}}"},"usage":{"inputTokens":7220,"outputTokens":174,"cacheReadTokens":0,"cacheWriteTokens":0,"totalTokens":7394},"stream":[{"type":"chunk","time":1789984418848,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1789984418848,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," activate"," a"," plugin"," via"," plugin","_manager",","," then"," call"," an"," M","CP"," tool","."," Let"," me"," do"," the"," plugin","_manager"," call"," first",".\n\n","Note",":"," they"," say"," \"","Do"," not"," use"," other"," tools","\""," other"," than"," plugin","_manager"," and"," the"," m","cp"," tool","."," But"," I"," need"," to"," first"," enable"," it","."," Let","'s"," call"," plugin","_manager"," set","_","plugin",".\n\n","Let"," me"," also"," list"," plugins"," first","?"," The"," instruction"," says"," use"," plugin","_manager"," with"," action"," set","_","plugin",","," target"," include",":","demo",","," enabled"," true","."," Let","'s"," just"," do"," that","."]},{"type":"chunk","time":1789984418849,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to activate a plugin via plugin_manager, then call an MCP tool. Let me do the plugin_manager call first.\n\nNote: they say \"Do not use other tools\" other than plugin_manager and the mcp tool. But I need to first enable it. Let's call plugin_manager set_plugin.\n\nLet me also list plugins first? The instruction says use plugin_manager with action set_plugin, target include:demo, enabled true. Let's just do that."}}},{"type":"chunk","time":1789984418849,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1789984418849,"index":1,"dt":[],"id":"call_00_xLhr9wia3A5m7wuSRWwD0436","name":"plugin_manager","args":[""]},{"type":"tool-call-chunks","time0":1789984418849,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_xLhr9wia3A5m7wuSRWwD0436","args":["{","\"","action","\"",": ","\"","set","_","plugin","\"",", ","\"","target","\"",": ","\"","include",":","demo","\"",", ","\"","en","abled","\"",": ","true","}"]},{"type":"chunk","time":1789984418849,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_xLhr9wia3A5m7wuSRWwD0436","name":"plugin_manager","arguments":"{\"action\": \"set_plugin\", \"target\": \"include:demo\", \"enabled\": true}"}}},{"type":"chunk","time":1789984418849,"chunk":{"type":"usage","usage":{"inputTokens":7220,"outputTokens":174,"cacheReadTokens":0,"cacheWriteTokens":0,"totalTokens":7394}}},{"type":"chunk","time":1789984418849,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"75d99e21-1d47-4a33-bb88-340a1e84b749"},{"type":"tool-call"}]}}}]},"surfaceOp":"append"} @@ -18,7 +18,7 @@ {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_mcp_resource_templates","list_mcp_resources","mcp__demo__ping","plugin_manager","read","read_image","read_mcp_resource","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_mcp_resource_templates","list_mcp_resources","mcp__demo__ping","plugin_manager","read","read_image","read_mcp_resource","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"list_mcp_resource_templates"},{"type":"tool-addition","toolName":"list_mcp_resources"},{"type":"tool-addition","toolName":"mcp__demo__ping"},{"type":"tool-addition","toolName":"read_mcp_resource"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The plugin was enabled. Now call mcp__demo__ping.\n\nWait — the tool was already listed in my tools before enabling. Let's call it anyway."},{"type":"tool-call","id":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"b1ec1efe-4be6-4836-a10c-e5d6bfdce520"},{"type":"tool-call"}]}},"id":"{{message:8}}"},"usage":{"inputTokens":7035,"outputTokens":59,"cacheReadTokens":768,"cacheWriteTokens":0,"totalTokens":7862},"stream":[{"type":"chunk","time":1789984418998,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1789984418998,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," plugin"," was"," enabled","."," Now"," call"," m","cp","__","demo","__","ping",".\n\n","Wait"," —"," the"," tool"," was"," already"," listed"," in"," my"," tools"," before"," enabling","."," Let","'s"," call"," it"," anyway","."]},{"type":"chunk","time":1789984418998,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The plugin was enabled. Now call mcp__demo__ping.\n\nWait — the tool was already listed in my tools before enabling. Let's call it anyway."}}},{"type":"chunk","time":1789984418998,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1789984418998,"index":1,"dt":[],"id":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","args":[""]},{"type":"tool-call-chunks","time0":1789984418998,"index":1,"dt":[],"id":"call_00_miuKfLCTz5Cb38gdp8kK5186","args":["{}"]},{"type":"chunk","time":1789984418998,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","arguments":"{}"}}},{"type":"chunk","time":1789984418998,"chunk":{"type":"usage","usage":{"inputTokens":7035,"outputTokens":59,"cacheReadTokens":768,"cacheWriteTokens":0,"totalTokens":7862}}},{"type":"chunk","time":1789984418998,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"b1ec1efe-4be6-4836-a10c-e5d6bfdce520"},{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","arguments":"{}"}} diff --git a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json index f44f52640a..31ec55c6a1 100644 --- a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", @@ -801,22 +785,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/plugin-manager/tool-schemas.expected.json b/snapshots/session/plugin-manager/tool-schemas.expected.json index 4596dae7a1..692d89f859 100644 --- a/snapshots/session/plugin-manager/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/product-subagent-both/tool-schemas.expected.json b/snapshots/session/product-subagent-both/tool-schemas.expected.json index 2b36308f92..1b05d09fe2 100644 --- a/snapshots/session/product-subagent-both/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-both/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/product-subagent-codex/tool-schemas.expected.json b/snapshots/session/product-subagent-codex/tool-schemas.expected.json index d9f3cfa9d0..1c48e82c80 100644 --- a/snapshots/session/product-subagent-codex/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-codex/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json index 11f07197a2..da2d9314ae 100644 --- a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/provider-cwd/tool-schemas.expected.json b/snapshots/session/provider-cwd/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/session/provider-cwd/tool-schemas.expected.json +++ b/snapshots/session/provider-cwd/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/ptc-node-read-only/system-prompt.expected.md b/snapshots/session/ptc-node-read-only/system-prompt.expected.md index 39529ce115..33387c0251 100644 --- a/snapshots/session/ptc-node-read-only/system-prompt.expected.md +++ b/snapshots/session/ptc-node-read-only/system-prompt.expected.md @@ -89,11 +89,6 @@ interface ToolArgsMap { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ justification?: string; } & Record; - /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ - exit_plan_mode: { - /** The complete plan, as markdown, starting with a # heading that names it. */ - plan: string; - } & Record; /** Read the current session goal, including the id and revision that update_goal requires. */ get_goal: Record; /** Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 100 paths in modification-time order; a larger result keeps the first paths and reports where the complete list was saved. */ @@ -330,9 +325,6 @@ interface ToolOutputMap { before: string; after: string; }; - exit_plan_mode: { - approved: true; - }; get_goal: { goal: null; } | { diff --git a/snapshots/session/ptc-node-workspace/system-prompt.expected.md b/snapshots/session/ptc-node-workspace/system-prompt.expected.md index 39529ce115..33387c0251 100644 --- a/snapshots/session/ptc-node-workspace/system-prompt.expected.md +++ b/snapshots/session/ptc-node-workspace/system-prompt.expected.md @@ -89,11 +89,6 @@ interface ToolArgsMap { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ justification?: string; } & Record; - /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ - exit_plan_mode: { - /** The complete plan, as markdown, starting with a # heading that names it. */ - plan: string; - } & Record; /** Read the current session goal, including the id and revision that update_goal requires. */ get_goal: Record; /** Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 100 paths in modification-time order; a larger result keeps the first paths and reports where the complete list was saved. */ @@ -330,9 +325,6 @@ interface ToolOutputMap { before: string; after: string; }; - exit_plan_mode: { - approved: true; - }; get_goal: { goal: null; } | { diff --git a/snapshots/session/ptc-python-turn/system-prompt.expected.md b/snapshots/session/ptc-python-turn/system-prompt.expected.md index 23dc2e1494..2f30fb8164 100644 --- a/snapshots/session/ptc-python-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-python-turn/system-prompt.expected.md @@ -152,14 +152,6 @@ class EditOutput(TypedDict): before: str after: str -class ExitPlanModeArgs(TypedDict): - # The complete plan, as markdown, starting with a # heading that names it. - plan: str - # Additional keys beyond those declared are allowed. - -class ExitPlanModeOutput(TypedDict): - approved: Literal[True] - class GetGoalOutput1(TypedDict): goal: None @@ -517,8 +509,6 @@ class Tools(Protocol): """Create a persisted goal that keeps this session working across automatic continuation rounds. Use it when the direct human request is a long-running objective, even if the user did not say \"goal\"; not for single-turn work.""" async def edit(self, args: EditArgs) -> EditOutput: """Edit an existing UTF-8 text file by replacing literal text.""" - async def exit_plan_mode(self, args: ExitPlanModeArgs) -> ExitPlanModeOutput: - """Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.""" async def get_goal(self, args: dict[str, Any]) -> GetGoalOutput1 | GetGoalOutput2: """Read the current session goal, including the id and revision that update_goal requires.""" async def glob(self, args: GlobArgs) -> GlobOutput: diff --git a/snapshots/session/ptc-read-image/system-prompt.expected.md b/snapshots/session/ptc-read-image/system-prompt.expected.md index f0a3c2426f..a8ac831e43 100644 --- a/snapshots/session/ptc-read-image/system-prompt.expected.md +++ b/snapshots/session/ptc-read-image/system-prompt.expected.md @@ -89,11 +89,6 @@ interface ToolArgsMap { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ justification?: string; } & Record; - /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ - exit_plan_mode: { - /** The complete plan, as markdown, starting with a # heading that names it. */ - plan: string; - } & Record; /** Read the current session goal, including the id and revision that update_goal requires. */ get_goal: Record; /** Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 100 paths in modification-time order; a larger result keeps the first paths and reports where the complete list was saved. */ @@ -326,9 +321,6 @@ interface ToolOutputMap { before: string; after: string; }; - exit_plan_mode: { - approved: true; - }; get_goal: { goal: null; } | { diff --git a/snapshots/session/ptc-turn/system-prompt.expected.md b/snapshots/session/ptc-turn/system-prompt.expected.md index 39529ce115..33387c0251 100644 --- a/snapshots/session/ptc-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-turn/system-prompt.expected.md @@ -89,11 +89,6 @@ interface ToolArgsMap { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. Use the language of the user’s current request. */ justification?: string; } & Record; - /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ - exit_plan_mode: { - /** The complete plan, as markdown, starting with a # heading that names it. */ - plan: string; - } & Record; /** Read the current session goal, including the id and revision that update_goal requires. */ get_goal: Record; /** Find files, not directories, whose paths match a glob pattern, including hidden and ignored files. Returns up to 100 paths in modification-time order; a larger result keeps the first paths and reports where the complete list was saved. */ @@ -330,9 +325,6 @@ interface ToolOutputMap { before: string; after: string; }; - exit_plan_mode: { - approved: true; - }; get_goal: { goal: null; } | { diff --git a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json index 95ffc0255b..bf0cb36d10 100644 --- a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json +++ b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/ralph-loop/tool-schemas.1.expected.json b/snapshots/session/ralph-loop/tool-schemas.1.expected.json index 02ebdc4baf..404eb6dc87 100644 --- a/snapshots/session/ralph-loop/tool-schemas.1.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.1.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/ralph-loop/tool-schemas.2.expected.json b/snapshots/session/ralph-loop/tool-schemas.2.expected.json index 02ebdc4baf..404eb6dc87 100644 --- a/snapshots/session/ralph-loop/tool-schemas.2.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.2.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/ralph-loop/tool-schemas.expected.json b/snapshots/session/ralph-loop/tool-schemas.expected.json index deabaf4a0d..49057f142e 100644 --- a/snapshots/session/ralph-loop/tool-schemas.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/session-query-spill/tool-schemas.expected.json b/snapshots/session/session-query-spill/tool-schemas.expected.json index 9d66381297..986c2c612b 100644 --- a/snapshots/session/session-query-spill/tool-schemas.expected.json +++ b/snapshots/session/session-query-spill/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json index 46abe32771..9b0246b8c1 100644 --- a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json index b8dc7c20a8..2d0622de05 100644 --- a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json +++ b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json @@ -170,22 +170,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json index e48983205c..892f4a6d05 100644 --- a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json @@ -65,22 +65,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json index d217c78441..fd65fad3b9 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json index 25793c1cd4..32e1b67539 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json index 45ab57c7c0..b7041366f4 100644 --- a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json @@ -65,22 +65,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json +++ b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/team-targets/tool-schemas.expected.json b/snapshots/session/team-targets/tool-schemas.expected.json index 4926edb120..d33b95f8d9 100644 --- a/snapshots/session/team-targets/tool-schemas.expected.json +++ b/snapshots/session/team-targets/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/text-turn/tool-schemas.expected.json b/snapshots/session/text-turn/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/session/text-turn/tool-schemas.expected.json +++ b/snapshots/session/text-turn/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/web-fetch/tool-schemas.expected.json b/snapshots/session/web-fetch/tool-schemas.expected.json index 5ef902c18a..ef6c62a3fb 100644 --- a/snapshots/session/web-fetch/tool-schemas.expected.json +++ b/snapshots/session/web-fetch/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/workflow-confinement/tool-schemas.expected.json b/snapshots/session/workflow-confinement/tool-schemas.expected.json index 2b4b1f069f..1949c7d605 100644 --- a/snapshots/session/workflow-confinement/tool-schemas.expected.json +++ b/snapshots/session/workflow-confinement/tool-schemas.expected.json @@ -107,22 +107,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/workspace-dependencies/tool-schemas.expected.json b/snapshots/session/workspace-dependencies/tool-schemas.expected.json index 4d3db178b9..c930eafbdc 100644 --- a/snapshots/session/workspace-dependencies/tool-schemas.expected.json +++ b/snapshots/session/workspace-dependencies/tool-schemas.expected.json @@ -62,22 +62,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.", diff --git a/snapshots/session/worktree/tool-schemas.expected.json b/snapshots/session/worktree/tool-schemas.expected.json index 6641f8f0b2..652502f14c 100644 --- a/snapshots/session/worktree/tool-schemas.expected.json +++ b/snapshots/session/worktree/tool-schemas.expected.json @@ -124,22 +124,6 @@ ] } }, - { - "name": "exit_plan_mode", - "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "The complete plan, as markdown, starting with a # heading that names it." - } - }, - "required": [ - "plan" - ] - } - }, { "name": "get_goal", "description": "Read the current session goal, including the id and revision that update_goal requires.",