dsh-taskforce
English | 简体中文
Multi-agent orchestration plugin for DeepSeek Harness (dsh).
Splits long tasks across multiple purpose-built agents so no single agent's context
accumulates the whole task: a planner decomposes work into a task graph, each worker
runs in a fresh one-shot context holding only what its task needs, and workers
return structured reports instead of transcripts.
Core loop: plan → dispatch (parallel) → structured reports → review → pass | redo.
Status: full loop + cross-restart recovery + Web panel + adoption prompt —
plan → dispatch → structured reports → review (pass/fail) → reroute → re-dispatch
→ finalize all work; on top of that the task graph survives a restart (the host's
own log is replayed into it) and the conversation composer shows an orchestration
panel — both verified live. A system-prompt section exists to get the pipeline
chosen for large requests that never name it: it was verified that the section
reaches the model, positioned after the workflow and subagent sections, but the
behavioural half — whether the agent actually calls orchestrate_plan when nobody
names it — is not verified yet (the run that would settle it was blocked on an
invalid API key). State is keyed per conversation; worker tool access is
whitelist-constrained; worker profiles are configurable. 16 offline test groups
pass (CI runs them on every push/PR). See ../DESIGN.zh.md.
Naming: the repository and the local folder keep the historical name
dsh-orchestra; the GitHub repo and the package are dsh-taskforce. The tool
names orchestrate_*, the service name orchestra and the projection key
orchestraGraph are frozen on purpose — they appear in the host's own
tool-call log, so renaming them would break recovery for sessions recorded
before the change. Do not "tidy" them up.
Install
From a local checkout (run pnpm install inside this directory after cloning —
the bundle has one npm dependency; see Compatibility for the
dsh versions this was verified against):
git clone https://github.com/LiSheng5/dsh-taskforce.git
pnpm install
dsh plugin --profile web add <path-to>/dsh-taskforce
<path-to> must be an absolute path. dsh plugin add also accepts git specs
directly (github:LiSheng5/dsh-taskforce) — pnpm then resolves the dependency at
install time.
Using the desktop app rather than the CLI? It ships its own launcher
(resources\runtime\cli\bin\dsh.cmd on Windows) and manages the desktop
profile exclusively: booting or dumping that profile from the CLI is refused,
installing into it is not.
"<DeepSeek Harness>\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add <path-to>\dsh-taskforce
The app's own sidebar Plugins page installs the same way — a registry name,
a git address, a tarball or an absolute local path — and the bundle is enabled by
default. Restart the app to load it.
Compatibility
| dsh version | status |
|---|
0.1.7-rc.2 | developed against; full loop, restart recovery and Web panel verified |
0.2.0-rc.2 | the desktop app's runtime; install, mount and prompt assembly verified headless (the section renders where it should, tool text reaches the model) — the full orchestration loop and the Web panel have not been re-run on it |
dsh is a developer preview and officially announces breaking changes. After
upgrading dsh, run the offline regression suite once — it needs no dsh, no API
key, no network:
node test/local-loop.mjs
The same suite runs in CI on every push and pull request
(.github/workflows/offline-regression.yml, Node 20 / 22 / 24), so "run it once
after upgrading dsh" and "CI blocks regressions" are the same command.
Upgrading from a version that predates the restart-recovery fix? Conversations
it touched need a one-time repair — see
Upgrading from a version that predates the fix.
A composition without @deepseek-ai/dsh-system-prompt loses exactly one thing:
the prompt section that makes the pipeline get chosen on its own. Every tool
still works when it is named explicitly.
Safety
This plugin spawns real sub-agents with real powers: file reads/writes and
shell access inside the session workspace, bounded by each worker profile's
tool restrictions. Read dsh's own SAFETY notes
before running orchestrations on workspaces you care about, keep the per-task
timeout and attempt caps enabled, and treat worker reports as claims — that is
what the reviewer is for.
Workers also lose the host's agent-plane control tools by default
(send_message, interrupt_agent, list_agents, subagent, subagent_fork):
send_message would post a worker's text straight into the orchestrator's
session, which is exactly what this plugin exists to prevent, and the others
let a worker disturb its parent or start more agents. This list was calibrated
by measurement (a real researcher worker reported exactly these tools); set
denyControlPlane: false to keep them.
Layout
index.js plugin entry: name / inject / apply
client.js browser half: the panel above the composer (lazy __ModuleLoader__ factory)
cordis.patch.yml bundle patch inserted into the profile composition
src/protocol.js Task / Report / Verdict JSON schemas (host JSON-Schema subset)
src/graph.js DAG validation, readiness, status transitions (CAS)
src/service.js orchestration state (ctx.provide('orchestra'))
src/events.js restart recovery: folds the host's tool/call + tool/result back into a graph
src/router.js context router: the task book a worker receives
src/profiles.js agent_type → persona / reasoning effort
src/prompts.js every model-facing string: the adoption section + planner guidance
src/json-schema.js argument validator (subset semantics)
src/retry.js auto-retry for malformed tool-JSON stream failures
src/tools/ plan / status / dispatch / review / reroute / finalize (+ ping)
scripts/ maintenance: repair session logs written by older versions
test/local-loop.mjs offline regression suite (fake host, no network)
Restart recovery
The graph lives in memory, keyed per conversation, so a restart loses it. What
brings it back is the session log the host already keeps:
- dsh logs every tool call as
tool/call (full arguments) and tool/result
(rendered output), so orchestration state is durable in the host's own
vocabulary — the plugin writes no session events of its own.
- The plugin registers an
orchestraGraph session projection (under
ctx.inject([...]), so a composition without the projection registry is
unaffected) that purely folds those events into the task graph, and loads it
the first time a fresh process touches the conversation.
- A call's effect lands when its result settles. The host writes
tool/call before the tool body runs, so applying eagerly would apply the
call being executed right now a second time. A dispatch that was cut short by
a restart is therefore never half-applied either: its task stays where the
orchestrator last saw it — ready to run again.
- Without the projection service the plugin simply runs in-memory, as before.
- Conversations recorded before the
✓ <id> — <detail> render contract
(early builds) recover their plan, attempts, verdicts and finalized flag but
not their dispatch outcomes: the old digest carries no success marker, and
inventing one would be worse than leaving the task where it stopped. Such a
result still consumes its pending call, so it is never misreported as
interrupted by a restart.
orchestrate_status answers with the recovered graph after a restart.
Upgrading from a version that predates the fix
Early versions appended their own event types, which made those conversations
unloadable after a dsh restart (contains event type "orchestra/graph" … refusing to interpret the log). The repair script adds the harness's own
compatibility marker (ignorable) so they load again, backing up first:
node scripts/repair-session-logs.mjs --dry-run # report first
node scripts/repair-session-logs.mjs # repair (self-checked, backup kept)
node scripts/repair-session-logs.mjs --restore # undo
Web UI panel
Conversations that have an orchestration show a panel above the composer (a
conversation that never planned anything renders nothing at all):
▾ ▣ orchestra #1 · 1/2 · review 1 PASS
─────────────────────────────────────
✓ t1 researcher completed att 1
read package.json — name dsh-taskforce, version 0.1.0 …
· t2 coder pending → ready
- It reads the wire view of the same
orchestraGraph projection
(client.js calls useProjection('orchestraGraph')), so it shows exactly
what orchestrate_status reports, updates itself as events arrive, and after
a restart shows the recovered graph.
- Only ids, agent types, statuses, attempts, readiness, report summaries,
diagnostics and issue counts cross to the browser; worker personas stay
host-side.
- The browser half is a build-free plain script (
client.js, declared by
dsh.client.platform=web with immediately). It inherits the host theme via
currentColor and pulls in no stylesheet or third-party dependency.
- Uninstalling the plugin removes the panel with its loader row; the rest of the
conversation UI is untouched.
Tools
The plugin also registers a system-prompt section (tool:orchestrate_plan,
order 2810) so the host agent reaches for the pipeline on a large request it was
never told to name — measured behaviour before this was todo_write plus doing
the work itself. It follows the host's own convention (workflow owns order 2600,
subagent 2800), is registered under ctx.inject(['systemPrompt'], …) so a
composition without that service merely loses the advocacy, and renders as an
empty string for any agent that cannot see orchestrate_plan — which is every
worker, because a spawned child's persona only shadows the deployment persona
and global sections do reach it. All model-facing text lives in src/prompts.js.
orchestrate_plan — submit the task graph (mission_brief + tasks); validates
fail-loud and returns the ready set. Its description is where the
decomposition rules live: workstreams are how you find the cut but must be
restated as file ownership, task counts scale with the request, the
parallel-writer hazard, two worked skeletons, and the anti-patterns.
orchestrate_status — compact one-row-per-task view plus dispatchable ids,
and the diagnostic for tasks that failed without producing a report.
orchestrate_dispatch — spawn ready tasks as isolated one-shot workers,
block until they settle, return one report digest per task (≤4 concurrent).
orchestrate_review — run the reviewer over the reports; returns a verdict.
orchestrate_reroute — send review issues back to workers with guidance.
orchestrate_finalize — record the delivery summary and lock the graph.
orchestra_ping — liveness probe.
Configuration (worker personas, models, limits)
The orchestrator writes per-task personas itself: each task in a plan may
carry a persona field — a role definition A tailors to that task ("你是数据库
迁移专家,只改 SQL 文件"). It becomes the worker's primary system prompt, layered
above the profile's discipline (non-negotiable constraints that A cannot
override: researchers don't write, reviewers don't fix, …). model and
reasoning_effort are likewise per-task fields A sets at plan time.
Two more ways to tune what each worker role is like — both take effect after
restarting the dsh process:
-
Edit src/profiles.js directly. Each role has role (identity line),
discipline (permanent constraints), effort (off|low|high|max), and
toolDenyPatterns — regexes matched (case-insensitively) against the host's
global tool names to hard-restrict that role's tools.
-
Config overrides (no code edit). Add a config: block to the plugin
entry in the profile's cordis.patch.yml (e.g.
~/.dsh/profiles/web/cordis.patch.yml):
- id: dsh-taskforce
name: dsh-taskforce
config:
taskTimeoutMs: 1200000
maxConcurrentWorkers: 6
profiles:
researcher:
role: |
你是资深代码考古学家……
discipline: |
只读,不修改任何文件。
effort: off
toolDenyPatterns: ["write", "bash"]
coder:
model: deepseek-chat
Config role/discipline/model/effort replace the builtin values;
toolDenyPatterns are concatenated with the builtin ones. Engine knobs:
provider (default spawn), maxConcurrentWorkers (4), maxAttempts (3),
maxReviewRounds (3), taskTimeoutMs (600000).
Plan-time worker programming
The orchestrator is not limited to the four builtin worker types. In a plan it
may declare new worker types — orchestrate_plan accepts a workers field
where A programs one agent spec per type:
"workers": [
{
"type": "ui-builder",
"role": "你是界面构建师,专注像素级还原。", // identity (required for new types)
"discipline": "不得改动 src/api/ 下的文件。", // optional; generic floor otherwise
"effort": "high",
"model": "deepseek-flash", // optional defaults
"toolDenyPatterns": ["bash"]
}
]
- Tasks then reference
"agent_type": "ui-builder". Shadowing a builtin type
("type": "coder", no role) replaces only the parts it names.
- The layering stays: a task-level
persona still layers above the declared
role, and the declared discipline is always appended — A cannot write it away.
- The review loop is not reprogrammable from plans: the reviewer always runs on
the operator-configured profile.
- Engine-level guarantees are unaffected: orchestration tools are always denied
to workers, delegation depth prevents grandchild spawning, tasks are one-shot
isolated sessions.
Design notes
- Hand-built tool definitions, near-zero imports: tool definitions match
what
defineTool compiles to; the only dependency is the npm-published
@deepseek-ai/schemastery for the Config schema, so pnpm-local-path
installs stay trivial.
- Context isolation: workers are spawned (never forked) one-shot subagents;
upstream information travels only as structured reports, never as history.
- Prompt text is layered and frozen: a system-prompt section says WHEN to
enter the pipeline, the
orchestrate_plan description says HOW to decompose,
and the parameter descriptions carry per-field rules — all of it in
src/prompts.js as module constants, because sections and tool schemas are
re-sent on every request and byte-stable text is what keeps the provider's
prefix cache reusable. Both texts reach the orchestrator only: a worker gets
an empty section and never sees the planner's schema, so TASK_ITEM_SCHEMA
wording costs nothing per worker turn. The offline suite enforces a character
budget on each, so the per-request cost cannot silently grow.
- Decompose by deliverable, never by role: the planner guidance was settled
against a survey of four other agent systems — Claude Code subagents, dsh's own
workflow/subagent sections, Anthropic's multi-agent research system and ZCode.
None of them splits work by role (UI / art / gameplay, or frontend / backend);
each asks for a self-contained deliverable or a subsystem, and Anthropic scales
the count to the request instead of fixing a range. The reason is mechanical: a
role owns no files, so role-shaped tasks either collide in the one shared
workspace or hand back prose. Workstreams are therefore taught as the way to
find the cut, which must then be restated as file ownership — and the planner
is told not to promise artifacts no worker can produce, since this host has no
image, audio or video generation tool.
- Per-conversation state: the orchestration graph is keyed by the calling
agent's session id — simultaneous conversations never share state.
- Layered worker restriction: orchestration tools are always denied to
workers; profile deny-patterns are resolved against the host's global tool
view at dispatch time (unknown names are never passed, so a bad pattern
degrades instead of throwing). Grandchild delegation is impossible by
delegation depth. Control-plane tools can only be denied by PATTERN — they
are not ours and may not exist in every composition — so
resolveToolFilter
always enumerates first and matches second.
- Only the host's vocabulary reaches the log: orchestration state rides the
host's own tool-call records (
tool/call + tool/result); the plugin adds
host-log audit lines (, …) and no session event type the
harness would refuse to read back.
License
MIT — see LICENSE.