dsh-model-router 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,6 +46,15 @@ Everything else is a planner. That logic lives in `src/policy.ts` as a plain fun
46
46
 
47
47
  The router always stamps `provider` + `model`. `reasoningEffort` and `maxTokens` are *optional per role*: set them in the config and they're enforced for that role; leave them out and those fields inherit from your session's selection. So picking "max effort" in the UI but not pinning `reasoningEffort` in the config still gives you max-effort thinking — it just happens on the routed model.
48
48
 
49
+ ### What still differs while the models are unified
50
+
51
+ With both routes on `deepseek-flash`, the model stamp is a pass-through — but the roles are not identical:
52
+
53
+ - **Effort and token pins are per role.** Planner `high` + executor `low`, for example, genuinely behaves and costs differently per role today.
54
+ - **The convention still steers behavior.** The prompt section and skill keep the root agent planning and delegating instead of doing everything itself, regardless of model.
55
+ - **Enforcement still pins the model.** Every request lands on `deepseek-flash` no matter what the session or UI selects.
56
+ - **The split is parked, not removed.** When V4.1-Pro launches, pointing the planner route at it restores the two-model setup with a one-line change.
57
+
49
58
  ## Turning the router off
50
59
 
51
60
  Routing is on by default. Two ways to switch it off:
@@ -100,9 +109,11 @@ When enabled, **any request sent while the session log carries an image is stamp
100
109
  vision model** — `deepseek-flash` from `deepseek-official` by
101
110
  default (V4.1 Flash is natively multimodal, so this matches the role routes
102
111
  unless you pin something else) — **from every role**: the root (planner) agent and all delegated
103
- subagents. Everything else keeps the pro/flash role routing untouched. The
104
- vision branch is checked first, so a subagent reading an image still lands on
105
- the vision model, not on flash. Optional `vision.reasoningEffort` /
112
+ subagents. Everything else keeps the role routing untouched. The
113
+ vision branch is checked first, so an image-bearing request lands on the
114
+ vision model even when it comes from a subagent. With the default config the
115
+ vision model is the same `deepseek-flash` as the role routes, so the branch
116
+ is a no-op until you pin a different vision model. Optional `vision.reasoningEffort` /
106
117
  `vision.maxTokens` pins work exactly like the per-role ones. Image detection
107
118
  reads the session event log (`user/message`, `assistant/message`, and
108
119
  `tool/result`, including images nested in `tool-result` blocks): once an image
@@ -128,7 +139,11 @@ The plugin ships the support in its own `cordis.patch.yml`:
128
139
 
129
140
  Both are defaults you can override in your **profile**'s `cordis.patch.yml` —
130
141
  the profile layer applies after the plugin layer, so a `patch:` targeting
131
- `llm-deepseek` or `attachment-local` in the profile wins. One hard requirement
142
+ `llm-deepseek` or `attachment-local` in the profile wins. Note these are
143
+ id-targeted rows that **replace the whole row config** (not a deep merge):
144
+ installing the plugin resets `llm-deepseek` to exactly those 4 models and
145
+ `attachment-local` to exactly those 3 image keys — restate anything else you
146
+ need in the profile layer. One hard requirement
132
147
  remains: the vision model **must be present in the catalog with image input
133
148
  modality**, or the provider rejects the request at call time with
134
149
  `UNSUPPORTED_CONTENT` — the shipped catalog row is what satisfies that.
@@ -158,11 +173,11 @@ All configuration lives on the plugin row. Patch it in the profile's `cordis.pat
158
173
  skill: true # register the pro-flash-routing skill
