dsh-agent-dispatch · Agent Dispatch
One settings pane: decide whether the main agent gets help, which helpers play — and it actually works. Policy is injected as a
systemPromptsection; save and it takes effect.
Pairs with Our-Free-model. This panel ships no provider of its own: it dispatches into the free lane that plugin exposes, and every factory-default helper is one of its models. Details in Pairs with the Our-Free-model plugin.
- Visible: one settings section showing status, channels, and helper roster (with measured TTFT).
- Actionable: master switch / four-mode selector / channel checkboxes / helper checkboxes / concurrency limit / extra notes — change and save.
- Actually effective: policy text is a function, re-evaluated every step; when disabled it injects an explicit "no dispatch this turn" instruction.
- Speaks first: the default stance scans every turn for a possible split instead of waiting to be named, and never hands the "should I dispatch helpers?" question back.
- Self-checking: before each run the agent answers one line — can this split into independent blocks? — and a helper that stops producing gets its work reassigned, then handed to a fallback.
What it solves · Why it saves tokens
The main agent resends the full context (system prompt + history) every step, billed by cacheMiss — the biggest line on the token bill. Independent helpers each have their own context, and the free lane has zero cost for input and output. So the key to saving money isn't "talk less", it's moving self-contained work out. This plugin manages "what to move, to whom, how many".
Division of labor (hard-coded in injected text): simple, mechanical, self-contained work that does not depend on main-session context (bulk retrieval, item-by-item audit, copy-edit-reorder, list making, translation, formatting, first drafts) goes to free-lane helpers; the hardest, most judgment-heavy, consequence-sensitive parts (architecture and key design, cross-module reasoning, correctness and safety judgments, dispute resolution, final delivery) stay with the main agent.
Pairs with the Our-Free-model plugin
This panel ships no model provider. It picks a lane and a roster, and the lane it assumes is Our-Free-model (dsh-our-free-model, measured at v2.7.2). Install that plugin and the free lane stops being a slogan: the models exist, cost nothing per token, and show up in this panel's roster with a measured TTFT.
What is already wired together:
- The roster peer defaults to
our-free-model.peeris'our-free-model'out of the box, and every factory-defaulthelpersentry carries that provider prefix —our-free-model:nemotron-3-ultra-free,our-free-model:ling-3.0-flash-fin-free, and so on. Without the plugin those keys point at a provider the machine does not have; with it they resolve to live models. - The built-in reference roster is that plugin's model list — an 11-row
our-free-modelsnapshot tagged "built-in", so the panel is never empty on first open. - Nothing here is a hard dependency. No provider is required:
peeronly decides whose name the reference roster sits under, and an absent provider removes labels, not features. Any other provider works the same way — pointhelpersat it and the injected policy follows.
Install order: Our-Free-model first (so the free lane exists in /models), then this plugin, then restart dsh once so both bundles load. Reverse the order and the panel still opens; it just shows keys pointing at an absent provider until the lane is there.
Install
Dispatching to the free lane: install Our-Free-model before this plugin — see Pairs with the Our-Free-model plugin.
Plugin directory: your local clone of this repo (<repo-path>). Two ways:
A. Auto-edit profile manifest (recommended)
node scripts/install-into-profile.mjs # dry run, prints the two lines to change
node scripts/install-into-profile.mjs --apply # actually edit (auto-backup package.json)
node scripts/install-into-profile.mjs --revert # restore from backup
The script touches only one file: <DSH_HOME>/profiles/<profile>/package.json in two places — the link: dependency in dependencies, and the package name in dsh.profile.bundles. After editing, run pnpm install in that profile directory (to turn the link: into a junction in node_modules) and restart dsh (patchReload: "live" handles hot reload, but not a new bundle; restart ends the current GUI session).
Note:
pnpm installalignsnode_modulesto the versions declared inpackage.json— if a plugin was previously updated to a higher version by the plugin manager, this step will downgrade it to the declared value. To keep the newer version:pnpm add <pkg>@<version>then restart.
B. Manual
Equivalent to manually editing <DSH_HOME>/profiles/<profile>/package.json in two places:
- In
dependenciesadd:"dsh-agent-dispatch": "link:<repo-path>" - In the
dsh.profile.bundlesarray add the package name:"dsh-agent-dispatch"
Then pnpm install + restart dsh.
After install: Settings → Agent Dispatch.
Panel overview
Eight sections, top to bottom:
| Section | Content |
|---|---|
| Master switch & four-mode selector | On / Off; four modes: Off / Ask me / Proactive / Auto |
| Helper channels | workflow fan-out / single subagent / Agency experts / Agent Teams; unchecked channels go to the deny list |
| Helper roster | Three merged sources (host-registered / lane probe / manual + built-in reference), each row labeled with source; ★ marks the default primary |
| Dispatch scale | Max simultaneous helpers (1–8), step threshold for dispatch, whether short tasks are blocked from dispatch |
| Pre-flight estimate | Whether every run starts with a one-sentence estimate; the minimum independent block count that makes a team worth building; whether a passing estimate may build the Agent Teams team on the spot |
| Stall failover | How many steps without output count as stalled, how many reassignments per unit of work, and who finishes it afterwards (main agent, or a named checked helper) |
| Extra notes | Written verbatim into injected text |
| View injected text | Expand to see the actual policy sent to the agent, not a mockup |
Top also has first-run guide (four steps, shown once) and host adaptation check (explains why each feature is present or absent). On first run the sidebar's settings area also shows a compact onboarding card with its own layout: "Open settings" jumps straight here, "Not now" reads it once and done.
Proactive dispatch and the four modes
The injected text opens with a stance rather than waiting to be spoken to: scan every turn for a possible split before starting. When it does not dispatch, it says in one line why it is doing the work itself, and it never hands a bare "should I dispatch helpers?" question back.
The four modes differ only in who nods:
| Mode | Behaviour |
|---|---|
| Off | No dispatch at all this turn: no fan-out, no summons, no team |
| Ask me | The agent scans itself and states a concrete split (which blocks, how many helpers, who does what), then waits for one nod |
| Proactive (factory default) | Low-risk mechanical blocks go out immediately with a one-line notice; high-cost actions ask first |
| Auto | Dispatches whenever the rules match, without asking |
Low-risk mechanical blocks are read-only retrieval, multi-source research, per-item audits, copy-edit-reorder, translation, formatting and first drafts. High-cost actions are building an Agent Teams team, summoning an Agency expert, leaving the free lane, writing / deleting / committing / publishing, and opening more helpers at once than the concurrency limit — under Proactive these still ask first.
The injected text also carries a dispatch signal list: a large goal with several sub-parts under it, wording like "each / respectively / one by one / in bulk / compare N of them", several files or sources to read, repeated formatting and translation, and one block that can be cut into self-contained pieces. Judge blocks individually: one self-contained block with a clear acceptance check is enough to dispatch it; the remaining design and judgement stay with the main agent. Signals only point; they never require waiting for every sub-task to qualify.
The dispatch rules also name the anti-patterns: waiting for the user to speak first, handing the "should I dispatch helpers?" question back, splitting a job apart just to be parallel, dispatching the same block twice, and dispatching the judgement itself — none of those count as dispatching.
Pre-flight estimate and stall failover
Two behaviours added in config v3. Both are injected as policy text — the agent reads them every step — and both are switched in the panel.
Pre-flight estimate (plan). Before starting, judge each block: can it be delivered independently, written as a self-contained prompt, and checked with a clear acceptance rule? One qualifying block is enough to dispatch it; the main agent keeps the remaining design and judgement. Low-risk blocks go first through enabled workflow / subagent channels. Only when shared task-board state is genuinely needed, and at least plan.minBlocks blocks exist, should the agent consider an Agent Teams team; confirmation for building the team does not block other independent dispatches. In ask and proactive mode it still asks the user once before building the team (building a team counts as a high-cost action); in auto mode it does not; with autoTeam off, an explicit request is always required. The minSteps threshold is a pre-start estimate, not a count of steps already spent.
Stall failover (failover). A helper that produces nothing for failover.waitSteps steps counts as stalled: the agent first sends it one question (long research may simply be quiet), then reassigns that block to another model or another upstream first — swapping members while keeping the same model repairs nothing, because the observed stall roots were upstream overload and a deprecated model — up to failover.maxRetry times, then hands it to failover.fallback. With Agent Teams checked, the check-and-reassign steps use agent_teams_status / agent_teams_reassign_task; otherwise list_agents, and interrupt_agent + subagent when the subagent channel is on. Handing over carries the confirmed constraints and whatever has already been produced; a block that was already dispatched is never dispatched twice, and waiting is never an excuse to idle.
Switching either one off does not remove the text — it replaces it with an explicit "not this turn" instruction, so the agent never improvises its own estimate or failover.
Configuration keys
Persisted to $DSH_HOME/agent-dispatch/config.json, atomic write (tmp + rename). Panel re-reads on every open; hand-editing this file is equivalent to panel changes.
| Key | Type | Default | Purpose |
|---|---|---|---|
version |
number | 3 | Config structure version; v1 bare model keys get provider prefix auto-added on load, v2 files gain the plan / failover blocks |
enabled |
boolean | true |
Master switch; off = injects explicit "no dispatch" instruction |
mode |
'off' | 'ask' | 'proactive' | 'auto' |
'proactive' |
Dispatch mode |
peer |
string | 'our-free-model' |
Roster default provider name (only decides which provider the reference roster sits under; does not require this machine to actually have it) |
discover.llm |
boolean | true |
Whether to ask host LLM service to enumerate provider/model |
discover.siblings |
boolean | true |
Whether to scan sibling plugins' status files under $DSH_HOME |
channels.workflow |
boolean | true |
Allow workflow fan-out |
channels.subagent |
boolean | true |
Allow single subagent dispatch |
channels.experts |
boolean | false |
Allow Agency experts |
channels.teams |
boolean | false |
Allow Agent Teams |
helpers.{provider}:{model} |
object | see below | Key is provider:model, value { enabled: boolean, label?: string } |
primary |
string | 'our-free-model:nemotron-3-ultra-free' |
Default primary helper; must be an enabled model, otherwise falls back to first enabled |
maxHelpers |
number | 4 | Max simultaneous helpers (clamped to 1–8) |
minSteps |
number | 3 | Task must exceed this many steps to be worth dispatching (clamped to 1–20) |
longTaskOnly |
boolean | true |
When on, tasks answerable in one sentence are not dispatched |
plan.enabled |
boolean | true |
Whether every run starts with a one-sentence pre-flight estimate |
plan.minBlocks |
number | 2 | Independent blocks that make a team worth building (clamped to 2–8) |
plan.autoTeam |
boolean | true |
Only counts while channels.teams is on: a passing estimate may build the team without a fresh request; off means an explicit request is still required |
failover.enabled |
boolean | true |
Whether a stalled helper's work is reassigned instead of waited on |
failover.waitSteps |
number | 6 | Steps without output after which a helper counts as stalled (clamped to 1–50) |
failover.maxRetry |
number | 1 | Reassignments per unit of work before falling back (clamped to 0–3; 0 = never retry) |
failover.fallback |
string | 'main' |
Who finishes it: 'main' (the main agent takes over) or a provider:model key; anything else reads as main |
onboarding.seen |
boolean | false |
Set true after first-run guide viewed |
notes |
string | '' |
Extra notes, written verbatim into injected text (truncated to 2000 characters) |
Factory default helpers enables six verified-available our-free-model models: nemotron-3-ultra-free, nemotron-3.5-lightning-free, space-bunny-free, longcat-2.5-preview-free, ling-3.0-flash-fin-free, mimo-v2.6-flash-free (v1 config keys without provider prefix auto-prefixed with our-free-model:).
Channels and roster
Channels (CHANNEL_IDS order = panel order)
| id | Label | Description |
|---|---|---|
workflow |
workflow fan-out | Run multiple independent sub-tasks side by side in one script, each can specify its own provider/model |
subagent |
single subagent | Delegate one whole independent task to another context, only get the result back |
experts |
Agency experts | Summon expert personas by domain (requires enabling in settings) |
teams |
Agent Teams | Multi-member shared task board collaboration (needs an explicit request, unless the pre-flight estimate passes with plan.autoTeam on) |
Roster three sources
Every row has a source tag; panel displays as "lane probe / host-registered / manual / built-in":
- Host LLM service actual enumeration (source
llm, tag "host-registered") — the only source that can prove "this provider/model can actually be called right now". - Sibling plugin status files (source
peer, tag "lane probe") — scans$DSH_HOME/<any plugin dir>/catalog.json+availability.json, no hardcoded names, gets measured TTFT. - Panel manual entry (source
manual, tag "manual") — user can enter anyprovider:model, even other paid lanes. - Built-in reference roster (source
seed, tag "built-in") — 11-rowour-free-modelmodel snapshot, ensures panel is not empty on first open. Not a factual source, only shows "what it looks like".
Merge priority: llm enumeration → verified=true; sibling plugin availability → state / ttftMs; built-in reference → only fills appearance, does not override the above two judgments.
HTTP API
All routes under same-origin /api/agent-dispatch/*, co-located with host half in the same process. Only serves local browser; non-loopback returns 403.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/agent-dispatch/summary |
Returns current config, roster, preview policy, stats, host adaptation status |
| GET | /api/agent-dispatch/health |
Returns service connectivity status, config path, roster source counts |
| POST | /api/agent-dispatch/config |
Body { patch } merges config and persists, returns full summary |
| POST | /api/agent-dispatch/rescan |
Re-enumerates LLM and rebuilds roster |
| POST | /api/agent-dispatch/reset |
Restores factory default and persists |
Compatibility and graceful degradation
inject = [] + nested fiber opportunistic acquisition of webServer / systemPrompt / llm three services. Any one missing only loses the corresponding feature; plugin remains usable:
- No
webServer: panel API unavailable (browser half cannot read/write config), butsystemPromptsection still injects. - No
systemPrompt: policy not injected (panel changes don't take effect), but API and roster work normally. - No
llm: roster only has lane probe + built-in reference + manual; manual entry of oneprovider:modelstill works. - No services at all: panel still usable (pure in-memory config + built-in reference roster).
Write failure is not fatal: config updates memory first and takes effect immediately; panel shows "write failed, will revert on restart".
Uninstall
node scripts/install-into-profile.mjs --revert (restore backup), then pnpm install + restart dsh.
$DSH_HOME/agent-dispatch/ can stay or be deleted.
Development and self-test
node --check index.js # syntax check host half
node --check client.js # syntax check browser half
node scripts/check-i18n.mjs # zh/en dictionaries same keys, and all t() keys in code exist in dictionaries
node scripts/check-links.mjs # every relative Markdown link and image target exists in the repo
node scripts/selftest.mjs # pure function self-test (policy render / config convergence / roster build), 148 checks
node scripts/smoke-host.mjs # fake cordis ctx runs real apply(), includes 403, persist, instant effect, 44 checks
node scripts/smoke-client.mjs # stub React + real /summary data, renders Panel four times, 37 checks
selftest.mjs creates fake peer states in a temp directory, verifies roster priority, region-blocked and unavailable degradation, config convergence (0 concurrency, illegal peer, illegal key names all clamped to safe values). smoke-host.mjs uses fake webServer / systemPrompt services to run real apply(), confirms routes mounted, config persists, segment text is a function (so "save then next step effective" is a structural fact), covers non-loopback 403, bad JSON 500, disposer callable — plus the whole v3 path: the estimate and failover blocks appearing in the injected text, the teams ban lifting once the estimate passes, and both blocks surviving /reset. smoke-client.mjs uses mini hooks runtime as stub React, feeds Panel with actual /summary from host half, asserts render tree has roster keys, has actual injected policy text, guide card disappears after viewed, and re-renders with Agent Teams armed and the estimate / failover values tuned so both new blocks — including their disabled and locked states — are proven to render. node --check can't catch "opens blank" crashes, this layer catches those specifically.
npm run check (syntax), npm run i18n (dictionary consistency), npm run links (relative link and image check), npm run test (self-test), npm run smoke (host half), npm run smoke:client (browser half), npm run verify (all six) are configured in package.json scripts.
Related documents
| File | Purpose |
|---|---|
| README.zh-CN.md | Chinese version (Simplified Chinese mirror) |
| AGENTS.md | Working notes for AI coding agents: layout, contract, checks |
| INSTALL.md | Install, verify and uninstall steps |
| docs/GLOSSARY.md | Glossary: single terminology source for all docs and UI strings in this repo |
| CONTRIBUTING.md | Contributing guide: commit message format, branch flow, doc conventions |
| SECURITY.md | Security policy and how to report a vulnerability |
| CODE_OF_CONDUCT.md | Community code of conduct |
| CHANGELOG.md | Version change log |
| docs/market-entry.md | DSH plugin market entry and inclusion rules |
| docs/publish.md | Release process (including steps requiring manual authorization) |
License
MIT. Repository: github.com/Napstablooky233/dsh-agent-dispatch. Chinese version: README.zh-CN.md.
No comments yet. Be the first to write one.