dsh-model-router 0.6.3 → 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 +87 -31
- package/cordis.patch.yml +26 -9
- package/lib/client.js +4 -4
- package/lib/index.d.ts +14 -4
- package/lib/index.js +38 -14
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/pro-flash-routing/SKILL.md +10 -6
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
A small plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) that stops treating every model call the same. It splits your session into two roles:
|
|
4
4
|
|
|
5
|
-
- **The planner** — your main agent —
|
|
6
|
-
- **The executors** — every subagent it delegates to —
|
|
5
|
+
- **The planner** — your main agent — defaults to `deepseek-flash` (V4.1 Flash, native multimodal). That's where the thinking happens: understanding what you want, designing the approach, reviewing results, writing the final answer.
|
|
6
|
+
- **The executors** — every subagent it delegates to — default to `deepseek-flash` as well.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Both roles share the same model until V4.1-Pro launches (V4.1 Flash already beats V4-Pro on performance, cost, and speed, so DeepSeek is retiring `deepseek-v4-pro` / `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` — all three now route to V4.1 Flash server-side). The role split stays in the config, so flipping the planner back to Pro later is a one-line change. You keep the careful plan/delegate/review rhythm without paying Pro prices for every tool call.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -29,7 +29,7 @@ Once it's in, restart the profile. You should see the row under `model-router` i
|
|
|
29
29
|
|
|
30
30
|
Three small surfaces, one rule:
|
|
31
31
|
|
|
32
|
-
1. **Request routing** — every model request gets stamped with a role. Root agents get
|
|
32
|
+
1. **Request routing** — every model request gets stamped with a role. Root agents get the planner route; delegation children (`subagent`, `subagent_fork`, workflow workers, ralph rounds) get the executor route. Both default to `deepseek-flash` (V4.1 Flash) until V4.1-Pro launches, so today the stamp unifies while the role split stays configurable. The rewrite sits at the outermost layer of the request pipeline, so it wins — even over the harness's own default model and over whatever model you pick in the UI for the session. That's intentional: it's the "enforce" knob.
|
|
33
33
|
2. **A prompt section** — a short note that renders before the agent's persona, telling the planner: you're the thinker, delegate the implementation. Without this, the model tends to just do everything itself.
|
|
34
34
|
3. **A skill** — the `pro-flash-routing` skill shows up in the session's skill catalog and spells out the working rhythm: plan, delegate, review, report. Same convention, but loadable on demand when the agent wants details.
|
|
35
35
|
|
|
@@ -46,11 +46,20 @@ 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:
|
|
52
61
|
|
|
53
|
-
- **GUI (Settings → Plugins →
|
|
62
|
+
- **GUI (Settings → Plugins → Model router):** the plugin registers a live
|
|
54
63
|
settings section; flip `enabled` off. It applies immediately (no restart),
|
|
55
64
|
persists in `settings.yaml` under `model-router:`, and unregisters the prompt
|
|
56
65
|
section and the skill too. Flip it back on and everything returns. The same
|
|
@@ -68,15 +77,18 @@ Since v0.5.0 the package ships a browser half, and the harness serves it
|
|
|
68
77
|
automatically — no extra config. After installing (or updating to) v0.5.0+
|
|
69
78
|
and restarting the profile, **Settings → Plugins** shows a **Model router**
|
|
70
79
|
card with a live **Enabled** switch, an "Overridden" badge and **Reset to
|
|
71
|
-
default** button once you've changed it,
|
|
72
|
-
planner/executor routes and mode
|
|
80
|
+
default** button once you've changed it, a read-only view of the current
|
|
81
|
+
planner/executor routes and mode, and a Vision section (v0.6.0+) with its own
|
|
82
|
+
live **Vision** switch, **Reset vision to default** button, and read-only
|
|
83
|
+
vision-model line. Flipping the switch applies immediately (no
|
|
73
84
|
restart) and persists in `settings.yaml` under `model-router:` — the same
|
|
74
85
|
mechanism the GUI toggle described above uses.
|
|
75
86
|
|
|
76
87
|
One harness-wide caveat (it applies to **all** settings pages — Models,
|
|
77
88
|
Plugins, everything — not to this plugin specifically): the harness serves
|
|
78
89
|
settings pages only to **loopback** browsers (`localhost` / `127.x`). A remote
|
|
79
|
-
browser
|
|
90
|
+
browser may not get the settings page at all; where the card does render it
|
|
91
|
+
shows a read-only note. Fallbacks that
|
|
80
92
|
work everywhere:
|
|
81
93
|
|
|
82
94
|
- **Patch row** — set `enabled: false` in the profile's `cordis.patch.yml`
|
|
@@ -93,31 +105,45 @@ and off by default** (`vision.enabled: false`). Two ways to turn it on:
|
|
|
93
105
|
no restart, persists in `settings.yaml`), or
|
|
94
106
|
- **Patch row / settings:** set `vision.enabled: true` on the plugin's config.
|
|
95
107
|
|
|
96
|
-
When enabled, **any request
|
|
97
|
-
vision model** — `deepseek-
|
|
98
|
-
default
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
When enabled, **any request sent while the session log carries an image is stamped with the
|
|
109
|
+
vision model** — `deepseek-flash` from `deepseek-official` by
|
|
110
|
+
default (V4.1 Flash is natively multimodal, so this matches the role routes
|
|
111
|
+
unless you pin something else) — **from every role**: the root (planner) agent and all delegated
|
|
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` /
|
|
102
117
|
`vision.maxTokens` pins work exactly like the per-role ones. Image detection
|
|
103
118
|
reads the session event log (`user/message`, `assistant/message`, and
|
|
104
119
|
`tool/result`, including images nested in `tool-result` blocks): once an image
|
|
105
120
|
appears anywhere in the log, subsequent requests stay on the vision model for
|
|
106
121
|
the rest of the session (sticky — the image stays in request context until
|
|
107
|
-
compaction or pruning drops it).
|
|
122
|
+
compaction or pruning drops it). Detection shapes are verified against real
|
|
123
|
+
session logs: `user/message` carries the image at `data.content`, while
|
|
124
|
+
`assistant/message` and `tool/result` carry it at `data.message.content`
|
|
125
|
+
(tool results nest it inside a `tool-result` block). Possible follow-up:
|
|
126
|
+
optional `vision.historyLimit` to bound the scan to the trailing N events
|
|
127
|
+
(unset keeps today's sticky entire-log scan).
|
|
108
128
|
|
|
109
129
|
The plugin ships the support in its own `cordis.patch.yml`:
|
|
110
130
|
|
|
111
|
-
- a **catalog entry** for
|
|
112
|
-
`inputModalities: [text, image]`,
|
|
113
|
-
|
|
131
|
+
- a **catalog entry** for `deepseek-flash` on the `llm-deepseek` row (with
|
|
132
|
+
`inputModalities: [text, image]`, `contextWindow: 1000000`, `maxTokens:
|
|
133
|
+
384000`), plus the retired `deepseek-v4-flash` / `deepseek-v4-pro` /
|
|
134
|
+
`deepseek-v4-flash-vision-exp` ids kept as compat aliases through the
|
|
135
|
+
transition, and
|
|
114
136
|
- **raised `attachment-local` image admission limits** so normal screenshots
|
|
115
137
|
(~8K, 15MB) attach without being rejected (`maxImageDimension: 8192`,
|
|
116
138
|
`maxImagePixels: 100000000`, `maxImageBytes: 15728640`).
|
|
117
139
|
|
|
118
140
|
Both are defaults you can override in your **profile**'s `cordis.patch.yml` —
|
|
119
141
|
the profile layer applies after the plugin layer, so a `patch:` targeting
|
|
120
|
-
`llm-deepseek` or `attachment-local` in the profile wins.
|
|
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
|
|
121
147
|
remains: the vision model **must be present in the catalog with image input
|
|
122
148
|
modality**, or the provider rejects the request at call time with
|
|
123
149
|
`UNSUPPORTED_CONTENT` — the shipped catalog row is what satisfies that.
|
|
@@ -130,14 +156,14 @@ All configuration lives on the plugin row. Patch it in the profile's `cordis.pat
|
|
|
130
156
|
- patch:
|
|
131
157
|
- id: model-router
|
|
132
158
|
config:
|
|
133
|
-
planner: # root-agent route
|
|
159
|
+
planner: # root-agent route (deepseek-flash until V4.1-Pro lands)
|
|
134
160
|
provider: deepseek-official
|
|
135
|
-
model: deepseek-
|
|
161
|
+
model: deepseek-flash
|
|
136
162
|
reasoningEffort: high # off | low | high | max (omit to inherit)
|
|
137
163
|
maxTokens: 8192 # output cap (omit to inherit)
|
|
138
164
|
executor: # subagent route
|
|
139
165
|
provider: deepseek-official
|
|
140
|
-
model: deepseek-
|
|
166
|
+
model: deepseek-flash
|
|
141
167
|
reasoningEffort: high
|
|
142
168
|
escalateOnError: true # after a failed step…
|
|
143
169
|
escalateTo: max # …bump effort for the next request
|
|
@@ -147,19 +173,20 @@ All configuration lives on the plugin row. Patch it in the profile's `cordis.pat
|
|
|
147
173
|
skill: true # register the pro-flash-routing skill
|
|
148
174
|
```
|
|
149
175
|
|
|
150
|
-
`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
|
|
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.
|
|
151
177
|
|
|
152
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.
|
|
153
179
|
|
|
154
|
-
The defaults are exactly the
|
|
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.
|
|
155
181
|
|
|
156
|
-
## Reduce
|
|
182
|
+
## Reduce token usage
|
|
157
183
|
|
|
158
|
-
|
|
184
|
+
With both roles on `deepseek-flash`, the bill is already far below the old
|
|
185
|
+
pro-based setup — most of the remaining savings come from shrinking spend:
|
|
159
186
|
|
|
160
|
-
- **Lower `reasoningEffort`.** The harness default runs
|
|
187
|
+
- **Lower `reasoningEffort`.** The harness default runs at `max`, which produces a lot of reasoning tokens. `high` (or `low`) on a route keeps most of the quality at a fraction of the cost.
|
|
161
188
|
- **Cap output** with `maxTokens` on the planner route so a verbose turn can't balloon.
|
|
162
|
-
- **Reserve
|
|
189
|
+
- **Reserve the planner route for planning** with `mode: plan` — trivial Q&A and execution-style turns stop hitting the planner route at all (matters again once V4.1-Pro lands and the routes split).
|
|
163
190
|
- **Keep the planner's context lean.** Input tokens dominate after reasoning. Delegate aggressively and trust the subagent's report; don't re-read big files or full transcripts on the planner. Use targeted reads and let auto-compaction (`/compact`) trim history.
|
|
164
191
|
- **Tune the host pruner.** The tool-result pruner truncates oversized results before they reach the model (default ~8 KB); lowering `tool-result-pruner` → `thresholdChars` trims more planner input. That's harness config, not this plugin's row.
|
|
165
192
|
- **Exploit DeepSeek's context cache.** Repeated prefixes are served from cache at a big discount, so keep the system prompt and conversation prefix stable between turns.
|
|
@@ -168,14 +195,30 @@ The first three are one-line changes on this plugin's row; the last three are di
|
|
|
168
195
|
|
|
169
196
|
## Does it work?
|
|
170
197
|
|
|
171
|
-
|
|
198
|
+
Verify against a real session log. Run a task that makes the agent plan and delegate, then check which models actually made the requests:
|
|
172
199
|
|
|
173
200
|
```bash
|
|
174
201
|
zstd -d -c "$DSH_HOME"/sessions/<workspace>/<session>/session.jsonl.zstd \
|
|
175
|
-
| grep
|
|
202
|
+
| grep '"type":"assistant/message"' \
|
|
203
|
+
| grep -o '"model":"deepseek-[a-z-]*"' | sort | uniq -c
|
|
176
204
|
```
|
|
177
205
|
|
|
178
|
-
|
|
206
|
+
Two details matter in that command: filtering to `assistant/message` counts only real model responses
|
|
207
|
+
(the raw log also records `request/header`, session-title, and web-search calls, which would inflate
|
|
208
|
+
the numbers), and the `[a-z-]*` pattern keeps hyphenated model names
|
|
209
|
+
(`deepseek-flash`, legacy `deepseek-v4-flash-vision-exp`) intact — a plain
|
|
210
|
+
`[a-z]*` silently truncates them.
|
|
211
|
+
|
|
212
|
+
Since v0.7.0 both roles log `deepseek-flash` (unified until V4.1-Pro lands and
|
|
213
|
+
the planner route points at it). For reference, the v4-era split re-verified
|
|
214
|
+
against production logs: a root session with delegations showed 170 pro / 182
|
|
215
|
+
flash responses, and every child session (`delegationDepth >= 1`) showed flash
|
|
216
|
+
only; with vision routing enabled, an image-heavy session logged 508
|
|
217
|
+
`deepseek-v4-flash-vision-exp` responses.
|
|
218
|
+
|
|
219
|
+
One operational note: the routing rewrite is loaded at harness boot. After updating the plugin (e.g.
|
|
220
|
+
0.6.3 → 0.7.0), restart the profile — a session that keeps running across the update can keep behaving
|
|
221
|
+
per the old code until the process reloads.
|
|
179
222
|
|
|
180
223
|
## Development
|
|
181
224
|
|
|
@@ -190,6 +233,19 @@ npm run build
|
|
|
190
233
|
|
|
191
234
|
The `prepare` script builds `lib/` automatically, which is what makes the git install work without shipping build artifacts in the repo. The `dsh.bundle` field in `package.json` is what tells `dsh plugin` how to compose the plugin into a profile.
|
|
192
235
|
|
|
236
|
+
## Releasing
|
|
237
|
+
|
|
238
|
+
Pushing a tag alone does not make a release. Every version needs all four:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
npm run typecheck && npm test && npm run build # green first
|
|
242
|
+
git tag vX.Y.Z && git push origin main && git push origin vX.Y.Z
|
|
243
|
+
gh release create vX.Y.Z --title "vX.Y.Z" --notes "<CHANGELOG entry>"
|
|
244
|
+
npm publish --access public # needs login + 2FA (--otp) or a publish token
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Keep `vX.Y.Z` flagged as Latest (`gh release edit vX.Y.Z --latest`) when backfilling older ones. Note: `scripts/publish.sh` is a one-shot bootstrap for a brand-new repo, not the per-release path.
|
|
248
|
+
|
|
193
249
|
## License
|
|
194
250
|
|
|
195
251
|
MIT
|
package/cordis.patch.yml
CHANGED
|
@@ -9,16 +9,17 @@
|
|
|
9
9
|
- id: model-router
|
|
10
10
|
name: 'dsh-model-router'
|
|
11
11
|
config:
|
|
12
|
+
# Both roles default to deepseek-flash (V4.1 Flash, native multimodal).
|
|
12
13
|
# The session's root agent: planning, design, review, synthesis.
|
|
13
14
|
planner:
|
|
14
15
|
provider: deepseek-official
|
|
15
|
-
model: deepseek-
|
|
16
|
+
model: deepseek-flash
|
|
16
17
|
# reasoningEffort: high # optional: off | low | high | max (omit to inherit)
|
|
17
18
|
# maxTokens: 8192 # optional output-token cap (omit to inherit)
|
|
18
19
|
# Every delegated subagent: code writing and execution.
|
|
19
20
|
executor:
|
|
20
21
|
provider: deepseek-official
|
|
21
|
-
model: deepseek-
|
|
22
|
+
model: deepseek-flash
|
|
22
23
|
# reasoningEffort: high
|
|
23
24
|
# maxTokens: 16384
|
|
24
25
|
# escalateOnError: true # v1: after a failed step, escalate effort
|
|
@@ -29,31 +30,47 @@
|
|
|
29
30
|
# falls back to the executor route to reserve pro for planning.
|
|
30
31
|
mode: strict
|
|
31
32
|
# enabled: false # off switch — GUI: Settings → Plugins → model-router (live, no restart); patch row needs a reboot
|
|
32
|
-
# vision.enabled: true #
|
|
33
|
+
# vision.enabled: true # native on deepseek-flash; only enable to pin a distinct vision route
|
|
33
34
|
# GUI: Settings → Plugins → "Model router" card (v0.5.0+, loopback browser only)
|
|
34
35
|
# Publish the always-on routing convention prompt section.
|
|
35
36
|
promptSection: true
|
|
36
37
|
# Register the `pro-flash-routing` skill in the session catalog.
|
|
37
38
|
skill: true
|
|
38
|
-
# Vision support:
|
|
39
|
-
#
|
|
39
|
+
# Vision support: deepseek-flash (V4.1 Flash) is natively multimodal, so the
|
|
40
|
+
# catalog lists it with image input. Legacy v4 names are kept as compat
|
|
41
|
+
# aliases (retired server-side, routed to V4.1 Flash): they let existing
|
|
42
|
+
# configs keep working through the transition. Max output is 384K per the
|
|
43
|
+
# current Models & Pricing page (was 256K on the v4 preview docs).
|
|
40
44
|
# Direct id-targeted entries (same shape as a profile cordis.patch.yml): they
|
|
41
|
-
#
|
|
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).
|
|
42
52
|
- id: llm-deepseek
|
|
43
53
|
config:
|
|
44
54
|
models:
|
|
55
|
+
- id: deepseek-flash
|
|
56
|
+
name: DeepSeek-V4.1-Flash
|
|
57
|
+
contextWindow: 1000000
|
|
58
|
+
maxTokens: 384000
|
|
59
|
+
inputModalities: [text, image]
|
|
45
60
|
- id: deepseek-v4-flash
|
|
46
61
|
name: DeepSeek-V4-Flash
|
|
47
62
|
contextWindow: 1000000
|
|
48
|
-
maxTokens:
|
|
63
|
+
maxTokens: 384000
|
|
64
|
+
inputModalities: [text, image]
|
|
49
65
|
- id: deepseek-v4-pro
|
|
50
66
|
name: DeepSeek-V4-Pro
|
|
51
67
|
contextWindow: 1000000
|
|
52
|
-
maxTokens:
|
|
68
|
+
maxTokens: 384000
|
|
69
|
+
inputModalities: [text, image]
|
|
53
70
|
- id: deepseek-v4-flash-vision-exp
|
|
54
71
|
name: DeepSeek-V4-Flash-Vision-Exp
|
|
55
72
|
contextWindow: 1000000
|
|
56
|
-
maxTokens:
|
|
73
|
+
maxTokens: 384000
|
|
57
74
|
inputModalities: [text, image]
|
|
58
75
|
- id: attachment-local
|
|
59
76
|
config:
|
package/lib/client.js
CHANGED
|
@@ -36,7 +36,7 @@ var inject = ["slots", "locale", "settingsScope"];
|
|
|
36
36
|
var en = {
|
|
37
37
|
nav: "Model router",
|
|
38
38
|
title: "Model router",
|
|
39
|
-
description: "Role-based model routing:
|
|
39
|
+
description: "Role-based model routing: both roles default to deepseek-flash (V4.1 Flash, native multimodal).",
|
|
40
40
|
enabled: "Enabled",
|
|
41
41
|
enabledHint: "Off stops rewriting requests and unregisters the prompt section and skill; on restores them.",
|
|
42
42
|
on: "On",
|
|
@@ -50,7 +50,7 @@ var en = {
|
|
|
50
50
|
modeStrict: "strict \u2014 the root agent is always the planner",
|
|
51
51
|
modePlan: "plan \u2014 the root agent is pro only while plan mode is active",
|
|
52
52
|
visionTitle: "Vision routing",
|
|
53
|
-
visionDesc: "Opt-in (default off): when enabled, any request containing an image routes to the vision model, from every role; other requests keep the
|
|
53
|
+
visionDesc: "Opt-in (default off, native on deepseek-flash): when enabled, any request containing an image routes to the vision model, from every role; other requests keep the role routing.",
|
|
54
54
|
visionSwitch: "Vision",
|
|
55
55
|
visionReset: "Reset vision to default",
|
|
56
56
|
visionRoute: "Vision model"
|
|
@@ -58,7 +58,7 @@ var en = {
|
|
|
58
58
|
var zh = {
|
|
59
59
|
nav: "\u6A21\u578B\u8DEF\u7531",
|
|
60
60
|
title: "\u6A21\u578B\u8DEF\u7531",
|
|
61
|
-
description: "\u6309\u89D2\u8272\u5206\u914D\u6A21\u578B\uFF1A\
|
|
61
|
+
description: "\u6309\u89D2\u8272\u5206\u914D\u6A21\u578B\uFF1A\u4E24\u4E2A\u89D2\u8272\u9ED8\u8BA4\u5747\u4E3A deepseek-flash\uFF08V4.1 Flash\uFF0C\u539F\u751F\u591A\u6A21\u6001\uFF09\u3002",
|
|
62
62
|
enabled: "\u542F\u7528",
|
|
63
63
|
enabledHint: "\u5173\u95ED\u540E\u505C\u6B62\u6539\u5199\u8BF7\u6C42\uFF0C\u5E76\u6CE8\u9500\u63D0\u793A\u533A\u5757\u4E0E\u6280\u80FD\uFF1B\u91CD\u65B0\u6253\u5F00\u5373\u6062\u590D\u3002",
|
|
64
64
|
on: "\u5F00",
|
|
@@ -72,7 +72,7 @@ var zh = {
|
|
|
72
72
|
modeStrict: "strict \u2014 \u6839 agent \u59CB\u7EC8\u4E3A\u89C4\u5212\u8005",
|
|
73
73
|
modePlan: "plan \u2014 \u6839 agent \u4EC5\u5728\u8BA1\u5212\u6A21\u5F0F\u6FC0\u6D3B\u65F6\u4F7F\u7528 pro",
|
|
74
74
|
visionTitle: "\u89C6\u89C9\u8DEF\u7531",
|
|
75
|
-
visionDesc: "\u53EF\u9009\uFF08\u9ED8\u8BA4\u5173\u95ED\uFF09\uFF1A\u542F\u7528\u540E\uFF0C\u4EFB\u4F55\u5305\u542B\u56FE\u7247\u7684\u8BF7\u6C42\u90FD\u4F1A\u8DEF\u7531\u5230\u89C6\u89C9\u6A21\u578B\uFF0C\u9002\u7528\u4E8E\u6240\u6709\u89D2\u8272\uFF1B\u5176\u4ED6\u8BF7\u6C42\u4FDD\u6301
|
|
75
|
+
visionDesc: "\u53EF\u9009\uFF08\u9ED8\u8BA4\u5173\u95ED\uFF0Cdeepseek-flash \u539F\u751F\u652F\u6301\uFF09\uFF1A\u542F\u7528\u540E\uFF0C\u4EFB\u4F55\u5305\u542B\u56FE\u7247\u7684\u8BF7\u6C42\u90FD\u4F1A\u8DEF\u7531\u5230\u89C6\u89C9\u6A21\u578B\uFF0C\u9002\u7528\u4E8E\u6240\u6709\u89D2\u8272\uFF1B\u5176\u4ED6\u8BF7\u6C42\u4FDD\u6301\u89D2\u8272\u8DEF\u7531\u3002",
|
|
76
76
|
visionSwitch: "\u89C6\u89C9",
|
|
77
77
|
visionReset: "\u6062\u590D\u89C6\u89C9\u9ED8\u8BA4\u503C",
|
|
78
78
|
visionRoute: "\u89C6\u89C9\u6A21\u578B"
|
package/lib/index.d.ts
CHANGED
|
@@ -177,8 +177,11 @@ declare function shouldUseVision(events: readonly unknown[] | undefined, vision:
|
|
|
177
177
|
/** Plugin row id; the bundle patch inserts it under this id. */
|
|
178
178
|
declare const name = "model-router";
|
|
179
179
|
/**
|
|
180
|
-
* Opt-in vision route. Defaults
|
|
181
|
-
* `deepseek-official`; `reasoningEffort` and `maxTokens`
|
|
180
|
+
* Opt-in vision route. Defaults to `deepseek-flash` (V4.1 Flash, native
|
|
181
|
+
* multimodal) from `deepseek-official`; `reasoningEffort` and `maxTokens`
|
|
182
|
+
* are optional pins. Native multimodal makes the vision branch a no-op when
|
|
183
|
+
* the role routes already point at the same model — kept for explicitness
|
|
184
|
+
* and for a future V4.1-Pro split.
|
|
182
185
|
*/
|
|
183
186
|
declare const VisionRouteSchema: z<Schemastery.ObjectS<{
|
|
184
187
|
enabled: z<boolean, boolean>;
|
|
@@ -304,7 +307,14 @@ declare const SETTINGS_NS: _deepseek_ai_dsh_settings.SettingsNamespace;
|
|
|
304
307
|
declare const SKILL_NAME = "pro-flash-routing";
|
|
305
308
|
declare const SKILL_DESCRIPTION = "Route planning and code execution across models: plan on the pro planner agent, delegate implementation to flash executor subagents.";
|
|
306
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.";
|
|
307
|
-
|
|
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;
|
|
308
318
|
/** The plugin row id the bundle patch must insert. */
|
|
309
319
|
declare const ROW_ID = "model-router";
|
|
310
320
|
/** Minimal structural view of the live agent object the router reads. */
|
|
@@ -373,4 +383,4 @@ declare class ModelRouter extends Service {
|
|
|
373
383
|
private render;
|
|
374
384
|
}
|
|
375
385
|
|
|
376
|
-
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
|
@@ -91,18 +91,18 @@ var ModelRouteSchema = z.object({
|
|
|
91
91
|
var VisionRouteSchema = z.object({
|
|
92
92
|
enabled: z.boolean().default(false),
|
|
93
93
|
provider: z.string().min(1).default("deepseek-official"),
|
|
94
|
-
model: z.string().min(1).default("deepseek-
|
|
94
|
+
model: z.string().min(1).default("deepseek-flash"),
|
|
95
95
|
reasoningEffort: z.union(["off", "low", "high", "max"]),
|
|
96
96
|
maxTokens: z.number().min(1)
|
|
97
97
|
});
|
|
98
98
|
var Config = z.object({
|
|
99
99
|
planner: ModelRouteSchema.default({
|
|
100
100
|
provider: "deepseek-official",
|
|
101
|
-
model: "deepseek-
|
|
101
|
+
model: "deepseek-flash"
|
|
102
102
|
}),
|
|
103
103
|
executor: ModelRouteSchema.default({
|
|
104
104
|
provider: "deepseek-official",
|
|
105
|
-
model: "deepseek-
|
|
105
|
+
model: "deepseek-flash"
|
|
106
106
|
}),
|
|
107
107
|
mode: z.union(["strict", "plan"]).default("strict"),
|
|
108
108
|
enabled: z.boolean().default(true),
|
|
@@ -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
|
|
131
|
+
var SKILL_TEMPLATE = `# Pro planner / Flash executor routing
|
|
132
132
|
|
|
133
|
-
This session routes models by role:
|
|
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 \`
|
|
136
|
-
- **Executors (every subagent)** \u2014 \`
|
|
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 \`
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
219
|
-
|
|
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:
|
|
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":["/**\n * dsh-model-router: role-based model routing for the DeepSeek Harness.\n *\n * The planner (the session's root agent) runs on `deepseek-v4-pro`; delegated\n * executor subagents run on `deepseek-v4-flash`. Enforcement is a per-agent\n * `agent/request` rewrite registered when the agent is created, so it applies\n * in every mode (web / headless / tui) and every agent preset, including\n * 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 are OFF on `deepseek-v4-flash-vision-exp` from\n * `deepseek-official`; `reasoningEffort` and `maxTokens` are optional pins.\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-v4-flash-vision-exp\"),\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-v4-pro\",\n } as never),\n executor: ModelRouteSchema.default({\n provider: \"deepseek-official\",\n model: \"deepseek-v4-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_CONTENT = `# Pro planner / Flash executor routing\n\nThis session routes models by role:\n\n- **Planner (this agent)** — \\`deepseek-v4-pro\\`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.\n- **Executors (every subagent)** — \\`deepseek-v4-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-v4-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 — 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- Executor output was produced by \\`deepseek-v4-flash\\`; planner output by \\`deepseek-v4-pro\\`. 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/** 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.\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 defaults to deepseek-v4-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 this.harness.on(\"agent/disposed\", (disposed) => {\n if (disposed === agent) dispose();\n });\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: SKILL_CONTENT,\n source: \"runtime\",\n });\n }\n }\n}\n\nexport {\n Config,\n ModelRouter,\n ModelRouter as default,\n SETTINGS_NS,\n VisionRouteSchema,\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"]}
|
|
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
|
-
"description": "DeepSeek Harness plugin: role-based model routing —
|
|
4
|
-
"version": "0.
|
|
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.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -6,26 +6,30 @@ 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:
|
|
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
|
-
- **Planner (
|
|
12
|
-
- **Executors (every subagent)** — `deepseek-
|
|
11
|
+
- **Planner (this agent)** — `deepseek-flash`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.
|
|
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.
|
|
13
13
|
|
|
14
14
|
## Working rhythm
|
|
15
15
|
|
|
16
16
|
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.
|
|
17
|
-
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-
|
|
17
|
+
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.
|
|
18
18
|
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.
|
|
19
19
|
4. **Report here.** Summaries, plans, and answers to the user come from this agent.
|
|
20
20
|
|
|
21
|
+
## Keep this agent's context lean
|
|
22
|
+
|
|
23
|
+
Input 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.
|
|
24
|
+
|
|
21
25
|
## Delegation guidelines
|
|
22
26
|
|
|
23
27
|
- Start independent delegations together in one assistant message and continue useful work while they run (background mode by default).
|
|
24
|
-
- 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.
|
|
25
29
|
- Do not delegate design: subagents execute decisions already made.
|
|
26
30
|
- If a subagent's task grows into design work, pull it back to this agent and re-delegate the narrowed execution.
|
|
27
31
|
|
|
28
32
|
## Verification
|
|
29
33
|
|
|
30
|
-
-
|
|
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.
|
|
31
35
|
- If routing ever looks wrong, the `model-router` plugin row in the profile composition is the single place that owns it.
|