159
174
  ```
160
175
 
161
- `mode` controls how the root agent is treated: `strict` keeps it on the planner route always; `plan` sends the root to the executor route unless plan mode is active, reserving pro for real planning.
176
+ `mode` controls how the root agent is treated: `strict` keeps it on the planner route always; `plan` sends the root to the executor route unless plan mode is active, reserving the planner route for real planning.
162
177
 
163
178
  **Error-driven escalation** (`escalateOnError`): when a route's agent hits a failed tool step, the *next* request bumps to `escalateTo` and wears off after `recoverySteps` clean steps. It's deterministic and stateless — the router folds the session log per request, so only prior steps are considered (a failure can't escalate the very request that caused it). It's a per-route knob: enable it on the executor to make flash think harder after a flubbed execution step, without touching the baseline.
164
179
 
165
- The defaults are exactly the list at the top of this page. To switch the router off for a session, disable the row (`disabled: true`) or remove the plugin — `dsh plugin --profile web remove dsh-model-router`.
180
+ The model defaults are exactly the two routes at the top of this page. To switch the router off for a session, disable the row (`disabled: true`) or remove the plugin — `dsh plugin --profile web remove dsh-model-router`. If even the convention surface is unwanted until V4.1-Pro lands, flip `enabled` off (see above) and leave the plugin installed — everything unregisters and you flip it back on later.
166
181
 
167
182
  ## Reduce token usage
168
183
 
package/cordis.patch.yml CHANGED
@@ -42,7 +42,13 @@
42
42
  # configs keep working through the transition. Max output is 384K per the
43
43
  # current Models & Pricing page (was 256K on the v4 preview docs).
44
44
  # Direct id-targeted entries (same shape as a profile cordis.patch.yml): they
45
- # replace the targeted row's whole config.
45
+ # REPLACE the targeted row's whole config (not a deep merge). Installing this
46
+ # plugin therefore resets `llm-deepseek` to exactly the 4 models below and
47
+ # `attachment-local` to exactly the 3 image keys below — any other models or
48
+ # keys those rows carried (host defaults, other plugins, profile edits) are
49
+ # dropped unless restated. If you need extra models or keys, restate them in
50
+ # your PROFILE's cordis.patch.yml (the profile layer applies after the plugin
51
+ # layer, so it wins).
46
52
  - id: llm-deepseek
47
53
  config:
48
54
  models:
@@ -55,10 +61,12 @@
55
61
  name: DeepSeek-V4-Flash
56
62
  contextWindow: 1000000
57
63
  maxTokens: 384000
64
+ inputModalities: [text, image]
58
65
  - id: deepseek-v4-pro
59
66
  name: DeepSeek-V4-Pro
60
67
  contextWindow: 1000000
61
68
  maxTokens: 384000
69
+ inputModalities: [text, image]
62
70
  - id: deepseek-v4-flash-vision-exp
63
71
  name: DeepSeek-V4-Flash-Vision-Exp
64
72
  contextWindow: 1000000
package/lib/index.d.ts CHANGED
@@ -307,7 +307,14 @@ declare const SETTINGS_NS: _deepseek_ai_dsh_settings.SettingsNamespace;
307
307
  declare const SKILL_NAME = "pro-flash-routing";
308
308
  declare const SKILL_DESCRIPTION = "Route planning and code execution across models: plan on the pro planner agent, delegate implementation to flash executor subagents.";
309
309
  declare const SKILL_WHEN_TO_USE = "Use when a task combines planning and implementation: before writing code, after a plan is approved, when delegating execution work, or when the user asks about the pro/flash routing convention.";
310
- declare const SKILL_CONTENT = "# Pro planner / Flash executor routing\n\nThis session routes models by role (both default to deepseek-flash, V4.1 Flash with native multimodal; vision needs no separate model):\n\n- **Planner (this agent)** \u2014 `deepseek-flash`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.\n- **Executors (every subagent)** \u2014 `deepseek-flash`. Implementation work happens there: writing code, running commands, builds, and tests. The harness forces the model automatically; you do not select it.\n\n## Working rhythm\n\n1. **Plan here.** Explore, decide the approach, and (when plan mode is on) submit the plan with `exit_plan_mode`. The plan stays on this agent.\n2. **Delegate the execution.** Once a plan is approved, hand each self-contained chunk of implementation to a subagent with a complete prompt: exact files to touch, the change to make, and how to verify. Subagents are automatically routed to `deepseek-flash`, so keep them execution-focused: give them the decision, not the decision to make.\n3. **Review here.** Read the subagent's result on this agent, verify it yourself (tests, diffs, logs), and iterate with follow-up messages to the same subagent when available.\n4. **Report here.** Summaries, plans, and answers to the user come from this agent.\n\n## Keep this agent's context lean\n\nInput tokens are the expensive part of the planner. Don't re-read large files or full transcripts on this agent \u2014 trust the subagent's final report. Prefer targeted reads (offset/limit) over whole files. When the context grows, compact rather than re-sending everything.\n\n## Delegation guidelines\n\n- Start independent delegations together in one assistant message and continue useful work while they run (background mode by default).\n- Prefer `subagent` for self-contained work and `workflow` when many independent pieces need fan-out; their workers run on flash as well.\n- Do not delegate design: subagents execute decisions already made.\n- If a subagent's task grows into design work, pull it back to this agent and re-delegate the narrowed execution.\n\n## Verification\n\n- Both roles produce `deepseek-flash` (unified since V4.1; split again when V4.1-Pro lands). If you need to confirm, check the session log's model metadata.\n- If routing ever looks wrong, the `model-router` plugin row in the profile composition is the single place that owns it.";
310
+ /**
311
+ * Render the skill content for one config, substituting the configured
312
+ * planner/executor models (mirrors the prompt-section templating).
313
+ * @param cfg - the resolved router configuration.
314
+ * @returns the skill markdown with model placeholders filled.
315
+ */
316
+ declare function buildSkillContent(cfg: RouterConfig): string;
317
+ declare const SKILL_CONTENT: string;
311
318
  /** The plugin row id the bundle patch must insert. */
312
319
  declare const ROW_ID = "model-router";
313
320
  /** Minimal structural view of the live agent object the router reads. */
@@ -376,4 +383,4 @@ declare class ModelRouter extends Service {
376
383
  private render;
377
384
  }
378
385
 
379
- export { type AgentRole, Config, type ModelRoute, ModelRouter, ROW_ID, type ReasoningEffort, type RouterConfig, type RoutingMode, SETTINGS_NS, SKILL_CONTENT, SKILL_DESCRIPTION, SKILL_NAME, SKILL_WHEN_TO_USE, type VisionRoute, VisionRouteSchema, ModelRouter as default, effortFor, hasImageContent, name, recentStepsHadError, roleFor, routeFor, sessionHasImage, shouldUseVision };
386
+ export { type AgentRole, Config, type ModelRoute, ModelRouter, ROW_ID, type ReasoningEffort, type RouterConfig, type RoutingMode, SETTINGS_NS, SKILL_CONTENT, SKILL_DESCRIPTION, SKILL_NAME, SKILL_WHEN_TO_USE, type VisionRoute, VisionRouteSchema, buildSkillContent, ModelRouter as default, effortFor, hasImageContent, name, recentStepsHadError, roleFor, routeFor, sessionHasImage, shouldUseVision };
package/lib/index.js CHANGED
@@ -128,17 +128,17 @@ var SECTION_TEXT = `Model routing is role-based. Planning runs on {PLANNER_MODEL
128
128
  var SKILL_NAME = "pro-flash-routing";
129
129
  var SKILL_DESCRIPTION = "Route planning and code execution across models: plan on the pro planner agent, delegate implementation to flash executor subagents.";
130
130
  var SKILL_WHEN_TO_USE = `Use when a task combines planning and implementation: before writing code, after a plan is approved, when delegating execution work, or when the user asks about the pro/flash routing convention.`;
131
- var SKILL_CONTENT = `# Pro planner / Flash executor routing
131
+ var SKILL_TEMPLATE = `# Pro planner / Flash executor routing
132
132
 
133
- This session routes models by role (both default to deepseek-flash, V4.1 Flash with native multimodal; vision needs no separate model):
133
+ This session routes models by role (planner \`{PLANNER_MODEL}\`, executor \`{EXECUTOR_MODEL}\`; both default to deepseek-flash, V4.1 Flash with native multimodal; vision needs no separate model):
134
134
 
135
- - **Planner (this agent)** \u2014 \`deepseek-flash\`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.
136
- - **Executors (every subagent)** \u2014 \`deepseek-flash\`. Implementation work happens there: writing code, running commands, builds, and tests. The harness forces the model automatically; you do not select it.
135
+ - **Planner (this agent)** \u2014 \`{PLANNER_MODEL}\`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.
136
+ - **Executors (every subagent)** \u2014 \`{EXECUTOR_MODEL}\`. Implementation work happens there: writing code, running commands, builds, and tests. The harness forces the model automatically; you do not select it.
137
137
 
138
138
  ## Working rhythm
139
139
 
140
140
  1. **Plan here.** Explore, decide the approach, and (when plan mode is on) submit the plan with \`exit_plan_mode\`. The plan stays on this agent.
141
- 2. **Delegate the execution.** Once a plan is approved, hand each self-contained chunk of implementation to a subagent with a complete prompt: exact files to touch, the change to make, and how to verify. Subagents are automatically routed to \`deepseek-flash\`, so keep them execution-focused: give them the decision, not the decision to make.
141
+ 2. **Delegate the execution.** Once a plan is approved, hand each self-contained chunk of implementation to a subagent with a complete prompt: exact files to touch, the change to make, and how to verify. Subagents are automatically routed to \`{EXECUTOR_MODEL}\`, so keep them execution-focused: give them the decision, not the decision to make.
142
142
  3. **Review here.** Read the subagent's result on this agent, verify it yourself (tests, diffs, logs), and iterate with follow-up messages to the same subagent when available.
143
143
  4. **Report here.** Summaries, plans, and answers to the user come from this agent.
144
144
 
@@ -149,14 +149,29 @@ Input tokens are the expensive part of the planner. Don't re-read large files or
149
149
  ## Delegation guidelines
150
150
 
151
151
  - Start independent delegations together in one assistant message and continue useful work while they run (background mode by default).
152
- - Prefer \`subagent\` for self-contained work and \`workflow\` when many independent pieces need fan-out; their workers run on flash as well.
152
+ - Prefer \`subagent\` for self-contained work and \`workflow\` when many independent pieces need fan-out; their workers run on \`{EXECUTOR_MODEL}\` as well.
153
153
  - Do not delegate design: subagents execute decisions already made.
154
154
  - If a subagent's task grows into design work, pull it back to this agent and re-delegate the narrowed execution.
155
155
 
156
156
  ## Verification
157
157
 
158
- - Both roles produce \`deepseek-flash\` (unified since V4.1; split again when V4.1-Pro lands). If you need to confirm, check the session log's model metadata.
158
+ - Planner produces \`{PLANNER_MODEL}\`, executor produces \`{EXECUTOR_MODEL}\` (unified when equal since V4.1; split again when V4.1-Pro lands). If you need to confirm, check the session log's model metadata.
159
159
  - If routing ever looks wrong, the \`model-router\` plugin row in the profile composition is the single place that owns it.`;
160
+ function buildSkillContent(cfg) {
161
+ return SKILL_TEMPLATE.replaceAll("{PLANNER_MODEL}", cfg.planner.model).replaceAll(
162
+ "{EXECUTOR_MODEL}",
163
+ cfg.executor.model
164
+ );
165
+ }
166
+ var SKILL_CONTENT = buildSkillContent({
167
+ planner: { provider: "deepseek-official", model: "deepseek-flash" },
168
+ executor: { provider: "deepseek-official", model: "deepseek-flash" },
169
+ mode: "strict",
170
+ enabled: true,
171
+ promptSection: true,
172
+ skill: true,
173
+ vision: { enabled: false, provider: "deepseek-official", model: "deepseek-flash" }
174
+ });
160
175
  var ROW_ID = "model-router";
161
176
  function isPlanModeActive(agent) {
162
177
  const events = agent.session?.events;
@@ -183,6 +198,14 @@ var ModelRouter = class extends Service {
183
198
  this.config = resolveConfig(rawConfig);
184
199
  this.source = () => this.config;
185
200
  this.harness = ctx;
201
+ const requestDisposers = /* @__PURE__ */ new Map();
202
+ this.harness.on("agent/disposed", (disposedAgent) => {
203
+ const dispose = requestDisposers.get(disposedAgent);
204
+ if (dispose !== void 0) {
205
+ dispose();
206
+ requestDisposers.delete(disposedAgent);
207
+ }
208
+ });
186
209
  this.harness.on("agent/created", ({ agent }) => {
187
210
  const dispose = agent.ctx.on(
188
211
  "agent/request",
@@ -215,9 +238,9 @@ var ModelRouter = class extends Service {
215
238
  },
216
239
  { prepend: true }
217
240
  );
218
- this.harness.on("agent/disposed", (disposed) => {
219
- if (disposed === agent) dispose();
220
- });
241
+ const prev = requestDisposers.get(agent);
242
+ if (prev !== void 0) prev();
243
+ requestDisposers.set(agent, dispose);
221
244
  });
222
245
  this.render(this.config);
223
246
  installSettingsSection(
@@ -264,7 +287,7 @@ var ModelRouter = class extends Service {
264
287
  name: SKILL_NAME,
265
288
  description: SKILL_DESCRIPTION,
266
289
  whenToUse: SKILL_WHEN_TO_USE,
267
- content: SKILL_CONTENT,
290
+ content: buildSkillContent(cfg),
268
291
  source: "runtime"
269
292
  });
270
293
  }
@@ -280,6 +303,7 @@ export {
280
303
  SKILL_NAME,
281
304
  SKILL_WHEN_TO_USE,
282
305
  VisionRouteSchema,
306
+ buildSkillContent,
283
307
  ModelRouter as default,
284
308
  effortFor,
285
309
  hasImageContent,
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/policy.ts"],"sourcesContent":["/**\r\n * dsh-model-router: role-based model routing for the DeepSeek Harness.\r\n *\r\n * Both roles default to `deepseek-flash` (V4.1 Flash, native multimodal):\r\n * the planner (the session's root agent) and delegated executor subagents\r\n * share the same model until V4.1-Pro launches, at which point the planner\r\n * route can be flipped back with a one-line config change. Enforcement is a\r\n * per-agent `agent/request` rewrite registered when the agent is created, so\r\n * it applies in every mode (web / headless / tui) and every agent preset,\r\n * including subagents the delegation tools create.\r\n *\r\n * Each role route may also pin `reasoningEffort` and `maxTokens`; when set,\r\n * they override the session's selection for that role. A `mode` switch lets a\r\n * deployment reserve the planner route for actual planning.\r\n *\r\n * The plugin also publishes:\r\n * - a system-prompt section stating the planner/executor convention, and\r\n * - the `pro-flash-routing` skill teaching the agent to plan itself and\r\n * delegate code execution to flash subagents.\r\n *\r\n * @module dsh-model-router\r\n */\r\nimport { Context, Service } from \"@deepseek-ai/cordis\";\r\nimport z from \"@deepseek-ai/schemastery\";\r\nimport { foldPlanMode } from \"@deepseek-ai/dsh-plan-mode\";\r\nimport { installSettingsSection, settingsNamespace } from \"@deepseek-ai/dsh-settings\";\r\nimport { effortFor, hasImageContent, recentStepsHadError, roleFor, routeFor, sessionHasImage, shouldUseVision, type RouterConfig } from \"./policy.js\";\r\n\r\n/** Plugin row id; the bundle patch inserts it under this id. */\r\nconst name = \"model-router\";\r\n\r\n/** One provider/model pair, with defaults and optional effort/token caps. */\r\nconst ModelRouteSchema = z.object({\r\n provider: z.string().min(1),\r\n model: z.string().min(1),\r\n reasoningEffort: z.union([\"off\", \"low\", \"high\", \"max\"]),\r\n maxTokens: z.number().min(1),\r\n escalateOnError: z.boolean(),\r\n escalateTo: z.union([\"off\", \"low\", \"high\", \"max\"]),\r\n recoverySteps: z.number().min(1),\r\n});\r\n\r\n/**\r\n * Opt-in vision route. Defaults to `deepseek-flash` (V4.1 Flash, native\r\n * multimodal) from `deepseek-official`; `reasoningEffort` and `maxTokens`\r\n * are optional pins. Native multimodal makes the vision branch a no-op when\r\n * the role routes already point at the same model — kept for explicitness\r\n * and for a future V4.1-Pro split.\r\n */\r\nconst VisionRouteSchema = z.object({\r\n enabled: z.boolean().default(false),\r\n provider: z.string().min(1).default(\"deepseek-official\"),\r\n model: z.string().min(1).default(\"deepseek-flash\"),\r\n reasoningEffort: z.union([\"off\", \"low\", \"high\", \"max\"]),\r\n maxTokens: z.number().min(1),\r\n});\r\n\r\n/** The plugin's public config, validated at row load. */\r\nconst Config = z.object({\r\n planner: ModelRouteSchema.default({\r\n provider: \"deepseek-official\",\r\n model: \"deepseek-flash\",\r\n } as never),\r\n executor: ModelRouteSchema.default({\r\n provider: \"deepseek-official\",\r\n model: \"deepseek-flash\",\r\n } as never),\r\n mode: z.union([\"strict\", \"plan\"]).default(\"strict\"),\r\n enabled: z.boolean().default(true),\r\n promptSection: z.boolean().default(true),\r\n skill: z.boolean().default(true),\r\n vision: VisionRouteSchema.default({} as never),\r\n});\r\n\r\n/** Settings namespace the live on/off toggle lives under (settings.yaml). */\r\nconst SETTINGS_NS = settingsNamespace(\"model-router\");\r\n\r\n/**\r\n * Resolve raw row config into the internal shape, failing loud on garbage.\r\n * Schemastery schemas are callable: invoking validates and applies defaults.\r\n * @param raw - the row's config object.\r\n * @returns the validated RouterConfig.\r\n */\r\nfunction resolveConfig(raw: unknown): RouterConfig {\r\n const parsed = Config(raw ?? {});\r\n return {\r\n planner: parsed.planner,\r\n executor: parsed.executor,\r\n mode: parsed.mode,\r\n enabled: parsed.enabled,\r\n promptSection: parsed.promptSection,\r\n skill: parsed.skill,\r\n vision: parsed.vision,\r\n };\r\n}\r\n\r\n/**\r\n * Always-on guidance section. Negative order renders before the persona, so\r\n * the convention is established before the agent's identity line.\r\n */\r\nconst SECTION_ORDER = -50;\r\n\r\nconst SECTION_TEXT = `Model routing is role-based. Planning runs on {PLANNER_MODEL}; implementation runs on {EXECUTOR_MODEL}. You are the root agent: plan, design, review subagent output, and write the final answer here. Delegate implementation — writing code, running commands, builds, tests — to subagents with complete, self-contained prompts, preferring background delegation for independent work. Keep plans and replies concise. Do not hand-write large amounts of code or run long executions here; delegate instead.{VISION_LINE}`;\r\n\r\nconst SKILL_NAME = \"pro-flash-routing\";\r\n\r\nconst SKILL_DESCRIPTION =\r\n \"Route planning and code execution across models: plan on the pro planner agent, delegate implementation to flash executor subagents.\";\r\n\r\nconst SKILL_WHEN_TO_USE = `Use when a task combines planning and implementation: before writing code, after a plan is approved, when delegating execution work, or when the user asks about the pro/flash routing convention.`;\r\n\r\nconst SKILL_CONTENT = `# Pro planner / Flash executor routing\r\n\r\nThis session routes models by role (both default to deepseek-flash, V4.1 Flash with native multimodal; vision needs no separate model):\r\n\r\n- **Planner (this agent)** — \\`deepseek-flash\\`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.\r\n- **Executors (every subagent)** — \\`deepseek-flash\\`. Implementation work happens there: writing code, running commands, builds, and tests. The harness forces the model automatically; you do not select it.\r\n\r\n## Working rhythm\r\n\r\n1. **Plan here.** Explore, decide the approach, and (when plan mode is on) submit the plan with \\`exit_plan_mode\\`. The plan stays on this agent.\r\n2. **Delegate the execution.** Once a plan is approved, hand each self-contained chunk of implementation to a subagent with a complete prompt: exact files to touch, the change to make, and how to verify. Subagents are automatically routed to \\`deepseek-flash\\`, so keep them execution-focused: give them the decision, not the decision to make.\r\n3. **Review here.** Read the subagent's result on this agent, verify it yourself (tests, diffs, logs), and iterate with follow-up messages to the same subagent when available.\r\n4. **Report here.** Summaries, plans, and answers to the user come from this agent.\r\n\r\n## Keep this agent's context lean\r\n\r\nInput tokens are the expensive part of the planner. Don't re-read large files or full transcripts on this agent — trust the subagent's final report. Prefer targeted reads (offset/limit) over whole files. When the context grows, compact rather than re-sending everything.\r\n\r\n## Delegation guidelines\r\n\r\n- Start independent delegations together in one assistant message and continue useful work while they run (background mode by default).\r\n- Prefer \\`subagent\\` for self-contained work and \\`workflow\\` when many independent pieces need fan-out; their workers run on flash as well.\r\n- Do not delegate design: subagents execute decisions already made.\r\n- If a subagent's task grows into design work, pull it back to this agent and re-delegate the narrowed execution.\r\n\r\n## Verification\r\n\r\n- Both roles produce \\`deepseek-flash\\` (unified since V4.1; split again when V4.1-Pro lands). If you need to confirm, check the session log's model metadata.\r\n- If routing ever looks wrong, the \\`model-router\\` plugin row in the profile composition is the single place that owns it.`;\r\n\r\n/** The plugin row id the bundle patch must insert. */\r\nconst ROW_ID = \"model-router\";\r\n\r\n/** Minimal structural view of the live agent object the router reads. */\r\ninterface AgentLike {\r\n ctx: AgentScopedContext;\r\n options?: { subagentDepth?: number };\r\n session?: { header?: { origin?: string }; events?: unknown[] };\r\n}\r\n\r\n/** The agent-scoped context's waterfall surface the router uses. */\r\ninterface AgentScopedContext {\r\n on(\r\n event: \"agent/request\",\r\n listener: (\r\n payload: Record<string, unknown>,\r\n next: () => Promise<Record<string, unknown>>,\r\n ) => Promise<Record<string, unknown>>,\r\n options?: { prepend?: boolean },\r\n ): () => void;\r\n}\r\n\r\n/** Host-plane surface the router consumes (events, prompt registry, skills). */\r\ninterface HarnessContext {\r\n on(\r\n event: \"agent/created\",\r\n listener: (payload: { agent: AgentLike }) => void,\r\n ): () => void;\r\n on(event: \"agent/disposed\", listener: (agent: unknown) => void): () => void;\r\n systemPrompt: {\r\n section(section: { name: string; order: number; text: string }): () => void;\r\n };\r\n skills: {\r\n register(skill: {\r\n name: string;\r\n description: string;\r\n whenToUse?: string;\r\n content: string;\r\n source: string;\r\n }): () => void;\r\n };\r\n}\r\n\r\n/** Fold plan-mode state for an agent without trusting the agent's exact shape. */\r\nfunction isPlanModeActive(agent: AgentLike): boolean {\r\n const events = agent.session?.events;\r\n if (!Array.isArray(events)) return false;\r\n try {\r\n return foldPlanMode(events as Parameters<typeof foldPlanMode>[0]);\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Cordis service: per-agent request routing plus the convention surface.\r\n */\r\nclass ModelRouter extends Service {\r\n static inject = [\"skills\", \"systemPrompt\"];\r\n\r\n config: RouterConfig;\r\n\r\n /** Currently authoritative config; swapped by the settings section when one is mounted. */\r\n source: () => RouterConfig;\r\n\r\n /** Host-plane surface the router consumes (events, prompt registry, skills). */\r\n harness: HarnessContext;\r\n\r\n /** Disposer of the currently registered prompt section, if any. */\r\n promptDispose?: () => void;\r\n\r\n /** Disposer of the currently registered skill, if any. */\r\n skillDispose?: () => void;\r\n\r\n constructor(ctx: Context, rawConfig: unknown = {}) {\r\n super(ctx, \"modelRouter\");\r\n this.config = resolveConfig(rawConfig);\r\n this.source = () => this.config;\r\n this.harness = ctx as unknown as HarnessContext;\r\n\r\n // Every agent that gets created — root sessions, delegation children,\r\n // workflow workers, ralph rounds — passes through here.\r\n this.harness.on(\"agent/created\", ({ agent }) => {\r\n // `prepend` puts this listener OUTERMOST in the `agent/request`\r\n // waterfall: the harness's model-selection listener runs inside it, so\r\n // this rewrite is applied LAST and wins over the session's selected\r\n // model (which dsh-base defaulted to deepseek-v4-flash, now deepseek-flash, and the user\r\n // settings or UI can change).\r\n const dispose = agent.ctx.on(\r\n \"agent/request\",\r\n async (payload, next) => {\r\n const resolved = await next();\r\n const cfg = this.source();\r\n if (!cfg.enabled) return resolved;\r\n // Vision branch FIRST: any request whose session log carries image\r\n // content is stamped with the vision model, regardless of role. The\r\n // `agent/request` waterfall payload never includes `messages`, so\r\n // detection reads the session event log (`user/message`,\r\n // `assistant/message`, `tool/result`). The harness's own\r\n // model-selection listener has already run (`await next()`), so\r\n // this rewrite wins over the session model.\r\n if (shouldUseVision(agent.session?.events, cfg.vision)) {\r\n const stamped: Record<string, unknown> = {\r\n ...resolved,\r\n provider: cfg.vision.provider,\r\n model: cfg.vision.model,\r\n };\r\n if (cfg.vision.maxTokens !== undefined) stamped.maxTokens = cfg.vision.maxTokens;\r\n const effort = effortFor(cfg.vision, agent.session?.events);\r\n if (effort !== undefined) stamped.reasoningEffort = effort;\r\n return stamped;\r\n }\r\n const route = routeFor(agent, cfg, isPlanModeActive(agent));\r\n if (route === undefined) return resolved;\r\n const stamped: Record<string, unknown> = {\r\n ...resolved,\r\n provider: route.provider,\r\n model: route.model,\r\n };\r\n if (route.maxTokens !== undefined) stamped.maxTokens = route.maxTokens;\r\n const effort = effortFor(route, agent.session?.events);\r\n if (effort !== undefined) stamped.reasoningEffort = effort;\r\n return stamped;\r\n },\r\n { prepend: true },\r\n );\r\n this.harness.on(\"agent/disposed\", (disposed) => {\r\n if (disposed === agent) dispose();\r\n });\r\n });\r\n\r\n // Register the convention surface from the composition config, then let\r\n // the settings section (when mounted) take over as the live source.\r\n this.render(this.config);\r\n installSettingsSection(\r\n ctx,\r\n SETTINGS_NS,\r\n Config,\r\n this.config as unknown as ReturnType<typeof Config>,\r\n {\r\n setSource: (current) => {\r\n this.source = current;\r\n },\r\n onChange: () => this.render(this.source()),\r\n },\r\n );\r\n }\r\n\r\n /**\r\n * Register the convention surface (prompt section + skill) for `cfg`,\r\n * replacing whatever is currently registered. Called on construction and\r\n * after every committed settings change; while `enabled` is false, nothing\r\n * stays registered.\r\n */\r\n private render(cfg: RouterConfig): void {\r\n if (this.promptDispose) {\r\n this.promptDispose();\r\n this.promptDispose = undefined;\r\n }\r\n if (this.skillDispose) {\r\n this.skillDispose();\r\n this.skillDispose = undefined;\r\n }\r\n if (!cfg.enabled) return;\r\n if (cfg.promptSection) {\r\n this.promptDispose = this.harness.systemPrompt.section({\r\n name: ROW_ID,\r\n order: SECTION_ORDER,\r\n text: SECTION_TEXT.replaceAll(\"{PLANNER_MODEL}\", cfg.planner.model)\r\n .replaceAll(\"{EXECUTOR_MODEL}\", cfg.executor.model)\r\n .replace(\r\n \"{VISION_LINE}\",\r\n cfg.vision.enabled\r\n ? ` Image-bearing requests route to ${cfg.vision.model}.`\r\n : \"\",\r\n ),\r\n });\r\n }\r\n if (cfg.skill) {\r\n this.skillDispose = this.harness.skills.register({\r\n name: SKILL_NAME,\r\n description: SKILL_DESCRIPTION,\r\n whenToUse: SKILL_WHEN_TO_USE,\r\n content: SKILL_CONTENT,\r\n source: \"runtime\",\r\n });\r\n }\r\n }\r\n}\r\n\r\nexport {\r\n Config,\r\n ModelRouter,\r\n ModelRouter as default,\r\n SETTINGS_NS,\r\n VisionRouteSchema,\r\n name,\r\n ROW_ID,\r\n SKILL_CONTENT,\r\n SKILL_DESCRIPTION,\r\n SKILL_NAME,\r\n SKILL_WHEN_TO_USE,\r\n effortFor,\r\n hasImageContent,\r\n recentStepsHadError,\r\n roleFor,\r\n routeFor,\r\n sessionHasImage,\r\n shouldUseVision,\r\n};\r\nexport type {\r\n AgentRole,\r\n ModelRoute,\r\n ReasoningEffort,\r\n RoutingMode,\r\n RouterConfig,\r\n VisionRoute,\r\n} from \"./policy.js\";\r\n","/**\n * Pure routing policy for dsh-model-router: which model each agent role gets.\n * Kept free of Cordis imports so the policy is trivially unit-testable.\n * @module dsh-model-router/policy\n */\n\n/** Reasoning-effort levels a route may pin (mirrors the harness vocabulary). */\nexport type ReasoningEffort = \"off\" | \"low\" | \"high\" | \"max\";\n\n/**\n * How the router treats the root agent.\n * - `strict`: the root agent is always the planner (pro).\n * - `plan`: the root agent is pro only while plan mode is active; otherwise it\n * falls back to the executor route, reserving pro for real planning.\n */\nexport type RoutingMode = \"strict\" | \"plan\";\n\n/** One route: a provider/model pair stamped onto an agent request. */\nexport interface ModelRoute {\n provider: string;\n model: string;\n /**\n * Optional reasoning-effort override. When omitted, the request inherits the\n * session's own selection; when set, the router pins it for that role.\n */\n reasoningEffort?: ReasoningEffort;\n /** Optional output-token cap for the role; omitted means inherit. */\n maxTokens?: number;\n /**\n * Error-driven escalation (v1): when true, a failed execution step bumps the\n * next request's effort to `escalateTo`, wearing off after `recoverySteps`\n * clean steps. Deterministic, stateless — the session log is folded per\n * request, so only *prior* steps are ever considered.\n */\n escalateOnError?: boolean;\n /** Effort used for the request after a failed step. */\n escalateTo?: ReasoningEffort;\n /** Clean steps before escalation wears off. Defaults to 2. */\n recoverySteps?: number;\n}\n\n/** The two roles the router distinguishes. */\nexport type AgentRole = \"planner\" | \"executor\";\n\n/**\n * Opt-in vision route: when enabled, any request whose messages carry image\n * content is stamped with the vision model, from every role (root agent and\n * subagents alike). Everything else keeps the pro/flash role routing.\n */\nexport interface VisionRoute {\n /** Master switch. Defaults to false — vision routing is opt-in. */\n enabled: boolean;\n /** Provider the vision model is served from. */\n provider: string;\n /** The vision model to stamp onto image-bearing requests. */\n model: string;\n /** Optional reasoning-effort pin; omitted means inherit the session's. */\n reasoningEffort?: ReasoningEffort;\n /** Optional output-token cap; omitted means inherit the session's. */\n maxTokens?: number;\n}\n\n/** Resolved router configuration: one route per role plus routing mode. */\nexport interface RouterConfig {\n planner: ModelRoute;\n executor: ModelRoute;\n mode: RoutingMode;\n /**\n * Live off-switch. Defaults to true; settable from Settings → Plugins →\n * model-router (applies immediately) or from the patch row (next boot).\n * When false, the router stops rewriting requests and unregisters the\n * prompt section and the skill.\n */\n enabled: boolean;\n promptSection: boolean;\n skill: boolean;\n /** Opt-in vision routing for image-bearing requests (v0.6.0+). */\n vision: VisionRoute;\n}\n\n/** Default recovery window: an error escalates for the next two completed steps. */\nexport const DEFAULT_RECOVERY_STEPS = 2;\n\n/**\n * Classify an agent as planner or executor.\n *\n * The main (root) agent of a session is the planner. Every agent created as a\n * delegation child — `subagent`, `subagent_fork`, workflow workers, ralph\n * rounds — is an executor. The harness stamps two durable facts on children:\n * `options.subagentDepth` (>= 1) and the session header `origin: \"subagent\"`.\n *\n * @param agent - the live agent (any subset of the runtime shape).\n * @returns the role the agent should be routed as.\n */\nexport function roleFor(agent: unknown): AgentRole {\n const options = (agent as { options?: unknown })?.options;\n const depth = (options as { subagentDepth?: unknown })?.subagentDepth;\n if (typeof depth === \"number\" && depth >= 1) return \"executor\";\n const session = (agent as { session?: unknown })?.session;\n const origin = (session as { header?: unknown })?.header\n ? ((session as { header: { origin?: unknown } }).header.origin)\n : undefined;\n if (origin === \"subagent\") return \"executor\";\n return \"planner\";\n}\n\n/**\n * Resolve the route for one agent.\n * @param agent - the live agent.\n * @param config - the resolved router configuration.\n * @param planModeActive - whether plan mode is currently folded active for the\n * agent's session; consulted only in `plan` routing mode.\n * @returns the model route to stamp, or `undefined` to leave the request alone.\n */\nexport function routeFor(\n agent: unknown,\n config: RouterConfig,\n planModeActive = false,\n): ModelRoute | undefined {\n const role = roleFor(agent);\n if (role === \"executor\") return config.executor;\n // Root agent. In `plan` mode, reserve the planner route for actual planning;\n // otherwise the root falls back to the executor route.\n if (config.mode === \"plan\" && !planModeActive) return config.executor;\n return config.planner;\n}\n\n/**\n * Whether any of the last `recoverySteps` completed steps carried a failed\n * tool result. A failure is a `tool/result` event whose data carries an\n * `error` field (the harness records tool failures there).\n *\n * Steps are deduplicated by `turn:step`, and only *completed* steps count —\n * events are scanned from the tail, so the current in-flight request is never\n * considered.\n *\n * @param events - the agent's session event log (or `undefined`).\n * @param recoverySteps - how many completed steps back to scan.\n * @returns true when a failed step is within the window.\n */\nexport function recentStepsHadError(\n events: readonly unknown[] | undefined,\n recoverySteps: number = DEFAULT_RECOVERY_STEPS,\n): boolean {\n if (!Array.isArray(events) || recoverySteps <= 0) return false;\n const seen = new Set<string>();\n let steps = 0;\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i] as\n | { type?: string; data?: { turn?: number; step?: number; error?: unknown } }\n | undefined;\n if (event?.type !== \"tool/result\" || event.data === undefined) continue;\n const key = `${event.data.turn}:${event.data.step}`;\n if (!seen.has(key)) {\n // A new step beyond the recovery window ends the scan; events for steps\n // already inside the window are still checked below.\n if (steps >= recoverySteps) break;\n seen.add(key);\n steps += 1;\n }\n if (event.data.error !== undefined && event.data.error !== null) return true;\n }\n return false;\n}\n\n/**\n * Resolve the reasoning effort to stamp for one request.\n *\n * Baseline is the route's `reasoningEffort`; when `escalateOnError` is enabled\n * and a recent step failed, the effort bumps to `escalateTo` (falling back to\n * the baseline when `escalateTo` is unset). Returns `undefined` to leave the\n * request's effort alone (inherit the session selection).\n *\n * @param route - the resolved route for the agent.\n * @param events - the agent's session event log.\n * @returns the effort to stamp, or `undefined` to inherit.\n */\nexport function effortFor(\n route: ModelRoute,\n events: readonly unknown[] | undefined,\n): ReasoningEffort | undefined {\n if (route.escalateOnError === true && recentStepsHadError(events, route.recoverySteps)) {\n return route.escalateTo ?? route.reasoningEffort;\n }\n return route.reasoningEffort;\n}\n\n/**\n * Whether any message in a request payload carries image content.\n *\n * A message carries an image when its `content` is a block array containing\n * an `image` block, directly or nested inside a `tool-result` block (tool\n * results may embed the images they produced). String content never does.\n *\n * @deprecated Superseded by {@link sessionHasImage} (v0.6.2): the\n * `agent/request` waterfall payload never carries `messages`, so production\n * routing reads the session event log instead. Kept for backward compat.\n * @param messages - the request's `messages` array (any runtime shape).\n * @returns true when at least one message contains image content.\n */\nexport function hasImageContent(messages: unknown): boolean {\n if (!Array.isArray(messages)) return false;\n for (const message of messages) {\n const content = (message as { content?: unknown })?.content;\n if (typeof content === \"string\") continue;\n if (Array.isArray(content) && blocksContainImage(content)) return true;\n }\n return false;\n}\n\n/**\n * Whether the agent's session log carries any image content.\n *\n * The `agent/request` waterfall payload never includes `messages`, so vision\n * detection reads the session event log instead. Shapes mirror the harness\n * `SessionEventMap` (`@deepseek-ai/dsh-session`):\n * - `user/message` event's data IS the `UserMessage` (content at `data.content`),\n * - `assistant/message` content lives at `data.message.content` (`AssistantMessage`),\n * - `tool/result` content lives at `data.message.content` (`ToolResultMessage`,\n * a single-element `[ToolResultBlock]` whose nested `content` may hold images).\n * Blocks may also nest images inside `tool-result` blocks. Extraction tries\n * `data.message.content` first, then `data.content`, so minor harness shape\n * drift still detects rather than silently missing.\n *\n * Sticky by design: one image anywhere in the log routes vision for the rest\n * of the session, because the image stays in request context until compaction\n * or pruning drops it. The scan is O(N) per request from the tail (early exit\n * on hit); text-only sessions scan the full log, which is fine at session scale.\n *\n * @param events - the agent's session event log (or `undefined`).\n * @returns true when any user, assistant, or tool-result message carries image content.\n */\nexport function sessionHasImage(events: readonly unknown[] | undefined): boolean {\n if (!Array.isArray(events)) return false;\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i] as\n | { type?: string; data?: { content?: unknown; message?: { content?: unknown } } }\n | undefined;\n if (\n event?.type !== \"user/message\" &&\n event?.type !== \"assistant/message\" &&\n event?.type !== \"tool/result\"\n )\n continue;\n // Robust extraction: prefer the wrapped message shape, fall back to direct.\n const content = event.data?.message?.content ?? event.data?.content;\n if (Array.isArray(content) && blocksContainImage(content)) return true;\n }\n return false;\n}\n\n/**\n * Whether a request should take the vision route.\n *\n * Pure, testable wiring helper for the `agent/request` listener in\n * `src/index.ts`: vision wins only when explicitly enabled AND the session\n * log carries an image. Extracted so the gating logic is covered without\n * spinning up Cordis.\n *\n * @param events - the agent's session event log.\n * @param vision - the resolved vision route (only `enabled` is read).\n * @returns true when the request must be stamped with the vision model.\n */\nexport function shouldUseVision(\n events: readonly unknown[] | undefined,\n vision: Pick<VisionRoute, \"enabled\"> | undefined,\n): boolean {\n if (vision?.enabled !== true) return false;\n return sessionHasImage(events);\n}\n\n/**\n * Whether any block (or nested tool-result content) is an image block.\n * Exact `type` matching is intentional: it mirrors the harness\n * `ContentBlockMap` vocabulary (`@deepseek-ai/dsh-llm`: `text`, `reasoning`,\n * `image`, `tool-call`, `tool-result`). Provider wire variants (`image_url`,\n * `input_image`, …) are normalized by adapters before reaching the log.\n */\nfunction blocksContainImage(blocks: readonly unknown[]): boolean {\n for (const block of blocks) {\n const candidate = block as { type?: unknown; content?: unknown };\n if (candidate?.type === \"image\") return true;\n if (candidate?.type === \"tool-result\" && Array.isArray(candidate.content) && blocksContainImage(candidate.content)) return true;\n }\n return false;\n}\n"],"mappings":"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","names":["stamped","effort"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/policy.ts"],"sourcesContent":["/**\n * dsh-model-router: role-based model routing for the DeepSeek Harness.\n *\n * Both roles default to `deepseek-flash` (V4.1 Flash, native multimodal):\n * the planner (the session's root agent) and delegated executor subagents\n * share the same model until V4.1-Pro launches, at which point the planner\n * route can be flipped back with a one-line config change. Enforcement is a\n * per-agent `agent/request` rewrite registered when the agent is created, so\n * it applies in every mode (web / headless / tui) and every agent preset,\n * including subagents the delegation tools create.\n *\n * Each role route may also pin `reasoningEffort` and `maxTokens`; when set,\n * they override the session's selection for that role. A `mode` switch lets a\n * deployment reserve the planner route for actual planning.\n *\n * The plugin also publishes:\n * - a system-prompt section stating the planner/executor convention, and\n * - the `pro-flash-routing` skill teaching the agent to plan itself and\n * delegate code execution to flash subagents.\n *\n * @module dsh-model-router\n */\nimport { Context, Service } from \"@deepseek-ai/cordis\";\nimport z from \"@deepseek-ai/schemastery\";\nimport { foldPlanMode } from \"@deepseek-ai/dsh-plan-mode\";\nimport { installSettingsSection, settingsNamespace } from \"@deepseek-ai/dsh-settings\";\nimport { effortFor, hasImageContent, recentStepsHadError, roleFor, routeFor, sessionHasImage, shouldUseVision, type RouterConfig } from \"./policy.js\";\n\n/** Plugin row id; the bundle patch inserts it under this id. */\nconst name = \"model-router\";\n\n/** One provider/model pair, with defaults and optional effort/token caps. */\nconst ModelRouteSchema = z.object({\n provider: z.string().min(1),\n model: z.string().min(1),\n reasoningEffort: z.union([\"off\", \"low\", \"high\", \"max\"]),\n maxTokens: z.number().min(1),\n escalateOnError: z.boolean(),\n escalateTo: z.union([\"off\", \"low\", \"high\", \"max\"]),\n recoverySteps: z.number().min(1),\n});\n\n/**\n * Opt-in vision route. Defaults to `deepseek-flash` (V4.1 Flash, native\n * multimodal) from `deepseek-official`; `reasoningEffort` and `maxTokens`\n * are optional pins. Native multimodal makes the vision branch a no-op when\n * the role routes already point at the same model — kept for explicitness\n * and for a future V4.1-Pro split.\n */\nconst VisionRouteSchema = z.object({\n enabled: z.boolean().default(false),\n provider: z.string().min(1).default(\"deepseek-official\"),\n model: z.string().min(1).default(\"deepseek-flash\"),\n reasoningEffort: z.union([\"off\", \"low\", \"high\", \"max\"]),\n maxTokens: z.number().min(1),\n});\n\n/** The plugin's public config, validated at row load. */\nconst Config = z.object({\n planner: ModelRouteSchema.default({\n provider: \"deepseek-official\",\n model: \"deepseek-flash\",\n } as never),\n executor: ModelRouteSchema.default({\n provider: \"deepseek-official\",\n model: \"deepseek-flash\",\n } as never),\n mode: z.union([\"strict\", \"plan\"]).default(\"strict\"),\n enabled: z.boolean().default(true),\n promptSection: z.boolean().default(true),\n skill: z.boolean().default(true),\n vision: VisionRouteSchema.default({} as never),\n});\n\n/** Settings namespace the live on/off toggle lives under (settings.yaml). */\nconst SETTINGS_NS = settingsNamespace(\"model-router\");\n\n/**\n * Resolve raw row config into the internal shape, failing loud on garbage.\n * Schemastery schemas are callable: invoking validates and applies defaults.\n * @param raw - the row's config object.\n * @returns the validated RouterConfig.\n */\nfunction resolveConfig(raw: unknown): RouterConfig {\n const parsed = Config(raw ?? {});\n return {\n planner: parsed.planner,\n executor: parsed.executor,\n mode: parsed.mode,\n enabled: parsed.enabled,\n promptSection: parsed.promptSection,\n skill: parsed.skill,\n vision: parsed.vision,\n };\n}\n\n/**\n * Always-on guidance section. Negative order renders before the persona, so\n * the convention is established before the agent's identity line.\n */\nconst SECTION_ORDER = -50;\n\nconst SECTION_TEXT = `Model routing is role-based. Planning runs on {PLANNER_MODEL}; implementation runs on {EXECUTOR_MODEL}. You are the root agent: plan, design, review subagent output, and write the final answer here. Delegate implementation — writing code, running commands, builds, tests — to subagents with complete, self-contained prompts, preferring background delegation for independent work. Keep plans and replies concise. Do not hand-write large amounts of code or run long executions here; delegate instead.{VISION_LINE}`;\n\nconst SKILL_NAME = \"pro-flash-routing\";\n\nconst SKILL_DESCRIPTION =\n \"Route planning and code execution across models: plan on the pro planner agent, delegate implementation to flash executor subagents.\";\n\nconst SKILL_WHEN_TO_USE = `Use when a task combines planning and implementation: before writing code, after a plan is approved, when delegating execution work, or when the user asks about the pro/flash routing convention.`;\n\nconst SKILL_TEMPLATE = `# Pro planner / Flash executor routing\n\nThis session routes models by role (planner \\`{PLANNER_MODEL}\\`, executor \\`{EXECUTOR_MODEL}\\`; both default to deepseek-flash, V4.1 Flash with native multimodal; vision needs no separate model):\n\n- **Planner (this agent)** — \\`{PLANNER_MODEL}\\`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.\n- **Executors (every subagent)** — \\`{EXECUTOR_MODEL}\\`. Implementation work happens there: writing code, running commands, builds, and tests. The harness forces the model automatically; you do not select it.\n\n## Working rhythm\n\n1. **Plan here.** Explore, decide the approach, and (when plan mode is on) submit the plan with \\`exit_plan_mode\\`. The plan stays on this agent.\n2. **Delegate the execution.** Once a plan is approved, hand each self-contained chunk of implementation to a subagent with a complete prompt: exact files to touch, the change to make, and how to verify. Subagents are automatically routed to \\`{EXECUTOR_MODEL}\\`, so keep them execution-focused: give them the decision, not the decision to make.\n3. **Review here.** Read the subagent's result on this agent, verify it yourself (tests, diffs, logs), and iterate with follow-up messages to the same subagent when available.\n4. **Report here.** Summaries, plans, and answers to the user come from this agent.\n\n## Keep this agent's context lean\n\nInput tokens are the expensive part of the planner. Don't re-read large files or full transcripts on this agent — trust the subagent's final report. Prefer targeted reads (offset/limit) over whole files. When the context grows, compact rather than re-sending everything.\n\n## Delegation guidelines\n\n- Start independent delegations together in one assistant message and continue useful work while they run (background mode by default).\n- Prefer \\`subagent\\` for self-contained work and \\`workflow\\` when many independent pieces need fan-out; their workers run on \\`{EXECUTOR_MODEL}\\` as well.\n- Do not delegate design: subagents execute decisions already made.\n- If a subagent's task grows into design work, pull it back to this agent and re-delegate the narrowed execution.\n\n## Verification\n\n- Planner produces \\`{PLANNER_MODEL}\\`, executor produces \\`{EXECUTOR_MODEL}\\` (unified when equal since V4.1; split again when V4.1-Pro lands). If you need to confirm, check the session log's model metadata.\n- If routing ever looks wrong, the \\`model-router\\` plugin row in the profile composition is the single place that owns it.`;\n\n/**\n * Render the skill content for one config, substituting the configured\n * planner/executor models (mirrors the prompt-section templating).\n * @param cfg - the resolved router configuration.\n * @returns the skill markdown with model placeholders filled.\n */\nfunction buildSkillContent(cfg: RouterConfig): string {\n return SKILL_TEMPLATE.replaceAll(\"{PLANNER_MODEL}\", cfg.planner.model).replaceAll(\n \"{EXECUTOR_MODEL}\",\n cfg.executor.model,\n );\n}\n\nconst SKILL_CONTENT = buildSkillContent({\n planner: { provider: \"deepseek-official\", model: \"deepseek-flash\" },\n executor: { provider: \"deepseek-official\", model: \"deepseek-flash\" },\n mode: \"strict\",\n enabled: true,\n promptSection: true,\n skill: true,\n vision: { enabled: false, provider: \"deepseek-official\", model: \"deepseek-flash\" },\n});\n\n/** The plugin row id the bundle patch must insert. */\nconst ROW_ID = \"model-router\";\n\n/** Minimal structural view of the live agent object the router reads. */\ninterface AgentLike {\n ctx: AgentScopedContext;\n options?: { subagentDepth?: number };\n session?: { header?: { origin?: string }; events?: unknown[] };\n}\n\n/** The agent-scoped context's waterfall surface the router uses. */\ninterface AgentScopedContext {\n on(\n event: \"agent/request\",\n listener: (\n payload: Record<string, unknown>,\n next: () => Promise<Record<string, unknown>>,\n ) => Promise<Record<string, unknown>>,\n options?: { prepend?: boolean },\n ): () => void;\n}\n\n/** Host-plane surface the router consumes (events, prompt registry, skills). */\ninterface HarnessContext {\n on(\n event: \"agent/created\",\n listener: (payload: { agent: AgentLike }) => void,\n ): () => void;\n on(event: \"agent/disposed\", listener: (agent: unknown) => void): () => void;\n systemPrompt: {\n section(section: { name: string; order: number; text: string }): () => void;\n };\n skills: {\n register(skill: {\n name: string;\n description: string;\n whenToUse?: string;\n content: string;\n source: string;\n }): () => void;\n };\n}\n\n/** Fold plan-mode state for an agent without trusting the agent's exact shape. */\nfunction isPlanModeActive(agent: AgentLike): boolean {\n const events = agent.session?.events;\n if (!Array.isArray(events)) return false;\n try {\n return foldPlanMode(events as Parameters<typeof foldPlanMode>[0]);\n } catch {\n return false;\n }\n}\n\n/**\n * Cordis service: per-agent request routing plus the convention surface.\n */\nclass ModelRouter extends Service {\n static inject = [\"skills\", \"systemPrompt\"];\n\n config: RouterConfig;\n\n /** Currently authoritative config; swapped by the settings section when one is mounted. */\n source: () => RouterConfig;\n\n /** Host-plane surface the router consumes (events, prompt registry, skills). */\n harness: HarnessContext;\n\n /** Disposer of the currently registered prompt section, if any. */\n promptDispose?: () => void;\n\n /** Disposer of the currently registered skill, if any. */\n skillDispose?: () => void;\n\n constructor(ctx: Context, rawConfig: unknown = {}) {\n super(ctx, \"modelRouter\");\n this.config = resolveConfig(rawConfig);\n this.source = () => this.config;\n this.harness = ctx as unknown as HarnessContext;\n\n // Every agent that gets created — root sessions, delegation children,\n // workflow workers, ralph rounds — passes through here. The\n // `agent/disposed` listener is registered once (not per agent) and fans\n // out via a map, so long sessions with many delegations don't accumulate\n // a global listener per agent.\n const requestDisposers = new Map<unknown, () => void>();\n this.harness.on(\"agent/disposed\", (disposedAgent) => {\n const dispose = requestDisposers.get(disposedAgent);\n if (dispose !== undefined) {\n dispose();\n requestDisposers.delete(disposedAgent);\n }\n });\n this.harness.on(\"agent/created\", ({ agent }) => {\n // `prepend` puts this listener OUTERMOST in the `agent/request`\n // waterfall: the harness's model-selection listener runs inside it, so\n // this rewrite is applied LAST and wins over the session's selected\n // model (which dsh-base defaulted to deepseek-v4-flash, now deepseek-flash, and the user\n // settings or UI can change).\n const dispose = agent.ctx.on(\n \"agent/request\",\n async (payload, next) => {\n const resolved = await next();\n const cfg = this.source();\n if (!cfg.enabled) return resolved;\n // Vision branch FIRST: any request whose session log carries image\n // content is stamped with the vision model, regardless of role. The\n // `agent/request` waterfall payload never includes `messages`, so\n // detection reads the session event log (`user/message`,\n // `assistant/message`, `tool/result`). The harness's own\n // model-selection listener has already run (`await next()`), so\n // this rewrite wins over the session model.\n if (shouldUseVision(agent.session?.events, cfg.vision)) {\n const stamped: Record<string, unknown> = {\n ...resolved,\n provider: cfg.vision.provider,\n model: cfg.vision.model,\n };\n if (cfg.vision.maxTokens !== undefined) stamped.maxTokens = cfg.vision.maxTokens;\n const effort = effortFor(cfg.vision, agent.session?.events);\n if (effort !== undefined) stamped.reasoningEffort = effort;\n return stamped;\n }\n const route = routeFor(agent, cfg, isPlanModeActive(agent));\n if (route === undefined) return resolved;\n const stamped: Record<string, unknown> = {\n ...resolved,\n provider: route.provider,\n model: route.model,\n };\n if (route.maxTokens !== undefined) stamped.maxTokens = route.maxTokens;\n const effort = effortFor(route, agent.session?.events);\n if (effort !== undefined) stamped.reasoningEffort = effort;\n return stamped;\n },\n { prepend: true },\n );\n const prev = requestDisposers.get(agent);\n if (prev !== undefined) prev();\n requestDisposers.set(agent, dispose);\n });\n\n // Register the convention surface from the composition config, then let\n // the settings section (when mounted) take over as the live source.\n this.render(this.config);\n installSettingsSection(\n ctx,\n SETTINGS_NS,\n Config,\n this.config as unknown as ReturnType<typeof Config>,\n {\n setSource: (current) => {\n this.source = current;\n },\n onChange: () => this.render(this.source()),\n },\n );\n }\n\n /**\n * Register the convention surface (prompt section + skill) for `cfg`,\n * replacing whatever is currently registered. Called on construction and\n * after every committed settings change; while `enabled` is false, nothing\n * stays registered.\n */\n private render(cfg: RouterConfig): void {\n if (this.promptDispose) {\n this.promptDispose();\n this.promptDispose = undefined;\n }\n if (this.skillDispose) {\n this.skillDispose();\n this.skillDispose = undefined;\n }\n if (!cfg.enabled) return;\n if (cfg.promptSection) {\n this.promptDispose = this.harness.systemPrompt.section({\n name: ROW_ID,\n order: SECTION_ORDER,\n text: SECTION_TEXT.replaceAll(\"{PLANNER_MODEL}\", cfg.planner.model)\n .replaceAll(\"{EXECUTOR_MODEL}\", cfg.executor.model)\n .replace(\n \"{VISION_LINE}\",\n cfg.vision.enabled\n ? ` Image-bearing requests route to ${cfg.vision.model}.`\n : \"\",\n ),\n });\n }\n if (cfg.skill) {\n this.skillDispose = this.harness.skills.register({\n name: SKILL_NAME,\n description: SKILL_DESCRIPTION,\n whenToUse: SKILL_WHEN_TO_USE,\n content: buildSkillContent(cfg),\n source: \"runtime\",\n });\n }\n }\n}\n\nexport {\n Config,\n ModelRouter,\n ModelRouter as default,\n SETTINGS_NS,\n VisionRouteSchema,\n buildSkillContent,\n name,\n ROW_ID,\n SKILL_CONTENT,\n SKILL_DESCRIPTION,\n SKILL_NAME,\n SKILL_WHEN_TO_USE,\n effortFor,\n hasImageContent,\n recentStepsHadError,\n roleFor,\n routeFor,\n sessionHasImage,\n shouldUseVision,\n};\nexport type {\n AgentRole,\n ModelRoute,\n ReasoningEffort,\n RoutingMode,\n RouterConfig,\n VisionRoute,\n} from \"./policy.js\";\n","/**\n * Pure routing policy for dsh-model-router: which model each agent role gets.\n * Kept free of Cordis imports so the policy is trivially unit-testable.\n * @module dsh-model-router/policy\n */\n\n/** Reasoning-effort levels a route may pin (mirrors the harness vocabulary). */\nexport type ReasoningEffort = \"off\" | \"low\" | \"high\" | \"max\";\n\n/**\n * How the router treats the root agent.\n * - `strict`: the root agent is always the planner (pro).\n * - `plan`: the root agent is pro only while plan mode is active; otherwise it\n * falls back to the executor route, reserving pro for real planning.\n */\nexport type RoutingMode = \"strict\" | \"plan\";\n\n/** One route: a provider/model pair stamped onto an agent request. */\nexport interface ModelRoute {\n provider: string;\n model: string;\n /**\n * Optional reasoning-effort override. When omitted, the request inherits the\n * session's own selection; when set, the router pins it for that role.\n */\n reasoningEffort?: ReasoningEffort;\n /** Optional output-token cap for the role; omitted means inherit. */\n maxTokens?: number;\n /**\n * Error-driven escalation (v1): when true, a failed execution step bumps the\n * next request's effort to `escalateTo`, wearing off after `recoverySteps`\n * clean steps. Deterministic, stateless — the session log is folded per\n * request, so only *prior* steps are ever considered.\n */\n escalateOnError?: boolean;\n /** Effort used for the request after a failed step. */\n escalateTo?: ReasoningEffort;\n /** Clean steps before escalation wears off. Defaults to 2. */\n recoverySteps?: number;\n}\n\n/** The two roles the router distinguishes. */\nexport type AgentRole = \"planner\" | \"executor\";\n\n/**\n * Opt-in vision route: when enabled, any request whose messages carry image\n * content is stamped with the vision model, from every role (root agent and\n * subagents alike). Everything else keeps the pro/flash role routing.\n */\nexport interface VisionRoute {\n /** Master switch. Defaults to false — vision routing is opt-in. */\n enabled: boolean;\n /** Provider the vision model is served from. */\n provider: string;\n /** The vision model to stamp onto image-bearing requests. */\n model: string;\n /** Optional reasoning-effort pin; omitted means inherit the session's. */\n reasoningEffort?: ReasoningEffort;\n /** Optional output-token cap; omitted means inherit the session's. */\n maxTokens?: number;\n}\n\n/** Resolved router configuration: one route per role plus routing mode. */\nexport interface RouterConfig {\n planner: ModelRoute;\n executor: ModelRoute;\n mode: RoutingMode;\n /**\n * Live off-switch. Defaults to true; settable from Settings → Plugins →\n * model-router (applies immediately) or from the patch row (next boot).\n * When false, the router stops rewriting requests and unregisters the\n * prompt section and the skill.\n */\n enabled: boolean;\n promptSection: boolean;\n skill: boolean;\n /** Opt-in vision routing for image-bearing requests (v0.6.0+). */\n vision: VisionRoute;\n}\n\n/** Default recovery window: an error escalates for the next two completed steps. */\nexport const DEFAULT_RECOVERY_STEPS = 2;\n\n/**\n * Classify an agent as planner or executor.\n *\n * The main (root) agent of a session is the planner. Every agent created as a\n * delegation child — `subagent`, `subagent_fork`, workflow workers, ralph\n * rounds — is an executor. The harness stamps two durable facts on children:\n * `options.subagentDepth` (>= 1) and the session header `origin: \"subagent\"`.\n *\n * @param agent - the live agent (any subset of the runtime shape).\n * @returns the role the agent should be routed as.\n */\nexport function roleFor(agent: unknown): AgentRole {\n const options = (agent as { options?: unknown })?.options;\n const depth = (options as { subagentDepth?: unknown })?.subagentDepth;\n if (typeof depth === \"number\" && depth >= 1) return \"executor\";\n const session = (agent as { session?: unknown })?.session;\n const origin = (session as { header?: unknown })?.header\n ? ((session as { header: { origin?: unknown } }).header.origin)\n : undefined;\n if (origin === \"subagent\") return \"executor\";\n return \"planner\";\n}\n\n/**\n * Resolve the route for one agent.\n * @param agent - the live agent.\n * @param config - the resolved router configuration.\n * @param planModeActive - whether plan mode is currently folded active for the\n * agent's session; consulted only in `plan` routing mode.\n * @returns the model route to stamp, or `undefined` to leave the request alone.\n */\nexport function routeFor(\n agent: unknown,\n config: RouterConfig,\n planModeActive = false,\n): ModelRoute | undefined {\n const role = roleFor(agent);\n if (role === \"executor\") return config.executor;\n // Root agent. In `plan` mode, reserve the planner route for actual planning;\n // otherwise the root falls back to the executor route.\n if (config.mode === \"plan\" && !planModeActive) return config.executor;\n return config.planner;\n}\n\n/**\n * Whether any of the last `recoverySteps` completed steps carried a failed\n * tool result. A failure is a `tool/result` event whose data carries an\n * `error` field (the harness records tool failures there).\n *\n * Steps are deduplicated by `turn:step`, and only *completed* steps count —\n * events are scanned from the tail, so the current in-flight request is never\n * considered.\n *\n * @param events - the agent's session event log (or `undefined`).\n * @param recoverySteps - how many completed steps back to scan.\n * @returns true when a failed step is within the window.\n */\nexport function recentStepsHadError(\n events: readonly unknown[] | undefined,\n recoverySteps: number = DEFAULT_RECOVERY_STEPS,\n): boolean {\n if (!Array.isArray(events) || recoverySteps <= 0) return false;\n const seen = new Set<string>();\n let steps = 0;\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i] as\n | { type?: string; data?: { turn?: number; step?: number; error?: unknown } }\n | undefined;\n if (event?.type !== \"tool/result\" || event.data === undefined) continue;\n const key = `${event.data.turn}:${event.data.step}`;\n if (!seen.has(key)) {\n // A new step beyond the recovery window ends the scan; events for steps\n // already inside the window are still checked below.\n if (steps >= recoverySteps) break;\n seen.add(key);\n steps += 1;\n }\n if (event.data.error !== undefined && event.data.error !== null) return true;\n }\n return false;\n}\n\n/**\n * Resolve the reasoning effort to stamp for one request.\n *\n * Baseline is the route's `reasoningEffort`; when `escalateOnError` is enabled\n * and a recent step failed, the effort bumps to `escalateTo` (falling back to\n * the baseline when `escalateTo` is unset). Returns `undefined` to leave the\n * request's effort alone (inherit the session selection).\n *\n * @param route - the resolved route for the agent.\n * @param events - the agent's session event log.\n * @returns the effort to stamp, or `undefined` to inherit.\n */\nexport function effortFor(\n route: ModelRoute,\n events: readonly unknown[] | undefined,\n): ReasoningEffort | undefined {\n if (route.escalateOnError === true && recentStepsHadError(events, route.recoverySteps)) {\n return route.escalateTo ?? route.reasoningEffort;\n }\n return route.reasoningEffort;\n}\n\n/**\n * Whether any message in a request payload carries image content.\n *\n * A message carries an image when its `content` is a block array containing\n * an `image` block, directly or nested inside a `tool-result` block (tool\n * results may embed the images they produced). String content never does.\n *\n * @deprecated Superseded by {@link sessionHasImage} (v0.6.2): the\n * `agent/request` waterfall payload never carries `messages`, so production\n * routing reads the session event log instead. Kept for backward compat.\n * @param messages - the request's `messages` array (any runtime shape).\n * @returns true when at least one message contains image content.\n */\nexport function hasImageContent(messages: unknown): boolean {\n if (!Array.isArray(messages)) return false;\n for (const message of messages) {\n const content = (message as { content?: unknown })?.content;\n if (typeof content === \"string\") continue;\n if (Array.isArray(content) && blocksContainImage(content)) return true;\n }\n return false;\n}\n\n/**\n * Whether the agent's session log carries any image content.\n *\n * The `agent/request` waterfall payload never includes `messages`, so vision\n * detection reads the session event log instead. Shapes mirror the harness\n * `SessionEventMap` (`@deepseek-ai/dsh-session`):\n * - `user/message` event's data IS the `UserMessage` (content at `data.content`),\n * - `assistant/message` content lives at `data.message.content` (`AssistantMessage`),\n * - `tool/result` content lives at `data.message.content` (`ToolResultMessage`,\n * a single-element `[ToolResultBlock]` whose nested `content` may hold images).\n * Blocks may also nest images inside `tool-result` blocks. Extraction tries\n * `data.message.content` first, then `data.content`, so minor harness shape\n * drift still detects rather than silently missing.\n *\n * Sticky by design: one image anywhere in the log routes vision for the rest\n * of the session, because the image stays in request context until compaction\n * or pruning drops it. The scan is O(N) per request from the tail (early exit\n * on hit); text-only sessions scan the full log, which is fine at session scale.\n *\n * @param events - the agent's session event log (or `undefined`).\n * @returns true when any user, assistant, or tool-result message carries image content.\n */\nexport function sessionHasImage(events: readonly unknown[] | undefined): boolean {\n if (!Array.isArray(events)) return false;\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i] as\n | { type?: string; data?: { content?: unknown; message?: { content?: unknown } } }\n | undefined;\n if (\n event?.type !== \"user/message\" &&\n event?.type !== \"assistant/message\" &&\n event?.type !== \"tool/result\"\n )\n continue;\n // Robust extraction: prefer the wrapped message shape, fall back to direct.\n const content = event.data?.message?.content ?? event.data?.content;\n if (Array.isArray(content) && blocksContainImage(content)) return true;\n }\n return false;\n}\n\n/**\n * Whether a request should take the vision route.\n *\n * Pure, testable wiring helper for the `agent/request` listener in\n * `src/index.ts`: vision wins only when explicitly enabled AND the session\n * log carries an image. Extracted so the gating logic is covered without\n * spinning up Cordis.\n *\n * @param events - the agent's session event log.\n * @param vision - the resolved vision route (only `enabled` is read).\n * @returns true when the request must be stamped with the vision model.\n */\nexport function shouldUseVision(\n events: readonly unknown[] | undefined,\n vision: Pick<VisionRoute, \"enabled\"> | undefined,\n): boolean {\n if (vision?.enabled !== true) return false;\n return sessionHasImage(events);\n}\n\n/**\n * Whether any block (or nested tool-result content) is an image block.\n * Exact `type` matching is intentional: it mirrors the harness\n * `ContentBlockMap` vocabulary (`@deepseek-ai/dsh-llm`: `text`, `reasoning`,\n * `image`, `tool-call`, `tool-result`). Provider wire variants (`image_url`,\n * `input_image`, …) are normalized by adapters before reaching the log.\n */\nfunction blocksContainImage(blocks: readonly unknown[]): boolean {\n for (const block of blocks) {\n const candidate = block as { type?: unknown; content?: unknown };\n if (candidate?.type === \"image\") return true;\n if (candidate?.type === \"tool-result\" && Array.isArray(candidate.content) && blocksContainImage(candidate.content)) return true;\n }\n return false;\n}\n"],"mappings":"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","names":["stamped","effort"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-model-router",
3
3
  "description": "DeepSeek Harness plugin: role-based model routing — both roles default to deepseek-flash (V4.1 Flash, native multimodal) until V4.1-Pro launches.",
4
- "version": "0.7.0",
4
+ "version": "0.7.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -6,7 +6,7 @@ whenToUse: Use when a task combines planning and implementation: before writing
6
6
 
7
7
  # Pro planner / Flash executor routing
8
8
 
9
- This session routes models by role (both default to deepseek-flash, V4.1 Flash with native multimodal; vision needs no separate model):
9
+ This session routes models by role (planner `deepseek-flash`, executor `deepseek-flash`; both default to deepseek-flash, V4.1 Flash with native multimodal; vision needs no separate model):
10
10
 
11
11
  - **Planner (this agent)** — `deepseek-flash`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.
12
12
  - **Executors (every subagent)** — `deepseek-flash`. Implementation work happens there: writing code, running commands, builds, and tests. The harness forces the model automatically; you do not select it.
@@ -25,11 +25,11 @@ Input tokens are the expensive part of the planner. Don't re-read large files or
25
25
  ## Delegation guidelines
26
26
 
27
27
  - Start independent delegations together in one assistant message and continue useful work while they run (background mode by default).
28
- - Prefer `subagent` for self-contained work and `workflow` when many independent pieces need fan-out; their workers run on flash as well.
28
+ - Prefer `subagent` for self-contained work and `workflow` when many independent pieces need fan-out; their workers run on `deepseek-flash` as well.
29
29
  - Do not delegate design: subagents execute decisions already made.
30
30
  - If a subagent's task grows into design work, pull it back to this agent and re-delegate the narrowed execution.
31
31
 
32
32
  ## Verification
33
33
 
34
- - Both roles produce `deepseek-flash` output (unified since V4.1; split again when V4.1-Pro lands). If you need to confirm, check the session log's model metadata.
34
+ - Planner produces `deepseek-flash`, executor produces `deepseek-flash` (unified when equal since V4.1; split again when V4.1-Pro lands). If you need to confirm, check the session log's model metadata.
35
35
  - If routing ever looks wrong, the `model-router` plugin row in the profile composition is the single place that owns it.