dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH plugin: run several agents on different models at once, with a shared real-time progress board.
A DSH plugin that runs several agents on different models at the same time on one objective, and shows them exchanging progress on a single board.
DSH can already delegate to subagents, but you only ever see one of them. This plugin merges dispatch and watching into one surface:
deepseek-official/deepseek-flash,
zai-coding-cn/glm-4.7, … picked per row. This is the key difference from
Agent Teams, whose members carry a provider but no per-teammate model.hub_post, and can hand a line straight into a peer's inbox to wake it; you can
interject into any agent, broadcast to all, or interrupt one.codex-chatgpt/* does not run inside DSH: it is an external Codex process whose
tools are Codex's own (apply_patch, exec_command, MCP apps). It has no
hub_post/hub_read and none of DSH's file tools, and its write access belongs to
Codex's own sandbox (subagent-codex's permissionMode defaults to never →
read-only). In one launch, doing the same task: the deepseek child received 39 DSH
tools and its write landed, the codex child received neither. Send work that reads
files, reports to the board, or divides by file to an in-process route.It drives real DSH subagent sessions (with tools, workspace access and multi-turn continuation), not "several models each say a paragraph".
# straight from GitHub
pnpm dsh plugin --profile web add github:HuaimaoCy/dsh-agent-hub
# or from the released tarball
pnpm dsh plugin --profile web add https://github.com/HuaimaoCy/dsh-agent-hub/releases/latest/download/plugin.tgz
# or from a local checkout
pnpm dsh plugin --profile web add <absolute path to this directory>
The Host half requires a restart of dsh web (it is an in-process ESM
module; replacing an installed package does not hot-reload it). The browser half
hot-reloads on its own. restart-web.ps1 in this directory stops the current
listener and starts a fresh server.
Dispatching requires the parent conversation to have a live agent. A cold
conversation returns a 409 with an actionable message. That is deliberate: the
plugin does not resume a session on your behalf, because a resumed handle is
owned by the caller — unloading the plugin would then tear down the conversation
you were using.
| Tool | Purpose |
|---|---|
hub_post | Publish one line of progress to the shared board. With to: '<name>' it is also delivered into that teammate's inbox and wakes it. |
hub_read | Read the roster plus everyone's latest progress — call it before starting so you do not duplicate a peer. |
A board nobody opens is decoration, and a tool nobody calls is invisible. So the plugin does two things beyond the UI:
A standing policy in every agent's prompt (src/policy.js). It is written as
a policy rather than a slogan, because both failure modes are real: an agent that
has never been told what the board is for keeps solving everything
single-threaded, while an agent told to "use it a lot" parallelises a two-line
edit and pays a coordinator call plus N agent contexts for it. So it states
A hub_launch tool, because the policy is unactionable without it: before
this, an agent could post to a board and read one, but only a human pressing a
button could create one. hub_launch takes an objective (the coordinator splits
it) or an objective plus an explicit roster (the coordinator call is skipped),
and returns the launched team. Roster rows default to read-only — a model
that did not ask to write does not get a writing agent by accident.
The policy text rides on every request of every session in the deployment, so it
can be switched off with policy: false; the board then exists purely as a UI
surface.
Three levels, from least to most explicit:
| You want | How |
|---|---|
| A different model to review the work | Name provider / model on that row; the policy tells the agent which routes exist |
| To spread the work without picking row by row | Pass models: ["provider/model", …]; rows that name no model are assigned from it in turn |
| The coordinator to assign per role | Pass nothing: the coordinator is given the catalogue and assigns (judgement-heavy roles get a stronger route, mechanical ones a cheaper one, and an adversarial reviewer deliberately gets a different model) |
Three pieces make that work:
可用模型路由(provider/model,共 N 条):…,
read live from the catalogue, capped at 12). Without it an agent has to invent route
names — and an invented route is dropped by validation, so the entire team silently
lands on one model with nothing reporting a problem.model
must be copied exactly, and that one route for every role is the right answer when the
roles are alike: do not manufacture variety.hub_launch takes models: it fills rows of an explicit roster that name no model,
and it backstops the coordinator's rows when it named none. Malformed entries
(broken/, /nope) are dropped rather than failing the launch.The gap this closed: the coordinator's prompt had no model field at all and nothing told it which providers existed, so an auto-split team always ran on one model. Per-row routes worked, but only if you wrote the roster yourself.
A per-row route that no provider serves fails that row's launch with the reason (the other rows keep running) rather than being quietly replaced with another model.
The two overlap — a roster, a shared board, messages between members — so this is an integration, not two features standing side by side:
| Who owns it | |
|---|---|
| Durability: members, tasks, dependencies, write scopes | Agent Teams — written to the Lead's session log, survives a restart |
| Per-agent model, per-agent tool scope, live token view | the hub — the native spawn_teammate carries provider but no model |
Three things follow:
op=state reads ctx.agentTeams for its team field instead of mirroring it,
so a change on the native side — including tasks created by the native tools —
shows up here immediately.hub_launch is published as native
tasks (createTask → claim), and each agent's task is completed by
compare-and-set when it settles. The Team panel, team_task_list and this
board therefore read the same revisioned, dependency-aware task table — not a
free-text "plan" beside a real one.The native list_agents / send_message do not see hub-dispatched agents —
they are not team members. The reason is specific: SpawnTeammateRequest carries
provider but no model, and the team service has no API to adopt an existing
child, so going through the native spawn would cost every agent its model, which
is the point of the feature.
Two ways to close it, neither implemented:
agentOptions to SpawnTeammateRequest and thread it
into the internal startContinuable. The capability already exists — the spawn
provider advertises agentOptions/persona/toolFilter and the continuable path
does not check capabilities — the Team layer simply never passes it down.session.append('team/member', …) does register a hub
agent as a member, and hub children happen to satisfy the
"Lead's direct continuable child" requirement for delivery. But the projection
validates strictly, and one invalid record puts that session's Agent Teams
into a permanent failure where no further event applies. That is not a cost a
third-party plugin should impose on someone else's session, so it is not used.It does a share of the work — the policy tells it to "get on with your own share" — so it is the first lane: status, model, current step, output tail and token usage, exactly like a dispatched agent.
It is not in the dispatchable roster, though. That array is the set you can
steer, wake and interrupt, and putting the lead in it would have three
consequences: a re-launch would replace it as a stale roster, "stop all" would
interrupt the conversation you are talking in, and the plan editor would offer it
as an editable draft row. So it lives in the Host's state.lead, travels on its own
lead event, and renders as a lane with no action bar — you do not interrupt
yourself.
Its events are attributed by the same rule: the parent conversation's own session events (tool calls, replies, usage) fold into its card, while a deeper descendant — a helper one of our agents started — is on the same board but its output is not credited to the lead.
An agent sees the summaries of the agents on its own conversation's board, and nothing else. Another conversation's board is invisible to it. The boundary is enforced in code, not by convention:
hub_read to look,
hub_post to report.Why this had to be code: a child session's
session.idis its own. Without resolution, a subagent calling launch would allocate a second board keyed by its own id, and from that moment itshub_readwould return that empty board instead of the team it is working with — the roster would fragment silently, with no error anywhere. So "a board belongs to a conversation" is a resolution rule, not a sentence in a prompt.
An agent whose card was replaced loses access with it: after a re-launch or a cleared board, a stale child reporting in gets "not on any board" rather than writing into an unfamiliar empty one.
| Status | Meaning |
|---|---|
draft | Not launched; still editable |
running | Executing a turn |
idle | Launched, currently between turns — wakeable, not finished |
done | The run settled normally (still wakeable for another turn) |
error / stopped | Failed / interrupted |
A terminal status comes from subagent/end alone. Session events report process
and never set a terminal state — otherwise one turn/end would show a waiting
agent as finished.
A hub board is not limited to what the hub launched. When any agent starts a
child in this conversation — the native subagent tool, an Agent Teams teammate,
or anything else — that child is adopted: it gets a lane with its own name,
route, status, activity and output, and its progress streams onto the board like any
other row.
subagent/start carries the child's session id but not its parent, so the parent
is read from the child's own session header. Adoption also creates the board when
none exists yet: a conversation that starts help before anyone opens the hub would
otherwise have nowhere to show it.
Two boundaries, both deliberate:
hub_read and hub_post) but does not get
a row. The parent must be the board's own session, not merely resolve to it;
otherwise the board would turn into a call tree.Three slots: the main panel, a conversation.view tab, and a sidebar entry.
There is deliberately no composer dock. The hub used to render a strip above the input box with a "run it in parallel with the Agent Hub" hint and an "open the hub" button; both are gone. The strip made a claim about every message the user typed — whether or not this message was parallel work — and the panel was already one click away. A tool does not need to advertise itself in the middle of the place you type.
Optional, in cordis.patch.yml or the profile's patch layer:
| Option | Default | Meaning |
|---|---|---|
provider | '' | Subagent provider; empty = the first the runtime reports (spawn) |
maxAgents | 8 | Launch ceiling |
defaultWrite / defaultShell | true / false | Powers a new row starts with |
policy | true | Whether the "when to use the hub" section rides in every agent's system prompt |
outputLimit | 4000 | Retained output tail per agent, in characters |
feedLimit | 200 | Feed items retained per board |
coordinatorTimeoutMs / coordinatorMaxTokens | 120000 / 4000 | Split call |
An invalid value fails at load time rather than degrading at first use.
Nowhere durable. A board is a projection inside the host process, built from
subagent/start|end, session/event, agent/assistant-stream and
agent/status; it is empty after a restart. The agents themselves are durable
sessions and remain in the ordinary session list. The board describes what is
happening, so it is not a second source of truth — conclusions worth keeping
belong in your docs or a memory vault.
agent/assistant-stream; without streaming the
panel updates per step.toolFilter — tools.restrict() throws on an unregistered name, so a guessed
name would break every launch.node --no-warnings tests/smoke.mjs # host half: route edges, orchestration, projection, stream
node --no-warnings tests/integration.mjs # cross-half contract: host state → browser, browser payload → host
The host suite drives the real apply, route handler and event handlers
through a mock context, so it asserts the code that ships.
The integration suite is the valuable one: the two halves share only
CONTRACT.md and the wire, so either half can be green while the seam is broken.
It feeds the Host's actual snapshot into the browser half's pure functions, then
posts the payload the browser half builds back through the Host's validation.
tests/check-primitives.mjs needs the DSH checkout and is not part of
npm test: it diffs every primitive and icon name client.js destructures
against the shell's real artifact (packages/client/ui-primitives/lib/index.js).
A wrong name white-screens the panel in the browser, where no Node-side test can
see it.
Not yet declared.