diff --git a/apps/cli/composition.md b/apps/cli/composition.md
index 8b2cfb83ab..61251f34ad 100644
--- a/apps/cli/composition.md
+++ b/apps/cli/composition.md
@@ -40,6 +40,8 @@ flowchart LR
cfg --> plugin_dsh_base_user_questions
plugin_dsh_base_agent["agent
@deepseek-ai/dsh-agent"]
cfg --> plugin_dsh_base_agent
+ plugin_dsh_base_agent_model["agent-model
@deepseek-ai/dsh-agent-model"]
+ cfg --> plugin_dsh_base_agent_model
plugin_dsh_base_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek
@deepseek-ai/dsh-plugin-package-inventory-deepseek"]
cfg --> plugin_dsh_base_plugin_package_inventory_deepseek
plugin_dsh_base_agent_default_model["agent-default-model
@deepseek-ai/dsh-agent-default-model"]
@@ -218,6 +220,7 @@ flowchart LR
| `session-title-llm` | `@deepseek-ai/dsh-session-title-first-prompt-llm` |
| `user-questions` | `@deepseek-ai/dsh-user-questions` |
| `agent` | `@deepseek-ai/dsh-agent` |
+| `agent-model` | `@deepseek-ai/dsh-agent-model` |
| `plugin-package-inventory-deepseek` | `@deepseek-ai/dsh-plugin-package-inventory-deepseek` |
| `agent-default-model` | `@deepseek-ai/dsh-agent-default-model` |
| `jobs` | `@deepseek-ai/dsh-jobs-local` |
diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml
index 8fbb3bbe4a..d9f5df598e 100644
--- a/docs/architecture.i18n.yaml
+++ b/docs/architecture.i18n.yaml
@@ -24,11 +24,11 @@
en: e89802f066e4955d
zh: ea2fb4bef94e086d
/deepseek-harness-architecture/turn-flow:
- en: a7667781d1cd774c
- zh: ee638cc27f65ea1d
+ en: f270fd4ca334d417
+ zh: 3c789548e555508e
/deepseek-harness-architecture/session-log:
- en: ece5a91f0c437ff8
- zh: baa13dfdd23d48f8
+ en: b0a610cb75f0684a
+ zh: 724526f3ac94b34f
/deepseek-harness-architecture/capability-seams:
en: a5bc20c3f2a1b370
zh: 91773a281410b2f4
diff --git a/docs/architecture.md b/docs/architecture.md
index 4abed2b8f8..43932a6919 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -107,7 +107,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.
-The loop snapshots Session model selection before assembly and shares its route with `agent/pre-step`; retries retain that selection. `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`, the deprecated `agent/request` hook adjusts controls without changing the route, and `prepareCall()` binds the adapter 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 system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details.
+The loop snapshots Session model selection before assembly and shares the complete choice with `agent/pre-step`; retries retain that selection. `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`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter 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 system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details.
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.
@@ -123,7 +123,7 @@ Session consumers know only the current logical format. Header-only `stat` and `
**Model-visible means logged.** Every model request must be reconstructable from the log. New model-visible inputs require session events. Plugins changing existing message content register [pure message projections](subsystems/session.md#plugin-owned-message-projections); detached readers supply the same definitions explicitly. Tool changes are capability-independent; [Session tool history](../packages/core/session/README.md) supplies provider declarations.
-**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, host consumers read one typed state with `stateOf()`, and carriers batch cropped client views with `snapshot()`. A host reader either requires this service during activation or fails explicitly when the registry or required key is absent. Contributors may retain `ctx.inject(['sessionProjections'], ...)` registration without silently defaulting a missing host value. The Agent registry owns `modelSelection`; the agent loop registers shared `turnBoundary` state for its readers ([decision](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md)).
+**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, host consumers read one typed state with `stateOf()`, and carriers batch cropped client views with `snapshot()`. A host reader either requires this service during activation or fails explicitly when the registry or required key is absent. Contributors may retain `ctx.inject(['sessionProjections'], ...)` registration without silently defaulting a missing host value. `dsh-agent-model` owns `modelSelection`; the agent loop registers shared `turnBoundary` state for its readers ([decision](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md)).
[Working directories](subsystems/working-directory.md) supply user context and execution paths without changing original project identity, sandbox write roots, or existing process directories.
diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md
index f2c9e8ca64..59d0fd7b12 100644
--- a/docs/architecture.zh.md
+++ b/docs/architecture.zh.md
@@ -111,7 +111,7 @@ turn/end
输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。
-循环在组装前快照 Session 模型选择,并将其路由传给 `agent/pre-step`;重试保留该选择。`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变路由的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。
+循环在组装前快照 Session 模型选择,并将完整选择传给 `agent/pre-step`;重试保留该选择。`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。
循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。
@@ -127,7 +127,7 @@ Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list`
**模型可见即已记录。** 每个模型请求都必须能从日志重建。新增模型可见输入需要会话事件。修改现有消息内容的插件注册[纯消息投影](subsystems/session.zh.md#plugin-owned-message-projections),独立读取器显式传入相同的处理器。 工具变更不依赖能力;[Session 工具历史](../packages/core/session/README.zh.md)提供提供方声明。
-**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,host 消费方通过 `stateOf()` 读取单个类型化状态,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方要么在激活时要求该服务,要么在注册表或必需 key 缺席时明确失败。贡献方可以保留 `ctx.inject(['sessionProjections'], ...)` 注册,但不能为缺失的 host 值静默提供默认值。Agent 注册表拥有 `modelSelection`;agent loop 为读取方注册共享的 `turnBoundary` 状态([决策](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md))。
+**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,host 消费方通过 `stateOf()` 读取单个类型化状态,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方要么在激活时要求该服务,要么在注册表或必需 key 缺席时明确失败。贡献方可以保留 `ctx.inject(['sessionProjections'], ...)` 注册,但不能为缺失的 host 值静默提供默认值。`dsh-agent-model` 拥有 `modelSelection`;agent loop 为读取方注册共享的 `turnBoundary` 状态([决策](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md))。
[工作目录](subsystems/working-directory.zh.md) 提供用户上下文和执行路径,不改变原始项目标识、沙箱写入根目录或已有进程目录。
diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml
index 25b949f17a..24b1a9397e 100644
--- a/docs/capability-seams.i18n.yaml
+++ b/docs/capability-seams.i18n.yaml
@@ -6,5 +6,5 @@
en: 41d772192075e133
zh: 5ef3da69b4de1d8a
/capability-seams-and-core-services:
- en: 048c8c150734d0dd
- zh: 836dd6c78d4c8278
+ en: a96723cbb190d1b4
+ zh: a86a4ae9d8b5a58b
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index dce33aa090..fc89183294 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -187,8 +187,10 @@ flowchart LR
pkg_skill_office["skill-office"]
svc_agents["ctx.agents
Agent service"]
pkg_acp["acp"]
- svc_agentDefaultModel["ctx.agentDefaultModel
Default Agent model selection"]
+ pkg_agent_model["agent-model"]
+ svc_agentModel["ctx.agentModel
Session model selection"]
pkg_headless["headless"]
+ svc_agentDefaultModel["ctx.agentDefaultModel
Default Agent model selection"]
svc_agentLoop["ctx.agentLoop
Concrete loop driver"]
pkg_base["base"]
pkg_sdk_minimal["sdk-minimal"]
@@ -296,6 +298,7 @@ flowchart LR
pkg_agent --> svc_agents
pkg_agent_default_model --> svc_agentDefaultModel
pkg_agent_loop --> svc_agentLoop
+ pkg_agent_model --> svc_agentModel
pkg_agent_preset_registry --> svc_agentPresets
pkg_api_gateway --> svc_typertGateway
pkg_api_job_controller --> svc_jobController
@@ -453,6 +456,11 @@ flowchart LR
svc_agentDefaultModel --> pkg_headless
svc_agentLoop --> pkg_base
svc_agentLoop --> pkg_sdk_minimal
+ svc_agentModel --> pkg_acp
+ svc_agentModel --> pkg_agent_loop
+ svc_agentModel --> pkg_api_session_controller
+ svc_agentModel --> pkg_headless
+ svc_agentModel --> pkg_sdk_jsonrpc_server
svc_agentTeams --> pkg_experimental_tool_agent_team
svc_agents --> pkg_acp
svc_agents --> pkg_agent_loop
@@ -675,6 +683,7 @@ flowchart LR
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points), serves cached projection views, and accelerates prepared-Session projection hydration. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/experimental/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent`](../packages/subagent/subagent) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
+| `ctx.agentModel` | `core` | [`agent-model`](../packages/core/agent-model) | - | [`agent-loop`](../packages/core/agent-loop), [`api-session-controller`](../packages/api/session-controller), [`acp`](../packages/acp/acp), [`sdk-jsonrpc-server`](../packages/sdk/server), [`headless`](../packages/bundle/headless) | - | Reads and records Session model choices; owns the modelSelection projection and optional scoped route-switch notices. |
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.schedule` | `core` | [`schedule`](../packages/schedule/schedule) | - | - | - | Stores tasks independently of Session activation and queues due messages in the original Session. |
diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md
index 20f165490d..cd5da4890d 100644
--- a/docs/capability-seams.zh.md
+++ b/docs/capability-seams.zh.md
@@ -189,8 +189,10 @@ flowchart LR
pkg_skill_office["skill-office"]
svc_agents["ctx.agents
Agent service"]
pkg_acp["acp"]
- svc_agentDefaultModel["ctx.agentDefaultModel
Default Agent model selection"]
+ pkg_agent_model["agent-model"]
+ svc_agentModel["ctx.agentModel
Session model selection"]
pkg_headless["headless"]
+ svc_agentDefaultModel["ctx.agentDefaultModel
Default Agent model selection"]
svc_agentLoop["ctx.agentLoop
Concrete loop driver"]
pkg_base["base"]
pkg_sdk_minimal["sdk-minimal"]
@@ -298,6 +300,7 @@ flowchart LR
pkg_agent --> svc_agents
pkg_agent_default_model --> svc_agentDefaultModel
pkg_agent_loop --> svc_agentLoop
+ pkg_agent_model --> svc_agentModel
pkg_agent_preset_registry --> svc_agentPresets
pkg_api_gateway --> svc_typertGateway
pkg_api_job_controller --> svc_jobController
@@ -455,6 +458,11 @@ flowchart LR
svc_agentDefaultModel --> pkg_headless
svc_agentLoop --> pkg_base
svc_agentLoop --> pkg_sdk_minimal
+ svc_agentModel --> pkg_acp
+ svc_agentModel --> pkg_agent_loop
+ svc_agentModel --> pkg_api_session_controller
+ svc_agentModel --> pkg_headless
+ svc_agentModel --> pkg_sdk_jsonrpc_server
svc_agentTeams --> pkg_experimental_tool_agent_team
svc_agents --> pkg_acp
svc_agents --> pkg_agent_loop
@@ -677,6 +685,7 @@ flowchart LR
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | 按会话持久保存投影单元状态的检查点(节流检查点,以及轮次/结束/分离时的必选检查点),提供缓存投影视图,并加速 prepared Session 的投影恢复。 |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/experimental/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent`](../packages/subagent/subagent) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 |
+| `ctx.agentModel` | `core` | [`agent-model`](../packages/core/agent-model) | - | [`agent-loop`](../packages/core/agent-loop), [`api-session-controller`](../packages/api/session-controller), [`acp`](../packages/acp/acp), [`sdk-jsonrpc-server`](../packages/sdk/server), [`headless`](../packages/bundle/headless) | - | 读取并记录 Session 的模型选择;拥有 modelSelection 投影和可选的作用域路由切换提示。 |
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | 唯一的具体循环插件;扩展包依赖 dsh-agent 的事件和服务,而不依赖此包。 |
| `ctx.schedule` | `core` | [`schedule`](../packages/schedule/schedule) | - | - | - | 独立于 Session 的加载状态存储任务,并将到期消息排入原 Session。 |
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index be71df999e..b42ac817a4 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -14,9 +14,9 @@ Each package entry labels its data with three identifiers: `inject` lists the se
## `@deepseek-ai/dsh-acp`
-- `inject`: `agents` · `llm` · `sessionPersistence` · `sessionProjections` · `sessions`
+- `inject`: `agents` · `llm` · `sessionPersistence` · `agentModel` · `sessions`
- `refs`: `Stream` (`@agentclientprotocol/sdk`)
-- `source`: [`packages/acp/acp/src/index.ts:74`](../packages/acp/acp/src/index.ts)
+- `source`: [`packages/acp/acp/src/index.ts:75`](../packages/acp/acp/src/index.ts)
```ts config-catalog
/** Plugin config: the provider/model selection used for each ACP-created agent. */
@@ -90,7 +90,7 @@ export interface Config {
## `@deepseek-ai/dsh-agent-loop`
-- `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections`
+- `inject`: `agentModel` · `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections`
- `refs`: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md) · `Volatile` (`@deepseek-ai/cosmokit`)
- `source`: [`packages/core/agent-loop/src/index.ts:304`](../packages/core/agent-loop/src/index.ts)
@@ -219,8 +219,8 @@ export interface Config {
## `@deepseek-ai/dsh-api-session-controller`
-- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
-- `source`: [`packages/api/session-controller/src/index.ts:93`](../packages/api/session-controller/src/index.ts)
+- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `agentModel` · `sessionQuery` · `typert` · `workspaceRegistry`
+- `source`: [`packages/api/session-controller/src/index.ts:94`](../packages/api/session-controller/src/index.ts)
```ts config-catalog
/** Session Controller deployment policy. */
@@ -1434,8 +1434,8 @@ export interface Config {
## `@deepseek-ai/dsh-headless`
-- `inject`: `agentDefaultModel` · `agents` · `sessionProjections` · `sessions`
-- `source`: [`packages/bundle/headless/src/index.ts:43`](../packages/bundle/headless/src/index.ts)
+- `inject`: `agentDefaultModel` · `agents` · `agentModel` · `sessions`
+- `source`: [`packages/bundle/headless/src/index.ts:44`](../packages/bundle/headless/src/index.ts)
```ts config-catalog
/** Plugin config: the task and run options resolved from this app's injected provider service. */
@@ -2744,9 +2744,9 @@ export interface Config {
## `@deepseek-ai/dsh-sdk-jsonrpc-server`
-- `inject`: `agents` · `sessionProjections` · `workingDirectory`
+- `inject`: `agents` · `agentModel` · `workingDirectory`
- `refs`: `Readable` (`node:stream`) · `Writable` (`node:stream`)
-- `source`: [`packages/sdk/server/src/index.ts:24`](../packages/sdk/server/src/index.ts)
+- `source`: [`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts)
```ts config-catalog
/** JSON-RPC deployment config plus runtime-only test hooks. */
@@ -4562,7 +4562,8 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `package` | `inject` | `source` |
| --- | --- | --- |
| `@deepseek-ai/dsh-acp-app` | `cmdlineArgs` | [`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts) |
-| `@deepseek-ai/dsh-agent` | `sessionProjections` | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) |
+| `@deepseek-ai/dsh-agent` | — | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) |
+| `@deepseek-ai/dsh-agent-model` | `sessionProjections` | [`packages/core/agent-model/src/index.ts`](../packages/core/agent-model/src/index.ts) |
| `@deepseek-ai/dsh-api-account-controller` | `deepseekAccount` · `agents` | [`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts) |
| `@deepseek-ai/dsh-api-remote-access` | — | [`packages/api/remote-access/src/index.ts`](../packages/api/remote-access/src/index.ts) |
| `@deepseek-ai/dsh-api-remotes` | `typertGateway` | [`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts) |
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index d535d7771b..3110219844 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -16,9 +16,9 @@
## `@deepseek-ai/dsh-acp`
-- `inject`: `agents` · `llm` · `sessionPersistence` · `sessionProjections` · `sessions`
+- `inject`: `agents` · `llm` · `sessionPersistence` · `agentModel` · `sessions`
- `refs`: `Stream` (`@agentclientprotocol/sdk`)
-- `source`: [`packages/acp/acp/src/index.ts:74`](../packages/acp/acp/src/index.ts)
+- `source`: [`packages/acp/acp/src/index.ts:75`](../packages/acp/acp/src/index.ts)
```ts config-catalog
/** Plugin config: the provider/model selection used for each ACP-created agent. */
@@ -92,7 +92,7 @@ export interface Config {
## `@deepseek-ai/dsh-agent-loop`
-- `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections`
+- `inject`: `agentModel` · `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections`
- `refs`: [`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md) · `Volatile` (`@deepseek-ai/cosmokit`)
- `source`: [`packages/core/agent-loop/src/index.ts:304`](../packages/core/agent-loop/src/index.ts)
@@ -221,8 +221,8 @@ export interface Config {
## `@deepseek-ai/dsh-api-session-controller`
-- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
-- `source`: [`packages/api/session-controller/src/index.ts:93`](../packages/api/session-controller/src/index.ts)
+- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `agentModel` · `sessionQuery` · `typert` · `workspaceRegistry`
+- `source`: [`packages/api/session-controller/src/index.ts:94`](../packages/api/session-controller/src/index.ts)
```ts config-catalog
/** Session Controller deployment policy. */
@@ -1436,8 +1436,8 @@ export interface Config {
## `@deepseek-ai/dsh-headless`
-- `inject`: `agentDefaultModel` · `agents` · `sessionProjections` · `sessions`
-- `source`: [`packages/bundle/headless/src/index.ts:43`](../packages/bundle/headless/src/index.ts)
+- `inject`: `agentDefaultModel` · `agents` · `agentModel` · `sessions`
+- `source`: [`packages/bundle/headless/src/index.ts:44`](../packages/bundle/headless/src/index.ts)
```ts config-catalog
/** Plugin config: the task and run options resolved from this app's injected provider service. */
@@ -2746,9 +2746,9 @@ export interface Config {
## `@deepseek-ai/dsh-sdk-jsonrpc-server`
-- `inject`: `agents` · `sessionProjections` · `workingDirectory`
+- `inject`: `agents` · `agentModel` · `workingDirectory`
- `refs`: `Readable` (`node:stream`) · `Writable` (`node:stream`)
-- `source`: [`packages/sdk/server/src/index.ts:24`](../packages/sdk/server/src/index.ts)
+- `source`: [`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts)
```ts config-catalog
/** JSON-RPC deployment config plus runtime-only test hooks. */
@@ -4564,7 +4564,8 @@ export interface Config {
| `package` | `inject` | `source` |
| --- | --- | --- |
| `@deepseek-ai/dsh-acp-app` | `cmdlineArgs` | [`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts) |
-| `@deepseek-ai/dsh-agent` | `sessionProjections` | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) |
+| `@deepseek-ai/dsh-agent` | — | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) |
+| `@deepseek-ai/dsh-agent-model` | `sessionProjections` | [`packages/core/agent-model/src/index.ts`](../packages/core/agent-model/src/index.ts) |
| `@deepseek-ai/dsh-api-account-controller` | `deepseekAccount` · `agents` | [`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts) |
| `@deepseek-ai/dsh-api-remote-access` | — | [`packages/api/remote-access/src/index.ts`](../packages/api/remote-access/src/index.ts) |
| `@deepseek-ai/dsh-api-remotes` | `typertGateway` | [`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts) |
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index bfc93b5edb..640768a74d 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -10,22 +10,22 @@ 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:250`](../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:375`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
+| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:376`](../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:405`](../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/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:406`](../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:327`](../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-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:365`](../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/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:327`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`agent-model`](../packages/core/agent-model), `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-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:366`](../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:393`](../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:602`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:582`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:609`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:588`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:595`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:394`](../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:600`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:580`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:607`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:586`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:593`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:54`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
@@ -101,7 +101,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `internal/dispatch` | - | [`terminal-bash`](../packages/terminal/terminal-bash) |
| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` |
| `internal/service` | - | `gateway` |
-| `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, `translator`, [`web`](../packages/web/web) |
+| `internal/status` | - | [`agent`](../packages/core/agent), [`agent-model`](../packages/core/agent-model), `inspector`, `translator`, [`web`](../packages/web/web) |
| `internal/update` | - | [`app-boot`](../packages/boot/app-boot) |
| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` |
| `slots/changed` | `ui-renderer` (`emit`) | - |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index 408beb3a0c..1def94b11d 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/docs/event-producer-consumer.zh.md
@@ -12,22 +12,22 @@
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:250`](../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:375`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
+| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:376`](../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:405`](../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/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:406`](../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:327`](../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-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:365`](../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/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:327`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`agent-model`](../packages/core/agent-model), `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-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:366`](../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:393`](../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:602`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:582`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:609`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:588`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:595`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:394`](../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:600`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:580`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:607`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:586`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:593`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:54`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
@@ -103,7 +103,7 @@
| `internal/dispatch` | - | [`terminal-bash`](../packages/terminal/terminal-bash) |
| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` |
| `internal/service` | - | `gateway` |
-| `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, `translator`, [`web`](../packages/web/web) |
+| `internal/status` | - | [`agent`](../packages/core/agent), [`agent-model`](../packages/core/agent-model), `inspector`, `translator`, [`web`](../packages/web/web) |
| `internal/update` | - | [`app-boot`](../packages/boot/app-boot) |
| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` |
| `slots/changed` | `ui-renderer` (`emit`) | - |
diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml
index fbb132f733..2a7ec422e5 100644
--- a/docs/persistence-catalog.i18n.yaml
+++ b/docs/persistence-catalog.i18n.yaml
@@ -21,8 +21,8 @@
en: 9188862058c6986b
zh: 9188862058c6986b
/session-persistence-event-catalog/events/agent/agent-inbox-spliced-log-only:
- en: 5eec817110f76d99
- zh: b8c36632d99ad776
+ en: 83f3d60e1cf13127
+ zh: 620989a3c84f20ee
/session-persistence-event-catalog/events/agent-preset:
en: 49891ca9be5ac123
zh: 49891ca9be5ac123
@@ -141,8 +141,8 @@
en: 9edf4ef31e60e880
zh: 9edf4ef31e60e880
/session-persistence-event-catalog/events/model/model-selection-log-only:
- en: cfa258833e29b683
- zh: 5209de008e1a0ead
+ en: 0cd15df61a3175b5
+ zh: a2730b18b83c7f4b
/session-persistence-event-catalog/events/permission:
en: 19917bc5438e60af
zh: 19917bc5438e60af
@@ -1344,8 +1344,8 @@
en: 2786fef560851bb4
zh: 5d787475356b2606
/session-persistence-event-catalog/resolved-persistence-types/string:
- en: 1525c04d0ebfc56e
- zh: 902d37c1de80bfe7
+ en: a9d7dc010ada133d
+ zh: 9bd682adee72d04e
/session-persistence-event-catalog/resolved-persistence-types/string~2:
en: 0089620bf4638263
zh: 1922bef1713094bb
@@ -1521,8 +1521,8 @@
en: 8a9199b1784c70f5
zh: 76c98bc74686fda0
/session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start:
- en: 74b8b426499a4203
- zh: 5400c0fe94ef9cdf
+ en: a6aabaa5aa9ef880
+ zh: 01d74ee2ceebda7c
/session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream:
en: ec10cd8e9c202a81
zh: e08f859bcb13f0d4
@@ -1830,8 +1830,8 @@
en: 809a23c25ded8497
zh: e9cd23e281f8d31f
/session-persistence-event-catalog/resolved-persistence-types/kind-user:
- en: 1dd9740285c43aff
- zh: 093fab99146cac11
+ en: 81620af099525ee9
+ zh: 812db2fdf9e5d309
/session-persistence-event-catalog/resolved-persistence-types/kind-user-approval:
en: a52d2dfbaf844127
zh: 4c5306ca40f20873
diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md
index 8815b79cd5..f1aaf40cff 100644
--- a/docs/persistence-catalog.md
+++ b/docs/persistence-catalog.md
@@ -185,7 +185,7 @@ Sources: [`packages/core/session/src/types.ts:432`](../packages/core/session/src
}
```
-Source: [`packages/core/agent/src/types.ts:138`](../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:107`](../packages/core/agent/src/types.ts)
### `agent-preset/*`
@@ -700,7 +700,7 @@ Source: [`packages/llm/llm-retry/src/types.ts:11`](../packages/llm/llm-retry/src
'model/selection': ModelSelection
```
-Source: [`packages/core/agent/src/types.ts:149`](../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:118`](../packages/core/agent/src/types.ts)
### `permission/*`
@@ -5575,7 +5575,7 @@ Array of `number`.
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
-Sources: [`packages/api/session-controller/src/types.ts:382`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
+Sources: [`packages/api/session-controller/src/types.ts:380`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -6369,7 +6369,7 @@ Sources: [`packages/core/session/src/types.ts:428`](../packages/core/session/src
SHA-256: `fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42`
-Sources: [`packages/core/agent/src/types.ts:138`](../packages/core/agent/src/types.ts)
+Sources: [`packages/core/agent/src/types.ts:107`](../packages/core/agent/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -7546,7 +7546,7 @@ Sources: [`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/inde
SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def`
-Sources: [`packages/api/session-controller/src/types.ts:391`](../packages/api/session-controller/src/types.ts)
+Sources: [`packages/api/session-controller/src/types.ts:389`](../packages/api/session-controller/src/types.ts)
| Property | Presence | Type |
|---|---|---|
diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md
index 461880aa21..54f6d9b072 100644
--- a/docs/persistence-catalog.zh.md
+++ b/docs/persistence-catalog.zh.md
@@ -187,7 +187,7 @@ export type SessionEvent = {
}
```
-来源:[`packages/core/agent/src/types.ts:138`](../packages/core/agent/src/types.ts)
+来源:[`packages/core/agent/src/types.ts:107`](../packages/core/agent/src/types.ts)
### `agent-preset/*`
@@ -702,7 +702,7 @@ export type SessionEvent = {
'model/selection': ModelSelection
```
-来源:[`packages/core/agent/src/types.ts:149`](../packages/core/agent/src/types.ts)
+来源:[`packages/core/agent/src/types.ts:118`](../packages/core/agent/src/types.ts)
### `permission/*`
@@ -5577,7 +5577,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
-来源:[`packages/api/session-controller/src/types.ts:382`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
+来源:[`packages/api/session-controller/src/types.ts:380`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -6371,7 +6371,7 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3`
SHA-256: `fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42`
-来源:[`packages/core/agent/src/types.ts:138`](../packages/core/agent/src/types.ts)
+来源:[`packages/core/agent/src/types.ts:107`](../packages/core/agent/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -7548,7 +7548,7 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def`
-来源:[`packages/api/session-controller/src/types.ts:391`](../packages/api/session-controller/src/types.ts)
+来源:[`packages/api/session-controller/src/types.ts:389`](../packages/api/session-controller/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json
index 0d01df773a..013d1a7d50 100644
--- a/docs/persistence-schema.json
+++ b/docs/persistence-schema.json
@@ -23546,7 +23546,7 @@
"packages/workflow/workflow/src/types.ts#WorkflowRunId"
],
"sources": [
- "packages/api/session-controller/src/types.ts:382",
+ "packages/api/session-controller/src/types.ts:380",
"packages/attachment/attachment/src/brand.ts:6",
"packages/compaction/compaction/src/brand.ts:4",
"packages/core/session/src/types.ts:21",
@@ -24068,7 +24068,7 @@
"digest": "6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def",
"names": [],
"sources": [
- "packages/api/session-controller/src/types.ts:391"
+ "packages/api/session-controller/src/types.ts:389"
]
},
{
@@ -26047,7 +26047,7 @@
"digest": "fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42",
"names": [],
"sources": [
- "packages/core/agent/src/types.ts:138"
+ "packages/core/agent/src/types.ts:107"
]
},
{
diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md
index d521ea543a..5976647ba4 100644
--- a/docs/subsystems/core.md
+++ b/docs/subsystems/core.md
@@ -524,6 +524,45 @@ Types: [SessionHeader](persistence.md)
Source: [`packages/core/agent-loop/src/index.ts`](../../packages/core/agent-loop/src/index.ts)
+
+
+### `ctx.agentModel` — `AgentModel`
+
+Read and record Session model choices without resolving adapter defaults.
+
+```ts cordis-catalog
+/**
+ * Resolve the choice for the next step to begin preparation. Recorded choices
+ * take precedence over the last request and then the Agent's live options.
+ * This read neither starts a step nor changes an already-prepared step.
+ * @param agent - Agent whose Session and fallback options are read.
+ * @returns a detached choice, or undefined when no source names both provider and model.
+ */
+resolveForNextStep(agent: Agent): Readonly | undefined
+
+/**
+ * Replace the next step's complete selection. Omitted effort follows adapter
+ * defaults. An equal recorded choice is a no-op; an options-only fallback is
+ * always recorded. Callers own model availability and capability validation.
+ * The append uses the Session's existing persistence and failure guarantees.
+ * @param agent - Agent whose Session receives the model/selection event.
+ * @param selection - complete provider, model, and optional explicit effort.
+ */
+select(agent: Agent, selection: Readonly): void
+
+/**
+ * Apply an explicit deployment route, including on Session resume. An omitted
+ * effort inherits the current explicit effort only for the same route;
+ * otherwise it follows adapter defaults. Call only when the entry point
+ * explicitly overrides recorded selection, not for ordinary resume fallback.
+ * @param agent - Agent whose Session receives the configured choice.
+ * @param selection - complete configured provider/model and optional effort.
+ */
+applyConfiguredRoute(agent: Agent, selection: Readonly): void
+```
+
+Source: [`packages/core/agent-model/src/service.ts`](../../packages/core/agent-model/src/service.ts)
+
### `ctx.agentPresets` — `AgentPresetRegistry`
@@ -982,14 +1021,14 @@ Reject a proposed step or replace the messages that enter it. Calling `next()` p
* @param payload.turn - the turn that will own the step.
* @param payload.step - the step proposed by the loop.
* @param payload.signal - the current turn's cancellation signal.
- * @param payload.callConfig - provider and model fixed for this step and its retries.
+ * @param payload.callConfig - model selection fixed for this step and its retries; omitted effort uses adapter defaults.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
-'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; callConfig: Readonly> }, next: () => Promise): Promise
+'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; callConfig: Readonly }, next: () => Promise): Promise
```
-Types: [LlmCallConfig](llm-streaming.md) · [Scoped](scope.md) · [UserMessage](session.md)
+Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md
index 005a29fafe..d872a7c010 100644
--- a/docs/subsystems/core.zh.md
+++ b/docs/subsystems/core.zh.md
@@ -536,6 +536,45 @@ Types: [SessionHeader](persistence.zh.md)
Source: [`packages/core/agent-loop/src/index.ts`](../../packages/core/agent-loop/src/index.ts)
+
+
+### `ctx.agentModel` — `AgentModel`
+
+Read and record Session model choices without resolving adapter defaults.
+
+```ts cordis-catalog
+/**
+ * Resolve the choice for the next step to begin preparation. Recorded choices
+ * take precedence over the last request and then the Agent's live options.
+ * This read neither starts a step nor changes an already-prepared step.
+ * @param agent - Agent whose Session and fallback options are read.
+ * @returns a detached choice, or undefined when no source names both provider and model.
+ */
+resolveForNextStep(agent: Agent): Readonly | undefined
+
+/**
+ * Replace the next step's complete selection. Omitted effort follows adapter
+ * defaults. An equal recorded choice is a no-op; an options-only fallback is
+ * always recorded. Callers own model availability and capability validation.
+ * The append uses the Session's existing persistence and failure guarantees.
+ * @param agent - Agent whose Session receives the model/selection event.
+ * @param selection - complete provider, model, and optional explicit effort.
+ */
+select(agent: Agent, selection: Readonly): void
+
+/**
+ * Apply an explicit deployment route, including on Session resume. An omitted
+ * effort inherits the current explicit effort only for the same route;
+ * otherwise it follows adapter defaults. Call only when the entry point
+ * explicitly overrides recorded selection, not for ordinary resume fallback.
+ * @param agent - Agent whose Session receives the configured choice.
+ * @param selection - complete configured provider/model and optional effort.
+ */
+applyConfiguredRoute(agent: Agent, selection: Readonly): void
+```
+
+Source: [`packages/core/agent-model/src/service.ts`](../../packages/core/agent-model/src/service.ts)
+
### `ctx.agentPresets` — `AgentPresetRegistry`
@@ -994,14 +1033,14 @@ Reject a proposed step or replace the messages that enter it. Calling `next()` p
* @param payload.turn - the turn that will own the step.
* @param payload.step - the step proposed by the loop.
* @param payload.signal - the current turn's cancellation signal.
- * @param payload.callConfig - provider and model fixed for this step and its retries.
+ * @param payload.callConfig - model selection fixed for this step and its retries; omitted effort uses adapter defaults.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
-'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; callConfig: Readonly> }, next: () => Promise): Promise
+'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; callConfig: Readonly }, next: () => Promise): Promise
```
-Types: [LlmCallConfig](llm-streaming.zh.md) · [Scoped](scope.zh.md) · [UserMessage](session.zh.md)
+Types: [Scoped](scope.zh.md) · [UserMessage](session.zh.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml
index d20afcaf09..368a82dc27 100644
--- a/docs/subsystems/llm-streaming.i18n.yaml
+++ b/docs/subsystems/llm-streaming.i18n.yaml
@@ -39,8 +39,8 @@
en: 2b0831aaae8e1cdf
zh: f669d93d1b9d6025
/llm-streaming/the-model-request/the-request-envelope-llmcallconfig-and-the-logged-header:
- en: 97abfab9178d6cdf
- zh: 97323c79492390f1
+ en: b0eb4620c6953646
+ zh: fabf51c7245a4855
/llm-streaming/official-deepseek-request-extensions:
en: 42bc50394b070e96
zh: 007caa743ec3c790
diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md
index ef493c2701..23b7c6c2bd 100644
--- a/docs/subsystems/llm-streaming.md
+++ b/docs/subsystems/llm-streaming.md
@@ -760,7 +760,7 @@ interface LlmDiscoveredModel {
The loop builds each request from logged state. `EpochHeader` records call config, marks the fields supplied by adapter defaults, and records the authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. The rendered prompt is derived history — the `system/message` at surface node 0, plus any later system node an `in-history` route appended — so the header and the derived history together make the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
-The deprecated `agent/request` hook remains available to existing listeners. New code selects models and reasoning effort through [Session model selection](../../packages/core/agent/README.md#use-this-package). The hook receives a frozen call-config seed based on the step's captured model selection and may return a replacement for reasoning effort or sampling. A replacement must preserve provider and model; record a new Session selection before the next step to change the route. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus the fields supplied by adapter defaults under the turn signal. On step admission, this waterfall and preparation run after assembly and `step/start` but before the system prompt and accepted user batch are committed; cancellation during either commits neither. The prepared capability governs prompt reconciliation, and the call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
+The deprecated `agent/request` hook remains available to existing listeners. New code selects models and reasoning effort through [Session model selection](../../packages/core/agent/README.md#use-this-package). The hook receives a frozen call-config seed based on the step's captured model selection and may replace controls such as sampling. A replacement must preserve provider, model, and the optional explicit reasoning effort; record a new Session selection before the next step to change them. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus the fields supplied by adapter defaults under the turn signal. On step admission, this waterfall and preparation run after assembly and `step/start` but before the system prompt and accepted user batch are committed; cancellation during either commits neither. The prepared capability governs prompt reconciliation, and the call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
On the wire, a loop-built request is the derived history alone: the rendered prompt travels as the leading `system`-role message (surface node 0, a `system/message` event) and, when the prepared call declares `systemPromptUpdate: 'in-history'`, a non-empty changed prompt may follow the cached history as a later `system`-role message that the model reads as the effective prompt; the request's `system` field is unset — `GenerateOptions.system` serves direct one-shot callers such as title providers. An empty rendering leaves no system messages in derived history, even when earlier requests retained several prompt versions. The logged request ends with the newest `user/message` on a turn's first step and the previous step's tool results on later steps.
diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md
index a09ccba4b3..98783a10e1 100644
--- a/docs/subsystems/llm-streaming.zh.md
+++ b/docs/subsystems/llm-streaming.zh.md
@@ -766,7 +766,7 @@ interface LlmDiscoveredModel {
循环从已记录状态构建每个请求。`EpochHeader` 记录调用配置,标记由适配器默认值提供的字段,并通过完整的 `request/header` 快照记录权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。渲染后的提示词是派生历史——surface 第 0 号节点上的 `system/message`,加上 `in-history` 路由追加的任何后续系统节点——因此请求头与派生历史共同使请求可由会话日志重建。见 [session.md](session.zh.md#the-request-header-event-requestheader) 与[可重建性 Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md)。
-已弃用的 `agent/request` hook 仍可供现有监听器使用。新代码通过 [Session 模型选择](../../packages/core/agent/README.zh.md#use-this-package) 设置模型与推理强度。该 hook 收到基于步骤已捕获模型选择的冻结调用配置初值,可以返回替换值来调整推理强度或采样。替换值必须保留提供方与模型;要改变路由,需在下一个步骤之前记录新的 Session 选择。waterfall(瀑布式事件)开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置以及由适配器默认值提供的字段。步骤准入时,该 waterfall 与准备过程在组装和 `step/start` 之后、系统提示词与已接纳用户批次提交之前运行;在任一阶段取消都不会提交这两者。已准备调用的能力决定提示词协调,调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
+已弃用的 `agent/request` hook 仍可供现有监听器使用。新代码通过 [Session 模型选择](../../packages/core/agent/README.zh.md#use-this-package) 设置模型与推理强度。该 hook 收到基于步骤已捕获模型选择的冻结调用配置初值,可以替换采样等控制参数。替换值必须保留提供方、模型及可选的显式推理强度;要改变这些选择,需在下一个步骤之前记录新的 Session 选择。waterfall(瀑布式事件)开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置以及由适配器默认值提供的字段。步骤准入时,该 waterfall 与准备过程在组装和 `step/start` 之后、系统提示词与已接纳用户批次提交之前运行;在任一阶段取消都不会提交这两者。已准备调用的能力决定提示词协调,调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
在协议中,循环构建的请求只有派生历史:渲染后的提示词作为开头的 `system` 角色消息(surface 第 0 号节点,即一个 `system/message` 事件)传输,并且当已准备调用声明 `systemPromptUpdate: 'in-history'` 时,变化后的非空提示词可以作为后续的 `system` 角色消息跟在已缓存历史之后,由模型读作有效提示词;请求的 `system` 字段不设置——`GenerateOptions.system` 服务于标题提供方等直接单次调用方。空渲染文本使派生历史不包含任何系统消息,即使先前请求保留了多个提示词版本。已记录的请求会以最新的 `user/message`(轮次首步)或上一步的工具结果(后续步骤)结尾。
diff --git a/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.i18n.yaml
index b086db6c7a..8b95ff903e 100644
--- a/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.i18n.yaml
+++ b/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.i18n.yaml
@@ -3,14 +3,14 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.md
/:
- en: b587cfd9808e132f
- zh: f3a5ddb2b458a751
+ en: 3890f393c1af5500
+ zh: 64fce473a9f35fae
/session-model-selection-applies-at-the-next-step:
en: e8582a68349890cd
zh: a385414164b09b8a
/session-model-selection-applies-at-the-next-step/change:
- en: c0591f3f484f17b7
- zh: 26e8688ed4d94771
+ en: 5a4adcaa813068c0
+ zh: 3f41f12ad64d1baa
/session-model-selection-applies-at-the-next-step/migration:
- en: bfe78d6dcdb58441
- zh: f33086495dcb1bae
+ en: 32a29602f2487011
+ zh: ae596527ef1c97b3
diff --git a/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.md b/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.md
index 1c27bdeccf..b78b2c97aa 100644
--- a/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.md
+++ b/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.md
@@ -1,6 +1,6 @@
---
kind: upgrade-guide
-description: "Session model choices now apply at step preparation, and request listeners must preserve the selected route."
+description: "Session model choices are fixed at step preparation, and projection types move to their owning package."
---
# Session model selection applies at the next step
@@ -11,15 +11,18 @@ English | [中文](guide.zh.md)
ACP model and reasoning changes take effect at the next model step, including a tool continuation in the current turn. Previously, ACP pinned the selection for the complete prompt turn. A prepared step and its retries retain their captured selection. Image admission checks the route selected when admission begins; a later option change can select a different route before the step starts.
-The Agent registry now requires `sessionProjections` and owns the durable `modelSelection` projection. Programmatic resume restores recorded choices, including a selection not yet used by a request, before falling back to `AgentOptions`. Configured Agent-loop resumes, headless adoption, and SDK resume explicitly record their deployment route.
+`dsh-agent-model` provides `ctx.agentModel` and owns the durable `modelSelection` projection. The Agent registry no longer registers it. Programmatic resume restores recorded choices, including unused selections, before falling back to `AgentOptions`. Configured Agent-loop resumes, headless adoption, and SDK resume record their deployment route; an omitted effort preserves saved explicit effort only for the same route.
-Plugins must select a complete provider/model route before prompt assembly. `agent/request` is deprecated. Existing listeners still run on each main-loop request attempt and can adjust controls, but provider or model changes are rejected. Auxiliary calls such as compaction do not dispatch it. `installModelSelection` and `ModelSelectionRef` are removed; selection readers and writers use the shared Session projection.
+Plugins must select provider, model, and optional explicit reasoning effort before prompt assembly. `agent/request` is deprecated. Existing listeners still run on each main-loop request attempt and can adjust other controls, but cannot change provider or model, or add, change, or clear reasoning effort. Absent effort still resolves through adapter defaults on each attempt. Auxiliary calls such as compaction do not dispatch this event. `installModelSelection` and `ModelSelectionRef` are removed.
+
+The client projection type moves to `dsh-agent-model/types`; API package re-exports and the named Host projection-state export are removed.
## Migration
-1. In custom Cordis compositions, mount `dsh-session-projection` before `dsh-agent`.
-2. Do not add new `agent/request` listeners. Replace mutable selection references and listeners that select provider, model, or reasoning effort with `recordModelSelection(ctx.sessionProjections, agent, selection)` from `@deepseek-ai/dsh-agent`. Validate the route before recording it. Use `currentModelSelection()` to read the choice and `installModelSwitchNotice(agent.ctx)` when the composition needs a durable model-switch notice.
+1. In custom Cordis compositions, mount `dsh-session-projection` and `dsh-agent-model`. Consumers inject `agentModel`; shipped bundles include it.
+2. Replace mutable references and selection hooks with `ctx.agentModel.select(agent, selection)` after validation. Read with `resolveForNextStep(agent)`. Replace switch-notice helpers by importing the `@deepseek-ai/dsh-agent-model/notice` plugin and awaiting `agentCtx.plugin(ModelSwitchNotice)` in Agent setup. Do not add `agent/request` listeners.
3. Set the output cap through `AgentOptions.maxTokens`. Other per-request control overrides have no replacement API; retain an existing listener where that behavior is still required.
-4. To override a saved route during programmatic resume, call `recordDeclaredRoute()` in the Agent setup callback. Supplying `agentOptions` alone provides only a fallback for a Session without a selection or request.
-5. Pass the fixed route in `agent/pre-step.callConfig` when dispatching that event. Direct `SessionReferenceResolver.prepare()` callers pass the route as the fourth argument, before the optional signal.
+4. To override a saved route during programmatic resume, call `ctx.agentModel.applyConfiguredRoute(agent, selection)` in the Agent setup callback. Supplying `agentOptions` alone provides only a fallback for a Session without a selection or request.
+5. Pass the captured `Readonly` in `agent/pre-step.callConfig`, including explicit effort when selected; `AssembleContext.callConfig` carries the same selection. Direct `SessionReferenceResolver.prepare()` callers pass the route as the fourth argument, before the optional signal.
6. ACP controllers that require one model throughout a turn must wait for the active prompt to finish before changing its options. Verify that a mid-turn change is recorded as `model/selection`, the next step uses it, and resume retains an unused choice.
+7. Import `ModelSelectionProjection` from `@deepseek-ai/dsh-agent-model/types`, replacing Agent/API package aliases. `ModelSelection` remains in `dsh-agent/types`. Replace named Host state imports with `SessionProjectionStateMap['modelSelection']` after importing the new `/types` module; ordinary reads use `resolveForNextStep()`. Effort values use `ReasoningEffortId` from `dsh-llm/brand`.
diff --git a/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.zh.md
index 68a9da20d5..bb2c5edebc 100644
--- a/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.zh.md
+++ b/docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.zh.md
@@ -1,6 +1,6 @@
---
kind: upgrade-guide
-description: "Session 模型选择现在于步骤准备时生效,请求监听器必须保留已选路由。"
+description: "Session 模型选择在步骤准备时固定,投影类型移至其所属包。"
---
# Session 模型选择从下一个步骤开始生效
@@ -11,15 +11,18 @@ description: "Session 模型选择现在于步骤准备时生效,请求监听
ACP(Agent Client Protocol)的模型与推理强度变更从下一个模型步骤开始生效,包括当前轮次的工具后续步骤。此前,ACP 会在整个提示词轮次中固定选择。已准备的步骤及其重试保留捕获的选择。图片准入检查准入开始时选择的路由;后续选项变更可能在步骤开始前选中另一条路由。
-Agent 注册表现在依赖 `sessionProjections`,并拥有持久 `modelSelection` 投影。程序化恢复优先还原记录中的选择,包括尚未被请求使用的选择,然后才回退到 `AgentOptions`。配置式 Agent-loop 恢复、headless 接管与 SDK 恢复会显式记录各自的部署路由。
+`dsh-agent-model` 提供 `ctx.agentModel`,并拥有持久 `modelSelection` 投影。Agent 注册表不再注册该投影。程序化恢复优先还原记录中的选择,包括尚未使用的选择,然后才回退到 `AgentOptions`。配置式 Agent-loop 恢复、headless 接管与 SDK 恢复会记录各自的部署路由;省略推理强度时,仅在路由相同的情况下保留已保存的显式强度。
-插件必须在提示词组装前选定完整的提供方/模型路由。`agent/request` 已弃用。现有监听器仍在主循环的每次请求尝试中运行,可以调整控制参数,但提供方或模型变更会被拒绝。压缩等辅助调用不会分发该事件。`installModelSelection` 与 `ModelSelectionRef` 已移除,选择的读取方与写入方使用共享 Session 投影。
+插件必须在提示词组装前选定提供方、模型及可选的显式推理强度。`agent/request` 已弃用。现有监听器仍在主循环的每次请求尝试中运行,可以调整其他控制参数,但不能改变提供方或模型,也不能添加、修改或清除推理强度。未指定的强度仍在每次尝试时通过适配器默认值解析。压缩等辅助调用不会分发该事件。`installModelSelection` 与 `ModelSelectionRef` 已移除。
+
+客户端投影类型移至 `dsh-agent-model/types`;API 包的转导出与具名 Host 投影状态导出已移除。
## 迁移
-1. 在自定义 Cordis 组合中,先挂载 `dsh-session-projection`,再挂载 `dsh-agent`。
-2. 不要添加新的 `agent/request` 监听器。将可变选择引用与选择提供方、模型或推理强度的监听器替换为 `@deepseek-ai/dsh-agent` 导出的 `recordModelSelection(ctx.sessionProjections, agent, selection)`。记录前先校验路由。使用 `currentModelSelection()` 读取选择;组合需要持久模型切换提示时,调用 `installModelSwitchNotice(agent.ctx)`。
+1. 在自定义 Cordis 组合中挂载 `dsh-session-projection` 与 `dsh-agent-model`。消费方注入 `agentModel`;随附 bundle 已包含该服务。
+2. 将可变引用与选择 hook 替换为校验后的 `ctx.agentModel.select(agent, selection)`,使用 `resolveForNextStep(agent)` 读取选择。将切换提示 helper 替换为导入 `@deepseek-ai/dsh-agent-model/notice` 插件,并在 Agent setup 中等待 `agentCtx.plugin(ModelSwitchNotice)`。不要添加 `agent/request` 监听器。
3. 通过 `AgentOptions.maxTokens` 设置输出上限。其他逐请求控制参数覆盖没有替代 API;仍需要该行为时,保留现有监听器。
-4. 要在程序化恢复时覆盖已保存路由,在 Agent setup 回调中调用 `recordDeclaredRoute()`。仅提供 `agentOptions` 时,它只为没有选择或请求记录的 Session 提供回退值。
-5. 分发 `agent/pre-step` 时通过 `callConfig` 传入固定路由。直接调用 `SessionReferenceResolver.prepare()` 时,将路由作为第四个参数,放在可选信号之前。
+4. 要在程序化恢复时覆盖已保存路由,在 Agent setup 回调中调用 `ctx.agentModel.applyConfiguredRoute(agent, selection)`。仅提供 `agentOptions` 时,它只为没有选择或请求记录的 Session 提供回退值。
+5. 通过 `agent/pre-step.callConfig` 传入捕获的 `Readonly`,包含已选定的显式强度;`AssembleContext.callConfig` 携带同一选择。直接调用 `SessionReferenceResolver.prepare()` 时,将路由作为第四个参数,放在可选信号之前。
6. 要求整轮使用同一个模型的 ACP 控制器,必须等待活跃提示词完成后再更改选项。验证轮次中的变更被记录为 `model/selection`、下一个步骤使用该选择,且恢复保留尚未使用的选择。
+7. 从 `@deepseek-ai/dsh-agent-model/types` 导入 `ModelSelectionProjection`,替换 Agent/API 包别名。`ModelSelection` 仍位于 `dsh-agent/types`。导入新 `/types` 模块后,将具名 Host 状态导入替换为 `SessionProjectionStateMap['modelSelection']`;普通读取使用 `resolveForNextStep()`。强度值使用 `dsh-llm/brand` 中的 `ReasoningEffortId`。
diff --git a/packages/acp/acp/package.json b/packages/acp/acp/package.json
index d846a8a6dc..15407df948 100644
--- a/packages/acp/acp/package.json
+++ b/packages/acp/acp/package.json
@@ -40,7 +40,8 @@
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-persistence": "workspace:*",
"@deepseek-ai/dsh-token-meter": "workspace:*",
- "@deepseek-ai/dsh-user-approval": "workspace:*"
+ "@deepseek-ai/dsh-user-approval": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"peerDependenciesMeta": {
"@deepseek-ai/dsh-token-meter": {
@@ -62,6 +63,7 @@
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-token-meter": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
- "@deepseek-ai/dsh-user-approval": "workspace:*"
+ "@deepseek-ai/dsh-user-approval": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/acp/acp/src/index.ts b/packages/acp/acp/src/index.ts
index 5d72e5990f..32211c1856 100644
--- a/packages/acp/acp/src/index.ts
+++ b/packages/acp/acp/src/index.ts
@@ -16,6 +16,7 @@ import { realpath } from 'node:fs/promises'
import { isAbsolute, resolve } from 'node:path'
import { Readable, Writable } from 'node:stream'
import Schema from '@deepseek-ai/schemastery'
+import type {} from '@deepseek-ai/dsh-agent-model'
import { brandString } from '@deepseek-ai/dsh-brand'
import { errorChain } from '@deepseek-ai/dsh-llm'
import {
@@ -58,7 +59,7 @@ const DEFAULT_SESSION_LIST_PAGE_SIZE = 100
export const name = 'acp'
/** Core services required by the standard automation controls. */
-export const inject = ['agents', 'llm', 'sessionPersistence', 'sessionProjections', 'sessions']
+export const inject = ['agents', 'llm', 'sessionPersistence', 'agentModel', 'sessions']
/** Preserve invalid-parameter detail in the SDK wire error message. */
function invalidParams(detail: string): RequestError {
diff --git a/packages/acp/acp/src/model-control.ts b/packages/acp/acp/src/model-control.ts
index a6c6a6840f..95541c9d54 100644
--- a/packages/acp/acp/src/model-control.ts
+++ b/packages/acp/acp/src/model-control.ts
@@ -1,9 +1,9 @@
/** Standard ACP session configuration over one Agent's model selection. */
import type { SessionConfigOption, SessionConfigValueId } from '@agentclientprotocol/sdk'
-import { currentModelSelection, recordModelSelection, type Agent, type ModelSelection } from '@deepseek-ai/dsh-agent'
+import { type Agent, type ModelSelection } from '@deepseek-ai/dsh-agent'
+import type { AgentModel } from '@deepseek-ai/dsh-agent-model'
import { ReasoningEffortId, type LlmCallConfig, type LlmRuntime } from '@deepseek-ai/dsh-llm'
-import type SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
const MODEL_CONFIG_ID = 'model'
const REASONING_CONFIG_ID = 'reasoning_effort'
@@ -39,7 +39,7 @@ export class AcpModelControl {
constructor(
private readonly llm: LlmRuntime,
- private readonly projections: SessionProjectionRegistry,
+ private readonly agentModel: AgentModel,
private readonly agent: Agent,
) {}
@@ -48,7 +48,7 @@ export class AcpModelControl {
* @returns a detached selection, or undefined when neither the Session nor the deployment names a route.
*/
current(): ModelSelection | undefined {
- return currentModelSelection(this.projections, this.agent)
+ return this.agentModel.resolveForNextStep(this.agent)
}
/**
@@ -77,7 +77,7 @@ export class AcpModelControl {
const selected = state.choices.get(value)
if (selected === undefined) throw new AcpModelConfigError(`unknown model option: ${value}`)
await this.resolveSelection(selected, signal)
- recordModelSelection(this.projections, this.agent, selected)
+ this.agentModel.select(this.agent, selected)
} else if (configId === REASONING_CONFIG_ID) {
const info = await this.llm.resolveModelInfo(current.provider, current.model, signal)
const providerDefault = value === PROVIDER_DEFAULT_REASONING_VALUE
@@ -88,7 +88,7 @@ export class AcpModelControl {
) {
throw new AcpModelConfigError(`unknown reasoning effort for ${current.provider}/${current.model}: ${value}`)
}
- recordModelSelection(this.projections, this.agent, await this.resolveSelection({
+ this.agentModel.select(this.agent, await this.resolveSelection({
provider: current.provider,
model: current.model,
...providerDefault ? {} : { reasoningEffort: ReasoningEffortId(value) },
diff --git a/packages/acp/acp/src/session.ts b/packages/acp/acp/src/session.ts
index b2e0c59079..b8243e71ce 100644
--- a/packages/acp/acp/src/session.ts
+++ b/packages/acp/acp/src/session.ts
@@ -10,8 +10,8 @@ import {
type SessionNotification,
type StopReason,
} from '@agentclientprotocol/sdk'
-import { installModelSwitchNotice } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentHandle, AgentOptions, AgentStatus } from '@deepseek-ai/dsh-agent'
+import * as ModelSwitchNotice from '@deepseek-ai/dsh-agent-model/notice'
import { createUserMessage, errorChain, type UserMessage } from '@deepseek-ai/dsh-llm'
import { type Session, type SessionEvent, type SessionId, type TurnEndReason } from '@deepseek-ai/dsh-session'
import { AcpContentError, admitAcpPrompt } from './content.ts'
@@ -80,7 +80,7 @@ function internalError(detail: string): RequestError {
* @param options - workspace and MCP servers requested for the session.
*/
async function composeAgent(agentCtx: Context, options: AcpSessionBuildOptions): Promise {
- installModelSwitchNotice(agentCtx)
+ await agentCtx.plugin(ModelSwitchNotice)
await mountAcpMcpServers(agentCtx, options.mcpServers, options.cwd)
}
@@ -103,7 +103,7 @@ export class AcpSession {
private readonly notify: (notification: SessionNotification) => Promise,
) {
this.agent = handle.agent
- this.modelControl = new AcpModelControl(ctx.llm, ctx.sessionProjections, handle.agent)
+ this.modelControl = new AcpModelControl(ctx.llm, ctx.agentModel, handle.agent)
this.disposeAgent = () => handle.dispose()
}
@@ -173,7 +173,7 @@ export class AcpSession {
}
/**
- * Apply one standard configuration option to later ACP turns.
+ * Apply one standard configuration option from the next model step.
* @param configId - advertised standard option id.
* @param value - selected standard option value.
* @param signal - optional request cancellation.
diff --git a/packages/acp/acp/tests/bridge.spec.ts b/packages/acp/acp/tests/bridge.spec.ts
index e0bb334fc5..9b3d21a1ea 100644
--- a/packages/acp/acp/tests/bridge.spec.ts
+++ b/packages/acp/acp/tests/bridge.spec.ts
@@ -1,4 +1,3 @@
-import { recordModelSelection } from '@deepseek-ai/dsh-agent'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { mkdtemp, rm } from 'node:fs/promises'
@@ -863,7 +862,7 @@ describe('automation-only ACP bridge', () => {
expect(created.configOptions).toEqual([])
const agent = harness.ctx.agents.get(SessionId(created.sessionId))!
- recordModelSelection(harness.ctx.sessionProjections, agent, { provider: 'mock', model: 'mock' })
+ harness.ctx.agentModel.select(agent, { provider: 'mock', model: 'mock' })
await expect(harness.client.prompt({
sessionId: created.sessionId,
prompt: [{ type: 'text', text: 'route me' }],
diff --git a/packages/acp/acp/tests/model-control.spec.ts b/packages/acp/acp/tests/model-control.spec.ts
index 174e5f48d5..701b39b72e 100644
--- a/packages/acp/acp/tests/model-control.spec.ts
+++ b/packages/acp/acp/tests/model-control.spec.ts
@@ -1,5 +1,6 @@
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent, type ModelSelection } from '@deepseek-ai/dsh-agent'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { describe, expect, it, onTestFinished, vi } from 'vitest'
@@ -39,10 +40,11 @@ async function createControl(runtime: LlmRuntime, initial?: ModelSelection) {
onTestFinished(() => ctx.fiber.dispose())
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(SessionId('model-control'))
const agent = { id: session.id, session, options: initial ?? {} } as Agent
- return { control: new AcpModelControl(runtime, ctx.sessionProjections, agent), session }
+ return { control: new AcpModelControl(runtime, ctx.agentModel, agent), session }
}
describe('ACP model configuration control', () => {
diff --git a/packages/acp/acp/tsconfig.json b/packages/acp/acp/tsconfig.json
index ea1239b9f1..92a0e595f2 100644
--- a/packages/acp/acp/tsconfig.json
+++ b/packages/acp/acp/tsconfig.json
@@ -40,6 +40,9 @@
},
{
"path": "../../interaction/user-approval"
+ },
+ {
+ "path": "../../core/agent-model"
}
]
}
diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json
index c48a6b4fe4..2e93a52162 100644
--- a/packages/api/session-controller/package.json
+++ b/packages/api/session-controller/package.json
@@ -108,7 +108,8 @@
"@deepseek-ai/dsh-util-values": "workspace:*",
"@deepseek-ai/dsh-util-workspace-path": "workspace:*",
"@deepseek-ai/dsh-workspace": "workspace:*",
- "@deepseek-ai/dsh-working-directory": "workspace:*"
+ "@deepseek-ai/dsh-working-directory": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"peerDependenciesMeta": {
"@deepseek-ai/dsh-session-persistence": {
@@ -166,6 +167,7 @@
"@deepseek-ai/dsh-util-workspace-path": "workspace:*",
"@deepseek-ai/dsh-workspace": "workspace:*",
"@types/mime-types": "^3.0.1",
- "@deepseek-ai/dsh-working-directory": "workspace:*"
+ "@deepseek-ai/dsh-working-directory": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/api/session-controller/src/agent.ts b/packages/api/session-controller/src/agent.ts
index 79aae5b468..3d7c268a33 100644
--- a/packages/api/session-controller/src/agent.ts
+++ b/packages/api/session-controller/src/agent.ts
@@ -3,9 +3,9 @@
import { randomUUID } from 'node:crypto'
import { mkdir } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
-import { installModelSwitchNotice } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentOptions, AgentSetup } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-default-model'
+import * as ModelSwitchNotice from '@deepseek-ai/dsh-agent-model/notice'
import type {} from '@deepseek-ai/dsh-agent-preset-registry'
import { brandString } from '@deepseek-ai/dsh-brand'
import type { Session, SessionId } from '@deepseek-ai/dsh-session'
@@ -300,13 +300,13 @@ export class ApiSessionAgentController {
async composeAgent(presetId: string | undefined): Promise {
const presets = this.ctx.get('agentPresets')
if (presets === undefined) {
- return { setup: (agentCtx) => { installModelSwitchNotice(agentCtx) } }
+ return { setup: async (agentCtx) => { await agentCtx.plugin(ModelSwitchNotice) } }
}
const resolvedId = (await presets.resolve(presetId)).id
return {
agentPreset: resolvedId,
setup: async (agentCtx) => {
- installModelSwitchNotice(agentCtx)
+ await agentCtx.plugin(ModelSwitchNotice)
await presets.mount(agentCtx, resolvedId)
},
}
diff --git a/packages/api/session-controller/src/commands.ts b/packages/api/session-controller/src/commands.ts
index fa2dd4a66d..f2de7028b9 100644
--- a/packages/api/session-controller/src/commands.ts
+++ b/packages/api/session-controller/src/commands.ts
@@ -4,7 +4,6 @@ import { modelAvailable } from './catalog.ts'
import { randomUUID } from 'node:crypto'
import type { Context } from '@deepseek-ai/cordis'
import { brandString } from '@deepseek-ai/dsh-brand'
-import { currentModelSelection, recordModelSelection } from '@deepseek-ai/dsh-agent'
import type { Agent, ModelSelection as AgentModelSelection } from '@deepseek-ai/dsh-agent'
import { AttachmentError } from '@deepseek-ai/dsh-attachment'
import type {
@@ -174,7 +173,7 @@ export class SessionCommandController {
? {}
: { reasoningEffort: resolved.reasoningEffort }),
}
- recordModelSelection(this.ctx.sessionProjections, agent, selected)
+ this.ctx.agentModel.select(agent, selected)
void this.ctx.agentDefaultModel.saveSelection(selected).catch((error: unknown) => {
this.ctx.logger.warn(
`session-controller: model selection changed for the Session but the default was not saved: ${String(error)}`,
@@ -323,7 +322,7 @@ export class SessionCommandController {
const hasImage = request.content.some(part => part.type === 'image')
const admit = async (): Promise => {
try {
- const current = hasImage ? currentModelSelection(this.ctx.sessionProjections, agent) : undefined
+ const current = hasImage ? this.ctx.agentModel.resolveForNextStep(agent) : undefined
if (current !== undefined) {
const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model)
if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
diff --git a/packages/api/session-controller/src/index.ts b/packages/api/session-controller/src/index.ts
index ad94e63364..9814aa0b1b 100644
--- a/packages/api/session-controller/src/index.ts
+++ b/packages/api/session-controller/src/index.ts
@@ -3,6 +3,7 @@
import { hostname } from 'node:os'
import { resolve } from 'node:path'
import type { Agent } from '@deepseek-ai/dsh-agent'
+import type {} from '@deepseek-ai/dsh-agent-model'
import type {} from '@deepseek-ai/dsh-fs'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
@@ -126,6 +127,7 @@ export class SessionController extends TypertRemoteService {
'remoteAccess',
'sessions',
'sessionProjections',
+ 'agentModel',
'sessionQuery',
'typert',
'workspaceRegistry',
diff --git a/packages/api/session-controller/src/types.ts b/packages/api/session-controller/src/types.ts
index e1a00a0851..5974171077 100644
--- a/packages/api/session-controller/src/types.ts
+++ b/packages/api/session-controller/src/types.ts
@@ -1,6 +1,7 @@
/** Browser-safe request, result, and lifecycle vocabulary for the Session Remote service. */
-import type { NativeFileApplication } from '@deepseek-ai/dsh-native-command/types'
+import type {} from '@deepseek-ai/dsh-agent-model/types'
+import type { NativeFileApplication } from '@deepseek-ai/dsh-native-command/types'
import type {
AttachmentIdType, ImageAttachmentLimits, ImageAttachmentRef, ImageMediaType,
} from '@deepseek-ai/dsh-attachment'
@@ -90,9 +91,6 @@ export interface ModelSelection {
readonly reasoningEffort?: string
}
-/** Client view of the durable model-selection fold owned by the Agent loop. */
-export type { ModelSelectionProjection } from '@deepseek-ai/dsh-agent/types'
-
/** One adapter-owned reasoning effort for an exact model route. */
export interface ModelReasoningEffort {
readonly id: string
diff --git a/packages/api/session-controller/tests/commands-upload-file.host.spec.ts b/packages/api/session-controller/tests/commands-upload-file.host.spec.ts
index 06dc357073..5e6cf0592a 100644
--- a/packages/api/session-controller/tests/commands-upload-file.host.spec.ts
+++ b/packages/api/session-controller/tests/commands-upload-file.host.spec.ts
@@ -4,6 +4,7 @@ import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit'
import type { Agent } from '@deepseek-ai/dsh-agent'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import AttachmentStore, { AttachmentId } from '@deepseek-ai/dsh-attachment'
import type {
FileAttachmentRef, ImageAttachmentRef, SaveFileAttachment, SaveFileStreamAttachment,
@@ -38,6 +39,7 @@ async function uploadHarness(origin?: 'subagent'): Promise<{
onTestFinished(() => ctx.fiber.dispose())
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
await ctx.plugin(TypertRegistry)
await ctx.plugin(CommandRuntime)
diff --git a/packages/api/session-controller/tests/controller.host.spec.ts b/packages/api/session-controller/tests/controller.host.spec.ts
index c2691736f4..7673d7ba9a 100644
--- a/packages/api/session-controller/tests/controller.host.spec.ts
+++ b/packages/api/session-controller/tests/controller.host.spec.ts
@@ -2,6 +2,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
@@ -25,6 +26,7 @@ describe('SessionController facade', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = SessionId('controller-session')
const header: SessionHeader = {
diff --git a/packages/api/session-controller/tests/session-cold.host.spec.ts b/packages/api/session-controller/tests/session-cold.host.spec.ts
index 8a380b942f..af88bc7373 100644
--- a/packages/api/session-controller/tests/session-cold.host.spec.ts
+++ b/packages/api/session-controller/tests/session-cold.host.spec.ts
@@ -17,6 +17,7 @@ import { createUserMessage, MessageId } from '@deepseek-ai/dsh-llm'
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import { createInboxStub, mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit'
import type { Agent, Inbox } from '@deepseek-ai/dsh-agent'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import AttachmentStore from '@deepseek-ai/dsh-attachment'
import type { SessionPromptRequest, SessionRequestId } from '../src/types.ts'
@@ -83,6 +84,7 @@ describe('sessions.list cold merge', () => {
try {
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const historical = header('format-historical', 100)
const current = header('format-current', 200)
@@ -249,6 +251,7 @@ describe('attached updatedAt tracks human prompts', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
await new Promise(resolve => setTimeout(resolve, 0))
@@ -394,6 +397,7 @@ describe('Remote Agent and Session lookup policy', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
@@ -415,6 +419,7 @@ describe('Remote Agent and Session lookup policy', () => {
await ctx.plugin(TypertRegistry)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('session-remote-cold')
const meta = header(sessionId, 1000)
@@ -456,6 +461,7 @@ describe('Remote Agent and Session lookup policy', () => {
await ctx.plugin(TypertRegistry)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const coldId = sid('session-remote-cold-child')
const coldMeta = header(coldId, 1000, {
@@ -501,6 +507,7 @@ describe('Remote Agent and Session lookup policy', () => {
await ctx.plugin(TypertRegistry)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('session-remote-resumed-child')
const meta = header(sessionId, 1000)
@@ -537,6 +544,7 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('session-child')
const meta = header('session-child', 1000, {
@@ -616,6 +624,7 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('session-legacy-child')
const meta = header('session-legacy-child', 1000, {
@@ -654,6 +663,7 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const parentSession = ctx.sessions.create(sid('session-parent'), { meta: { cwd: '/proj' } })
const parent = { id: parentSession.id, session: parentSession, status: 'idle', ctx } as Agent
@@ -714,6 +724,7 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(sid('session-ordinary-fork'), {
seed: [{
@@ -745,6 +756,7 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(sid('session-browser-zone'), { meta: { cwd: '/proj' } })
const followup = vi.fn()
@@ -817,6 +829,7 @@ describe('degenerate composition (no persistence, no factory)', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
@@ -839,6 +852,7 @@ describe('degenerate composition (no persistence, no factory)', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const inspect = vi.fn()
const stat = vi.fn(() => Promise.resolve(undefined))
@@ -866,6 +880,7 @@ describe('sessions.prompt synchronous rejection', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(sid('session-empty-prompt'))
const followup = vi.fn()
@@ -969,6 +984,7 @@ describe('sessions.prompt synchronous rejection', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(sid('session-throwing'))
// A live structural stub whose delivery verbs throw synchronously, the
@@ -1003,6 +1019,7 @@ describe('sessions.prompt synchronous rejection', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('race-resume')
const meta: SessionHeader = header('race-resume', 1000)
diff --git a/packages/api/session-controller/tests/session-fork.host.spec.ts b/packages/api/session-controller/tests/session-fork.host.spec.ts
index fea1d64a35..9e2a537ed0 100644
--- a/packages/api/session-controller/tests/session-fork.host.spec.ts
+++ b/packages/api/session-controller/tests/session-fork.host.spec.ts
@@ -3,9 +3,10 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { currentModelSelection } from '@deepseek-ai/dsh-agent'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { ToolCallId, createMessage, createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
@@ -33,6 +34,7 @@ async function composed(workspaces: readonly Workspace[] = []): Promise
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
installSessionReadTestServices(ctx)
ctx.provide('workspaceRegistry', { list: () => workspaces, archivedSessionIds: [] } as never)
@@ -663,7 +665,7 @@ describe('sessions.fork', () => {
const child = ctx.agents.get(response.value.sessionId)
if (child === undefined) throw new Error('fork did not publish the child agent')
expect(child.session.snapshotEvents().slice(0, child.session.inheritedEventCount)).toEqual(inherited)
- expect(currentModelSelection(ctx.sessionProjections, child)).toEqual({
+ expect(ctx.agentModel.resolveForNextStep(child)).toEqual({
provider: 'inherited-provider', model: 'inherited-model', reasoningEffort: 'high',
})
await ctx.fiber.dispose()
diff --git a/packages/api/session-controller/tests/session-models.host.spec.ts b/packages/api/session-controller/tests/session-models.host.spec.ts
index 02e4de8450..e5b640c557 100644
--- a/packages/api/session-controller/tests/session-models.host.spec.ts
+++ b/packages/api/session-controller/tests/session-models.host.spec.ts
@@ -9,8 +9,9 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { describe, expect, it, vi, onTestFinished } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { currentModelSelection } from '@deepseek-ai/dsh-agent'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import AttachmentStore from '@deepseek-ai/dsh-attachment'
import LlmRuntime, { LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type {
@@ -98,6 +99,7 @@ async function harness(logged?: {
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', [
{ provider: 'deepseek-official', id: 'deepseek-chat', name: 'DeepSeek Chat' },
@@ -518,7 +520,7 @@ describe('Web session model selection', () => {
model: 'deepseek-reasoner',
reasoningEffort: 'max',
})
- expect(currentModelSelection(ctx.sessionProjections, agent)).toEqual(selected.selected)
+ expect(ctx.agentModel.resolveForNextStep(agent)).toEqual(selected.selected)
expect(agent.session.snapshotEvents().at(-1)).toMatchObject({
type: 'model/selection', data: selected.selected,
})
@@ -589,9 +591,9 @@ describe('Web session model selection', () => {
const first = expectValue(await remote.create({ cwd: '/tmp' }))
const second = expectValue(await remote.create({ cwd: '/tmp' }))
const agent = ctx.agents.get(first.sessionId)!
- expect(currentModelSelection(ctx.sessionProjections, agent)).toEqual(stored)
+ expect(ctx.agentModel.resolveForNextStep(agent)).toEqual(stored)
stored = { provider: 'deepseek-official', model: 'deepseek-reasoner' }
- expect(currentModelSelection(ctx.sessionProjections, agent)).toEqual(stored)
+ expect(ctx.agentModel.resolveForNextStep(agent)).toEqual(stored)
const selection = { ...stored, reasoningEffort: ReasoningEffortId('high') }
// The explicit selection is identical to the live fallback, including effort.
stored = selection
@@ -599,7 +601,7 @@ describe('Web session model selection', () => {
expect(agent.session.snapshotEvents().filter(event => event.type === 'model/selection'))
.toMatchObject([{ data: selection }])
expectValue(await remote.selectModel({ sessionId: second.sessionId, provider: 'deepseek-official', model: 'deepseek-chat' }))
- expect(currentModelSelection(ctx.sessionProjections, agent)).toEqual(selection)
+ expect(ctx.agentModel.resolveForNextStep(agent)).toEqual(selection)
expect(currentSelection(ctx, first.sessionId)).toEqual(selection)
})
@@ -632,7 +634,7 @@ describe('Web session model selection', () => {
cwd: '/tmp',
})
- expect(currentModelSelection(ctx.sessionProjections, agent))
+ expect(ctx.agentModel.resolveForNextStep(agent))
.toEqual({ provider: 'deepseek-official', model: 'deepseek-chat' })
await ctx.fiber.dispose()
})
diff --git a/packages/api/session-controller/tests/session-projections.host.spec.ts b/packages/api/session-controller/tests/session-projections.host.spec.ts
index ed4f4a6b0c..43a870f7e0 100644
--- a/packages/api/session-controller/tests/session-projections.host.spec.ts
+++ b/packages/api/session-controller/tests/session-projections.host.spec.ts
@@ -31,6 +31,7 @@ import {
mountAgentLoopTestDependencies,
mountAgentLoopTestHarness,
} from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { createSessionTestRemote, testSessionPersistence, type TestSessionRemote } from './test-remote.ts'
const ownedContexts = new Set()
@@ -168,6 +169,7 @@ describe('session.history projections block', () => {
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
const parent = ctx.sessions.create(SessionId('wire-seed-parent'), { meta: { cwd: '/workspace' } })
parent.append('turn/start', { turn: 1 })
parent.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
@@ -580,6 +582,7 @@ describe('session.list projections column', () => {
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
ctx.sessionProjections.register(privatePromptUnit())
await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
const gateway = remote(ctx)
@@ -635,6 +638,7 @@ describe('session.list projections column', () => {
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
ctx.sessionProjections.register(titleProjectionDefinition)
await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
return ctx
diff --git a/packages/api/session-controller/tsconfig.client.json b/packages/api/session-controller/tsconfig.client.json
index a1375420ca..d49ddb1620 100644
--- a/packages/api/session-controller/tsconfig.client.json
+++ b/packages/api/session-controller/tsconfig.client.json
@@ -11,24 +11,65 @@
"src/remote-events.ts"
],
"references": [
- { "path": "../../../vendor/cordis" },
- { "path": "../gateway/tsconfig.client.json" },
- { "path": "../../attachment/attachment" },
- { "path": "../../client/connection/tsconfig.client.json" },
- { "path": "../../client/file-upload/tsconfig.client.json" },
- { "path": "../../client/store" },
- { "path": "../../context/file-reference" },
- { "path": "../../core/session" },
- { "path": "../../jobs/jobs" },
- { "path": "../../llm/llm" },
- { "path": "../../session/session-projection" },
- { "path": "../../session/session-title" },
- { "path": "../../subagent/subagent" },
- { "path": "../../util/brand" },
- { "path": "../../util/crypto" },
- { "path": "../../util/workspace-path" },
- { "path": "../../workspace/workspace" },
- { "path": "../../typert/protocol" },
- { "path": "../../util/native-command" }
+ {
+ "path": "../../../vendor/cordis"
+ },
+ {
+ "path": "../gateway/tsconfig.client.json"
+ },
+ {
+ "path": "../../attachment/attachment"
+ },
+ {
+ "path": "../../client/connection/tsconfig.client.json"
+ },
+ {
+ "path": "../../client/file-upload/tsconfig.client.json"
+ },
+ {
+ "path": "../../client/store"
+ },
+ {
+ "path": "../../context/file-reference"
+ },
+ {
+ "path": "../../core/session"
+ },
+ {
+ "path": "../../jobs/jobs"
+ },
+ {
+ "path": "../../llm/llm"
+ },
+ {
+ "path": "../../session/session-projection"
+ },
+ {
+ "path": "../../session/session-title"
+ },
+ {
+ "path": "../../subagent/subagent"
+ },
+ {
+ "path": "../../util/brand"
+ },
+ {
+ "path": "../../util/crypto"
+ },
+ {
+ "path": "../../util/workspace-path"
+ },
+ {
+ "path": "../../workspace/workspace"
+ },
+ {
+ "path": "../../typert/protocol"
+ },
+ {
+ "path": "../../util/native-command"
+ },
+ {
+ "path": "../../core/agent-model"
+ }
]
}
diff --git a/packages/api/session-controller/tsconfig.host.json b/packages/api/session-controller/tsconfig.host.json
index 8a5fa75166..dba370504c 100644
--- a/packages/api/session-controller/tsconfig.host.json
+++ b/packages/api/session-controller/tsconfig.host.json
@@ -114,6 +114,9 @@
},
{
"path": "../../session/working-directory"
+ },
+ {
+ "path": "../../core/agent-model"
}
]
}
diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml
index ee2f8f280b..b7a74fe5e6 100644
--- a/packages/bundle/base/cordis.patch.yml
+++ b/packages/bundle/base/cordis.patch.yml
@@ -82,6 +82,9 @@
- id: agent
name: '@deepseek-ai/dsh-agent'
+ - id: agent-model
+ name: '@deepseek-ai/dsh-agent-model'
+
- id: plugin-package-inventory-deepseek
name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json
index 2e6e13bcb5..b04f0fb9c2 100644
--- a/packages/bundle/base/package.json
+++ b/packages/bundle/base/package.json
@@ -127,12 +127,14 @@
"@deepseek-ai/dsh-settings": "workspace:*",
"@deepseek-ai/dsh-otel": "workspace:*",
"@deepseek-ai/dsh-working-directory": "workspace:*",
- "@deepseek-ai/dsh-tool-working-directory": "workspace:*"
+ "@deepseek-ai/dsh-tool-working-directory": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
- "@deepseek-ai/cordis": "workspace:~"
+ "@deepseek-ai/cordis": "workspace:~",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/bundle/base/tsconfig.json b/packages/bundle/base/tsconfig.json
index 2f01995a6b..71452d7959 100644
--- a/packages/bundle/base/tsconfig.json
+++ b/packages/bundle/base/tsconfig.json
@@ -16,6 +16,9 @@
},
{
"path": "../../settings/settings"
+ },
+ {
+ "path": "../../core/agent-model"
}
]
}
diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json
index 0ea5e51ab9..97da81d9d6 100644
--- a/packages/bundle/headless/package.json
+++ b/packages/bundle/headless/package.json
@@ -55,7 +55,8 @@
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-query": "workspace:*",
"@deepseek-ai/dsh-fs": "workspace:*",
- "@deepseek-ai/dsh-subagent": "workspace:*"
+ "@deepseek-ai/dsh-subagent": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
@@ -68,7 +69,8 @@
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-query": "workspace:*",
"@deepseek-ai/dsh-fs": "workspace:*",
- "@deepseek-ai/dsh-subagent": "workspace:*"
+ "@deepseek-ai/dsh-subagent": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"peerDependenciesMeta": {
"@deepseek-ai/dsh-subagent": {
diff --git a/packages/bundle/headless/src/index.ts b/packages/bundle/headless/src/index.ts
index ebac05d977..5bfe6d2d39 100644
--- a/packages/bundle/headless/src/index.ts
+++ b/packages/bundle/headless/src/index.ts
@@ -14,9 +14,10 @@ import { randomUUID } from 'node:crypto'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { brandString } from '@deepseek-ai/dsh-brand'
-import { installModelSwitchNotice, recordDeclaredRoute } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-default-model'
+import type {} from '@deepseek-ai/dsh-agent-model'
+import * as ModelSwitchNotice from '@deepseek-ai/dsh-agent-model/notice'
import type {} from '@deepseek-ai/dsh-fs'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { assertNever } from '@deepseek-ai/dsh-util-values'
@@ -37,7 +38,7 @@ import { projectJsonRun, boundJsonLine } from './json-stream.ts'
export const name = 'headless-runner'
/** Core services required before the one-shot turn can start. */
-export const inject = ['agentDefaultModel', 'agents', 'sessionProjections', 'sessions']
+export const inject = ['agentDefaultModel', 'agents', 'agentModel', 'sessions']
/** Plugin config: the task and run options resolved from this app's injected provider service. */
export interface Config {
@@ -248,7 +249,7 @@ async function resolveAgent(
agents: Context['agents'],
sessionId: SessionId,
agentOptions: AgentOptions,
- setup: (agentCtx: Context, agent: Agent) => void,
+ setup: (agentCtx: Context, agent: Agent) => void | Promise,
cwd: string,
): Promise {
// Resuming promises the caller a log a later process can continue. Without a
@@ -337,10 +338,10 @@ async function run(ctx: Context, config: Config, io: HeadlessIo): Promise
// host plane and the agent reads them from the global layer. A deployment
// that DOES configure one has to join it here first
// (@deepseek-ai/dsh-agent-preset-registry README, "Composing a child agent").
- const setup = (agentCtx: Context, agent: Agent): void => {
+ const setup = async (agentCtx: Context, agent: Agent): Promise => {
// A resumed Session continues with this run's configured model.
- if (config.sessionId !== undefined) recordDeclaredRoute(ctx.sessionProjections, agent, selection)
- installModelSwitchNotice(agentCtx)
+ if (config.sessionId !== undefined) ctx.agentModel.applyConfiguredRoute(agent, selection)
+ await agentCtx.plugin(ModelSwitchNotice)
}
const sessionId = brandString(config.sessionId ?? `session-${randomUUID()}`)
const fs = ctx.get('fs')
diff --git a/packages/bundle/headless/tests/headless.spec.ts b/packages/bundle/headless/tests/headless.spec.ts
index 54aa1f2050..2414baad36 100644
--- a/packages/bundle/headless/tests/headless.spec.ts
+++ b/packages/bundle/headless/tests/headless.spec.ts
@@ -4,7 +4,7 @@ import { Readable } from 'node:stream'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { brandString } from '@deepseek-ai/dsh-brand'
-import AgentRegistry, { currentModelSelection } from '@deepseek-ai/dsh-agent'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
import type {
Agent,
AgentHandle,
@@ -19,6 +19,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import type { Session, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
import { SessionQueryError } from '@deepseek-ai/dsh-session-query'
import { createInboxStub, provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { apply, Config } from '../src/index.ts'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import { internals } from '../src/runner-internals.ts'
@@ -179,6 +180,7 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentDefaultModelConfig, { provider: 'test-provider', model: 'test-model' })
ctx.agents.setFactory({
@@ -741,7 +743,7 @@ describe('headless runner', () => {
it.each(['test-model', 'other-model'])('restores explicit effort only for the declared route (%s)', async (savedModel) => {
const test = await bench({
afterPrompt(session, message, agent) {
- expect(currentModelSelection(test.ctx.sessionProjections, agent)).toEqual({
+ expect(test.ctx.agentModel.resolveForNextStep(agent)).toEqual({
provider: 'test-provider', model: 'test-model',
...savedModel === 'test-model' ? { reasoningEffort: 'high' } : {},
})
diff --git a/packages/bundle/headless/tsconfig.json b/packages/bundle/headless/tsconfig.json
index 640ee3b2ad..3c8895610d 100644
--- a/packages/bundle/headless/tsconfig.json
+++ b/packages/bundle/headless/tsconfig.json
@@ -40,6 +40,9 @@
},
{
"path": "../../subagent/subagent"
+ },
+ {
+ "path": "../../core/agent-model"
}
]
}
diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml
index 79922e7113..2467ef7c14 100644
--- a/packages/bundle/sdk-minimal/cordis.patch.yml
+++ b/packages/bundle/sdk-minimal/cordis.patch.yml
@@ -126,6 +126,9 @@
- id: agent
name: '@deepseek-ai/dsh-agent'
+ - id: agent-model
+ name: '@deepseek-ai/dsh-agent-model'
+
- id: llm-retry
name: '@deepseek-ai/dsh-llm-retry'
diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json
index 1dce9046a8..b3441cb458 100644
--- a/packages/bundle/sdk-minimal/package.json
+++ b/packages/bundle/sdk-minimal/package.json
@@ -67,12 +67,14 @@
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-working-directory": "workspace:*",
"@deepseek-ai/dsh-tool-working-directory": "workspace:*",
- "@deepseek-ai/dsh-fs-local": "workspace:*"
+ "@deepseek-ai/dsh-fs-local": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
- "@deepseek-ai/cordis": "workspace:~"
+ "@deepseek-ai/cordis": "workspace:~",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
index f4e24e5bc5..7720e9a02b 100644
--- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
+++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
@@ -55,6 +55,7 @@ describe('dsh-sdk-minimal bundle', () => {
['tools', '@deepseek-ai/dsh-tools'],
['mcp-resources', '@deepseek-ai/dsh-mcp-resources'],
['agent', '@deepseek-ai/dsh-agent'],
+ ['agent-model', '@deepseek-ai/dsh-agent-model'],
['llm-retry', '@deepseek-ai/dsh-llm-retry'],
['jobs', '@deepseek-ai/dsh-jobs-local'],
['agent-loop', '@deepseek-ai/dsh-agent-loop'],
diff --git a/packages/bundle/sdk-minimal/tsconfig.json b/packages/bundle/sdk-minimal/tsconfig.json
index f1a449634c..3e60750a3e 100644
--- a/packages/bundle/sdk-minimal/tsconfig.json
+++ b/packages/bundle/sdk-minimal/tsconfig.json
@@ -10,6 +10,9 @@
"references": [
{
"path": "../../../vendor/cordis"
+ },
+ {
+ "path": "../../core/agent-model"
}
]
}
diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json
index f46b4affa7..28898f9677 100644
--- a/packages/client/ui-conversation/package.json
+++ b/packages/client/ui-conversation/package.json
@@ -96,7 +96,8 @@
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-settings": "workspace:*",
"@deepseek-ai/dsh-client-shortcuts": "workspace:*",
- "@deepseek-ai/dsh-client-product-analytics": "workspace:*"
+ "@deepseek-ai/dsh-client-product-analytics": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"files": [
"locale/*.json",
diff --git a/packages/client/ui-conversation/src/client/input/hub.ts b/packages/client/ui-conversation/src/client/input/hub.ts
index fb8d8a736f..0b232e7b08 100644
--- a/packages/client/ui-conversation/src/client/input/hub.ts
+++ b/packages/client/ui-conversation/src/client/input/hub.ts
@@ -5,7 +5,6 @@
* returns; Session-scope disposal releases the shell and its resources.
*/
import type {} from '@deepseek-ai/dsh-client-product-analytics/client'
-import type { ModelSelectionProjection } from '@deepseek-ai/dsh-api-session-controller/types'
import type { PlanProjection } from '@deepseek-ai/dsh-plan-mode/types'
import type { GoalProjection } from '@deepseek-ai/dsh-goal/types'
import type { Context } from '@deepseek-ai/cordis'
@@ -15,6 +14,7 @@ import type {
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client'
import type { InboxState } from '@deepseek-ai/dsh-agent/types'
+import type { ModelSelectionProjection } from '@deepseek-ai/dsh-agent-model/types'
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type {
DraftAttachmentId, DraftAttachmentSerializationResult, DraftInitializationOptions, DraftInitializationResult, InputTriggerController,
diff --git a/packages/client/ui-conversation/tsconfig.client.json b/packages/client/ui-conversation/tsconfig.client.json
index 0339eebbe6..fffdd9ca09 100644
--- a/packages/client/ui-conversation/tsconfig.client.json
+++ b/packages/client/ui-conversation/tsconfig.client.json
@@ -104,6 +104,9 @@
},
{
"path": "../product-analytics/tsconfig.client.json"
+ },
+ {
+ "path": "../../core/agent-model"
}
],
"exclude": [
diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json
index ed88e48db8..c431dd6881 100644
--- a/packages/client/ui-model-selection/package.json
+++ b/packages/client/ui-model-selection/package.json
@@ -46,6 +46,7 @@
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
+ "@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-api-remotes": "workspace:*",
"@deepseek-ai/dsh-api-session-controller": "workspace:*",
"@deepseek-ai/dsh-client-locale": "workspace:*",
@@ -65,7 +66,8 @@
"@deepseek-ai/dsh-client-ui-renderer": "workspace:*",
"@deepseek-ai/dsh-client-ui-session": "workspace:*",
"clsx": "^2.1.1",
- "@deepseek-ai/dsh-client-product-analytics": "workspace:*"
+ "@deepseek-ai/dsh-client-product-analytics": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"files": [
"locale/*.json",
diff --git a/packages/client/ui-model-selection/src/client/directory.ts b/packages/client/ui-model-selection/src/client/directory.ts
index 41f67832a0..66d07d7890 100644
--- a/packages/client/ui-model-selection/src/client/directory.ts
+++ b/packages/client/ui-model-selection/src/client/directory.ts
@@ -4,9 +4,10 @@
* Session's durable selection projection, then submit through the same
* selectModel call. A switch made in either entry updates this shared state.
*/
+import type { ModelSelectionProjection } from '@deepseek-ai/dsh-agent-model/types'
import type { TrackProductEvent } from '@deepseek-ai/dsh-client-product-analytics/client'
import type {
- ModelCatalogFailure, ModelProviderGroup, ModelSelection, ModelSelectionProjection,
+ ModelCatalogFailure, ModelProviderGroup, ModelSelection,
} from '@deepseek-ai/dsh-api-session-controller/types'
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
import type { RemoteResult, TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol'
diff --git a/packages/client/ui-model-selection/tests/browser-plugin.client.spec.ts b/packages/client/ui-model-selection/tests/browser-plugin.client.spec.ts
index cbe2dd9e4e..745ba351c6 100644
--- a/packages/client/ui-model-selection/tests/browser-plugin.client.spec.ts
+++ b/packages/client/ui-model-selection/tests/browser-plugin.client.spec.ts
@@ -15,8 +15,8 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
import { RemoteError, TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
-import type { ModelSelectionProjection } from '@deepseek-ai/dsh-api-session-controller/types'
import type { ModelSelection } from '@deepseek-ai/dsh-agent/types'
+import type { ModelSelectionProjection } from '@deepseek-ai/dsh-agent-model/types'
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm/brand'
import type { CommandContribution, PopupSelectSpec, SelectOption } from '@deepseek-ai/dsh-client-ui-commands/client'
import type { ModelSelectInjected } from '../src/client/slots.ts'
diff --git a/packages/client/ui-model-selection/tsconfig.json b/packages/client/ui-model-selection/tsconfig.json
index 82f037f52a..2af4805c5e 100644
--- a/packages/client/ui-model-selection/tsconfig.json
+++ b/packages/client/ui-model-selection/tsconfig.json
@@ -8,6 +8,9 @@
"src"
],
"references": [
+ {
+ "path": "../../core/agent"
+ },
{
"path": "../../api/remotes/tsconfig.client.json"
},
@@ -55,6 +58,9 @@
},
{
"path": "../product-analytics/tsconfig.client.json"
+ },
+ {
+ "path": "../../core/agent-model"
}
]
}
diff --git a/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts b/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts
index 021fddb842..8ed00e815d 100644
--- a/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts
+++ b/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts
@@ -5,7 +5,7 @@ import { createUserMessage, createSystemMessage, CONTEXT_WINDOW_EXCEEDED_CODE, L
import type { ContentBlock, GenerateOptions, LlmResolvedModelInfo, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm'
import { ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
-import { recordModelSelection, type Agent } from '@deepseek-ai/dsh-agent'
+import { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic'
@@ -212,7 +212,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', ()
provider: 'unconfigured-agent-fallback',
model: 'unconfigured-agent-fallback',
})
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'mock', model: 'mock' })
+ ctx.agentModel.select(agent, { provider: 'mock', model: 'mock' })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'do a routed multi-step task' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
@@ -397,7 +397,7 @@ describe('context-overflow recovery across the real loop and compaction-basic',
},
})
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'mock', model: 'mock' })
+ ctx.agentModel.select(agent, { provider: 'mock', model: 'mock' })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } }))
await agent.whenIdle()
diff --git a/packages/context/agent-instructions/package.json b/packages/context/agent-instructions/package.json
index dac4a52208..22c149a688 100644
--- a/packages/context/agent-instructions/package.json
+++ b/packages/context/agent-instructions/package.json
@@ -60,6 +60,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tool-fs": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
- "@deepseek-ai/dsh-working-directory": "workspace:*"
+ "@deepseek-ai/dsh-working-directory": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/context/agent-instructions/tests/agent-instructions.spec.ts b/packages/context/agent-instructions/tests/agent-instructions.spec.ts
index 3ce3077f7e..0c2f071cda 100644
--- a/packages/context/agent-instructions/tests/agent-instructions.spec.ts
+++ b/packages/context/agent-instructions/tests/agent-instructions.spec.ts
@@ -49,6 +49,7 @@ import {
mountAgentLoopTestDependencies,
mountAgentLoopTestHarness,
} from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
@@ -1255,6 +1256,7 @@ describe('workspace context request injection', () => {
const ctx = new Context()
provideWorkingDirectoryFixture(ctx)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentInstructions, { maxBytes: 65536 })
const exec = stubToolExecution({
callId: ToolCallId('missing-turn-boundary'),
@@ -3633,6 +3635,7 @@ describe('dynamic nested workspace context injection', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json
index 198bd422f2..f5b3ac22a6 100644
--- a/packages/context/time-context/package.json
+++ b/packages/context/time-context/package.json
@@ -55,6 +55,7 @@
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-subprocess-local": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
- "@deepseek-ai/dsh-tools": "workspace:*"
+ "@deepseek-ai/dsh-tools": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts
index 5197bf6803..48c0322ab4 100644
--- a/packages/context/time-context/tests/time-context.spec.ts
+++ b/packages/context/time-context/tests/time-context.spec.ts
@@ -10,6 +10,7 @@ import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { unsupportedInbox, mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import * as timeContext from '@deepseek-ai/dsh-time-context'
import type { Config } from '@deepseek-ai/dsh-time-context'
@@ -40,6 +41,7 @@ afterEach(() => {
async function mount(config: Config = {}) {
const ctx = new Context()
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const fiber = await ctx.plugin(timeContext, config)
return { ctx, fiber }
@@ -383,6 +385,7 @@ describe('configuration and lifecycle', () => {
it('fails loud for an invalid explicit zone or an unavailable process zone', async () => {
const invalid = new Context()
await invalid.plugin(SessionProjectionRegistry)
+ await invalid.plugin(AgentModelPlugin)
await invalid.plugin(AgentRegistry)
await expect(invalid.plugin(timeContext, { timeZone: 'Not/A_Real_Zone' })).rejects.toThrow(
/invalid IANA timeZone/,
@@ -393,6 +396,7 @@ describe('configuration and lifecycle', () => {
})
const unresolved = new Context()
await unresolved.plugin(SessionProjectionRegistry)
+ await unresolved.plugin(AgentModelPlugin)
await unresolved.plugin(AgentRegistry)
await expect(unresolved.plugin(timeContext, {})).rejects.toThrow(/failed to resolve the system time zone/)
})
@@ -532,6 +536,7 @@ describe('real Loader export path', () => {
const ctx = new Context()
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const plugin = loader.unwrapExports(timeContext) as Parameters[0]
await ctx.plugin(plugin)
diff --git a/packages/core/README.i18n.yaml b/packages/core/README.i18n.yaml
index 60936e1b9e..141cd4008b 100644
--- a/packages/core/README.i18n.yaml
+++ b/packages/core/README.i18n.yaml
@@ -15,8 +15,8 @@
en: 1eb35b015a6c423f
zh: a8a3dceee2c9e943
/packages-core/packages:
- en: 083b59ee474230e8
- zh: f11c7bc21bbcdbe6
+ en: 67a6cc9ad66e7196
+ zh: 77ca2120b5b19038
/packages-core/related-documentation:
en: dfe5990edf146a91
zh: b64e4b2d812286d4
diff --git a/packages/core/README.md b/packages/core/README.md
index d88c679a4f..06dde2376d 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -30,6 +30,7 @@ Use the core packages to build or extend an agent that records durable session h
| [`tools/`](tools/README.md) | The tool registry and guarded execution pipeline the loop dispatches through | `ctx.tools` |
| [`agent-tool-presentation/`](agent-tool-presentation/README.md) | Per-agent tool-presentation selector for presets | no ctx key |
| [`agent/`](agent/README.md) | The `Agent` handle plugins program against, plus its live registry and events | `ctx.agents` |
+| [`agent-model/`](agent-model/README.md) | Durable Session model selection and optional scoped switch notices | `ctx.agentModel` |
| [`agent-default-model/`](agent-default-model/README.md) | The deployment default model selection entry points apply to fresh agents | `ctx.agentDefaultModel` |
| [`agent-loop/`](agent-loop/README.md) | The default agent driver: creates agents and runs the turn and step lifecycle | `ctx.agentLoop` |
diff --git a/packages/core/README.zh.md b/packages/core/README.zh.md
index 128ad5cbd0..21f4bc2b29 100644
--- a/packages/core/README.zh.md
+++ b/packages/core/README.zh.md
@@ -30,6 +30,7 @@ kind: "package-group"
| [`tools/`](tools/README.zh.md) | 供循环分发使用的工具注册表与带防护机制的执行流水线 | `ctx.tools` |
| [`agent-tool-presentation/`](agent-tool-presentation/README.zh.md) | 为 preset 提供按 agent 的工具呈现方式选择器 | 无 ctx key |
| [`agent/`](agent/README.zh.md) | 供插件编程使用的 `Agent` 句柄,以及其实时注册表与事件 | `ctx.agents` |
+| [`agent-model/`](agent-model/README.zh.md) | 持久 Session 模型选择与可选的作用域切换提示 | `ctx.agentModel` |
| [`agent-default-model/`](agent-default-model/README.zh.md) | 入口对全新 agent 应用的部署默认模型选择 | `ctx.agentDefaultModel` |
| [`agent-loop/`](agent-loop/README.zh.md) | 默认 agent 驱动器:创建 agent 并运行轮次与步骤生命周期 | `ctx.agentLoop` |
diff --git a/packages/core/agent-default-model/README.i18n.yaml b/packages/core/agent-default-model/README.i18n.yaml
index 2bf3657666..58a0c9c03c 100644
--- a/packages/core/agent-default-model/README.i18n.yaml
+++ b/packages/core/agent-default-model/README.i18n.yaml
@@ -9,8 +9,8 @@
en: 87e9d106cedbbe08
zh: 5eb47e4bd7f5b51d
/deepseek-ai-dsh-agent-default-model/summary:
- en: bf59a0974610ef81
- zh: 451b967a5d2be7f1
+ en: 0a14206daf366b59
+ zh: 8e163fa67aa3774c
/deepseek-ai-dsh-agent-default-model/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
@@ -45,8 +45,8 @@
en: 814ad1a657587419
zh: 5c1968a55beb1af4
/deepseek-ai-dsh-agent-default-model/known-limitations-and-deferred-work:
- en: 3d41fadae2c3e40b
- zh: 52dcf7e4ea6a5e45
+ en: 7cafbd9a83d14852
+ zh: 2c298c1a671ec00f
/deepseek-ai-dsh-agent-default-model/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 3bb0c20c93f16f98
diff --git a/packages/core/agent-default-model/README.md b/packages/core/agent-default-model/README.md
index 5da7eb6eea..5bc09a5ea9 100644
--- a/packages/core/agent-default-model/README.md
+++ b/packages/core/agent-default-model/README.md
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
-Give newly created agents a shared default provider and model when their sessions do not specify one. Provider, model, and reasoning effort are live Config fields. Saved selections update the active profile patch and apply to subsequent reads; per-session selection remains owned by the entry point.
+Give newly created agents a shared default provider and model when their sessions do not specify one. Provider, model, and reasoning effort are live Config fields. Saved selections update the active profile patch and apply to subsequent reads; per-session selection belongs to the [model-selection service](../agent-model/README.md).
## Table of Contents
@@ -112,7 +112,7 @@ Changing the default affects only agents that subsequently resolve from it. An e
These limits define the service's scope. They are current package constraints, not a task backlog.
-- **One process-wide default** — the service owns a single default; per-session model selection remains the entry point's responsibility.
+- **One process-wide default** — the service owns a single default; per-session model selection belongs to the [model-selection service](../agent-model/README.md).
- **Persistence requires a profile configuration editor** — without it, saving a default does not retain the selection.
diff --git a/packages/core/agent-default-model/README.zh.md b/packages/core/agent-default-model/README.zh.md
index f635b44a95..7aa22c97cc 100644
--- a/packages/core/agent-default-model/README.zh.md
+++ b/packages/core/agent-default-model/README.zh.md
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
-为会话未指定模型的新 agent 提供共享默认 provider 和模型。Provider、模型和推理强度都是即时 Config 字段。保存的选择更新当前 profile patch,并用于后续读取;会话级选择仍由入口负责。
+为会话未指定模型的新 agent 提供共享默认 provider 和模型。Provider、模型和推理强度都是即时 Config 字段。保存的选择更新当前 profile patch,并用于后续读取;会话级选择由[模型选择服务](../agent-model/README.zh.md) 负责。
## 目录
@@ -112,7 +112,7 @@ await ctx.agentDefaultModel.saveSelection({ provider, model, reasoningEffort: 'h
这些限制界定该服务的范围。它们是当前包约束,不是任务积压。
-- **单一的进程级默认值**——该服务只拥有一个默认值;按会话的模型选择仍由入口负责。
+- **单一的进程级默认值**——该服务只拥有一个默认值;按会话的模型选择由[模型选择服务](../agent-model/README.zh.md) 负责。
- **持久化需要 profile 配置编辑器**——没有编辑器时,保存默认值不会保留选择。
diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml
index c2f8d24bc3..5ad1fe1a79 100644
--- a/packages/core/agent-loop/README.i18n.yaml
+++ b/packages/core/agent-loop/README.i18n.yaml
@@ -33,8 +33,8 @@
en: 030202c112f89366
zh: 621d53ed755cafa3
/deepseek-ai-dsh-agent-loop/understand-the-implementation/request-headers-and-adapter-defaults:
- en: d24cc88c67952a3c
- zh: 29c33b600b1be394
+ en: 158382cfe3cbf499
+ zh: b4e6de5a50fd3a65
/deepseek-ai-dsh-agent-loop/understand-the-implementation/source-map:
en: 2a8b93869eb5a2f9
zh: ddab8e738d188a61
diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md
index 67b7c3bb19..7d3bea7167 100644
--- a/packages/core/agent-loop/README.md
+++ b/packages/core/agent-loop/README.md
@@ -92,7 +92,7 @@ The package is the one concrete implementation of the public `Agent` contract. I
### Request headers and adapter defaults
-Before prompt assembly, the loop reads the shared `modelSelection` projection and captures one selection for the step and its retries. Prompt variables and `agent/pre-step.callConfig` use that route. The deprecated `agent/request` hook remains available to existing listeners and may replace controls such as reasoning effort or sampling, but changing provider or model rejects the step before input is committed. A later recorded choice applies to the next step, including a tool continuation in the same turn.
+Before prompt assembly, the loop calls `ctx.agentModel.resolveForNextStep()` and captures provider, model, and optional explicit reasoning effort for the step and its retries. `AssembleContext.callConfig` and `agent/pre-step.callConfig` carry that selection; prompt variables use its route. The deprecated `agent/request` hook remains available to existing listeners and may replace other controls such as sampling. Changing provider or model, or adding, changing, or clearing reasoning effort rejects the step before input is committed. A later recorded choice applies to the next step, including a tool continuation in the same turn.
After `agent/request`, `ctx.llm.prepareCall()` validates adapter-owned fields and resolves reasoning-effort and output-token defaults under the active turn signal. The loop retains that exact adapter through resolution, `request/header` logging, and dispatch. It writes a full header for the first request, a changed envelope (config or tools — the prompt is not part of the header), an explicit message-series start, a request after a surface replacement or image-offload decision, and resume; unchanged steps, retries, and ordinary later turns in the same series inherit the latest header, and an in-history prompt append is not a replacement, so the request that follows it inherits the header too. Beside the header, the loop logs `request/context` — provider, model, `contextWindow`, and the route's `systemPromptUpdate` mode from `prepareCall()` — only when one of those differs from the latest snapshot. Before the next waterfall, the loop removes adapter-default fields so the current route resolves them again, while explicit settings persist. An unhandled route still fails with `NO_ADAPTER`.
diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md
index beddf25c34..8b2cce4e9b 100644
--- a/packages/core/agent-loop/README.zh.md
+++ b/packages/core/agent-loop/README.zh.md
@@ -92,7 +92,7 @@ const handle = await ctx.agents.create({
### 请求 header 与适配器默认值
-循环在提示词组装前读取共享 `modelSelection` 投影,并为该步骤及其重试捕获一次选择。提示词变量与 `agent/pre-step.callConfig` 使用该路由。已弃用的 `agent/request` hook 仍可供现有监听器使用,可以替换推理强度、采样等控制参数,但改变提供方或模型会在提交输入前拒绝该步骤。之后记录的选择从下一个步骤开始生效,包括同一轮次中的工具后续步骤。
+循环在提示词组装前调用 `ctx.agentModel.resolveForNextStep()`,并为该步骤及其重试捕获提供方、模型及可选的显式推理强度。`AssembleContext.callConfig` 与 `agent/pre-step.callConfig` 携带该选择;提示词变量使用其中的路由。已弃用的 `agent/request` hook 仍可供现有监听器使用,可以替换采样等其他控制参数。改变提供方或模型,或添加、修改、清除推理强度,都会在提交输入前拒绝该步骤。之后记录的选择从下一个步骤开始生效,包括同一轮次中的工具后续步骤。
`agent/request` 返回后,`ctx.llm.prepareCall()` 会在活跃轮次信号下校验适配器持有的字段,并解析推理强度和输出 token 默认值。循环会在解析、`request/header` 记录与分派期间保留同一个适配器。循环会为首次请求、变化的 envelope(配置或工具——提示词不属于 header)、显式消息序列起点、surface 替换或图片省略决定后的请求及恢复写入完整 header;同一序列内内容未变的步骤、重试与普通后续轮次继承最新 header,历史内追加提示词不是替换,因此紧随其后的请求同样继承 header。在 header 之外,循环还会记录 `request/context`——提供方、模型、`contextWindow` 以及来自 `prepareCall()` 的路由 `systemPromptUpdate` 模式——且仅在其中任何一项与最新快照不同时记录。下一次 waterfall 分发前,循环移除适配器默认字段,使当前路由重新解析它们;显式设置则保留。未处理的路由仍以 `NO_ADAPTER` 失败。
diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json
index 46543087d2..2b85f6c9ad 100644
--- a/packages/core/agent-loop/package.json
+++ b/packages/core/agent-loop/package.json
@@ -36,7 +36,8 @@
"@deepseek-ai/dsh-session-persistence": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
- "@deepseek-ai/dsh-tools": "workspace:*"
+ "@deepseek-ai/dsh-tools": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"dependencies": {
"@deepseek-ai/dsh-brand": "workspace:*",
@@ -56,6 +57,7 @@
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
- "@deepseek-ai/dsh-tools": "workspace:*"
+ "@deepseek-ai/dsh-tools": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts
index 491074006b..cd967eb095 100644
--- a/packages/core/agent-loop/src/agent.ts
+++ b/packages/core/agent-loop/src/agent.ts
@@ -16,7 +16,7 @@ import type {
PreStepDecision,
RequestErrorAction,
} from '@deepseek-ai/dsh-agent'
-import { agentEvents, assembleContextFor, currentModelSelection } from '@deepseek-ai/dsh-agent'
+import { agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
import type { GenerateOptions, LlmCallConfig, Message, PreparedLlmCall } from '@deepseek-ai/dsh-llm'
import {
LlmError,
@@ -59,7 +59,7 @@ type PreparedStep =
messages: UserMessage[]
startsRequestSeries?: true
assembly: PromptAssembly
- selection: ModelSelection
+ selection: Readonly
}
/** Remove adapter-derived values before plugins propose the next request config. */
@@ -271,7 +271,7 @@ 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 selection = currentModelSelection(this.loopCtx.sessionProjections, this)
+ const selection = this.loopCtx.agentModel.resolveForNextStep(this)
if (selection === undefined) {
throw new Error(`agent "${this.id}" has no provider/model: set AgentOptions.provider and AgentOptions.model or record a model selection`)
}
@@ -572,7 +572,7 @@ export class ReactLoopAgent implements Agent {
private async prepareRequest(
turn: number,
step: number,
- selection: ModelSelection,
+ selection: Readonly,
signal: AbortSignal,
): Promise<{ config: LlmCallConfig; preparedCall?: PreparedLlmCall }> {
const { session } = this
@@ -592,7 +592,10 @@ export class ReactLoopAgent implements Agent {
)
signal.throwIfAborted()
if (proposedConfig.provider !== selection.provider || proposedConfig.model !== selection.model) {
- throw new Error(`agent "${this.id}": agent/request proposed "${proposedConfig.provider}/${proposedConfig.model}" after step preparation fixed "${selection.provider}/${selection.model}"; use recordModelSelection() before step preparation`)
+ throw new Error(`agent "${this.id}": agent/request proposed "${proposedConfig.provider}/${proposedConfig.model}" after step preparation fixed "${selection.provider}/${selection.model}"; use ctx.agentModel.select() before step preparation`)
+ }
+ if (proposedConfig.reasoningEffort !== selection.reasoningEffort) {
+ throw new Error(`agent "${this.id}": agent/request proposed reasoning effort "${proposedConfig.reasoningEffort ?? 'adapter default'}" after step preparation fixed "${selection.reasoningEffort ?? 'adapter default'}"; use ctx.agentModel.select() before step preparation`)
}
let config: LlmCallConfig
let preparedCall: PreparedLlmCall | undefined
diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts
index 923577f49f..7993c9beaa 100644
--- a/packages/core/agent-loop/src/index.ts
+++ b/packages/core/agent-loop/src/index.ts
@@ -22,8 +22,8 @@ import type {
SessionStartSource,
TurnBoundaryProjection,
} from '@deepseek-ai/dsh-agent'
+import type {} from '@deepseek-ai/dsh-agent-model'
import { errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
-import { recordDeclaredRoute } from '@deepseek-ai/dsh-agent'
import { interruptedTurnClosers, SessionLogOffset, SessionPreparation, SessionSeq } from '@deepseek-ai/dsh-session'
import type { Session, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-system-prompt'
@@ -340,7 +340,7 @@ function validateConfiguredAgents(agents: Config['agents']): void {
/** Concrete agent factory and driver service. */
export class AgentLoop extends Service implements AgentFactory {
- static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt', 'sessionProjections']
+ static inject = ['agentModel', 'agents', 'sessions', 'llm', 'tools', 'systemPrompt', 'sessionProjections']
/** Runtime schema for declarative agents. */
static Config: z<{ agents?: Config['agents']; maxParallelToolCalls?: number }, Config> = z.object({
@@ -413,7 +413,7 @@ export class AgentLoop extends Service implements AgentFactory {
void this.resumeWith(ctx, childCtx.sessionPersistence, {
resumeSessionId,
agentOptions: options,
- setup: (_agentCtx, agent) => { recordDeclaredRoute(ctx.sessionProjections, agent, options) },
+ setup: (_agentCtx, agent) => { this.applyConfiguredRoute(agent, options) },
}).catch((error: unknown) => {
this.reportConfiguredStartupFailure(id, 'resume', resumeSessionId, error)
})
@@ -423,6 +423,13 @@ export class AgentLoop extends Service implements AgentFactory {
}
}
+ private applyConfiguredRoute(agent: Agent, options: AgentOptions): void {
+ const { provider, model, reasoningEffort } = options
+ if (provider && model) this.runtime.ctx.agentModel.applyConfiguredRoute(agent, {
+ provider, model, ...reasoningEffort === undefined ? {} : { reasoningEffort },
+ })
+ }
+
/** Report a contained declarative-start failure to identity-bound consumers. */
private reportConfiguredStartupFailure(
configId: string,
@@ -459,7 +466,7 @@ export class AgentLoop extends Service implements AgentFactory {
await this.resumeWith(ownerCtx, persistence, {
resumeSessionId: sessionId,
agentOptions,
- setup: (_agentCtx, agent) => { recordDeclaredRoute(this.runtime.ctx.sessionProjections, agent, agentOptions) },
+ setup: (_agentCtx, agent) => { this.applyConfiguredRoute(agent, agentOptions) },
})
return
} catch (error: unknown) {
diff --git a/packages/core/agent-loop/tests/agent-initiator.spec.ts b/packages/core/agent-loop/tests/agent-initiator.spec.ts
index 0b9d32ec06..87ee982080 100644
--- a/packages/core/agent-loop/tests/agent-initiator.spec.ts
+++ b/packages/core/agent-loop/tests/agent-initiator.spec.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { Context, type Fiber } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LlmRuntime, { createUserMessage, ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
@@ -23,6 +24,7 @@ async function harness(adapter: LlmAdapter): Promise {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
const agentsFiber = await ctx.plugin(AgentRegistry)
@@ -124,6 +126,7 @@ describe('AgentLoop initiator scope', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -385,6 +388,7 @@ describe('AgentLoop initiator scope', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/agent.spec.ts b/packages/core/agent-loop/tests/agent.spec.ts
index 180edf291d..d5b63c4e6a 100644
--- a/packages/core/agent-loop/tests/agent.spec.ts
+++ b/packages/core/agent-loop/tests/agent.spec.ts
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import type { MessageSource } from '@deepseek-ai/dsh-llm'
@@ -24,6 +25,7 @@ async function harness(adapter: MockAdapter): Promise {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/cancel.spec.ts b/packages/core/agent-loop/tests/cancel.spec.ts
index add362835b..5786827d76 100644
--- a/packages/core/agent-loop/tests/cancel.spec.ts
+++ b/packages/core/agent-loop/tests/cancel.spec.ts
@@ -1,4 +1,3 @@
-import { ToolCallId, createUserMessage, expandAssistantStream } from '@deepseek-ai/dsh-llm'
/**
* Tests for the queue-aware `Agent.cancel()` primitive. The default clears
* queued and steering work, while `keepInbox` preserves pending input for a
@@ -11,11 +10,13 @@ import { ToolCallId, createUserMessage, expandAssistantStream } from '@deepseek-
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmRuntime from '@deepseek-ai/dsh-llm'
+import { ToolCallId, createUserMessage, expandAssistantStream } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, SessionLogOffset, TurnEndReason } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent, type AgentCancelCause } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -28,6 +29,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -826,6 +828,7 @@ describe('Agent.cancel()', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/config-session-id.spec.ts b/packages/core/agent-loop/tests/config-session-id.spec.ts
index 0d73ab4ba7..525a80f15e 100644
--- a/packages/core/agent-loop/tests/config-session-id.spec.ts
+++ b/packages/core/agent-loop/tests/config-session-id.spec.ts
@@ -15,6 +15,7 @@ import type { SessionHandle } from '@deepseek-ai/dsh-session-persistence'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop, { CONFIGURED_AGENT_IDENTITIES_KEY } from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -40,6 +41,7 @@ async function makeCoreContext(): Promise {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -371,6 +373,7 @@ describe('config-driven session id', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -394,6 +397,7 @@ describe('config-driven session id', () => {
await ctx1.plugin(LlmRuntime)
await ctx1.plugin(SessionStore)
await ctx1.plugin(SessionProjectionRegistry)
+ await ctx1.plugin(AgentModelPlugin)
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRuntime)
await ctx1.plugin(AgentRegistry)
@@ -417,6 +421,7 @@ describe('config-driven session id', () => {
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SessionProjectionRegistry)
+ await ctx2.plugin(AgentModelPlugin)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -443,6 +448,7 @@ describe('config-driven session id', () => {
await ctx1.plugin(LlmRuntime)
await ctx1.plugin(SessionStore)
await ctx1.plugin(SessionProjectionRegistry)
+ await ctx1.plugin(AgentModelPlugin)
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRuntime)
await ctx1.plugin(AgentRegistry)
@@ -461,6 +467,7 @@ describe('config-driven session id', () => {
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SessionProjectionRegistry)
+ await ctx2.plugin(AgentModelPlugin)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -487,6 +494,7 @@ describe('config-driven session id', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts
index e440e045d8..2bdd1bc67f 100644
--- a/packages/core/agent-loop/tests/contract-regressions.spec.ts
+++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts
@@ -1,4 +1,3 @@
-import { recordModelSelection } from '@deepseek-ai/dsh-agent'
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmRuntime, { type RequestMessage, createUserMessage, ToolCallId, LlmError, MessageSource, ProviderRequestId, StreamChunk } from '@deepseek-ai/dsh-llm'
@@ -8,6 +7,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { defineContentToolFixture, type PostToolDecision } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -28,6 +28,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -459,7 +460,7 @@ describe('adapter registration, routing, and accepted-input ownership', () => {
const ctx = await harness(adapter)
const agent = await ctx.agentLoop.create(SessionId('a1'), {})
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'mock', model: 'mock' })
+ ctx.agentModel.select(agent, { provider: 'mock', model: 'mock' })
send(agent, 'go')
await waitForIdle(ctx, agent)
@@ -575,6 +576,7 @@ describe('turn numbering continues across seeded sessions', () => {
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SessionProjectionRegistry)
+ await ctx2.plugin(AgentModelPlugin)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
@@ -1291,6 +1293,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1341,6 +1344,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1391,6 +1395,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1437,6 +1442,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1485,6 +1491,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/coverage-edges.spec.ts b/packages/core/agent-loop/tests/coverage-edges.spec.ts
index 38387dc3ad..023ab48e14 100644
--- a/packages/core/agent-loop/tests/coverage-edges.spec.ts
+++ b/packages/core/agent-loop/tests/coverage-edges.spec.ts
@@ -9,6 +9,7 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -27,6 +28,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/interception.spec.ts b/packages/core/agent-loop/tests/interception.spec.ts
index 352583df36..31f3921675 100644
--- a/packages/core/agent-loop/tests/interception.spec.ts
+++ b/packages/core/agent-loop/tests/interception.spec.ts
@@ -17,6 +17,7 @@ import AgentRegistry, {
} from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -45,6 +46,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts
index fdfeed03cc..038bd2e80c 100644
--- a/packages/core/agent-loop/tests/loop.spec.ts
+++ b/packages/core/agent-loop/tests/loop.spec.ts
@@ -6,9 +6,10 @@ import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
-import AgentRegistry, { recordModelSelection, type Agent, type AssistantStreamFrame } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { type Agent, type AssistantStreamFrame } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -33,6 +34,7 @@ async function harness(adapter: MockAdapter, persona = '') {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: persona })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -606,7 +608,7 @@ describe('agent loop', () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter, 'You run on {{model}}.')
const agent = await ctx.agentLoop.create(SessionId('a-recorded-model'), {})
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'mock', model: 'mock' })
+ ctx.agentModel.select(agent, { provider: 'mock', model: 'mock' })
send(agent, 'hi')
await waitForIdle(ctx, agent)
@@ -1314,7 +1316,7 @@ describe('agent loop', () => {
.toEqual([])
expect(agent.session.snapshotEvents().at(-1)).toMatchObject({
type: 'turn/end', data: { reason: { kind: 'error', error: {
- message: `agent "a1": agent/request proposed "${field === 'provider' ? 'other/mock' : 'mock/other'}" after step preparation fixed "mock/mock"; use recordModelSelection() before step preparation`,
+ message: `agent "a1": agent/request proposed "${field === 'provider' ? 'other/mock' : 'mock/other'}" after step preparation fixed "mock/mock"; use ctx.agentModel.select() before step preparation`,
} } },
})
})
@@ -1902,6 +1904,7 @@ describe('agent loop', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1930,6 +1933,7 @@ describe('agent loop', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/properties.spec.ts b/packages/core/agent-loop/tests/properties.spec.ts
index daea36f629..74d9966802 100644
--- a/packages/core/agent-loop/tests/properties.spec.ts
+++ b/packages/core/agent-loop/tests/properties.spec.ts
@@ -20,6 +20,7 @@ import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import fc from 'fast-check'
@@ -41,6 +42,7 @@ async function harness() {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/request-cache.e2e.ts b/packages/core/agent-loop/tests/request-cache.e2e.ts
index e36281607d..83cb03487a 100644
--- a/packages/core/agent-loop/tests/request-cache.e2e.ts
+++ b/packages/core/agent-loop/tests/request-cache.e2e.ts
@@ -8,6 +8,7 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
@@ -43,6 +44,7 @@ async function loopHarness(): Promise {
await created.plugin(LlmRuntime)
await created.plugin(SessionStore)
await created.plugin(SessionProjectionRegistry)
+ await created.plugin(AgentModelPlugin)
await created.plugin(SystemPrompt, { personaPrefix: SYSTEM })
await created.plugin(ToolRuntime)
await created.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/request-error.spec.ts b/packages/core/agent-loop/tests/request-error.spec.ts
index 616a817082..60df866c46 100644
--- a/packages/core/agent-loop/tests/request-error.spec.ts
+++ b/packages/core/agent-loop/tests/request-error.spec.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LlmRuntime, { createUserMessage, LlmError } from '@deepseek-ai/dsh-llm'
import type { LlmFailure, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
@@ -15,6 +16,7 @@ async function harness(adapter: MockAdapter): Promise {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/request-reconstruction.spec.ts b/packages/core/agent-loop/tests/request-reconstruction.spec.ts
index f7907333f0..6bfaee04d5 100644
--- a/packages/core/agent-loop/tests/request-reconstruction.spec.ts
+++ b/packages/core/agent-loop/tests/request-reconstruction.spec.ts
@@ -4,7 +4,6 @@
* request/header snapshot. Each request extends its predecessor unless a logged compaction
* replacement or header change explains the difference.
*/
-import { recordDeclaredRoute, recordModelSelection } from '@deepseek-ai/dsh-agent'
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
@@ -17,6 +16,7 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -40,6 +40,7 @@ async function harnessRoutes(
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: persona })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -219,13 +220,11 @@ describe('request stability across the loop', () => {
const adapter = new MockAdapter([textResponse('one'), textResponse('two')], reasoning)
const ctx = await harness(adapter)
const agent = await ctx.agentLoop.create(SessionId('effort'), { provider: 'mock', model: 'mock' })
- ctx.on('agent/request', async ({ turn }, next) => {
- const config = await next()
- return turn === 2 ? { ...config, reasoningEffort: ReasoningEffortId('max') } : config
- })
-
send(agent, 'first')
await waitForIdle(ctx, agent)
+ ctx.agentModel.select(agent, {
+ provider: 'mock', model: 'mock', reasoningEffort: ReasoningEffortId('max'),
+ })
send(agent, 'second')
await waitForIdle(ctx, agent)
@@ -254,7 +253,7 @@ describe('request stability across the loop', () => {
sessionId: SessionId(`effort-${model}`),
seed: structuredClone(agent.session.snapshotEvents()),
agentOptions: { provider: 'mock', model },
- setup: (_ctx, agent) => { recordDeclaredRoute(resumedCtx.sessionProjections, agent, agent.options) },
+ setup: (_ctx, agent) => { resumedCtx.agentModel.applyConfiguredRoute(agent, { provider: 'mock', model }) },
})
send(resumedHandle.agent, 'resumed')
await waitForIdle(resumedCtx, resumedHandle.agent)
@@ -298,7 +297,7 @@ describe('request stability across the loop', () => {
})
send(agent, 'first')
await waitForIdle(ctx, agent)
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'other', model: 'other-model' })
+ ctx.agentModel.select(agent, { provider: 'other', model: 'other-model' })
send(agent, 'second')
await waitForIdle(ctx, agent)
@@ -326,7 +325,7 @@ describe('request stability across the loop', () => {
})
send(agent, 'first')
await waitForIdle(ctx, agent)
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'other', model: 'other-model' })
+ ctx.agentModel.select(agent, { provider: 'other', model: 'other-model' })
send(agent, 'second')
await waitForIdle(ctx, agent)
@@ -342,6 +341,7 @@ describe('request stability across the loop', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: 'stable base' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -461,6 +461,7 @@ describe('request stability across the loop', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: 'stable base' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -938,7 +939,7 @@ describe('request/context capacity records', () => {
send(agent, 'first')
await waitForIdle(ctx, agent)
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'mock', model: 'large' })
+ ctx.agentModel.select(agent, { provider: 'mock', model: 'large' })
send(agent, 'second')
await waitForIdle(ctx, agent)
@@ -965,7 +966,7 @@ describe('request/context capacity records', () => {
const agent = await ctx.agentLoop.create(SessionId('capacity-clear'), { provider: 'mock', model: 'known' })
send(agent, 'first')
await waitForIdle(ctx, agent)
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'mock', model: 'unknown' })
+ ctx.agentModel.select(agent, { provider: 'mock', model: 'unknown' })
send(agent, 'second')
await waitForIdle(ctx, agent)
diff --git a/packages/core/agent-loop/tests/resume.spec.ts b/packages/core/agent-loop/tests/resume.spec.ts
index 287593936e..e5bc0a7030 100644
--- a/packages/core/agent-loop/tests/resume.spec.ts
+++ b/packages/core/agent-loop/tests/resume.spec.ts
@@ -15,6 +15,7 @@ import type { SessionHandle } from '@deepseek-ai/dsh-session-persistence'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -38,6 +39,7 @@ async function mountPersistentHarness(root: string, adapter: MockAdapter, compre
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -870,6 +872,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1085,6 +1088,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -1342,6 +1346,7 @@ describe('configured-start failure edges', () => {
await configured.plugin(LlmRuntime)
await configured.plugin(SessionStore)
await configured.plugin(SessionProjectionRegistry)
+ await configured.plugin(AgentModelPlugin)
await configured.plugin(SystemPrompt)
await configured.plugin(ToolRuntime)
await configured.plugin(AgentRegistry)
@@ -1376,6 +1381,7 @@ describe('configured-start failure edges', () => {
await configured.plugin(LlmRuntime)
await configured.plugin(SessionStore)
await configured.plugin(SessionProjectionRegistry)
+ await configured.plugin(AgentModelPlugin)
await configured.plugin(SystemPrompt)
await configured.plugin(ToolRuntime)
await configured.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts
index 2addf9cba2..e14b7a5535 100644
--- a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts
+++ b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts
@@ -10,6 +10,7 @@ import AgentRegistry, { agentEvents, assembleContextFor } from '@deepseek-ai/dsh
import type { Agent } from '@deepseek-ai/dsh-agent'
import { scopeOf } from '@deepseek-ai/dsh-scope'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -19,6 +20,7 @@ async function harnessWithLoop(adapter: MockAdapter = new MockAdapter([textRespo
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: 'You are the deployment.' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/settings.spec.ts b/packages/core/agent-loop/tests/settings.spec.ts
index 4a0fd1c154..f151bae742 100644
--- a/packages/core/agent-loop/tests/settings.spec.ts
+++ b/packages/core/agent-loop/tests/settings.spec.ts
@@ -10,12 +10,14 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { liveConfig } from '../../../settings/settings/tests/live-config.ts'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
async function boot() {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/shutdown-drain.spec.ts b/packages/core/agent-loop/tests/shutdown-drain.spec.ts
index d89928395b..6a77843114 100644
--- a/packages/core/agent-loop/tests/shutdown-drain.spec.ts
+++ b/packages/core/agent-loop/tests/shutdown-drain.spec.ts
@@ -14,6 +14,7 @@ import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { MockAdapter, textResponse } from './mock-adapter.ts'
const dirs: string[] = []
@@ -34,6 +35,7 @@ async function mount(order: 'backend-first' | 'loop-first'): Promise<{ ctx: Cont
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
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 a3f2482191..8a97b4625f 100644
--- a/packages/core/agent-loop/tests/system-prompt-admission.spec.ts
+++ b/packages/core/agent-loop/tests/system-prompt-admission.spec.ts
@@ -1,8 +1,10 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { installModelSwitchNotice, recordDeclaredRoute, recordModelSelection, type Agent } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { type Agent, type ModelSelection } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
-import LlmRuntime, { createUserMessage, LlmError, type GenerateOptions } from '@deepseek-ai/dsh-llm'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
+import * as ModelSwitchNotice from '@deepseek-ai/dsh-agent-model/notice'
+import LlmRuntime, { createUserMessage, LlmError, ReasoningEffortId, type GenerateOptions } from '@deepseek-ai/dsh-llm'
import { toPiContext } from '@deepseek-ai/dsh-llm-pi-ai/src/context.ts'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -27,6 +29,7 @@ async function harness(capable = new MockAdapter(Array.from({ length: 8 }, () =>
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: '', personaSuffix: '' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -40,7 +43,7 @@ async function harness(capable = new MockAdapter(Array.from({ length: 8 }, () =>
...await next(), sections: [{ name: 'test', text: prompt, order: 0 }],
}))
const agent = await ctx.agentLoop.create(SessionId('admission'), { provider: 'capable', model: 'model' })
- installModelSwitchNotice(agent.ctx)
+ await agent.ctx.plugin(ModelSwitchNotice)
ctx.on('llm/stream', (request, next) => {
// Rebuild from copied source events, not the live projection's cached state.
const subject = ctx.agents.get(request.sessionId!)!
@@ -182,7 +185,7 @@ describe('prepared-route prompt admission', () => {
h.setPrompt('prompt three')
await send(h.agent, 'third')
expect(systemTexts(h.capable.requests[2]!)).toHaveLength(3)
- recordModelSelection(h.ctx.sessionProjections, h.agent, { provider, model: 'model' })
+ h.ctx.agentModel.select(h.agent, { provider, model: 'model' })
h.setPrompt('')
await send(h.agent, 'clear')
const adapter = provider === 'capable' ? h.capable : h.plain
@@ -286,7 +289,7 @@ describe('prepared-route prompt admission', () => {
h.setPrompt('prompt two')
await send(h.agent, 'second')
expect(systemTexts(h.capable.requests[1]!)).toHaveLength(2)
- recordModelSelection(h.ctx.sessionProjections, h.agent, { provider: 'plain', model: 'model' })
+ h.ctx.agentModel.select(h.agent, { provider: 'plain', model: 'model' })
if (changed) h.setPrompt('prompt three')
await send(h.agent, 'third')
expectPlain(h.plain.requests[0]!, changed ? 'prompt three' : 'prompt two')
@@ -304,9 +307,9 @@ describe('prepared-route prompt admission', () => {
it('appends on the first capable request after a plain route', async () => {
const h = await harness()
- recordModelSelection(h.ctx.sessionProjections, h.agent, { provider: 'plain', model: 'model' })
+ h.ctx.agentModel.select(h.agent, { provider: 'plain', model: 'model' })
await send(h.agent, 'first')
- recordModelSelection(h.ctx.sessionProjections, h.agent, { provider: 'capable', model: 'model' })
+ h.ctx.agentModel.select(h.agent, { provider: 'capable', model: 'model' })
h.setPrompt('prompt two')
await send(h.agent, 'second')
expect(systemTexts(h.capable.requests[0]!)).toEqual([
@@ -366,7 +369,7 @@ describe('prepared-route prompt admission', () => {
sessionId: SessionId('resumed'),
agentOptions: { provider: 'plain', model: 'model' },
seed: [...seed],
- setup: (_ctx, agent) => { recordDeclaredRoute(h.ctx.sessionProjections, agent, agent.options) },
+ setup: (_ctx, agent) => { h.ctx.agentModel.applyConfiguredRoute(agent, { provider: 'plain', model: 'model' }) },
})
await send(resumed, 'resume')
expectPlain(h.plain.requests[0]!, 'prompt two')
@@ -402,22 +405,61 @@ describe('prepared-route prompt admission', () => {
expect(types.filter(type => ['system/message', 'user/message', 'request/header'].includes(type))).toEqual([])
})
+ it.each([
+ { selected: undefined, proposed: ReasoningEffortId('high') },
+ { selected: ReasoningEffortId('high'), proposed: ReasoningEffortId('low') },
+ { selected: ReasoningEffortId('high'), proposed: undefined },
+ ])('rejects an agent/request effort change from $selected to $proposed before input admission', async ({ selected, proposed }) => {
+ const adapter = new MockAdapter([textResponse('never')], {
+ efforts: [{ id: ReasoningEffortId('low'), name: 'Low' }, { id: ReasoningEffortId('high'), name: 'High' }],
+ defaultEffort: ReasoningEffortId('low'),
+ })
+ const h = await harness(adapter)
+ h.ctx.agentModel.select(h.agent, {
+ provider: 'capable', model: 'model', ...selected === undefined ? {} : { reasoningEffort: selected },
+ })
+ h.ctx.on('agent/request', async (_payload, next) => {
+ const { reasoningEffort: _effort, ...config } = await next()
+ return { ...config, ...proposed === undefined ? {} : { reasoningEffort: proposed } }
+ })
+
+ await send(h.agent, 'reject the changed effort')
+ expect(adapter.requests).toEqual([])
+ const events = h.agent.session.snapshotEvents()
+ expect(events.filter(event => ['request/header', 'user/message', 'system/message'].includes(event.type))).toEqual([])
+ expect(events.at(-1)).toMatchObject({
+ type: 'turn/end', data: { reason: { kind: 'error', error: {
+ message: `agent "admission": agent/request proposed reasoning effort "${proposed ?? 'adapter default'}" after step preparation fixed "${selected ?? 'adapter default'}"; use ctx.agentModel.select() before step preparation`,
+ } } },
+ })
+ })
+
it('retains the captured selection for a retry and uses a pending choice on the next step', async () => {
const adapter = new MockAdapter([
() => { throw new LlmError('retry this request', 'SERVER') },
textResponse('recovered'),
- ])
+ ], { efforts: [{ id: ReasoningEffortId('high'), name: 'High' }] })
const h = await harness(adapter)
+ const selection = { provider: 'capable', model: 'model', reasoningEffort: ReasoningEffortId('high') }
+ h.ctx.agentModel.select(h.agent, selection)
+ const prepared: (Readonly | undefined)[] = []
+ h.ctx.on('system-prompt/assemble', (_assembly, context, next) => {
+ prepared.push(context.callConfig)
+ return next()
+ })
+ h.ctx.on('agent/pre-step', ({ callConfig }, next) => {
+ prepared.push(callConfig)
+ return next()
+ })
h.ctx.on('agent/request-error', () => {
- recordModelSelection(h.ctx.sessionProjections, h.agent, { provider: 'plain', model: 'next-model' })
+ h.ctx.agentModel.select(h.agent, { provider: 'plain', model: 'next-model' })
return Promise.resolve({ kind: 'retry' as const })
})
await send(h.agent, 'retry once')
- expect(adapter.requests.map(({ provider, model }) => ({ provider, model }))).toEqual([
- { provider: 'capable', model: 'model' },
- { provider: 'capable', model: 'model' },
- ])
+ expect(prepared).toEqual([selection, selection])
+ expect(adapter.requests.map(({ provider, model, reasoningEffort }) => ({ provider, model, reasoningEffort })))
+ .toEqual([selection, selection])
expect(h.plain.requests).toHaveLength(0)
expect(h.ctx.sessionProjections.stateOf(h.agent.session, 'modelSelection')?.pending)
.toEqual({ provider: 'plain', model: 'next-model' })
@@ -433,7 +475,7 @@ describe('prepared-route prompt admission', () => {
h.setPrompt('prompt two')
h.ctx.on('agent/request', async (_payload, next) => {
const config = await next()
- recordModelSelection(h.ctx.sessionProjections, h.agent, { provider: 'plain', model: 'model' })
+ h.ctx.agentModel.select(h.agent, { provider: 'plain', model: 'model' })
return { ...config, temperature: 0.5, maxTokens: 100 }
})
await send(h.agent, 'second')
diff --git a/packages/core/agent-loop/tests/tool-calls.spec.ts b/packages/core/agent-loop/tests/tool-calls.spec.ts
index 8aa74cec2d..9ee3f17e61 100644
--- a/packages/core/agent-loop/tests/tool-calls.spec.ts
+++ b/packages/core/agent-loop/tests/tool-calls.spec.ts
@@ -13,6 +13,7 @@ import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import ToolRuntime, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH, TOOL_RUNTIME_SCHEDULER, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
import { PtcRuntime } from '@deepseek-ai/dsh-ptc-runtime'
@@ -29,6 +30,7 @@ async function harness(adapter: MockAdapter, maxParallelToolCalls?: number) {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -336,6 +338,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -896,6 +899,7 @@ describe('PTC mode native-tool denial through the agent loop', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
await ctx.plugin(ToolRuntime, { mode: 'ptc' })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- FakePtcRuntime is an internal test helper with an opaque type shape
diff --git a/packages/core/agent-loop/tests/tool-order.spec.ts b/packages/core/agent-loop/tests/tool-order.spec.ts
index 287f5fe761..b2f856927f 100644
--- a/packages/core/agent-loop/tests/tool-order.spec.ts
+++ b/packages/core/agent-loop/tests/tool-order.spec.ts
@@ -1,4 +1,3 @@
-import { createUserMessage } from '@deepseek-ai/dsh-llm'
/**
* Loop-level tool-order determinism: the request/header event — and therefore the frozen
* request the adapter receives — carries the assembly's canonical tool order (system-prompt's
@@ -10,6 +9,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmRuntime from '@deepseek-ai/dsh-llm'
+import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session'
import SystemPrompt, { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import type { Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt'
@@ -17,6 +17,7 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -25,6 +26,7 @@ async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['too
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: 'stable base', ...toolOrder !== undefined ? { toolOrder } : {} })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tests/tool-updates.spec.ts b/packages/core/agent-loop/tests/tool-updates.spec.ts
index 6eca806085..581fc5433c 100644
--- a/packages/core/agent-loop/tests/tool-updates.spec.ts
+++ b/packages/core/agent-loop/tests/tool-updates.spec.ts
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import LlmRuntime, { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
@@ -22,6 +23,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: '', personaSuffix: '' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/core/agent-loop/tsconfig.json b/packages/core/agent-loop/tsconfig.json
index 5c846680ee..1a055900a1 100644
--- a/packages/core/agent-loop/tsconfig.json
+++ b/packages/core/agent-loop/tsconfig.json
@@ -40,6 +40,9 @@
},
{
"path": "../../session/session-projection"
+ },
+ {
+ "path": "../../core/agent-model"
}
]
}
diff --git a/packages/core/agent-model/README.i18n.yaml b/packages/core/agent-model/README.i18n.yaml
new file mode 100644
index 0000000000..f24ad1985c
--- /dev/null
+++ b/packages/core/agent-model/README.i18n.yaml
@@ -0,0 +1,46 @@
+# Bilingual-pair consistency record for README.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 packages/core/agent-model/README.md
+/:
+ en: 47d6f306e5ae4f55
+ zh: 022ec46c0e23194b
+/deepseek-ai-dsh-agent-model:
+ en: 2b4c8fc77ac2b04e
+ zh: dd7d344a92a29cdd
+/deepseek-ai-dsh-agent-model/summary:
+ en: 21423e931b7ebf09
+ zh: f357ade62c85e9b5
+/deepseek-ai-dsh-agent-model/table-of-contents:
+ en: d152484eb41ac6b4
+ zh: 725b47765ee8f30b
+/deepseek-ai-dsh-agent-model/use-this-package:
+ en: f2f4388fbe782649
+ zh: de4cbde4a0195353
+/deepseek-ai-dsh-agent-model/understand-the-implementation:
+ en: 71a1536cb2330595
+ zh: f65233dfcba14af7
+/deepseek-ai-dsh-agent-model/further-exploration:
+ en: c519faa401a47398
+ zh: 20a6e428162dcf76
+/deepseek-ai-dsh-agent-model/model-experience:
+ en: 215e7ba838619b7b
+ zh: a311da3843709f90
+/deepseek-ai-dsh-agent-model/model-experience/optional-route-switch-notice:
+ en: 6c60206a2618ff38
+ zh: b193eed786c567ba
+/deepseek-ai-dsh-agent-model/model-experience/optional-route-switch-notice/what-the-model-sees:
+ en: e58a8d2ac2f2c6b5
+ zh: 63400baf224b841e
+/deepseek-ai-dsh-agent-model/model-experience/optional-route-switch-notice/token-effect:
+ en: 878389d2d3feb863
+ zh: 2c59e93f7f46af57
+/deepseek-ai-dsh-agent-model/model-experience/optional-route-switch-notice/kv-cache-effect:
+ en: 4ae01fff11ba7d69
+ zh: 1c94b66a848d594e
+/deepseek-ai-dsh-agent-model/known-limitations-and-deferred-work:
+ en: 4d18cc7cc2c58a63
+ zh: d36f8f799e6e45e2
+/deepseek-ai-dsh-agent-model/known-limitations-and-deferred-work/dev-note:
+ en: e67f633dc6fe7773
+ zh: e88aeb617ffb74ea
diff --git a/packages/core/agent-model/README.md b/packages/core/agent-model/README.md
new file mode 100644
index 0000000000..d5dac2d750
--- /dev/null
+++ b/packages/core/agent-model/README.md
@@ -0,0 +1,105 @@
+---
+description: "Choose the model for a Session's next step, retain that choice across resume, and optionally announce route changes to the model."
+kind: "package-reference"
+---
+
+# @deepseek-ai/dsh-agent-model
+
+English | [中文](README.zh.md)
+
+## Summary
+
+Choose a provider, model, and optional reasoning effort for a Session's next model step. Recorded choices survive resume, while a step already being prepared keeps its captured choice through retries. Empty Sessions can continue following their Agent's live defaults. An optional scoped plugin tells the model when a different route continues its conversation.
+
+## Table of Contents
+
+- [Use this package](#use-this-package)
+- [Understand the implementation](#understand-the-implementation)
+- [Further Exploration](#further-exploration)
+- [Model Experience](#model-experience)
+- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
+- [Dev Note](#dev-note)
+
+-----
+
+
+## Use this package
+
+The base and minimal SDK bundles mount the selection provider. Custom compositions mount it alongside the Session projection registry; consumers inject `agentModel`.
+
+```yaml
+- name: '@deepseek-ai/dsh-session'
+- name: '@deepseek-ai/dsh-session-projection'
+- name: '@deepseek-ai/dsh-agent-model'
+```
+
+The provider has no configuration fields. `ctx.agentModel.resolveForNextStep(agent)` reads the pending selection, then the latest request's selection, then `agent.options`; it returns a detached value or `undefined` when none supplies a complete route. Reading neither starts a step nor pins a choice. A Session without a selection or request continues reading its Agent's live fallback options.
+
+`select(agent, selection)` records the complete choice unless an equal choice is already recorded. An explicit choice equal to the options fallback is still recorded. Omitted reasoning effort follows adapter defaults; this operation does not materialize defaults, check catalog membership, or save the deployment default. Validate user choices with `llm` before recording them. Session append failures retain the Session's existing failure guarantees.
+
+Ordinary resume preserves recorded selection. An entry point that explicitly overrides it calls `applyConfiguredRoute(agent, selection)` during unpublished Agent setup. It requires a complete configured route and inherits an existing explicit effort only when the route is unchanged and the declaration omits effort. Use `select()` without effort to explicitly return to adapter defaults.
+
+To enable switch notices for one Agent, import `* as ModelSwitchNotice` from `@deepseek-ai/dsh-agent-model/notice` and await `agentCtx.plugin(ModelSwitchNotice)` inside that Agent's setup callback. The owning scope removes its listener on disposal. Other Agents receive no notice from that registration.
+
+-----
+
+
+## Understand the implementation
+
+
+Implementation internals — click to expand
+
+The provider registers one pure fold before publishing its Service. The Session log is authoritative; the projection registry owns fold caches, and the selection service holds no second per-Agent choice. A request header clears only its matching pending selection and omits adapter-defaulted effort from the retained choice. The loop captures a detached choice before assembly and owns that step snapshot through retries.
+
+- [`src/service.ts`](src/service.ts) owns selection reads, replacement, and configured-route application.
+- [`src/projection.ts`](src/projection.ts) owns the durable fold and client view; [`src/types.ts`](src/types.ts) exports only the client view type.
+- [`src/index.ts`](src/index.ts) owns projection registration and service lifetime.
+- [`src/notice.ts`](src/notice.ts) compares the prepared step route with the previous request header without reading a newer pending choice.
+
+
+
+-----
+
+
+## Further Exploration
+
+- [Agent package](../agent/README.md) — Agent handles, unpublished setup, and event vocabulary.
+- [Agent-loop package](../agent-loop/README.md) — step preparation and request retries.
+- [Default-model package](../agent-default-model/README.md) — live deployment defaults and profile persistence.
+- [Core subsystem](../../../docs/subsystems/core.md) — selection types and generated service API.
+
+-----
+
+
+## Model Experience
+
+### Optional route-switch notice
+
+#### What the model sees
+
+The scoped notice plugin appends `[model changed: assistant turns above this point were generated by ; the session continues with ]` to an accepted request step whose prepared route differs from the previous request. Labels include providers only for a cross-provider switch. Initial requests, effort-only changes, rejected steps, and empty no-request decisions add nothing. A failure before header persistence leaves the previous route unchanged, so the next request step receives the notice again.
+
+#### Token effect
+
+Each admitted notice adds its text to retained user-message history. Selection events themselves add no prompt text.
+
+#### KV Cache effect
+
+Notices append to history without replacing earlier messages. Switching the requested model may prevent reuse of the previous model's cache; cache availability remains provider-owned.
+
+## Known Limitations and Deferred Work
+
+
+
+- **Availability is caller-owned** — recording a choice does not prove an adapter is available. Entry points validate user choices, and request preparation resolves the actual adapter and defaults.
+- **Deployment defaults are separate** — changing a Session does not save the default for other Sessions; that policy belongs to the entry point and default-model service.
+
+
+### Dev Note
+
+
+Working context for maintainers — click to expand
+
+None.
+
+
diff --git a/packages/core/agent-model/README.zh.md b/packages/core/agent-model/README.zh.md
new file mode 100644
index 0000000000..b64e5a96e7
--- /dev/null
+++ b/packages/core/agent-model/README.zh.md
@@ -0,0 +1,105 @@
+---
+description: "选择 Session 下一步骤的模型,在恢复后保留该选择,并按需向模型提示路由变化。"
+kind: "package-reference"
+---
+
+# @deepseek-ai/dsh-agent-model
+
+[English](README.md) | 中文
+
+## 概述
+
+为 Session 的下一个模型步骤选择提供方、模型与可选的推理强度。记录的选择在恢复后保留,已经开始准备的步骤则在重试期间继续使用捕获的选择。空白 Session 可以继续跟随 Agent 的实时默认值。可选的作用域插件会在另一条路由继续对话时向模型添加提示。
+
+## 目录
+
+- [使用本包](#use-this-package)
+- [理解实现](#understand-the-implementation)
+- [进一步探索](#further-exploration)
+- [模型体验](#model-experience)
+- [已知限制与延后工作](#known-limitations-and-deferred-work)
+- [开发备注](#dev-note)
+
+-----
+
+
+## 使用本包
+
+基础 bundle 与最小 SDK bundle 会挂载选择提供方。自定义组合将其与 Session 投影注册表一起挂载;消费方注入 `agentModel`。
+
+```yaml
+- name: '@deepseek-ai/dsh-session'
+- name: '@deepseek-ai/dsh-session-projection'
+- name: '@deepseek-ai/dsh-agent-model'
+```
+
+提供方没有配置字段。`ctx.agentModel.resolveForNextStep(agent)` 依次读取待用选择、最近请求的选择和 `agent.options`;返回独立的值,各来源均未提供完整路由时返回 `undefined`。读取不会启动步骤或固定选择。没有选择或请求记录的 Session 会继续读取 Agent 的实时回退选项。
+
+`select(agent, selection)` 记录完整选择,已有相同选择时不重复记录。与 options 回退值相同的显式选择仍会记录。省略推理强度表示跟随适配器默认值;此操作不会补齐默认值、检查模型目录或保存部署默认值。记录用户选择前先使用 `llm` 校验。Session 追加失败沿用 Session 已有的失败保证。
+
+普通恢复保留记录中的选择。入口若明确覆盖它,在未发布的 Agent setup 中调用 `applyConfiguredRoute(agent, selection)`。此方法要求完整的配置路由,仅在路由不变且声明省略强度时继承现有显式强度。要明确恢复为适配器默认强度,调用不带强度的 `select()`。
+
+要为一个 Agent 启用切换提示,从 `@deepseek-ai/dsh-agent-model/notice` 导入 `* as ModelSwitchNotice`,并在该 Agent 的 setup 回调中等待 `agentCtx.plugin(ModelSwitchNotice)`。所属作用域在释放时移除监听器。此注册不会向其他 Agent 添加提示。
+
+-----
+
+
+## 理解实现
+
+
+实现内部机制——点击展开
+
+提供方先注册一个纯折叠,再发布 Service。Session 日志是权威记录;投影注册表拥有折叠缓存,选择服务不再保存另一份每个 Agent 的选择。请求头仅清除与其匹配的待用选择,并从保留的选择中省略由适配器默认补齐的强度。循环在组装前捕获独立的选择,并在重试期间持有该步骤快照。
+
+- [`src/service.ts`](src/service.ts) 负责选择读取、替换与配置路由应用。
+- [`src/projection.ts`](src/projection.ts) 负责持久折叠和客户端视图;[`src/types.ts`](src/types.ts) 仅导出客户端视图类型。
+- [`src/index.ts`](src/index.ts) 负责投影注册与服务生命周期。
+- [`src/notice.ts`](src/notice.ts) 比较已准备步骤的路由与上次请求头,不读取更新的待用选择。
+
+
+
+-----
+
+
+## 进一步探索
+
+- [Agent 包](../agent/README.zh.md)——Agent 句柄、未发布 setup 与事件词汇。
+- [Agent-loop 包](../agent-loop/README.zh.md)——步骤准备和请求重试。
+- [默认模型包](../agent-default-model/README.zh.md)——实时部署默认值与 profile 持久化。
+- [核心子系统](../../../docs/subsystems/core.zh.md)——选择类型与生成的服务 API。
+
+-----
+
+
+## 模型体验
+
+### 可选的路由切换提示
+
+#### 模型看到什么
+
+作用域提示插件会在已接纳请求步骤的准备路由不同于上次请求时,追加 `[model changed: assistant turns above this point were generated by ; the session continues with ]`。仅跨提供方切换时在标签中包含提供方。初始请求、只改变强度、被拒绝的步骤和不发出请求的空决策均不添加提示。请求头持久化前失败会保留先前路由,所以下一个请求步骤会再次收到提示。
+
+#### Token 影响
+
+每条被接纳的提示都将文本加入保留的用户消息历史。选择事件本身不添加提示词文本。
+
+#### KV Cache 影响
+
+提示追加到历史,不替换先前消息。切换请求模型可能使先前模型的缓存无法复用;缓存是否可用仍由提供方决定。
+
+## 已知限制与延后工作
+
+
+
+- **可用性由调用方负责**——记录选择不代表适配器可用。入口校验用户选择,请求准备解析实际适配器与默认值。
+- **部署默认值独立管理**——改变一个 Session 不会为其他 Session 保存默认值;该策略属于入口与默认模型服务。
+
+
+### 开发备注
+
+
+维护者工作上下文——点击展开
+
+无。
+
+
diff --git a/packages/core/agent-model/package.json b/packages/core/agent-model/package.json
new file mode 100644
index 0000000000..ffcd99d6da
--- /dev/null
+++ b/packages/core/agent-model/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@deepseek-ai/dsh-agent-model",
+ "description": "Session model selection and optional model-switch notices",
+ "version": "0.2.1-alpha.3",
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
+ "directory": "packages/core/agent-model"
+ },
+ "type": "module",
+ "main": "lib/index.js",
+ "types": "lib/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/types/index.d.ts",
+ "default": "./lib/index.js"
+ },
+ "./types": {
+ "types": "./lib/types/types.d.ts",
+ "default": "./lib/types/types.js"
+ },
+ "./notice": {
+ "types": "./lib/types/notice.d.ts",
+ "default": "./lib/types/notice.js"
+ },
+ "./src/*": "./src/*",
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "lib/index.js",
+ "lib/types/**/*.js",
+ "lib/types/**/*.d.ts"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "zod": "4.4.3"
+ },
+ "peerDependencies": {
+ "@deepseek-ai/dsh-agent": "workspace:*",
+ "@deepseek-ai/dsh-llm": "workspace:*",
+ "@deepseek-ai/dsh-session": "workspace:*",
+ "@deepseek-ai/dsh-session-projection": "workspace:*",
+ "@deepseek-ai/cordis": "workspace:~"
+ },
+ "devDependencies": {
+ "@deepseek-ai/dsh-agent": "workspace:*",
+ "@deepseek-ai/dsh-llm": "workspace:*",
+ "@deepseek-ai/dsh-session": "workspace:*",
+ "@deepseek-ai/dsh-session-projection": "workspace:*",
+ "@deepseek-ai/cordis": "workspace:~"
+ }
+}
diff --git a/packages/core/agent-model/src/index.ts b/packages/core/agent-model/src/index.ts
new file mode 100644
index 0000000000..3b79575251
--- /dev/null
+++ b/packages/core/agent-model/src/index.ts
@@ -0,0 +1,45 @@
+/**
+ * Session model-selection provider: owns the projection and publishes its
+ * synchronous operations after registration, retaining the fold through
+ * dependent service teardown.
+ * @module @deepseek-ai/dsh-agent-model
+ */
+import { FiberState, type Context, type Fiber } from '@deepseek-ai/cordis'
+import { AgentModel } from './service.ts'
+import { modelSelectionProjectionDefinition } from './projection.ts'
+
+export { AgentModel } from './service.ts'
+export type { ModelSelectionProjection } from './types.ts'
+
+/** Session model-selection provider label. */
+export const name = 'agent-model'
+/** The provider owns a fold in the Session projection registry. */
+export const inject = ['sessionProjections']
+
+/**
+ * Register the fold before publishing the selection service. Service withdrawal
+ * awaits dependent consumers before removing the projection registration.
+ * @param ctx - provider scope owning the projection and service child.
+ * @returns after the selection service has activated.
+ */
+export async function apply(ctx: Context): Promise {
+ let service: Fiber | undefined
+ const draining = new Set()
+ ctx.effect(function* () {
+ // Cordis removes an independently disposing consumer before service withdrawal
+ // can find it. Track it until it leaves UNLOADING. `ctx.plugin()` returns a
+ // wrapper around the fiber, so the shared `dispose` identifies it.
+ yield ctx.on('internal/status', (fiber) => {
+ if (fiber.state !== FiberState.UNLOADING) draining.delete(fiber)
+ else if (service !== undefined && fiber.store?.agentModel?.fiber.dispose === service.dispose) draining.add(fiber)
+ })
+ yield ctx.sessionProjections.register(modelSelectionProjectionDefinition)
+ yield async () => {
+ while (draining.size > 0) await Promise.allSettled([...draining].map(fiber => fiber.await()))
+ }
+ service = ctx.plugin(AgentModel)
+ yield service.dispose
+ }, 'agentModel.lifecycle()')
+ // The synchronous effect assigns the child before returning.
+ await (service as Fiber).await()
+}
diff --git a/packages/core/agent-model/src/notice.ts b/packages/core/agent-model/src/notice.ts
new file mode 100644
index 0000000000..6779f46e00
--- /dev/null
+++ b/packages/core/agent-model/src/notice.ts
@@ -0,0 +1,60 @@
+/** Optional model-switch notices scoped to one Agent.
+ * @module @deepseek-ai/dsh-agent-model/notice
+ */
+import type { Context } from '@deepseek-ai/cordis'
+import type { PreStepDecision } from '@deepseek-ai/dsh-agent'
+import type { ModelSelection } from '@deepseek-ai/dsh-agent/types'
+import { boundContextSummary, createUserMessage, type ContextFormed } from '@deepseek-ai/dsh-llm'
+import { sameRoute } from './selection.ts'
+
+declare module '@deepseek-ai/dsh-llm' {
+ interface MessageSourceMap {
+ 'model-selection': { kind: 'model-selection' } & ContextFormed
+ }
+}
+
+/** Scoped model-switch notice plugin label. */
+export const name = 'agent-model-notice'
+
+function routeLabel(route: ModelSelection, other: ModelSelection): string {
+ return route.provider === other.provider ? route.model : `${route.provider}/${route.model}`
+}
+
+function modelSwitchNotice(previous: ModelSelection, selected: ModelSelection) {
+ const from = routeLabel(previous, selected)
+ const to = routeLabel(selected, previous)
+ return createUserMessage({
+ 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: boundContextSummary(`${from} → ${to}`),
+ },
+ })
+}
+
+/**
+ * Append a model-switch notice when the step's fixed route differs from the
+ * last request header. Effort-only changes and empty no-request decisions add
+ * no notice; a failure before header persistence repeats it on the next step.
+ * Mount this plugin in the selected Agent's unpublished setup scope.
+ * @param ctx - The selected Agent's scoped context.
+ */
+export function apply(ctx: Context): void {
+ ctx.on(
+ 'agent/pre-step',
+ async ({ agent, messages, signal, step, callConfig }, next): Promise => {
+ const decision = await next()
+ if (decision.kind === 'reject' || signal.aborted) return decision
+ // The loop skips an empty first step and an emptied offered continuation.
+ if (decision.messages.length === 0 && (step === 1 || messages.length > 0)) return decision
+ const previous = agent.session.requestHeader()?.config
+ if (previous === undefined || sameRoute(callConfig, previous)) return decision
+ return { ...decision, messages: [...decision.messages, modelSwitchNotice(previous, callConfig)] }
+ },
+ { prepend: true },
+ )
+}
diff --git a/packages/core/agent-model/src/projection.ts b/packages/core/agent-model/src/projection.ts
new file mode 100644
index 0000000000..e0b1c7b576
--- /dev/null
+++ b/packages/core/agent-model/src/projection.ts
@@ -0,0 +1,43 @@
+/** Durable model-selection fold and its client projection. */
+import type { ModelSelection } from '@deepseek-ai/dsh-agent/types'
+import { ReasoningEffortId } from '@deepseek-ai/dsh-llm/brand'
+import type { EpochHeader } from '@deepseek-ai/dsh-session'
+import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
+import { z } from 'zod'
+import type {} from './types.ts'
+import { sameModelSelection, toSelection } from './selection.ts'
+
+/** Selection a recorded request used, omitting an effort the adapter defaulted. */
+function selectionOfHeader({ config, adapterDefaults }: EpochHeader): ModelSelection {
+ return toSelection(config.provider, config.model, adapterDefaults?.reasoningEffort === true ? undefined : config.reasoningEffort)
+}
+
+const selectionSchema = z.object({
+ provider: z.string().min(1),
+ model: z.string().min(1),
+ reasoningEffort: z.string().min(1).transform(ReasoningEffortId).exactOptional(),
+}).nullable()
+
+/**
+ * Session selection fold: recorded selections become
+ * the pending choice, and each request header retires a matching one.
+ */
+export const modelSelectionProjectionDefinition: Required> = {
+ key: 'modelSelection',
+ stateVersion: 3,
+ stateSchema: z.object({ lastUsed: selectionSchema, pending: selectionSchema }),
+ init: () => ({ lastUsed: null, pending: null }),
+ apply: (state, event) => {
+ if (event.type === 'model/selection') {
+ return sameModelSelection(state.pending, event.data) ? state : { ...state, pending: { ...event.data } }
+ }
+ if (event.type !== 'request/header') return state
+ const lastUsed = selectionOfHeader(event.data.header)
+ const pending = sameModelSelection(state.pending, lastUsed) ? null : state.pending
+ return sameModelSelection(state.lastUsed, lastUsed) && pending === state.pending ? state : { lastUsed, pending }
+ },
+ wire: {
+ viewSchema: z.object({ lastUsed: selectionSchema, next: selectionSchema }),
+ view: state => ({ lastUsed: state.lastUsed, next: state.pending ?? state.lastUsed }),
+ },
+}
diff --git a/packages/core/agent-model/src/selection.ts b/packages/core/agent-model/src/selection.ts
new file mode 100644
index 0000000000..782aa5a7d2
--- /dev/null
+++ b/packages/core/agent-model/src/selection.ts
@@ -0,0 +1,35 @@
+/** Pure comparisons and snapshots shared by the selection service and notice plugin. */
+import type { ModelSelection } from '@deepseek-ai/dsh-agent/types'
+import type { ReasoningEffortId } from '@deepseek-ai/dsh-llm/brand'
+
+/**
+ * Build a detached selection, omitting an absent effort.
+ * @param provider - provider route.
+ * @param model - provider-owned model id.
+ * @param reasoningEffort - explicit effort; omission follows adapter defaults.
+ * @returns a detached selection.
+ */
+export function toSelection(provider: string, model: string, reasoningEffort: ReasoningEffortId | undefined): ModelSelection {
+ return { provider, model, ...reasoningEffort === undefined ? {} : { reasoningEffort } }
+}
+
+/**
+ * Compare provider and model without comparing reasoning effort.
+ * @param left - first selection.
+ * @param right - second selection.
+ * @returns whether both selections name the same route.
+ */
+export function sameRoute(left: ModelSelection, right: ModelSelection): boolean {
+ return left.provider === right.provider && left.model === right.model
+}
+
+/**
+ * Compare complete selections, treating absent effort as its own value.
+ * @param left - first selection, or no recorded selection.
+ * @param right - second selection, or no recorded selection.
+ * @returns whether both selections agree.
+ */
+export function sameModelSelection(left: ModelSelection | null, right: ModelSelection | null): boolean {
+ return left === right || (left !== null && right !== null
+ && sameRoute(left, right) && left.reasoningEffort === right.reasoningEffort)
+}
diff --git a/packages/core/agent-model/src/service.ts b/packages/core/agent-model/src/service.ts
new file mode 100644
index 0000000000..04a7fd41a8
--- /dev/null
+++ b/packages/core/agent-model/src/service.ts
@@ -0,0 +1,76 @@
+/** Session selection operations; the provider owns projection registration. */
+import { Service, type Context } from '@deepseek-ai/cordis'
+import type { Agent, ModelSelection } from '@deepseek-ai/dsh-agent/types'
+import type { SessionProjectionRegistry } from '@deepseek-ai/dsh-session-projection'
+import type {} from './types.ts'
+import { sameModelSelection, sameRoute, toSelection } from './selection.ts'
+
+declare module '@deepseek-ai/cordis' {
+ interface Context {
+ /** Session model selection for steps that have not begun preparation. */
+ agentModel: AgentModel
+ }
+}
+
+/** Read and record Session model choices without resolving adapter defaults. */
+export class AgentModel extends Service {
+ static inject = ['sessionProjections']
+
+ /**
+ * Provider-bound projection reader, usable while dependent callers drain. The
+ * plain holder keeps Cordis from re-tracing it through each caller's context.
+ */
+ private readonly runtime: { projections: SessionProjectionRegistry }
+
+ constructor(ctx: Context) {
+ super(ctx, 'agentModel')
+ this.runtime = { projections: ctx.sessionProjections }
+ }
+
+ /**
+ * Resolve the choice for the next step to begin preparation. Recorded choices
+ * take precedence over the last request and then the Agent's live options.
+ * This read neither starts a step nor changes an already-prepared step.
+ * @param agent - Agent whose Session and fallback options are read.
+ * @returns a detached choice, or undefined when no source names both provider and model.
+ */
+ resolveForNextStep(agent: Agent): Readonly | undefined {
+ const recorded = this.recordedSelection(agent)
+ if (recorded !== null) return { ...recorded }
+ const { provider, model, reasoningEffort } = agent.options
+ return provider && model ? toSelection(provider, model, reasoningEffort) : undefined
+ }
+
+ /**
+ * Replace the next step's complete selection. Omitted effort follows adapter
+ * defaults. An equal recorded choice is a no-op; an options-only fallback is
+ * always recorded. Callers own model availability and capability validation.
+ * The append uses the Session's existing persistence and failure guarantees.
+ * @param agent - Agent whose Session receives the model/selection event.
+ * @param selection - complete provider, model, and optional explicit effort.
+ */
+ select(agent: Agent, selection: Readonly): void {
+ if (sameModelSelection(this.recordedSelection(agent), selection)) return
+ agent.session.append('model/selection', toSelection(selection.provider, selection.model, selection.reasoningEffort))
+ }
+
+ /**
+ * Apply an explicit deployment route, including on Session resume. An omitted
+ * effort inherits the current explicit effort only for the same route;
+ * otherwise it follows adapter defaults. Call only when the entry point
+ * explicitly overrides recorded selection, not for ordinary resume fallback.
+ * @param agent - Agent whose Session receives the configured choice.
+ * @param selection - complete configured provider/model and optional effort.
+ */
+ applyConfiguredRoute(agent: Agent, selection: Readonly): void {
+ const current = this.resolveForNextStep(agent)
+ const inherited = current !== undefined && sameRoute(current, selection) ? current.reasoningEffort : undefined
+ this.select(agent, toSelection(selection.provider, selection.model, selection.reasoningEffort ?? inherited))
+ }
+
+ private recordedSelection(agent: Agent): ModelSelection | null {
+ const state = this.runtime.projections.stateOf(agent.session, 'modelSelection')
+ if (state === undefined) throw new Error('agent-model: used after its provider unloaded the modelSelection projection')
+ return state.pending ?? state.lastUsed
+ }
+}
diff --git a/packages/core/agent-model/src/types.ts b/packages/core/agent-model/src/types.ts
new file mode 100644
index 0000000000..14cf2f4fde
--- /dev/null
+++ b/packages/core/agent-model/src/types.ts
@@ -0,0 +1,35 @@
+/** Client view and internal fold types for Session model selection.
+ * @module @deepseek-ai/dsh-agent-model/types
+ */
+import type { ModelSelection } from '@deepseek-ai/dsh-agent/types'
+
+/**
+ * Durable model-selection fold for one Session. An effort the adapter
+ * defaulted on a recorded request is omitted, so later requests resolve the
+ * default again instead of pinning it as a choice.
+ */
+interface ModelSelectionProjectionState {
+ /** Selection used by the latest recorded model request. */
+ readonly lastUsed: ModelSelection | null
+ /** Later recorded selection not yet used by a matching model request. */
+ readonly pending: ModelSelection | null
+}
+
+/** Client view of the durable model-selection fold. */
+export interface ModelSelectionProjection {
+ /** Selection used by the latest recorded model request. */
+ readonly lastUsed: ModelSelection | null
+ /** Selection the next model step uses; null while the Agent's initial options supply the route. */
+ readonly next: ModelSelection | null
+}
+
+declare module '@deepseek-ai/dsh-session-projection/types' {
+ interface SessionProjectionStateMap {
+ /** Recorded model selection and the selection used by the latest request. */
+ modelSelection: ModelSelectionProjectionState
+ }
+ interface SessionProjectionMap {
+ /** Recorded model selection for the next model step and the latest request. */
+ modelSelection: ModelSelectionProjection
+ }
+}
diff --git a/packages/core/agent/tests/model-selection.spec.ts b/packages/core/agent-model/tests/model-selection.spec.ts
similarity index 72%
rename from packages/core/agent/tests/model-selection.spec.ts
rename to packages/core/agent-model/tests/model-selection.spec.ts
index 69255624fd..9558e50325 100644
--- a/packages/core/agent/tests/model-selection.spec.ts
+++ b/packages/core/agent-model/tests/model-selection.spec.ts
@@ -1,15 +1,13 @@
import { describe, expect, it, onTestFinished } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, {
+import {
agentEvents,
- currentModelSelection,
- installModelSwitchNotice,
- recordDeclaredRoute,
- recordModelSelection,
type Agent,
type AgentOptions,
type ModelSelection,
-} from '../src/index.ts'
+} from '@deepseek-ai/dsh-agent'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
+import * as ModelSwitchNotice from '@deepseek-ai/dsh-agent-model/notice'
import { createUserMessage, ReasoningEffortId, type UserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, type EpochHeader } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -26,11 +24,11 @@ async function harness(options: AgentOptions = INITIAL) {
onTestFinished(() => ctx.fiber.dispose())
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
- const fiber = await ctx.plugin(AgentRegistry)
+ const fiber = await ctx.plugin(AgentModelPlugin)
const session = ctx.sessions.create(SessionId('model-selection'))
const agent = { id: session.id, session, ctx, options } as Agent
- const current = () => currentModelSelection(ctx.sessionProjections, agent)
- const record = (selection: ModelSelection) => { recordModelSelection(ctx.sessionProjections, agent, selection) }
+ const current = () => ctx.agentModel.resolveForNextStep(agent)
+ const record = (selection: ModelSelection) => { ctx.agentModel.select(agent, selection) }
const header = (config: ModelSelection, adapterDefaults?: EpochHeader['adapterDefaults']) => session.append('request/header', {
header: { config, ...adapterDefaults === undefined ? {} : { adapterDefaults } }, reason: 'initial',
})
@@ -76,7 +74,7 @@ describe('Session model selection', () => {
const selected = { provider: 'beta', model: 'b1', reasoningEffort: ReasoningEffortId('high') }
h.record(selected)
selected.model = 'mutated'
- const read = h.current()!
+ const read: ModelSelection = h.current()!
read.model = 'also mutated'
expect(h.current()).toEqual({ provider: 'beta', model: 'b1', reasoningEffort: 'high' })
h.record(h.current()!)
@@ -99,7 +97,7 @@ describe('Session model selection', () => {
expect(h.ctx.sessionProjections.stateOf(h.session, 'modelSelection')?.pending).toEqual(selected)
}
const restored = { ...h.agent, session: Session.create(SessionId('restored'), h.session.snapshotEvents()) }
- expect(currentModelSelection(h.ctx.sessionProjections, restored)).toEqual(selected)
+ expect(h.ctx.agentModel.resolveForNextStep(restored)).toEqual(selected)
h.header(selected)
expect(h.ctx.sessionProjections.stateOf(h.session, 'modelSelection')).toEqual({ lastUsed: selected, pending: null })
expect(h.current()).toEqual(selected)
@@ -130,12 +128,9 @@ describe('Session model selection', () => {
expect(h.ctx.sessionProjections.stateOf(h.session, 'modelSelection')).toBe(state)
})
- it('records declared routes, inherits explicit effort only for the same route, and ignores incomplete declarations', async () => {
+ it('applies configured routes and inherits explicit effort only for the same route', async () => {
const h = await harness({})
- const declare = (options: AgentOptions) => { recordDeclaredRoute(h.ctx.sessionProjections, h.agent, options) }
- declare({ provider: 'alpha' })
- declare({ model: 'a0' })
- expect(h.current()).toBeUndefined()
+ const declare = (options: ModelSelection) => { h.ctx.agentModel.applyConfiguredRoute(h.agent, options) }
declare({ ...INITIAL, reasoningEffort: ReasoningEffortId('high') })
declare(INITIAL)
expect(h.current()).toEqual({ ...INITIAL, reasoningEffort: 'high' })
@@ -146,20 +141,72 @@ describe('Session model selection', () => {
expect(h.current()?.reasoningEffort).toBe('low')
})
- it('removes the selection projection when the Agent registry unloads and fails without it', async () => {
+ it.each(['provider', 'root', 'consumer-then-provider', 'consumer-then-root'] as const)('retains the projection through %s teardown until consumer cleanup settles', async (owner) => {
const h = await harness()
+ const entered = Promise.withResolvers()
+ const release = Promise.withResolvers()
+ onTestFinished(() => { release.resolve(undefined) })
+ const projections = h.ctx.sessionProjections
+ let cleaned = false
+ let cleanupError: unknown
+ const consumer = await h.ctx.plugin({
+ name: 'selection-cleanup-consumer',
+ inject: ['agentModel'],
+ apply(ctx: Context) {
+ const selections = ctx.agentModel
+ ctx.effect(() => async () => {
+ entered.resolve(undefined)
+ await release.promise
+ try {
+ selections.select(h.agent, { provider: 'beta', model: 'b1' })
+ expect(selections.resolveForNextStep(h.agent)).toEqual({ provider: 'beta', model: 'b1' })
+ cleaned = true
+ } catch (error) {
+ cleanupError = error
+ }
+ })
+ },
+ })
+ const consumerDisposal = owner.startsWith('consumer-then-') ? consumer.dispose() : undefined
+ if (consumerDisposal !== undefined) await entered.promise
+ let settled = false
+ const disposal = (owner.endsWith('provider') ? h.fiber.dispose() : h.ctx.fiber.dispose())
+ .finally(() => { settled = true })
+ await entered.promise
+ await new Promise(resolve => setImmediate(resolve))
+ try {
+ expect(settled).toBe(false)
+ expect(projections.stateOf(h.session, 'modelSelection')).toBeDefined()
+ } finally {
+ release.resolve(undefined)
+ await disposal
+ await consumerDisposal
+ }
+ expect(cleanupError).toBeUndefined()
+ expect(cleaned).toBe(true)
+ expect(h.session.snapshotEvents().at(-1)).toMatchObject({
+ type: 'model/selection', data: { provider: 'beta', model: 'b1' },
+ })
+ })
+
+ it('removes the projection on provider unload and reconstructs selection on remount', async () => {
+ const h = await harness()
+ h.record({ provider: 'beta', model: 'b1' })
+ const stale = h.ctx.agentModel
await h.fiber.dispose()
+ expect(h.ctx.get('agentModel')).toBeUndefined()
expect(h.ctx.sessionProjections.snapshot(h.session).values.modelSelection).toBeUndefined()
- expect(h.current).toThrow('the modelSelection projection is not registered')
- expect(() => { h.record({ provider: 'beta', model: 'b1' }) }).toThrow('the modelSelection projection is not registered')
+ expect(() => stale.resolveForNextStep(h.agent)).toThrow('used after its provider unloaded')
+ await h.ctx.plugin(AgentModelPlugin)
+ expect(h.current()).toEqual({ provider: 'beta', model: 'b1' })
})
})
-describe('installModelSwitchNotice()', () => {
+describe('model-switch notice plugin', () => {
it('announces the fixed step route, preserves request-series declarations, and repeats before a header commits', async () => {
const h = await harness()
h.header(INITIAL)
- installModelSwitchNotice(h.ctx)
+ await h.ctx.plugin(ModelSwitchNotice)
h.record({ provider: 'alpha', model: 'future' })
const selected = { provider: 'beta', model: 'b1' }
for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -177,7 +224,7 @@ describe('installModelSwitchNotice()', () => {
it('does not announce initial, same-route, effort-only, rejected, aborted, or disposed steps', async () => {
const h = await harness()
- const dispose = installModelSwitchNotice(h.ctx)
+ const fiber = await h.ctx.plugin(ModelSwitchNotice)
await expect(preStep(h.agent, INITIAL)).resolves.toMatchObject({ messages: [INPUT] })
h.header(INITIAL)
await expect(preStep(h.agent, INITIAL)).resolves.toMatchObject({ messages: [INPUT] })
@@ -189,14 +236,14 @@ describe('installModelSwitchNotice()', () => {
() => Promise.resolve({ kind: 'reject' as const }),
)).resolves.toEqual({ kind: 'reject' })
await expect(preStep(h.agent, selected, { signal: AbortSignal.abort() })).resolves.toMatchObject({ messages: [INPUT] })
- dispose()
+ await fiber.dispose()
await expect(preStep(h.agent, selected)).resolves.toMatchObject({ messages: [INPUT] })
})
it('preserves empty no-call decisions and announces an empty tool continuation', async () => {
const h = await harness()
h.header(INITIAL)
- installModelSwitchNotice(h.ctx)
+ await h.ctx.plugin(ModelSwitchNotice)
const selected = { provider: 'alpha', model: 'a1' }
await expect(preStep(h.agent, selected, { messages: [] })).resolves.toMatchObject({ messages: [] })
await expect(preStep(h.agent, selected, { messages: [], step: 2 })).resolves.toMatchObject({ messages: [] })
diff --git a/packages/core/agent-model/tsconfig.json b/packages/core/agent-model/tsconfig.json
new file mode 100644
index 0000000000..3227fe9b95
--- /dev/null
+++ b/packages/core/agent-model/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "lib/types"
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "../../../vendor/cosmokit"
+ },
+ {
+ "path": "../../../vendor/cordis"
+ },
+ {
+ "path": "../agent"
+ },
+ {
+ "path": "../../llm/llm"
+ },
+ {
+ "path": "../session"
+ },
+ {
+ "path": "../../session/session-projection"
+ }
+ ]
+}
diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml
index 84c6f54a6b..cfc8c4531c 100644
--- a/packages/core/agent/README.i18n.yaml
+++ b/packages/core/agent/README.i18n.yaml
@@ -15,11 +15,11 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-agent/use-this-package:
- en: 326904be67f43b86
- zh: e143c7d9e1f30bc0
+ en: 5fd4a83e491faae1
+ zh: 40d9342260f75e2c
/deepseek-ai-dsh-agent/use-this-package/create-or-resume-an-agent:
- en: adb806cfe9e2e0a4
- zh: 8da57f9e62869523
+ en: cb45db3b7202461a
+ zh: a002e1b07e12172e
/deepseek-ai-dsh-agent/use-this-package/drive-an-agent-s-conversation:
en: 71e258897e2e7d73
zh: abf0991012faa17e
@@ -27,8 +27,8 @@
en: 2594a1e02adea801
zh: cae8860369728efb
/deepseek-ai-dsh-agent/use-this-package/intercept-or-observe-work-in-flight:
- en: ae14e4c604211d42
- zh: 208018c2d017d5ca
+ en: 81dc076889cd965f
+ zh: 1578d770f2c5d782
/deepseek-ai-dsh-agent/understand-the-implementation:
en: ce7e776e9d516bdb
zh: 59d2751743db7795
@@ -36,14 +36,14 @@
en: 7c850051d2857bfa
zh: 4cab817553114e15
/deepseek-ai-dsh-agent/understand-the-implementation/step-admission:
- en: 9389b6c0ee0026f4
- zh: 91879cd69d16226e
+ en: c5ebf07dbc1a883f
+ zh: 847735016e1727a3
/deepseek-ai-dsh-agent/understand-the-implementation/durable-inbox:
en: f6802c7f94f623b3
zh: cc3885db80765970
/deepseek-ai-dsh-agent/understand-the-implementation/source-map:
- en: 5385d7a8618df0eb
- zh: 35853b9d1a1406f1
+ en: 6c4b27402462be8c
+ zh: 466730834fa82810
/deepseek-ai-dsh-agent/understand-the-implementation/registry-and-lifecycle:
en: df92e97cedd0d02e
zh: f6040c145bffd933
@@ -63,11 +63,11 @@
en: 962c598672b0eed6
zh: d05b3bc936685ad3
/deepseek-ai-dsh-agent/model-experience/user-steering-and-injected-messages/what-the-model-sees:
- en: b32fa34babcb4ac4
- zh: 8bbdabbf2b0a21e1
+ en: 1da09e174588443f
+ zh: f6168d519339ac7b
/deepseek-ai-dsh-agent/model-experience/user-steering-and-injected-messages/token-effect:
- en: 5f428bfadaf4394b
- zh: 03308df9fd3492a7
+ en: 95ed6c912ead0fba
+ zh: d5ad3d7257f7836c
/deepseek-ai-dsh-agent/model-experience/user-steering-and-injected-messages/kv-cache-effect:
en: 75dc61fe240860f0
zh: 621720291d1b4bd3
diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md
index ac3b6d75ff..b9150490d6 100644
--- a/packages/core/agent/README.md
+++ b/packages/core/agent/README.md
@@ -25,7 +25,7 @@ Use `dsh-agent` to create or resume live agents, send follow-up or steering inpu
## Use this package
-Mount `dsh-session-projection` before `dsh-agent`. The Agent registry provides `ctx.agents`, the `Agent` handle, and the shared `modelSelection` projection. A driver must register a factory before callers create or resume Agents; the shipped driver is `dsh-agent-loop`.
+The Agent registry provides `ctx.agents` and the `Agent` handle. A driver must register a factory before callers create or resume Agents; the shipped driver is `dsh-agent-loop`. [Session model selection](../agent-model/README.md) is provided separately.
### Create or resume an agent
@@ -42,7 +42,7 @@ await handle.dispose() // stops the loop, unregisters, removes the session, un
`AgentOptions` supplies the initial provider/model route, optional adapter-owned `reasoningEffort`, and optional positive `maxTokens` output cap. The loop validates exact-model reasoning support, resolves adapter defaults, records the effective values in the request header, and applies them to each conversation request. An optional `setup(agentCtx, agent)` callback composes the agent's scoped world before it is published: `agentCtx` owns registrations, while the explicit unpublished Agent provides its Session; the Context has no reverse Agent property. Scoped tools, prompt sections, and listeners exist before any creation announcement. Setup is composition-only: drive the agent only after creation resolves.
-A Session's recorded model selection takes precedence over its latest request route, which takes precedence over `AgentOptions`. Validated choices are stored as `model/selection` events unless the same choice is already recorded; an explicit choice equal to the options fallback is still recorded. Selection reads use the shared projection without a separate mutable cache. Programmatic resume preserves the recorded choice. A deployment can override the route during setup; when its configuration omits reasoning effort, the override retains saved explicit effort only for the same route. Adapter-defaulted effort is resolved again.
+The [model-selection service](../agent-model/README.md) resolves recorded choices before the Agent options fallback and owns explicit selection changes and configured resume overrides.
### Drive an agent's conversation
@@ -66,7 +66,7 @@ await handle.agent.whenIdle()
### Intercept or observe work in flight
-`agent/request` is deprecated and retained for existing listeners. Do not add new listeners. Provider, model, and reasoning effort are selected before step preparation; `AgentOptions.maxTokens` sets the output cap. Other per-request control overrides have no replacement API. See the [migration guide](../../../docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.md) for affected callers.
+`agent/request` is deprecated and retained for existing listeners. Do not add new listeners. Provider, model, and optional explicit reasoning effort are fixed at step preparation; listeners must preserve all three, including an absent effort. `AgentOptions.maxTokens` sets the output cap. Other per-request control overrides have no replacement API. See the [migration guide](../../../docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.md) for affected callers.
The `agent/*` events let plugins act on live work without depending on the loop package. `agent/pre-step` can reject a proposed step or replace the messages entering it; `agent/request-error` lets a listener retry a failed model request; `agent/turn-stopping` runs before an otherwise completed turn closes and can steer to keep it open. `agent/assistant-stream` carries one process-local Assistant attempt's ordered start, transient chunk, and end frames. Start names the attempt's turn and step, chunk indexes are dense from zero, and `end.index` is the next chunk position. The loop commits the complete compact stream as one `assistant/message` or `assistant/attempt` before a committed end frame, so the live event remains presentation data rather than the replay source. `agent/status`, `agent/created`, and `agent/disposed` drive UI and coordination state, and the per-message `agent/inbox/*` notifications keep inbox projections in sync. Exact signatures, dispatch modes, and payload contracts live in the generated region of the [core subsystem page](../../../docs/subsystems/core.md#cordis-surface).
@@ -86,7 +86,7 @@ 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`, the deprecated `agent/request` hook adjusts controls without changing the step route, and `prepareCall()` binds the adapter 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. Acceptance does not commit it: after assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the step's model selection, and `prepareCall()` binds the adapter 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.
### Durable inbox
@@ -103,7 +103,6 @@ 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) | Session model-selection state and optional switch notices |
| [`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
@@ -144,11 +143,11 @@ The package-level contract is enough for most consumers; read these when you nee
#### What the model sees
-`followup`, `steer`, and `inject` feed the owning session as identified user-role messages; accepted content becomes part of the derived history the model reads on later steps. `agent/pre-step` and the other declared events let plugins reject a proposed step or add durable request material. When model-switch notices are enabled, the first step assembled for a different provider/model route that would send a model request receives `[model changed: assistant turns above this point were generated by ; the session continues with ]`; provider names appear only when the switch crosses providers, and reasoning-effort-only changes add nothing. An empty first decision and a decision that removes offered messages remain no-request results. If a request step fails before logging its header, the next request step receives the notice again because the durable previous route has not changed.
+`followup`, `steer`, and `inject` feed the owning session as identified user-role messages; accepted content becomes part of the derived history the model reads on later steps. `agent/pre-step` and the other declared events let plugins reject a proposed step or add durable request material. Optional [model-switch notices](../agent-model/README.md#model-experience) are owned by the selection package.
#### Token effect
-Accepted content becomes retained history or a repeated session prefix; blocked content contributes no request tokens. Each emitted model-switch notice adds its text to retained history. Size is caller- and plugin-dependent.
+Accepted content becomes retained history or a repeated session prefix; blocked content contributes no request tokens. Size is caller- and plugin-dependent.
#### KV Cache effect
diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md
index 5a4edc9a6c..156a9a95f1 100644
--- a/packages/core/agent/README.zh.md
+++ b/packages/core/agent/README.zh.md
@@ -25,7 +25,7 @@ kind: "package-reference"
## 使用本包
-先挂载 `dsh-session-projection`,再挂载 `dsh-agent`。Agent 注册表提供 `ctx.agents`、`Agent` 句柄与共享 `modelSelection` 投影。调用方创建或恢复 Agent 之前,必须由驱动器注册工厂;随附驱动器为 `dsh-agent-loop`。
+Agent 注册表提供 `ctx.agents` 与 `Agent` 句柄。调用方创建或恢复 Agent 之前,必须由驱动器注册工厂;随附驱动器为 `dsh-agent-loop`。[Session 模型选择](../agent-model/README.zh.md) 由独立服务提供。
### 创建或恢复 agent
@@ -42,7 +42,7 @@ await handle.dispose() // stops the loop, unregisters, removes the session, un
`AgentOptions` 提供初始提供方/模型路由、可选的由适配器定义的 `reasoningEffort`,以及可选的正数 `maxTokens` 输出上限。循环会校验确切模型的推理(reasoning)支持、解析适配器默认值、把生效值记录在请求头中,并将它们应用到每个对话请求。可选的 `setup(agentCtx, agent)` 回调会在 agent 发布之前组合其作用域世界:`agentCtx` 拥有注册,显式的未发布 Agent 则提供其 Session;Context 不含反向 Agent 属性。作用域工具、提示词段与监听器在任何创建公告之前就已存在。Setup 只做组合:创建完成后才能驱动 agent。
-Session 中记录的模型选择优先于最近请求的路由,最近请求的路由又优先于 `AgentOptions`。校验后的选择以 `model/selection` 事件保存,记录中已有相同选择时不重复写入;与 options 回退值相同的显式选择仍会记录。选择读取使用共享投影,不使用独立的可变缓存。程序化恢复保留记录中的选择。部署可以在 setup 期间覆盖路由;配置省略推理强度时,仅在路由相同的情况下保留已保存的显式强度。适配器默认的强度会重新解析。
+[模型选择服务](../agent-model/README.zh.md) 优先解析已记录选择,再读取 Agent options 回退值,并负责显式选择变更与配置恢复覆盖。
### 驱动 agent 的对话
@@ -66,7 +66,7 @@ await handle.agent.whenIdle()
### 拦截或观察进行中的工作
-`agent/request` 已弃用,仅为现有监听器保留。不要添加新监听器。提供方、模型和推理强度在步骤准备前选定;`AgentOptions.maxTokens` 设置输出上限。其他逐请求控制参数覆盖没有替代 API。受影响的调用方参见[迁移指南](../../../docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.zh.md)。
+`agent/request` 已弃用,仅为现有监听器保留。不要添加新监听器。提供方、模型和可选的显式推理强度在步骤准备时固定;监听器必须保留三者,包括未指定强度的状态。`AgentOptions.maxTokens` 设置输出上限。其他逐请求控制参数覆盖没有替代 API。受影响的调用方参见[迁移指南](../../../docs/upgrade-guide/v0.2.1-alpha.3/session-model-selection/guide.zh.md)。
`agent/*` 事件让插件无需依赖循环包即可作用于实时工作。`agent/pre-step` 可以拒绝拟进入的步骤或替换进入它的消息;`agent/request-error` 让监听器重试失败的模型请求;`agent/turn-stopping` 在本可完成的轮次关闭前运行,并可通过 steer 使其保持打开。`agent/assistant-stream` 携带一个进程本地 Assistant attempt 的有序 start、瞬态分片与 end frame。start 给出该 attempt 的轮次与步骤,分片索引从零开始密集递增,`end.index` 则是下一个分片位置。loop 会在 committed end frame 前把完整紧凑流提交为一个 `assistant/message` 或 `assistant/attempt`,因此实时事件仍是呈现数据而非回放来源。`agent/status`、`agent/created` 与 `agent/disposed` 驱动 UI 与协调状态,逐消息的 `agent/inbox/*` 通知则让收件箱投影保持同步。确切签名、分发 mode 与 payload 约定见 [core 子系统页](../../../docs/subsystems/core.zh.md#cordis-surface) 的生成区块。
@@ -86,7 +86,7 @@ await handle.agent.whenIdle()
### 步骤准入
-`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages, startsRequestSeries? }`。enter 分支包含完整、带标识且冻结的消息批次。接纳不等于提交:组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变步骤路由的前提下调整控制参数,`prepareCall()` 绑定适配器,循环随后才提交系统提示词与用户批次。在任一异步阶段取消都不会提交这两者。`startsRequestSeries: true` 声明一个独立的模型消息序列;包装下游 enter 的监听器会保留该声明与批次,除非有意替换其中一项。领取会从 inbox 移除候选消息,领取后插入的消息则等待后续边界。
+`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages, startsRequestSeries? }`。enter 分支包含完整、带标识且冻结的消息批次。接纳不等于提交:组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变步骤模型选择的前提下调整控制参数,`prepareCall()` 绑定适配器,循环随后才提交系统提示词与用户批次。在任一异步阶段取消都不会提交这两者。`startsRequestSeries: true` 声明一个独立的模型消息序列;包装下游 enter 的监听器会保留该声明与批次,除非有意替换其中一项。领取会从 inbox 移除候选消息,领取后插入的消息则等待后续边界。
### 持久 inbox
@@ -103,7 +103,6 @@ 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) | Session 模型选择状态与可选切换提示 |
| [`src/archive-admission.ts`](src/archive-admission.ts) | Workspace 注册表归档准入中的 `turn` 族:运行中的回合及其用户原因取消 |
### 注册表与生命周期
@@ -144,11 +143,11 @@ await handle.agent.whenIdle()
#### 模型看到什么
-`followup`、`steer` 与 `inject` 以带标识的 user 角色消息馈送所属会话;被接纳的内容成为模型在后续步骤中读取的派生历史的一部分。`agent/pre-step` 与其他已声明事件让插件能够拒绝拟进入的步骤或添加持久请求材料。启用模型切换提示时,首次为不同提供方/模型路由组装且原本会发出模型请求的步骤会收到 `[model changed: assistant turns above this point were generated by ; the session continues with ]`;仅跨提供方切换时显示提供方名称,只改变推理强度时不添加消息。第一个决策为空时,以及某个决策移除候选消息后为空时,都不会产生请求。如果请求步骤在记录请求头前失败,持久记录中的先前路由没有变化,所以下一个请求步骤会再次收到提示。
+`followup`、`steer` 与 `inject` 以带标识的 user 角色消息馈送所属会话;被接纳的内容成为模型在后续步骤中读取的派生历史的一部分。`agent/pre-step` 与其他已声明事件让插件能够拒绝拟进入的步骤或添加持久请求材料。可选的[模型切换提示](../agent-model/README.zh.md#model-experience) 由选择包负责。
#### Token 影响
-被接纳内容成为保留历史,或成为每次请求重复的会话前缀;被阻止内容不贡献请求 token。每条实际发出的模型切换提示都会把对应文本加入保留历史。大小取决于调用方与插件。
+被接纳内容成为保留历史,或成为每次请求重复的会话前缀;被阻止内容不贡献请求 token。大小取决于调用方与插件。
#### KV Cache 影响
diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json
index a2279c825c..6c51838df1 100644
--- a/packages/core/agent/package.json
+++ b/packages/core/agent/package.json
@@ -33,9 +33,7 @@
"lib/types/**/*.d.ts"
],
"license": "MIT",
- "dependencies": {
- "zod": "4.4.3"
- },
+ "dependencies": {},
"peerDependencies": {
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-scope": "workspace:*",
diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts
index 328dd245c4..50cb4658f2 100644
--- a/packages/core/agent/src/index.ts
+++ b/packages/core/agent/src/index.ts
@@ -13,7 +13,6 @@ import { scopeTarget } from '@deepseek-ai/dsh-scope'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import type { SessionEvent, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import { installTurnArchiveAdmission } from './archive-admission.ts'
-import { modelSelectionProjectionDefinition } from './model-selection.ts'
import type { Agent } from './types.ts'
import type { AgentOptions, SessionStartSource } from './runtime-types.ts'
@@ -21,7 +20,6 @@ export * from './runtime-types.ts'
export * from './types.ts'
export type * from './projection.ts'
export * from './consumed-work.ts'
-export { currentModelSelection, installModelSwitchNotice, recordDeclaredRoute, recordModelSelection } from './model-selection.ts'
export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from './dispatch.ts'
export type { AgentEventDispatch, AgentSubjectEvent } from './dispatch.ts'
@@ -244,7 +242,6 @@ interface FactorySlot {
* nested lineage that starts an owning-fiber unload is excluded from its own drain.
*/
export class AgentRegistry extends Service {
- static inject = ['sessionProjections']
private store = new Map()
private factory: FactorySlot | undefined
@@ -285,7 +282,6 @@ export class AgentRegistry extends Service {
// Archive admission: the Workspace registry asks what still runs for a
// Session before hiding it; a running turn answers here, for every Agent.
installTurnArchiveAdmission(ctx, sessionId => this.get(sessionId))
- ctx.sessionProjections.register(modelSelectionProjectionDefinition)
}
/**
diff --git a/packages/core/agent/src/model-selection.ts b/packages/core/agent/src/model-selection.ts
deleted file mode 100644
index 67e1fb9e83..0000000000
--- a/packages/core/agent/src/model-selection.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * Session model selection: the `modelSelection` projection fold, which the
- * Agent registry registers as the only selection state, plus the helpers
- * runtime entry points use to read it, record `model/selection` events, and
- * announce a route switch to the model.
- * @module @deepseek-ai/dsh-agent/model-selection
- */
-
-import type { Context } from '@deepseek-ai/cordis'
-import {
- boundContextSummary,
- createUserMessage,
- ReasoningEffortId,
-} from '@deepseek-ai/dsh-llm'
-import type { EpochHeader } from '@deepseek-ai/dsh-session'
-import type SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
-import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
-import { z } from 'zod'
-import type { AgentOptions, PreStepDecision } from './runtime-types.ts'
-import type { Agent, ModelSelection, ModelSelectionProjectionState } from './types.ts'
-import type { ContextFormed } from '@deepseek-ai/dsh-llm'
-declare module '@deepseek-ai/dsh-llm' {
- interface MessageSourceMap {
- 'model-selection': { kind: 'model-selection' } & ContextFormed
- }
-}
-
-/** Projection reader the selection helpers require. */
-type ModelSelectionProjections = Pick
-
-/** Build a selection, omitting an absent effort. */
-function toSelection(provider: string, model: string, reasoningEffort: ReasoningEffortId | undefined): ModelSelection {
- return { provider, model, ...reasoningEffort === undefined ? {} : { reasoningEffort } }
-}
-
-/** Compare two complete selections, treating an absent effort as its own value. */
-function sameModelSelection(left: ModelSelection | null, right: ModelSelection | null): boolean {
- return left === right || (left !== null && right !== null
- && sameRoute(left, right) && left.reasoningEffort === right.reasoningEffort)
-}
-
-/** Selection a recorded request used, omitting an effort the adapter defaulted. */
-function selectionOfHeader({ config, adapterDefaults }: EpochHeader): ModelSelection {
- return toSelection(config.provider, config.model, adapterDefaults?.reasoningEffort === true ? undefined : config.reasoningEffort)
-}
-
-const selectionSchema = z.object({
- provider: z.string().min(1),
- model: z.string().min(1),
- reasoningEffort: z.string().min(1).transform(ReasoningEffortId).exactOptional(),
-}).nullable()
-
-/**
- * Projection fold registered by the Agent registry: recorded selections become
- * the pending choice, and each request header retires a matching one.
- */
-export const modelSelectionProjectionDefinition = {
- key: 'modelSelection',
- stateVersion: 3,
- stateSchema: z.object({ lastUsed: selectionSchema, pending: selectionSchema }),
- init: () => ({ lastUsed: null, pending: null }),
- apply: (state, event) => {
- if (event.type === 'model/selection') {
- return sameModelSelection(state.pending, event.data) ? state : { ...state, pending: { ...event.data } }
- }
- if (event.type !== 'request/header') return state
- const lastUsed = selectionOfHeader(event.data.header)
- const pending = sameModelSelection(state.pending, lastUsed) ? null : state.pending
- return sameModelSelection(state.lastUsed, lastUsed) && pending === state.pending ? state : { lastUsed, pending }
- },
- wire: {
- viewSchema: z.object({ lastUsed: selectionSchema, next: selectionSchema }),
- view: state => ({ lastUsed: state.lastUsed, next: state.pending ?? state.lastUsed }),
- },
-} satisfies ProjectionDefinition<'modelSelection', ModelSelectionProjectionState>
-
-/**
- * Read the selection the Agent's next model step uses: the latest recorded
- * selection, then the latest request's selection, then the route in
- * `agent.options`. A step that already started keeps its own snapshot.
- * @param projections - registry carrying the `modelSelection` projection.
- * @param agent - Agent whose Session is read.
- * @returns a detached selection, or undefined when no source names both provider and model.
- * @throws when the `modelSelection` projection is not registered.
- */
-export function currentModelSelection(
- projections: ModelSelectionProjections,
- agent: Agent,
-): ModelSelection | undefined {
- const recorded = recordedModelSelection(projections, agent)
- if (recorded !== null) return { ...recorded }
- const { provider, model, reasoningEffort } = agent.options
- return provider && model ? toSelection(provider, model, reasoningEffort) : undefined
-}
-
-/** Read only durable selection state; deployment defaults are not an explicit choice. */
-function recordedModelSelection(projections: ModelSelectionProjections, agent: Agent): ModelSelection | null {
- const state = projections.stateOf(agent.session, 'modelSelection')
- if (state === undefined) {
- throw new Error(`agent "${agent.id}": the modelSelection projection is not registered; mount dsh-session-projection`)
- }
- return state.pending ?? state.lastUsed
-}
-
-/**
- * Record the selection the Agent's next model step uses. A selection equal to
- * the pending or last-used recorded selection records nothing; an options-only
- * fallback is always recorded. Callers validate the route first.
- * @param projections - registry carrying the `modelSelection` projection.
- * @param agent - Agent whose Session receives the `model/selection` event.
- * @param selection - complete provider, model, and optional reasoning effort.
- */
-export function recordModelSelection(
- projections: ModelSelectionProjections,
- agent: Agent,
- selection: ModelSelection,
-): void {
- if (sameModelSelection(recordedModelSelection(projections, agent), selection)) return
- agent.session.append('model/selection', toSelection(selection.provider, selection.model, selection.reasoningEffort))
-}
-
-/**
- * Record a route declared by deployment configuration. The declared provider
- * and model replace the current selection; a declaration without an effort
- * keeps the current explicit effort for the same route and otherwise leaves
- * the adapter default.
- * @param projections - registry carrying the `modelSelection` projection.
- * @param agent - Agent whose selection is recorded.
- * @param declared - configured options; a declaration without both provider and model changes nothing.
- */
-export function recordDeclaredRoute(
- projections: ModelSelectionProjections,
- agent: Agent,
- declared: AgentOptions,
-): void {
- const { provider, model } = declared
- if (!provider || !model) return
- const current = currentModelSelection(projections, agent)
- const inherited = current !== undefined && sameRoute(current, { provider, model }) ? current.reasoningEffort : undefined
- recordModelSelection(projections, agent, toSelection(provider, model, declared.reasoningEffort ?? inherited))
-}
-
-function sameRoute(left: ModelSelection, right: ModelSelection): boolean {
- return left.provider === right.provider && left.model === right.model
-}
-
-function routeLabel(route: ModelSelection, other: ModelSelection): string {
- return route.provider === other.provider ? route.model : `${route.provider}/${route.model}`
-}
-
-function modelSwitchNotice(previous: ModelSelection, selected: ModelSelection) {
- const from = routeLabel(previous, selected)
- const to = routeLabel(selected, previous)
- return createUserMessage({
- content: [{
- type: 'text' as const,
- text: `[model changed: assistant turns above this point were generated by ${from}; the session continues with ${to}]`,
- }],
- source: {
- kind: 'model-selection',
- form: 'notice',
- summary: boundContextSummary(`${from} → ${to}`),
- },
- })
-}
-
-/**
- * Append a model-switch notice when the step's fixed route differs from the
- * last request header. Effort-only changes and empty no-request decisions add
- * no notice; a failure before header persistence repeats it on the next step.
- * @param agentCtx - The selected Agent's scoped context.
- * @returns Disposer for the scoped pre-step listener.
- */
-export function installModelSwitchNotice(agentCtx: Context): () => void {
- return agentCtx.on(
- 'agent/pre-step',
- async ({ agent, messages, signal, step, callConfig }, next): Promise => {
- const decision = await next()
- if (decision.kind === 'reject' || signal.aborted) return decision
- // The loop skips an empty first step and an emptied offered continuation.
- if (decision.messages.length === 0 && (step === 1 || messages.length > 0)) return decision
- const previous = agent.session.requestHeader()?.config
- if (previous === undefined || sameRoute(callConfig, previous)) return decision
- return { ...decision, messages: [...decision.messages, modelSwitchNotice(previous, callConfig)] }
- },
- { prepend: true },
- )
-}
diff --git a/packages/core/agent/src/runtime-types.ts b/packages/core/agent/src/runtime-types.ts
index 3d7c6d1571..13d8bc754a 100644
--- a/packages/core/agent/src/runtime-types.ts
+++ b/packages/core/agent/src/runtime-types.ts
@@ -12,15 +12,15 @@ import type {
} from '@deepseek-ai/dsh-llm'
import type { AgentCancelCause, Session, SessionSeq, UserMessage } from '@deepseek-ai/dsh-session'
export type { AgentCancelCause } from '@deepseek-ai/dsh-session'
-import type { Agent, InboxTarget } from './types.ts'
+import type { Agent, InboxTarget, ModelSelection } from './types.ts'
export type { Agent } from './types.ts'
import type {} from '@deepseek-ai/dsh-system-prompt'
declare module '@deepseek-ai/dsh-system-prompt' {
interface AssembleContext {
/** Agent for this assembly; absent on diagnostics. When present, `scope` must identify the same agent. */
agent?: Agent
- /** Provider and model fixed for this step; absent outside step preparation. */
- callConfig?: Readonly>
+ /** Model selection fixed for this step; omitted effort uses adapter defaults. Absent outside step preparation. */
+ callConfig?: Readonly
}
}
@@ -320,23 +320,24 @@ declare module '@deepseek-ai/cordis' {
* @param payload.turn - the turn that will own the step.
* @param payload.step - the step proposed by the loop.
* @param payload.signal - the current turn's cancellation signal.
- * @param payload.callConfig - provider and model fixed for this step and its retries.
+ * @param payload.callConfig - model selection fixed for this step and its retries; omitted effort uses adapter defaults.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
- 'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; callConfig: Readonly> }, next: () => Promise): Promise
+ 'agent/pre-step'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; callConfig: Readonly }, next: () => Promise): Promise
/**
* Replace the frozen call configuration. `await next()` yields the config
* the machine would use (the step's model selection with agent options on
* the first request, the logged header's controls afterwards); a
- * replacement may adjust controls but must keep the provider and model. On step admission,
+ * replacement may adjust controls but must keep the provider, model, and
+ * reasoning effort selection. 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.
* @deprecated Retained for existing listeners; do not add new listeners.
- * Use recordModelSelection() before step preparation for model and reasoning
+ * Use ctx.agentModel.select() before step preparation for model and reasoning
* choices, and AgentOptions.maxTokens for the output cap. Other per-request
* control overrides have no replacement API.
* @param payload.agent - the agent making the model call.
diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts
index 9d62249b51..98e12e7373 100644
--- a/packages/core/agent/src/types.ts
+++ b/packages/core/agent/src/types.ts
@@ -97,37 +97,6 @@ export interface ModelSelection {
reasoningEffort?: ReasoningEffortId
}
-/**
- * Durable model-selection fold for one Session. An effort the adapter
- * defaulted on a recorded request is omitted, so later requests resolve the
- * default again instead of pinning it as a choice.
- */
-export interface ModelSelectionProjectionState {
- /** Selection used by the latest recorded model request. */
- readonly lastUsed: ModelSelection | null
- /** Later recorded selection not yet used by a matching model request. */
- readonly pending: ModelSelection | null
-}
-
-/** Client view of the durable model-selection fold. */
-export interface ModelSelectionProjection {
- /** Selection used by the latest recorded model request. */
- readonly lastUsed: ModelSelection | null
- /** Selection the next model step uses; null while the Agent's initial options supply the route. */
- readonly next: ModelSelection | null
-}
-
-declare module '@deepseek-ai/dsh-session-projection/types' {
- interface SessionProjectionStateMap {
- /** Recorded model selection and the selection used by the latest request. */
- modelSelection: ModelSelectionProjectionState
- }
- interface SessionProjectionMap {
- /** Recorded model selection for the next model step and the latest request. */
- modelSelection: ModelSelectionProjection
- }
-}
-
declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
/**
diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json
index b5a0509762..55677083ea 100644
--- a/packages/core/tools/package.json
+++ b/packages/core/tools/package.json
@@ -68,6 +68,7 @@
"@deepseek-ai/dsh-sandbox": "workspace:*",
"@deepseek-ai/dsh-sandbox-policy": "workspace:*",
"@deepseek-ai/dsh-working-directory": "workspace:*",
- "@deepseek-ai/dsh-agent-loop-testkit": "workspace:*"
+ "@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts
index 870c72fe4c..6d500ceff6 100644
--- a/packages/core/tools/tests/tools.spec.ts
+++ b/packages/core/tools/tests/tools.spec.ts
@@ -7,6 +7,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
import ToolRuntime, {
defineContentToolFixture, defineTool, JsonSchemaError, parameterSchemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError,
@@ -809,6 +810,7 @@ describe('ToolRuntime', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/experimental/browser-use-runtime/package.json b/packages/experimental/browser-use-runtime/package.json
index c54f1ab96c..705c5e0b48 100644
--- a/packages/experimental/browser-use-runtime/package.json
+++ b/packages/experimental/browser-use-runtime/package.json
@@ -64,6 +64,7 @@
"@deepseek-ai/dsh-attachment-local": "workspace:*",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
"@deepseek-ai/dsh-working-directory": "workspace:*",
- "@deepseek-ai/dsh-fs-local": "workspace:*"
+ "@deepseek-ai/dsh-fs-local": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/experimental/browser-use-runtime/tests/mcp.spec.ts b/packages/experimental/browser-use-runtime/tests/mcp.spec.ts
index b4fc9d4b36..611579b3b9 100644
--- a/packages/experimental/browser-use-runtime/tests/mcp.spec.ts
+++ b/packages/experimental/browser-use-runtime/tests/mcp.spec.ts
@@ -20,6 +20,7 @@ import Agents from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import Projections from '@deepseek-ai/dsh-session-projection'
import WorkingDirectory from '@deepseek-ai/dsh-working-directory'
@@ -71,7 +72,7 @@ async function load(exclusive = false, mode?: string, toolCallTimeoutMs?: number
const model = new FixtureModel()
const modules = new Map([
['browserUse', BrowserUse], ['prompt', SystemPrompt], ['tools', Tools], ['llm', Llm],
- ['sessions', Sessions], ['agents', Agents], ['loop', AgentLoop], ['projections', Projections],
+ ['sessions', Sessions], ['agents', Agents], ['loop', AgentLoop], ['projections', Projections], ['agentModel', AgentModelPlugin],
['fs', LocalFileSystem], ['workingDirectory', WorkingDirectory],
['model', { inject: ['llm'], apply(ctx: Context) { ctx.effect(() => ctx.llm.registerAdapter(['fixture'], model)) } }],
['browser', { inject: ['browserUse', 'agents', 'tools', 'systemPrompt', 'workingDirectory'], apply(ctx: Context) {
@@ -150,6 +151,7 @@ it('waits for SystemPrompt and ToolRuntime before reserving the provider', async
contexts.push(ctx)
await ctx.plugin(BrowserUse)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(Agents)
provideWorkingDirectoryFixture(ctx)
const tools = ctx.plugin(Tools)
diff --git a/packages/experimental/browser-use-stagehand-native/package.json b/packages/experimental/browser-use-stagehand-native/package.json
index 51164b5675..3786df0c34 100644
--- a/packages/experimental/browser-use-stagehand-native/package.json
+++ b/packages/experimental/browser-use-stagehand-native/package.json
@@ -54,6 +54,7 @@
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-agent-loop": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
"@deepseek-ai/dsh-attachment": "workspace:*",
"@deepseek-ai/dsh-attachment-local": "workspace:*",
diff --git a/packages/experimental/browser-use-stagehand-native/tests/loader-composition.spec.ts b/packages/experimental/browser-use-stagehand-native/tests/loader-composition.spec.ts
index 253f2dd6a9..7a851cf537 100644
--- a/packages/experimental/browser-use-stagehand-native/tests/loader-composition.spec.ts
+++ b/packages/experimental/browser-use-stagehand-native/tests/loader-composition.spec.ts
@@ -10,6 +10,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import BrowserUseRegistry from '@deepseek-ai/dsh-browser-use'
import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local'
import LlmRuntime, { LlmAdapter, ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm'
@@ -78,6 +79,7 @@ it('loads browser tools from cordis.yml, logs browser results, and admits the sc
['@deepseek-ai/dsh-llm', LlmRuntime],
['@deepseek-ai/dsh-session', SessionStore],
['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
+ ['@deepseek-ai/dsh-agent-model', AgentModelPlugin],
['@deepseek-ai/dsh-system-prompt', SystemPrompt],
['@deepseek-ai/dsh-tools', ToolRuntime],
['@deepseek-ai/dsh-agent', AgentRegistry],
diff --git a/packages/experimental/claude-code-mods/package.json b/packages/experimental/claude-code-mods/package.json
index 77762adebf..58106f3684 100644
--- a/packages/experimental/claude-code-mods/package.json
+++ b/packages/experimental/claude-code-mods/package.json
@@ -77,6 +77,7 @@
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-agent-loop": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
"@deepseek-ai/dsh-attachment": "workspace:*",
"@deepseek-ai/dsh-commands": "workspace:*",
diff --git a/packages/experimental/claude-code-mods/tests/loader-composition.spec.ts b/packages/experimental/claude-code-mods/tests/loader-composition.spec.ts
index 5eae8a59d6..6a1c03c8fa 100644
--- a/packages/experimental/claude-code-mods/tests/loader-composition.spec.ts
+++ b/packages/experimental/claude-code-mods/tests/loader-composition.spec.ts
@@ -10,6 +10,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import CommandRuntime from '@deepseek-ai/dsh-commands'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import WorkingDirectory from '@deepseek-ai/dsh-working-directory'
@@ -40,6 +41,7 @@ it('loads from cordis.yml, counts the model\'s tool calls, and answers /tally th
['@deepseek-ai/dsh-llm', LlmRuntime],
['@deepseek-ai/dsh-session', SessionStore],
['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
+ ['@deepseek-ai/dsh-agent-model', AgentModelPlugin],
['@deepseek-ai/dsh-system-prompt', SystemPrompt],
['@deepseek-ai/dsh-fs-local', LocalFileSystem],
['@deepseek-ai/dsh-working-directory', WorkingDirectory],
diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json
index cd52545450..4654802fb8 100644
--- a/packages/experimental/client-ui-agent-team/package.json
+++ b/packages/experimental/client-ui-agent-team/package.json
@@ -62,7 +62,8 @@
"@testing-library/react": "^16.3.3",
"@deepseek-ai/cordis": "workspace:~",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"files": [
"locale/*.json",
diff --git a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx
index b418f20336..a3b3422b96 100644
--- a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx
+++ b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx
@@ -1,5 +1,6 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
+import type {} from '@deepseek-ai/dsh-agent-model/types'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type {
TeamMemberProjection,
diff --git a/packages/experimental/client-ui-agent-team/tsconfig.json b/packages/experimental/client-ui-agent-team/tsconfig.json
index cf196ce4c6..777a876db2 100644
--- a/packages/experimental/client-ui-agent-team/tsconfig.json
+++ b/packages/experimental/client-ui-agent-team/tsconfig.json
@@ -4,19 +4,48 @@
"rootDir": "src",
"outDir": "lib/types"
},
- "include": ["src"],
+ "include": [
+ "src"
+ ],
"references": [
- { "path": "../../../vendor/cordis" },
- { "path": "../agent-team" },
- { "path": "../../api/session-controller/tsconfig.client.json" },
- { "path": "../../client/locale/tsconfig.client.json" },
- { "path": "../../client/store" },
- { "path": "../../client/ui-conversation/tsconfig.client.json" },
- { "path": "../../client/ui-primitives" },
- { "path": "../../client/ui-renderer" },
- { "path": "../../client/ui-session" },
- { "path": "../../client/ui-slots" },
- { "path": "../../client/ui-workspace" },
- { "path": "../../core/session" }
+ {
+ "path": "../../../vendor/cordis"
+ },
+ {
+ "path": "../agent-team"
+ },
+ {
+ "path": "../../api/session-controller/tsconfig.client.json"
+ },
+ {
+ "path": "../../client/locale/tsconfig.client.json"
+ },
+ {
+ "path": "../../client/store"
+ },
+ {
+ "path": "../../client/ui-conversation/tsconfig.client.json"
+ },
+ {
+ "path": "../../client/ui-primitives"
+ },
+ {
+ "path": "../../client/ui-renderer"
+ },
+ {
+ "path": "../../client/ui-session"
+ },
+ {
+ "path": "../../client/ui-slots"
+ },
+ {
+ "path": "../../client/ui-workspace"
+ },
+ {
+ "path": "../../core/session"
+ },
+ {
+ "path": "../../core/agent-model"
+ }
]
}
diff --git a/packages/experimental/computer-use-cua-driver-mcp/package.json b/packages/experimental/computer-use-cua-driver-mcp/package.json
index d0ec97477e..f105437072 100644
--- a/packages/experimental/computer-use-cua-driver-mcp/package.json
+++ b/packages/experimental/computer-use-cua-driver-mcp/package.json
@@ -47,6 +47,7 @@
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-agent-loop": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*",
"@deepseek-ai/dsh-attachment-local": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*"
}
diff --git a/packages/experimental/computer-use-cua-driver-mcp/tests/composition.spec.ts b/packages/experimental/computer-use-cua-driver-mcp/tests/composition.spec.ts
index b904d88429..f648d10dc0 100644
--- a/packages/experimental/computer-use-cua-driver-mcp/tests/composition.spec.ts
+++ b/packages/experimental/computer-use-cua-driver-mcp/tests/composition.spec.ts
@@ -16,6 +16,7 @@ import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepse
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local'
import * as Provider from '../src/index.ts'
@@ -68,6 +69,7 @@ async function load(mode?: string): Promise<{ ctx: Context; root: string; model:
['@deepseek-ai/dsh-agent', AgentRegistry],
['@deepseek-ai/dsh-agent-loop', AgentLoop],
['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
+ ['@deepseek-ai/dsh-agent-model', AgentModelPlugin],
['@deepseek-ai/dsh-attachment-local', LocalAttachmentStore],
['@fixture/model', { inject: ['llm'], apply(ctx: Context) { ctx.effect(() => ctx.llm.registerAdapter(['fixture'], model)) } }],
['@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp', Provider],
diff --git a/packages/experimental/computer-use-cua-driver-native/package.json b/packages/experimental/computer-use-cua-driver-native/package.json
index 715a6a9a01..9a0528faa2 100644
--- a/packages/experimental/computer-use-cua-driver-native/package.json
+++ b/packages/experimental/computer-use-cua-driver-native/package.json
@@ -46,6 +46,7 @@
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-agent-loop": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
"@deepseek-ai/dsh-attachment": "workspace:*",
"@deepseek-ai/dsh-attachment-local": "workspace:*",
diff --git a/packages/experimental/computer-use-cua-driver-native/tests/loader-composition.spec.ts b/packages/experimental/computer-use-cua-driver-native/tests/loader-composition.spec.ts
index e78af90f9a..fc0b13dd71 100644
--- a/packages/experimental/computer-use-cua-driver-native/tests/loader-composition.spec.ts
+++ b/packages/experimental/computer-use-cua-driver-native/tests/loader-composition.spec.ts
@@ -10,6 +10,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import ComputerUseRegistry from '@deepseek-ai/dsh-computer-use'
import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local'
import LlmRuntime, { LlmAdapter, ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm'
@@ -63,6 +64,7 @@ it('loads from cordis.yml and logs the native screenshot before the next model r
['@deepseek-ai/dsh-llm', LlmRuntime],
['@deepseek-ai/dsh-session', SessionStore],
['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
+ ['@deepseek-ai/dsh-agent-model', AgentModelPlugin],
['@deepseek-ai/dsh-system-prompt', SystemPrompt],
['@deepseek-ai/dsh-tools', ToolRuntime],
['@deepseek-ai/dsh-agent', AgentRegistry],
diff --git a/packages/experimental/cua-macos/package.json b/packages/experimental/cua-macos/package.json
index 10d039043a..4168f19a01 100644
--- a/packages/experimental/cua-macos/package.json
+++ b/packages/experimental/cua-macos/package.json
@@ -57,6 +57,7 @@
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-agent-loop": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
"@deepseek-ai/dsh-attachment": "workspace:*",
"@deepseek-ai/dsh-attachment-local": "workspace:*",
diff --git a/packages/experimental/cua-macos/tests/large-image-ptc.spec.ts b/packages/experimental/cua-macos/tests/large-image-ptc.spec.ts
index a8a93b1857..c371d2cd71 100644
--- a/packages/experimental/cua-macos/tests/large-image-ptc.spec.ts
+++ b/packages/experimental/cua-macos/tests/large-image-ptc.spec.ts
@@ -8,6 +8,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local'
import ComputerUseRegistry from '@deepseek-ai/dsh-computer-use'
@@ -64,7 +65,7 @@ it.skipIf(process.platform === 'win32')('delivers a valid PNG larger than 4 MiB
bounds: { x: 0, y: 0, width: 1024, height: 512 },
}))
const modules = {
- llm: LlmRuntime, session: SessionStore, projection: SessionProjectionRegistry,
+ llm: LlmRuntime, session: SessionStore, projection: SessionProjectionRegistry, agentModel: AgentModelPlugin,
systemPrompt: SystemPrompt, tools: ToolRuntime, agents: AgentRegistry, agentLoop: AgentLoop,
attachments: LocalAttachmentStore, fs: FileSystem, subprocess: SubprocessLocal,
sandbox: Sandbox, sandboxPolicy: SandboxPolicy, ptc: NodePtcRuntime,
diff --git a/packages/experimental/cua-macos/tests/loader-composition.spec.ts b/packages/experimental/cua-macos/tests/loader-composition.spec.ts
index 9b65b66398..b4a4ea4a05 100644
--- a/packages/experimental/cua-macos/tests/loader-composition.spec.ts
+++ b/packages/experimental/cua-macos/tests/loader-composition.spec.ts
@@ -9,6 +9,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import ComputerUseRegistry from '@deepseek-ai/dsh-computer-use'
import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local'
import LlmRuntime, { LlmAdapter, ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm'
@@ -76,7 +77,7 @@ it.skipIf(process.platform === 'win32')('shares one application between retained
await writeFile(join(root, 'observe-helper'), '')
const readEvents = async () => (await readFile(join(root, 'helper-events.jsonl'), 'utf8')).trim().split('\n').map(line => AuditEvent.parse(JSON.parse(line)))
const modules = {
- llm: LlmRuntime, session: SessionStore, projection: SessionProjectionRegistry,
+ llm: LlmRuntime, session: SessionStore, projection: SessionProjectionRegistry, agentModel: AgentModelPlugin,
systemPrompt: SystemPrompt, tools: ToolRuntime, agents: AgentRegistry, agentLoop: AgentLoop,
attachments: LocalAttachmentStore, computerUse: ComputerUseRegistry, subprocess: SubprocessLocal,
cua: NativeProvider,
diff --git a/packages/experimental/tool-worktree/package.json b/packages/experimental/tool-worktree/package.json
index a7c6511d10..532c5c037a 100644
--- a/packages/experimental/tool-worktree/package.json
+++ b/packages/experimental/tool-worktree/package.json
@@ -54,7 +54,8 @@
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-working-directory": "workspace:*",
"@deepseek-ai/dsh-experimental-worktree": "workspace:*",
- "@deepseek-ai/dsh-agent-loop": "workspace:*"
+ "@deepseek-ai/dsh-agent-loop": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"icon": "./icon.svg",
"dsh": {
diff --git a/packages/experimental/tool-worktree/tests/loader-composition.spec.ts b/packages/experimental/tool-worktree/tests/loader-composition.spec.ts
index f2f1367605..c35e85d32b 100644
--- a/packages/experimental/tool-worktree/tests/loader-composition.spec.ts
+++ b/packages/experimental/tool-worktree/tests/loader-composition.spec.ts
@@ -10,6 +10,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import LlmRuntime, { ToolCallId } from '@deepseek-ai/dsh-llm'
import LocalSandbox from '@deepseek-ai/dsh-sandbox-local'
@@ -54,6 +55,7 @@ describe('worktree tool through real Loader composition', () => {
['@deepseek-ai/dsh-agent', AgentRegistry],
['@deepseek-ai/dsh-session', SessionStore],
['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
+ ['@deepseek-ai/dsh-agent-model', AgentModelPlugin],
['@deepseek-ai/dsh-system-prompt', SystemPrompt],
['@deepseek-ai/dsh-llm', LlmRuntime],
['@deepseek-ai/dsh-tools', ToolRuntime],
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index 3557dd82d7..eb400f5dfa 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/packages/extensions/tool-cordis/src/api-catalog.ts
@@ -130,6 +130,29 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
],
},
+ {
+ key: 'agentModel',
+ summary: 'Read and record Session model choices without resolving adapter defaults.',
+ description: 'Read and record Session model choices without resolving adapter defaults.',
+ methods: [
+ {
+ signature: 'resolveForNextStep(agent: Agent): Readonly | undefined',
+ description: 'Resolve the choice for the next step to begin preparation. Recorded choices take precedence over the last request and then the Agent\'s live options. This read neither starts a step nor changes an already-prepared step.',
+ parameters: [{ name: 'agent', description: 'Agent whose Session and fallback options are read.' }],
+ returns: 'a detached choice, or undefined when no source names both provider and model.',
+ },
+ {
+ signature: 'select(agent: Agent, selection: Readonly): void',
+ description: 'Replace the next step\'s complete selection. Omitted effort follows adapter defaults. An equal recorded choice is a no-op; an options-only fallback is always recorded. Callers own model availability and capability validation. The append uses the Session\'s existing persistence and failure guarantees.',
+ parameters: [{ name: 'agent', description: 'Agent whose Session receives the model/selection event.' }, { name: 'selection', description: 'complete provider, model, and optional explicit effort.' }],
+ },
+ {
+ signature: 'applyConfiguredRoute(agent: Agent, selection: Readonly): void',
+ description: 'Apply an explicit deployment route, including on Session resume. An omitted effort inherits the current explicit effort only for the same route; otherwise it follows adapter defaults. Call only when the entry point explicitly overrides recorded selection, not for ordinary resume fallback.',
+ parameters: [{ name: 'agent', description: 'Agent whose Session receives the configured choice.' }, { name: 'selection', description: 'complete configured provider/model and optional effort.' }],
+ },
+ ],
+ },
{
key: 'agentPresets',
summary: 'Registry of YAML-declared presets and the revisions live Agents retain.',
@@ -4072,10 +4095,10 @@ export const EVENT_API: readonly EventApiEntry[] = [
{
name: 'agent/pre-step',
mode: 'waterfall',
- signature: '\'agent/pre-step\'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; callConfig: Readonly> }, next: () => Promise): Promise',
+ signature: '\'agent/pre-step\'(this: Scoped, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal; callConfig: Readonly }, next: () => Promise): Promise',
summary: 'Reject a proposed step or replace the messages that enter it.',
description: 'Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages.',
- parameters: [{ name: 'payload', description: '.callConfig - provider and model fixed for this step and its retries. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }],
+ parameters: [{ name: 'payload', description: '.callConfig - model selection fixed for this step and its retries; omitted effort uses adapter defaults. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }],
},
{
name: 'agent/request-error',
diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json
index d5c13eb48e..2ca8cad7a8 100644
--- a/packages/llm/llm-deepseek/package.json
+++ b/packages/llm/llm-deepseek/package.json
@@ -69,6 +69,7 @@
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
"@deepseek-ai/cordis-plugin-include": "workspace:~",
"@deepseek-ai/dsh-agent-loop": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
diff --git a/packages/llm/llm-deepseek/tests/account-routing.spec.ts b/packages/llm/llm-deepseek/tests/account-routing.spec.ts
index afdc4e5516..45e4477477 100644
--- a/packages/llm/llm-deepseek/tests/account-routing.spec.ts
+++ b/packages/llm/llm-deepseek/tests/account-routing.spec.ts
@@ -3,8 +3,9 @@ import { afterEach, expect, it, vi } from 'vitest'
import { installAccountTaskCancellation, type DeepSeekAccount } from '@deepseek-ai/dsh-deepseek-account'
import { Context } from '@deepseek-ai/cordis'
import LlmRuntime, { createUserMessage, LlmAdapter, type GenerateOptions, type StreamChunk, ToolCallId } from '@deepseek-ai/dsh-llm'
-import AgentRegistry, { recordModelSelection, type Agent } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
@@ -31,7 +32,7 @@ async function harness() {
const ctx = new Context()
contexts.push(ctx)
installAccountTaskCancellation(ctx)
- for (const plugin of [LlmRuntime, SessionStore, SessionProjectionRegistry, SystemPrompt, ToolRuntime, AgentRegistry]) {
+ for (const plugin of [LlmRuntime, SessionStore, SessionProjectionRegistry, AgentModelPlugin, SystemPrompt, ToolRuntime, AgentRegistry]) {
await ctx.plugin(plugin)
}
await ctx.plugin(AgentLoop, { agents: [] })
@@ -109,7 +110,7 @@ it.each(['tool', 'retry'])('keeps the active provider until a later step binds a
return config
})
const selectOfficial = (agent: Agent) => {
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
+ ctx.agentModel.select(agent, { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
}
ctx.on('agent/request-error', async ({ agent }, next) => {
await next()
@@ -279,7 +280,7 @@ it('ignores idle account history but uses the last bound route during a new turn
const cancel = vi.spyOn(agent, 'cancel')
ctx.emit('deepseek-account/signed-out')
expect(cancel).not.toHaveBeenCalled()
- recordModelSelection(ctx.sessionProjections, agent, { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
+ ctx.agentModel.select(agent, { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
const preparing = Promise.withResolvers()
const proceed = Promise.withResolvers()
ctx.on('agent/request', async (_event, next) => {
diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts
index f09aa74264..5704a39872 100644
--- a/packages/llm/llm-deepseek/tests/adapter.spec.ts
+++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts
@@ -11,6 +11,7 @@ import LocalAttachments from '@deepseek-ai/dsh-attachment-local'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -312,7 +313,7 @@ describe('Cordis provider composition', () => {
const modules = new Map([
['@deepseek-ai/dsh-llm', LlmRuntime], ['@deepseek-ai/dsh-llm-deepseek-api-key', Messages], ['@deepseek-ai/dsh-llm-deepseek-account', AccountProvider],
['@deepseek-ai/dsh-credentials-local', LocalCredentials],
- ['@deepseek-ai/dsh-agent', AgentRegistry], ['@deepseek-ai/dsh-agent-loop', AgentLoop],
+ ['@deepseek-ai/dsh-agent', AgentRegistry], ['@deepseek-ai/dsh-agent-loop', AgentLoop], ['@deepseek-ai/dsh-agent-model', AgentModelPlugin],
['@deepseek-ai/dsh-session', SessionStore], ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
['@deepseek-ai/dsh-system-prompt', SystemPrompt], ['@deepseek-ai/dsh-tools', ToolRuntime],
])
diff --git a/packages/llm/llm-deepseek/tests/fixtures/cordis.yml b/packages/llm/llm-deepseek/tests/fixtures/cordis.yml
index 0d95d810dd..7c90da43a1 100644
--- a/packages/llm/llm-deepseek/tests/fixtures/cordis.yml
+++ b/packages/llm/llm-deepseek/tests/fixtures/cordis.yml
@@ -4,6 +4,7 @@
- name: '@deepseek-ai/dsh-system-prompt'
- name: '@deepseek-ai/dsh-tools'
- name: '@deepseek-ai/dsh-agent'
+- name: '@deepseek-ai/dsh-agent-model'
- name: '@deepseek-ai/dsh-agent-loop'
config:
agents: []
diff --git a/packages/llm/llm-retry/package.json b/packages/llm/llm-retry/package.json
index b371ebba4a..d2836de0c3 100644
--- a/packages/llm/llm-retry/package.json
+++ b/packages/llm/llm-retry/package.json
@@ -62,6 +62,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-timeout": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
- "@deepseek-ai/cordis": "workspace:~"
+ "@deepseek-ai/cordis": "workspace:~",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/llm/llm-retry/tests/loader-composition.spec.ts b/packages/llm/llm-retry/tests/loader-composition.spec.ts
index 5c5a177040..d080ee079a 100644
--- a/packages/llm/llm-retry/tests/loader-composition.spec.ts
+++ b/packages/llm/llm-retry/tests/loader-composition.spec.ts
@@ -8,6 +8,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import LlmRuntime, { createUserMessage, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
@@ -62,6 +63,7 @@ async function loadYaml(lines: readonly string[]): Promise {
['@deepseek-ai/dsh-llm', LlmRuntime],
['@deepseek-ai/dsh-session', SessionStore],
['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
+ ['@deepseek-ai/dsh-agent-model', AgentModelPlugin],
['@deepseek-ai/dsh-system-prompt', SystemPrompt],
['@deepseek-ai/dsh-tools', ToolRuntime],
['@deepseek-ai/dsh-agent', AgentRegistry],
@@ -92,6 +94,7 @@ describe('real Loader composition', () => {
"- name: '@deepseek-ai/dsh-llm'",
"- name: '@deepseek-ai/dsh-session'",
"- name: '@deepseek-ai/dsh-session-projection'",
+ "- name: '@deepseek-ai/dsh-agent-model'",
"- name: '@deepseek-ai/dsh-system-prompt'",
"- name: '@deepseek-ai/dsh-tools'",
"- name: '@deepseek-ai/dsh-agent'",
diff --git a/packages/llm/llm-retry/tests/retry.spec.ts b/packages/llm/llm-retry/tests/retry.spec.ts
index 89c6bf7a4f..b31fd93c5c 100644
--- a/packages/llm/llm-retry/tests/retry.spec.ts
+++ b/packages/llm/llm-retry/tests/retry.spec.ts
@@ -17,9 +17,10 @@ import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session'
import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
-import AgentRegistry, { recordModelSelection } from '@deepseek-ai/dsh-agent'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent, RequestErrorAction } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import * as retry from '../src/index.ts'
type ScriptEntry = Error | Iterable | AsyncIterable
@@ -109,6 +110,7 @@ async function harness(
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
@@ -537,7 +539,7 @@ describe('provider-routed retry policy', () => {
provider: 'mock',
model: 'mock',
})
- recordModelSelection(context.sessionProjections, agent, { provider: 'other', model: 'mock' })
+ context.agentModel.select(agent, { provider: 'other', model: 'mock' })
const scheduled = waitForRetry(context, agent, 1)
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'reroute' }], source: { kind: 'user' } }))
@@ -578,7 +580,7 @@ describe('provider-routed retry policy', () => {
context.tools.register(defineContentToolFixture({
name: 'switch_provider', description: '', parameters: {},
execute: async () => {
- recordModelSelection(agent.ctx.sessionProjections, agent, { provider: 'other', model: 'mock' })
+ agent.ctx.agentModel.select(agent, { provider: 'other', model: 'mock' })
return [{ type: 'text', text: 'switched' }]
},
}))
diff --git a/packages/plan/plan-mode/package.json b/packages/plan/plan-mode/package.json
index 78f31dc964..df4e6fc589 100644
--- a/packages/plan/plan-mode/package.json
+++ b/packages/plan/plan-mode/package.json
@@ -69,6 +69,7 @@
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-user-questions": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
- "@deepseek-ai/dsh-agent-loop-testkit": "workspace:*"
+ "@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/plan/plan-mode/tests/integration.spec.ts b/packages/plan/plan-mode/tests/integration.spec.ts
index 5c39a792e8..04d79b04e4 100644
--- a/packages/plan/plan-mode/tests/integration.spec.ts
+++ b/packages/plan/plan-mode/tests/integration.spec.ts
@@ -7,6 +7,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import PlanModeController from '@deepseek-ai/dsh-plan-mode'
import UserQuestionService, { UserQuestionError } from '@deepseek-ai/dsh-user-questions'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -26,6 +27,7 @@ async function harness(adapter: MockAdapter): Promise {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/preset/agent-preset-registry/package.json b/packages/preset/agent-preset-registry/package.json
index 31b2314211..d831284b3e 100644
--- a/packages/preset/agent-preset-registry/package.json
+++ b/packages/preset/agent-preset-registry/package.json
@@ -89,6 +89,7 @@
"@deepseek-ai/dsh-sandbox-policy": "workspace:*",
"@deepseek-ai/dsh-user-approval": "workspace:*",
"@deepseek-ai/dsh-agent-preset": "workspace:*",
- "@types/js-yaml": "^4.0.9"
+ "@types/js-yaml": "^4.0.9",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/preset/agent-preset-registry/tests/harness.ts b/packages/preset/agent-preset-registry/tests/harness.ts
index c420624fe3..77dd3f2c60 100644
--- a/packages/preset/agent-preset-registry/tests/harness.ts
+++ b/packages/preset/agent-preset-registry/tests/harness.ts
@@ -8,6 +8,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import AgentPresets, { type PresetDefinition } from '../src/index.ts'
import { liveConfig } from '../../../settings/settings/tests/live-config.ts'
@@ -24,6 +25,7 @@ export async function harness(options: { live?: boolean } = {}): Promise(sessionId),
agentOptions,
// The initialized route replaces the restored Session's model selection.
- setup: (_agentCtx, agent) => { recordDeclaredRoute(this.ctx.sessionProjections, agent, agentOptions) },
+ setup: (_agentCtx, agent) => { this.ctx.agentModel.applyConfiguredRoute(agent, agentOptions) },
}) : this.ctx.agents.create({
sessionId: brandString(sessionId),
meta: { cwd: this.cwd },
diff --git a/packages/sdk/server/tests/plugin-shape.spec.ts b/packages/sdk/server/tests/plugin-shape.spec.ts
index 5c7c04b9ff..629cec8ea8 100644
--- a/packages/sdk/server/tests/plugin-shape.spec.ts
+++ b/packages/sdk/server/tests/plugin-shape.spec.ts
@@ -15,7 +15,7 @@ describe('dsh-sdk-jsonrpc-server plugin export shape', () => {
const unwrapped = loader.unwrapExports(jsonrpc) as Record
expect(unwrapped).toBe(jsonrpc)
expect(unwrapped.name).toBe('sdk-jsonrpc-server')
- expect(unwrapped.inject).toEqual(['agents', 'sessionProjections', 'workingDirectory'])
+ expect(unwrapped.inject).toEqual(['agents', 'agentModel', 'workingDirectory'])
expect(unwrapped.Config).toBeDefined()
expect(typeof unwrapped.apply).toBe('function')
})
diff --git a/packages/sdk/server/tests/server.spec.ts b/packages/sdk/server/tests/server.spec.ts
index 2961c6a428..96eae7b5db 100644
--- a/packages/sdk/server/tests/server.spec.ts
+++ b/packages/sdk/server/tests/server.spec.ts
@@ -15,6 +15,7 @@ import { randomUUID } from 'node:crypto'
import AgentRegistry, { type Agent, type AgentHandle } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
@@ -458,6 +459,7 @@ describe('HarnessSdkJsonRpcServer', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const transport = new FakeTransport()
const server = new HarnessSdkJsonRpcServer(ctx, transport)
diff --git a/packages/sdk/server/tsconfig.json b/packages/sdk/server/tsconfig.json
index 5bd5f18692..3d3e7d3763 100644
--- a/packages/sdk/server/tsconfig.json
+++ b/packages/sdk/server/tsconfig.json
@@ -37,6 +37,9 @@
},
{
"path": "../../session/working-directory"
+ },
+ {
+ "path": "../../core/agent-model"
}
]
}
diff --git a/packages/session/session-title/package.json b/packages/session/session-title/package.json
index 4f45d794d3..308b2a41f9 100644
--- a/packages/session/session-title/package.json
+++ b/packages/session/session-title/package.json
@@ -58,6 +58,7 @@
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
- "@deepseek-ai/dsh-session-projection": "workspace:*"
+ "@deepseek-ai/dsh-session-projection": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/session/session-title/tests/provider.spec.ts b/packages/session/session-title/tests/provider.spec.ts
index 8f22a930fc..ebc66a10f3 100644
--- a/packages/session/session-title/tests/provider.spec.ts
+++ b/packages/session/session-title/tests/provider.spec.ts
@@ -5,6 +5,7 @@ import { deepFreeze } from '@deepseek-ai/dsh-util-values'
import SessionStore, { SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionTitleService, {
SessionTitleProviderId,
type SessionTitleProvider,
@@ -56,6 +57,7 @@ describe('SessionTitleService Provider lifecycle', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, CONFIG)
const parent = ctx.sessions.create(SessionId('title-parent'))
parent.append('turn/start', {
@@ -121,6 +123,7 @@ describe('SessionTitleService Provider lifecycle', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, CONFIG)
const requests: SessionTitleProviderRequest[] = []
const provider: SessionTitleProvider = {
@@ -185,6 +188,7 @@ describe('SessionTitleService Provider lifecycle', () => {
onTestFinished(() => ctx.fiber.dispose())
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, { fallbackMaxWords: 5, fallbackMaxBytes: 1, maxTitleBytes: 24 })
const currentTitles: (SessionTitleSnapshot | undefined)[] = []
ctx.sessionTitle.register({
@@ -214,6 +218,7 @@ describe('SessionTitleService Provider lifecycle', () => {
onTestFinished(() => ctx.fiber.dispose())
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, CONFIG)
const currentTitles: (SessionTitleSnapshot | undefined)[] = []
ctx.sessionTitle.register({
@@ -255,6 +260,7 @@ describe('SessionTitleService Provider lifecycle', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, CONFIG)
const session = ctx.sessions.create(SessionId('chunked-provider-input'))
session.append('turn/start', { turn: 1 })
@@ -284,6 +290,7 @@ describe('SessionTitleService Provider lifecycle', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, CONFIG)
const requests: SessionTitleProviderRequest[] = []
ctx.sessionTitle.register({
@@ -320,6 +327,7 @@ describe('SessionTitleService Provider lifecycle', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, CONFIG)
const pending = deferred()
let observedSignal: AbortSignal | undefined
@@ -372,6 +380,7 @@ describe('SessionTitleService Provider lifecycle', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, CONFIG)
const pending = deferred()
const dispose = ctx.sessionTitle.register({
@@ -407,6 +416,7 @@ describe('SessionTitleService Provider lifecycle', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, CONFIG)
const firstResult = deferred()
const requests: SessionTitleProviderRequest[] = []
@@ -452,6 +462,7 @@ describe('SessionTitleService Provider lifecycle', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
await ctx.plugin(SessionTitleService, CONFIG)
const requests: SessionTitleProviderRequest[] = []
@@ -508,6 +519,7 @@ describe('SessionTitleService Provider lifecycle', () => {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
await ctx.plugin(SessionTitleService, CONFIG)
const generate = vi.fn(async (request: SessionTitleProviderRequest): Promise => ({
@@ -541,6 +553,7 @@ describe('SessionTitleService Provider lifecycle', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SessionTitleService, CONFIG)
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
const provider: SessionTitleProvider = {
diff --git a/packages/skill/skill-office/package.json b/packages/skill/skill-office/package.json
index db7285dac2..b3dae7596d 100644
--- a/packages/skill/skill-office/package.json
+++ b/packages/skill/skill-office/package.json
@@ -34,7 +34,8 @@
"@deepseek-ai/dsh-skill": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
- "@deepseek-ai/cordis-plugin-include": "workspace:~"
+ "@deepseek-ai/cordis-plugin-include": "workspace:~",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"dependencies": {
"@deepseek-ai/libreoffice-kit": "^0.1.5",
diff --git a/packages/skill/skill-office/tests/progressive-loading.spec.ts b/packages/skill/skill-office/tests/progressive-loading.spec.ts
index b1d9ea958e..2a572c4138 100644
--- a/packages/skill/skill-office/tests/progressive-loading.spec.ts
+++ b/packages/skill/skill-office/tests/progressive-loading.spec.ts
@@ -4,6 +4,7 @@ import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import LlmRuntime, { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -34,6 +35,7 @@ async function setup(mode: 'native' | 'ptc', names: string[]) {
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, { personaPrefix: '', personaSuffix: '' })
await ctx.plugin(ToolRuntime, { mode })
await ctx.plugin(AgentRegistry)
diff --git a/packages/subagent/subagent-fork-in-process/package.json b/packages/subagent/subagent-fork-in-process/package.json
index 1385911366..3d9d16b122 100644
--- a/packages/subagent/subagent-fork-in-process/package.json
+++ b/packages/subagent/subagent-fork-in-process/package.json
@@ -47,6 +47,7 @@
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
- "@deepseek-ai/dsh-session-projection": "workspace:*"
+ "@deepseek-ai/dsh-session-projection": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/subagent/subagent-fork-in-process/tests/subagent-fork-in-process.spec.ts b/packages/subagent/subagent-fork-in-process/tests/subagent-fork-in-process.spec.ts
index 8d2e646259..155c3b94a2 100644
--- a/packages/subagent/subagent-fork-in-process/tests/subagent-fork-in-process.spec.ts
+++ b/packages/subagent/subagent-fork-in-process/tests/subagent-fork-in-process.spec.ts
@@ -8,6 +8,7 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -194,6 +195,7 @@ describe('dsh-subagent-fork-in-process', () => {
it('unregisters the provider when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/subagent/subagent-spawn-in-process/package.json b/packages/subagent/subagent-spawn-in-process/package.json
index 9a119d4f62..c240a2174d 100644
--- a/packages/subagent/subagent-spawn-in-process/package.json
+++ b/packages/subagent/subagent-spawn-in-process/package.json
@@ -51,6 +51,7 @@
"@deepseek-ai/dsh-tool-subagent": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-session-projection": "workspace:*",
- "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*"
+ "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts b/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts
index e720547a79..e9f7e02deb 100644
--- a/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts
+++ b/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts
@@ -8,6 +8,7 @@ import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -276,6 +277,7 @@ describe('dsh-subagent-spawn-in-process', () => {
it('unregisters the provider when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(AgentRegistry)
diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json
index f44e116b46..ce5de52fa4 100644
--- a/packages/subagent/subagent/package.json
+++ b/packages/subagent/subagent/package.json
@@ -134,6 +134,7 @@
"@deepseek-ai/dsh-fs-sandbox": "workspace:*",
"@deepseek-ai/dsh-tool-fs": "workspace:*",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
- "@deepseek-ai/dsh-working-directory": "workspace:*"
+ "@deepseek-ai/dsh-working-directory": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/subagent/subagent/tests/list-children.spec.ts b/packages/subagent/subagent/tests/list-children.spec.ts
index 053bcdf7dc..6966f29824 100644
--- a/packages/subagent/subagent/tests/list-children.spec.ts
+++ b/packages/subagent/subagent/tests/list-children.spec.ts
@@ -7,6 +7,7 @@ import { Context } from '@deepseek-ai/cordis'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
@@ -189,6 +190,7 @@ describe('SubagentRuntime.listChildren', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(TestSessionQuery)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
@@ -219,6 +221,7 @@ describe('SubagentRuntime.listChildren', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(TestSessionQuery)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
diff --git a/packages/subagent/subagent/tests/service.spec.ts b/packages/subagent/subagent/tests/service.spec.ts
index fb105074c8..d4c527e8af 100644
--- a/packages/subagent/subagent/tests/service.spec.ts
+++ b/packages/subagent/subagent/tests/service.spec.ts
@@ -4,6 +4,7 @@ import { Context } from '@deepseek-ai/cordis'
import { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { HarnessError, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { carrierKeyOf } from '@deepseek-ai/dsh-scope'
@@ -83,6 +84,7 @@ async function service(withAgents = true): Promise<{ ctx: Context; subagents: Su
await ctx.plugin(AgentLoop, { agents: [] })
} else {
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
}
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
@@ -112,6 +114,7 @@ describe('SubagentRuntime', () => {
it('releases its catalog projection binding with the service fiber', async () => {
const ctx = new Context()
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await mountWorkingDirectoryFixture(ctx)
const fiber = await ctx.plugin(SubagentRuntime)
const parent = Session.create(SessionId('catalog-parent'))
diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json
index eb22e9c882..7b74e3b558 100644
--- a/packages/subagent/tool-subagent/package.json
+++ b/packages/subagent/tool-subagent/package.json
@@ -70,6 +70,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
- "@deepseek-ai/dsh-session-projection": "workspace:*"
+ "@deepseek-ai/dsh-session-projection": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts
index 191b79130e..5af787cb7c 100644
--- a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts
+++ b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts
@@ -11,6 +11,7 @@ import { bindScopeParent, createScope, scopeOf, scopeTarget } from '@deepseek-ai
import { liveConfig } from '../../../settings/settings/tests/live-config.ts'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
@@ -105,6 +106,7 @@ describe('SubagentModelSelectionConfig', () => {
selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig))
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition)
// A stored policy the service cannot apply fails when it is read, not when the profile row is written.
@@ -391,6 +393,7 @@ describe('SubagentModelSelectionConfig', () => {
try {
selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig))
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await mountWorkingDirectoryFixture(ctx)
subagentConfigs.set(ctx, await liveConfig(ctx, SubagentRuntime))
const childId = SessionId('child-without-session-registry')
diff --git a/packages/test-support/agent-loop-testkit/package.json b/packages/test-support/agent-loop-testkit/package.json
index f598721f67..321541272f 100644
--- a/packages/test-support/agent-loop-testkit/package.json
+++ b/packages/test-support/agent-loop-testkit/package.json
@@ -36,7 +36,8 @@
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-working-directory": "workspace:*",
- "@deepseek-ai/dsh-fs-local": "workspace:*"
+ "@deepseek-ai/dsh-fs-local": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
},
"dependencies": {},
"devDependencies": {
@@ -49,6 +50,7 @@
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-working-directory": "workspace:*",
- "@deepseek-ai/dsh-fs-local": "workspace:*"
+ "@deepseek-ai/dsh-fs-local": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*"
}
}
diff --git a/packages/test-support/agent-loop-testkit/src/index.ts b/packages/test-support/agent-loop-testkit/src/index.ts
index a3d906adf8..35872ef213 100644
--- a/packages/test-support/agent-loop-testkit/src/index.ts
+++ b/packages/test-support/agent-loop-testkit/src/index.ts
@@ -9,6 +9,7 @@ import type { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent, AgentOptions, Inbox, InboxTarget } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import type { SessionHeader, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
@@ -76,6 +77,7 @@ export async function mountAgentLoopTestDependencies(
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
+ await ctx.plugin(AgentModelPlugin)
await ctx.plugin(SystemPrompt, options.systemPrompt ?? {})
if (options.workingDirectory) {
if (ctx.get('fs') === undefined) await ctx.plugin(FsLocal)
diff --git a/packages/test-support/agent-loop-testkit/tsconfig.json b/packages/test-support/agent-loop-testkit/tsconfig.json
index 08f41e83ce..becc268c16 100644
--- a/packages/test-support/agent-loop-testkit/tsconfig.json
+++ b/packages/test-support/agent-loop-testkit/tsconfig.json
@@ -40,6 +40,9 @@
},
{
"path": "../../fs/fs-local"
+ },
+ {
+ "path": "../../core/agent-model"
}
]
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0ac35f64c1..d0b9c32d2a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1179,6 +1179,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-attachment':
specifier: workspace:*
version: link:../../attachment/attachment
@@ -1515,6 +1518,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-agent-preset-registry':
specifier: workspace:*
version: link:../../preset/agent-preset-registry
@@ -2219,6 +2225,9 @@ importers:
'@deepseek-ai/dsh-agent-loop':
specifier: workspace:*
version: link:../../core/agent-loop
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-api-gateway':
specifier: workspace:*
version: link:../../api/gateway
@@ -2527,6 +2536,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-fs':
specifier: workspace:*
version: link:../../fs/fs
@@ -2582,6 +2594,9 @@ importers:
'@deepseek-ai/dsh-agent-loop':
specifier: workspace:*
version: link:../../core/agent-loop
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-attachment-local':
specifier: workspace:*
version: link:../../attachment/attachment-local
@@ -3762,6 +3777,9 @@ importers:
'@deepseek-ai/dsh-agent':
specifier: workspace:*
version: link:../../core/agent
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-api-remotes':
specifier: workspace:*
version: link:../../api/remotes
@@ -4324,6 +4342,12 @@ importers:
'@deepseek-ai/cordis':
specifier: workspace:~
version: link:../../../vendor/cordis
+ '@deepseek-ai/dsh-agent':
+ specifier: workspace:*
+ version: link:../../core/agent
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-api-remotes':
specifier: workspace:*
version: link:../../api/remotes
@@ -6771,6 +6795,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-fs':
specifier: workspace:*
version: link:../../fs/fs
@@ -6985,6 +7012,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-app-boot':
specifier: workspace:*
version: link:../../boot/app-boot
@@ -7054,10 +7084,6 @@ importers:
version: link:../../core/system-prompt
packages/core/agent:
- dependencies:
- zod:
- specifier: 4.4.3
- version: 4.4.3
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:~
@@ -7133,6 +7159,9 @@ importers:
'@deepseek-ai/dsh-agent':
specifier: workspace:*
version: link:../agent
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../agent-model
'@deepseek-ai/dsh-llm':
specifier: workspace:*
version: link:../../llm/llm
@@ -7164,6 +7193,28 @@ importers:
specifier: workspace:*
version: link:../tools
+ packages/core/agent-model:
+ dependencies:
+ zod:
+ specifier: 4.4.3
+ version: 4.4.3
+ devDependencies:
+ '@deepseek-ai/cordis':
+ specifier: workspace:~
+ version: link:../../../vendor/cordis
+ '@deepseek-ai/dsh-agent':
+ specifier: workspace:*
+ version: link:../agent
+ '@deepseek-ai/dsh-llm':
+ specifier: workspace:*
+ version: link:../../llm/llm
+ '@deepseek-ai/dsh-session':
+ specifier: workspace:*
+ version: link:../session
+ '@deepseek-ai/dsh-session-projection':
+ specifier: workspace:*
+ version: link:../../session/session-projection
+
packages/core/agent-tool-presentation:
dependencies:
'@deepseek-ai/schemastery':
@@ -7260,6 +7311,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../agent-model
'@deepseek-ai/dsh-llm':
specifier: workspace:*
version: link:../../llm/llm
@@ -7838,6 +7892,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-attachment-local':
specifier: workspace:*
version: link:../../attachment/attachment-local
@@ -7926,6 +7983,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-attachment':
specifier: workspace:*
version: link:../../attachment/attachment
@@ -7987,6 +8047,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-attachment':
specifier: workspace:*
version: link:../../attachment/attachment
@@ -8053,6 +8116,9 @@ importers:
'@deepseek-ai/cordis':
specifier: workspace:~
version: link:../../../vendor/cordis
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-api-session-controller':
specifier: workspace:*
version: link:../../api/session-controller
@@ -8319,6 +8385,9 @@ importers:
'@deepseek-ai/dsh-agent-loop':
specifier: workspace:*
version: link:../../core/agent-loop
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-attachment-local':
specifier: workspace:*
version: link:../../attachment/attachment-local
@@ -8374,6 +8443,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-attachment':
specifier: workspace:*
version: link:../../attachment/attachment
@@ -8560,6 +8632,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-app-boot':
specifier: workspace:*
version: link:../../boot/app-boot
@@ -9367,6 +9442,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-fs-local':
specifier: workspace:*
version: link:../../fs/fs-local
@@ -10970,6 +11048,9 @@ importers:
'@deepseek-ai/dsh-agent-loop':
specifier: workspace:*
version: link:../../core/agent-loop
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-anonymous-user-id':
specifier: workspace:*
version: link:../../identity/anonymous-user-id
@@ -11190,6 +11271,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
@@ -11515,6 +11599,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-commands':
specifier: workspace:*
version: link:../../interaction/commands
@@ -11607,6 +11694,9 @@ importers:
'@deepseek-ai/dsh-agent-loop':
specifier: workspace:*
version: link:../../core/agent-loop
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-agent-preset':
specifier: workspace:*
version: link:../agent-preset
@@ -11997,6 +12087,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-attachment':
specifier: workspace:*
version: link:../../attachment/attachment
@@ -12581,6 +12674,9 @@ importers:
'@deepseek-ai/dsh-agent-loop':
specifier: workspace:*
version: link:../../core/agent-loop
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
@@ -13295,6 +13391,9 @@ importers:
'@deepseek-ai/cordis-plugin-loader':
specifier: workspace:~
version: link:../../../vendor/loader
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-skill':
specifier: workspace:*
version: link:../skill
@@ -13809,6 +13908,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-agent-preset-registry':
specifier: workspace:*
version: link:../../preset/agent-preset-registry
@@ -14165,6 +14267,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-llm':
specifier: workspace:*
version: link:../../llm/llm
@@ -14202,6 +14307,9 @@ importers:
'@deepseek-ai/dsh-agent-loop-testkit':
specifier: workspace:*
version: link:../../test-support/agent-loop-testkit
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-bash-local':
specifier: workspace:*
version: link:../../shell/bash-local
@@ -14254,6 +14362,9 @@ importers:
'@deepseek-ai/dsh-agent':
specifier: workspace:*
version: link:../../core/agent
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-agent-preset-registry':
specifier: workspace:*
version: link:../../preset/agent-preset-registry
@@ -14505,6 +14616,9 @@ importers:
'@deepseek-ai/dsh-agent-loop':
specifier: workspace:*
version: link:../../core/agent-loop
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../core/agent-model
'@deepseek-ai/dsh-fs-local':
specifier: workspace:*
version: link:../../fs/fs-local
@@ -15436,6 +15550,9 @@ importers:
'@deepseek-ai/dsh-agent-loop':
specifier: workspace:*
version: link:../../packages/core/agent-loop
+ '@deepseek-ai/dsh-agent-model':
+ specifier: workspace:*
+ version: link:../../packages/core/agent-model
'@deepseek-ai/dsh-agent-preset-registry':
specifier: workspace:*
version: link:../../packages/preset/agent-preset-registry
diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json
index 19f2de7e0c..b223623068 100644
--- a/python/sdk-runtime/package.json
+++ b/python/sdk-runtime/package.json
@@ -17,6 +17,7 @@
"@deepseek-ai/dsh-agent-default-model": "workspace:*",
"@deepseek-ai/dsh-agent-instructions": "workspace:*",
"@deepseek-ai/dsh-agent-loop": "workspace:*",
+ "@deepseek-ai/dsh-agent-model": "workspace:*",
"@deepseek-ai/dsh-agent-preset-registry": "workspace:*",
"@deepseek-ai/dsh-agent-tool-presentation": "workspace:*",
"@deepseek-ai/dsh-anonymous-user-id": "workspace:*",
diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts
index 7fea516387..92f10613b4 100644
--- a/scripts/gen-cordis-catalog.ts
+++ b/scripts/gen-cordis-catalog.ts
@@ -67,6 +67,7 @@ export const SERVICE_PAGE: Record = {
mcpResources: 'mcp.md',
agentLoop: 'core.md',
agentDefaultModel: 'core.md',
+ agentModel: 'core.md',
agentPresets: 'core.md',
agents: 'core.md',
approval: 'approval.md',
diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts
index 741343c755..c637973e5d 100644
--- a/scripts/gen-doc-graphs.ts
+++ b/scripts/gen-doc-graphs.ts
@@ -628,6 +628,14 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['agent-loop', 'acp', 'subagent'],
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
},
+ {
+ key: 'agentModel',
+ pkg: 'agent-model',
+ title: 'Session model selection',
+ mode: 'core',
+ consumers: ['agent-loop', 'api-session-controller', 'acp', 'sdk-jsonrpc-server', 'headless'],
+ note: 'Reads and records Session model choices; owns the modelSelection projection and optional scoped route-switch notices.',
+ },
{
key: 'agentDefaultModel',
pkg: 'agent-default-model',
diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json
index 3f56c67066..a81cd84cfb 100644
--- a/scripts/type-equiv.manifest.json
+++ b/scripts/type-equiv.manifest.json
@@ -2490,12 +2490,12 @@
{
"doc": "docs/subsystems/core.md",
"symbol": "ModelSelectionProjectionState",
- "source": "packages/core/agent/src/types.ts"
+ "source": "packages/core/agent-model/src/types.ts"
},
{
"doc": "docs/subsystems/core.md",
"symbol": "ModelSelectionProjection",
- "source": "packages/core/agent/src/types.ts"
+ "source": "packages/core/agent-model/src/types.ts"
}
]
}
diff --git a/snapshots/session/model-switch-notice/model-switch-driver.mjs b/snapshots/session/model-switch-notice/model-switch-driver.mjs
index ba41f43a73..7c7af721ee 100644
--- a/snapshots/session/model-switch-notice/model-switch-driver.mjs
+++ b/snapshots/session/model-switch-notice/model-switch-driver.mjs
@@ -1,11 +1,9 @@
/** Test-only explicit initial selection and model switch after the first step's tool call. */
-import { currentModelSelection, recordModelSelection } from '@deepseek-ai/dsh-agent'
-
-const SELECTED = { provider: 'deepseek-official', model: 'deepseek-v4-pro' }
+const SELECTED = { provider: 'deepseek-official', model: 'deepseek-v4-pro', reasoningEffort: 'max' }
export const name = 'model-switch-driver'
-export const inject = ['tools', 'sessionProjections']
+export const inject = ['tools', 'agentModel']
/**
* Record an explicit initial choice and a switch after `todo_write`.
@@ -13,16 +11,22 @@ export const inject = ['tools', 'sessionProjections']
*/
export function apply(ctx) {
ctx.on('agent/created', ({ agent }) => {
- const initial = currentModelSelection(ctx.sessionProjections, agent)
+ const initial = ctx.agentModel.resolveForNextStep(agent)
if (initial === undefined) throw new Error('model-switch driver requires an initial route')
- recordModelSelection(ctx.sessionProjections, agent, initial)
- recordModelSelection(ctx.sessionProjections, agent, initial)
+ ctx.agentModel.select(agent, initial)
+ ctx.agentModel.select(agent, initial)
+ })
+ ctx.on('agent/pre-step', ({ callConfig }, next) => {
+ if (callConfig.model === SELECTED.model && callConfig.reasoningEffort !== SELECTED.reasoningEffort) {
+ throw new Error('model-switch driver requires the explicit effort before the second step')
+ }
+ return next()
})
ctx.on('tools/execute', async (exec, next) => {
const result = await next()
if (exec.name === 'todo_write' && !result.isError) {
if (exec.agent === undefined) throw new Error('model-switch driver requires a calling Agent')
- recordModelSelection(ctx.sessionProjections, exec.agent, SELECTED)
+ ctx.agentModel.select(exec.agent, SELECTED)
}
return result
})
diff --git a/snapshots/session/model-switch-notice/session.v4.jsonl b/snapshots/session/model-switch-notice/session.v4.jsonl
index cc7214dc94..016ae53383 100644
--- a/snapshots/session/model-switch-notice/session.v4.jsonl
+++ b/snapshots/session/model-switch-notice/session.v4.jsonl
@@ -16,13 +16,13 @@
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user asks to use todo_write to record a single todo 'observe the switch', then reply with the single word DONE."},{"type":"tool-call","id":"call_00_UMfIATnWwwCBBh46ORVb5102","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"observe the switch\", \"status\": \"pending\"}]}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":8004,"outputTokens":88,"totalTokens":8092,"cacheReadTokens":0,"reasoningTokens":27},"stream":[{"type":"chunk","time":1792059125824,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1792059125824,"index":0,"dt":[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],"texts":["The"," user"," asks"," to"," use"," todo","_write"," to"," record"," a"," single"," todo"," '","ob","serve"," the"," switch","',"," then"," reply"," with"," the"," single"," word"," D","ONE","."]},{"type":"chunk","time":1792059125825,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792059125825,"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],"id":"call_00_UMfIATnWwwCBBh46ORVb5102","name":"todo_write","args":["","{","\"","t","odos","\"",": ","[","{\"","content","\":"," \"","ob","serve"," the"," switch","\","," \"","status","\":"," \"","pending","\"","}]","}"]},{"type":"chunk","time":1792059125825,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asks to use todo_write to record a single todo 'observe the switch', then reply with the single word DONE."}}},{"type":"chunk","time":1792059125825,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_UMfIATnWwwCBBh46ORVb5102","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"observe the switch\", \"status\": \"pending\"}]}"}}},{"type":"chunk","time":1792059125825,"chunk":{"type":"usage","usage":{"inputTokens":8004,"outputTokens":88,"totalTokens":8092,"cacheReadTokens":0,"reasoningTokens":27}}},{"type":"chunk","time":1792059125825,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_UMfIATnWwwCBBh46ORVb5102","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"observe the switch\", \"status\": \"pending\"}]}"}}
{"type":"todo/write","data":{"todos":[{"content":"observe the switch","status":"pending"}]}}
-{"type":"model/selection","data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}}
+{"type":"model/selection","data":{"provider":"deepseek-official","model":"deepseek-v4-pro","reasoningEffort":"max"}}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_00_UMfIATnWwwCBBh46ORVb5102"},"toolCallId":"call_00_UMfIATnWwwCBBh46ORVb5102","content":[{"type":"text","text":"Updated todo list: 1 pending, 0 in progress, 0 completed."}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"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":[8],"surfaceOp":{"op":"replace","startSeq":8,"endSeq":8}}
{"type":"user/message","data":{"content":[{"type":"text","text":"[model changed: assistant turns above this point were generated by deepseek-v4-flash; the session continues with deepseek-v4-pro]"}],"source":{"kind":"model-selection","form":"notice","summary":"deepseek-v4-flash → deepseek-v4-pro"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"}
-{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"change","startsSeries":true}}
+{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro","reasoningEffort":"max","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":"{{tools}}"},"reason":"change","startsSeries":true}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-pro","contextWindow":1000000}}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user asked me to record a single todo 'observe the switch' and then reply with the single word DONE. I already did the todo_write. Now I just reply with \"DONE\"."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"{{message:8}}"},"usage":{"inputTokens":8148,"outputTokens":44,"totalTokens":8192,"cacheReadTokens":0,"reasoningTokens":41},"stream":[{"type":"chunk","time":1792059125848,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1792059125848,"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],"texts":["The"," user"," asked"," me"," to"," record"," a"," single"," todo"," '","ob","serve"," the"," switch","'"," and"," then"," reply"," with"," the"," single"," word"," D","ONE","."," I"," already"," did"," the"," todo","_write","."," Now"," I"," just"," reply"," with"," \"","D","ONE","\"."]},{"type":"chunk","time":1792059125848,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1792059125848,"index":1,"dt":[0],"texts":["D","ONE"]},{"type":"chunk","time":1792059125848,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to record a single todo 'observe the switch' and then reply with the single word DONE. I already did the todo_write. Now I just reply with \"DONE\"."}}},{"type":"chunk","time":1792059125848,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1792059125848,"chunk":{"type":"usage","usage":{"inputTokens":8148,"outputTokens":44,"totalTokens":8192,"cacheReadTokens":0,"reasoningTokens":41}}},{"type":"chunk","time":1792059125848,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":2}}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 3573f6f8b6..a3c6101915 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -199,6 +199,9 @@
"@deepseek-ai/dsh-tool-subagent/model-selection-settings": ["./packages/subagent/tool-subagent/src/model-selection-settings.ts"],
"@deepseek-ai/dsh-user-approval/types": ["./packages/interaction/user-approval/src/types.ts"],
"@deepseek-ai/dsh-user-questions/types": ["./packages/interaction/user-questions/src/types.ts"],
+ "@deepseek-ai/dsh-agent-model/types": ["./packages/core/agent-model/src/types.ts"],
+ "@deepseek-ai/dsh-agent-model/notice": ["./packages/core/agent-model/src/notice.ts"],
+ "@deepseek-ai/dsh-agent-model": ["./packages/core/agent-model/src"],
"@deepseek-ai/dsh-agent/types": ["./packages/core/agent/src/types.ts"],
"@deepseek-ai/dsh-agent/brand": ["./packages/core/agent/src/brand.ts"],
"@deepseek-ai/dsh-headless/startup": ["./packages/bundle/headless/src/startup.ts"],
diff --git a/tsconfig.host.json b/tsconfig.host.json
index 90cb6d8b0b..1c418980ca 100644
--- a/tsconfig.host.json
+++ b/tsconfig.host.json
@@ -371,6 +371,7 @@
{ "path": "./packages/session/session-telemetry-otel" },
{ "path": "./packages/core/system-prompt" },
{ "path": "./packages/core/agent" },
+ { "path": "./packages/core/agent-model" },
{ "path": "./packages/core/agent-default-model" },
{ "path": "./packages/interaction/commands" },
{ "path": "./packages/goal/goal" },