dsh-agent-bus
Multi-agent orchestration for DeepSeek Harness. Sessions in one workspace assign work, review results, and run DAG workflows — without you as the messenger.
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 18, 2026
- Updated
- Aug 20, 2026
Introduction
dsh-agent-bus
English | 中文
Multi-agent orchestration for DeepSeek Harness. Stop being the messenger.
dsh-agent-bus is a DeepSeek Harness plugin that turns live sessions in one workspace into an orchestra: they assign work, review each other’s output, and run multi-step DAG workflows — on the inbox you already have.
You keep the specialists. You stop copy-pasting.
Why this exists
Harness already runs several agents in one workspace. It does not let them collaborate.
Without this plugin:
- A planner cannot give a coder a job. You paste the brief.
- A coder cannot wait for a reviewer. You paste the patch.
- When step 3 fails, you reconstruct steps 1–2 from chat logs.
Let the bus run the team. Don’t be the messenger.
What you actually get
Talk stays talk. A question, a ping, a “look at this” is send_note. No ledger, no review, no timeout theatre. If the peer is offline, the note waits and delivers when they come back.
Work stays work. create_task is a job with a body, an optional acceptance bar, and a reviewer. The worker reports; the reviewer accepts or sends the same task back with feedback. The id never changes across rework.
A plan can run without you in the loop. create_flow is a named DAG. You (or the planner agent) write the plan, then create tasks with flow_id and dependencies. Task B is not even delivered until A is accepted. If A is canceled or fails for good, B and C fail with it — no orphaned workers.
The next agent reads the chain, not the archaeology. After settle, the executor can attach a handoff (numbers, decisions, caveats). Dispatch concatenates that into the downstream task. Step 3 does not have to get_task its way through step 1.
You can see it. On the web profile a capsule on the right opens a workbench: a task list, and a per-flow DAG canvas. Click a node for the full requirement. Archived ancestors stay on the graph, faded.
The task log
Chat logs are a lousy work tracker. They mix pings with jobs, they vanish when a session is compacted, and the next agent cannot query “what was accepted yesterday.”
Agent-bus keeps a task log next to the conversation — a durable record of every real job, not a dump of every message.
Notes stay in the session. send_note is conversation. It does not create a ledger row, it does not show up on the panel, and it does not need a report. The jsonl of that session is the log.
Tasks get a ledger row. Each create_task writes who asked, who does it, who reviews, the requirement, optional acceptance criteria, dependencies, and later the verdict. Status moves on that row (queued → submitted → working → completed → settle). Rework is the same id; you can read the whole life of one job with get_task.
The report is stored as a document, not as more chat. Short reports sit inline on the row. Long ones spill to disk, keyed by task id — never by a filesystem path the model could leak:
| Zone | Where | What |
|---|---|---|
| Hot | ~/.dsh/agent-bus/cache/ | Active-task reports; unused files are swept after 7 days |
| Cold | ~/.dsh/agent-bus/archive/ | Terminal tasks (completed / failed / canceled); swept after 30 days |
get_task reads hot then cold. Agents never see the split. The ledger itself lives in the harness storage domain (agent_bus); every open also writes a JSON snapshot under ~/.dsh/agent-bus/backups/ (last 20 kept) so a schema rebuild cannot silently eat the table.
What you browse vs what agents list. The panel is the human log: active work, archive (settled more than 24 hours, or failed/canceled), tokens, and the DAG for a flow. list_tasks hides archived rows on purpose — the worker’s inbox is not a history dump. History is the panel, get_task, and the session log.
That is the point: the next specialist, the reviewer, and you all read the same record instead of reconstructing the job from three chat windows.
Quick start
dsh plugin --profile web add dsh-agent-bus
dsh web
From a local checkout:
dsh plugin --profile web add .
dsh --profile web --dump-config
dsh web
The web-app bundle already mounts storage and the workspace registry. A custom or headless profile must declare storage, storage-json, storage-domain, and workspace in its own cordis.patch.yml — load fails loudly otherwise. A gateway that cannot record must not boot as a silent prompt.
Requires Node.js ^22.19.0 or >=24.
How it works
Delivery is the harness inbox: one followup() per turn, idle sessions take the next item. This plugin does not add a second queue.
The plugin’s job is the ledger — who asked, who does it, what “done” means, what depends on what — plus a panel that reads that ledger.
There is no receive-side tool. The worker sees an ordinary turn. They do the work and call report_task.
note send_note → peer replies in prose (or not)
task create_task → queued → submitted → working → completed → settle
flow create_flow + tasks → DAG auto-dispatches each node after its predecessors settle
Pick the lightest channel that still matches the ask. Chat-as-task is how work gets stuck in working. Task-as-chat is how you lose review.
Agent Bus vs sub-agents
Why we did not build on sub-agents
Harness already ships sub-agents: the parent calls spawn_subagent, a child boots, does the job, and returns a summary. That is the right tool for “go explore this in isolation and come back.”
We did not put the team on that architecture. A child inherits the parent’s permission group and session config — skills, MCP servers, plugin set, model, allowlist. You can trim the toolbelt (agent type, capability mode, persona). You cannot give the coder a repo MCP, the researcher a web MCP, and the reviewer a tighter tenant allowlist as three different configurations. Fine-grained staffing is exactly what a specialist team needs, and inheritance makes it hard.
So every bus peer is a normal DeepSeek Harness session — the same object you already customize. It keeps its own skills, MCP servers, plugin group, permission preset, and model. That is how you staff a team, and it is the same session model a multi-tenant host hangs permission groups and plugin groups on: per tenant, per role, not “whatever the parent spawned.”
| Sub-agent | Agent Bus | |
|---|---|---|
| Unit of work | Child session spawned for one job, then gone | followup() into an existing peer session |
| What the worker is | A disposable child: type + capability mode + optional persona | A first-class session instance you configured in dsh |
| Skills / MCP / plugins | Inherited and usually trimmed for the spawn | Per session: its own skills, MCP servers, and plugin group |
| Permissions | Parent’s envelope, narrowed | Per session (and, in a multi-tenant host, per permission group) |
| Topology | Star: parent is the hub | Peers in one workspace + a durable ledger |
| Who reviews | The parent reads a summary | A first-class reviewer accepts or reworks the same task id |
| Ordering | Parent must orchestrate every next spawn | DAG: B is not delivered until A is settled |
| Failure | Parent has to notice | Terminal fail/cancel propagates down the chain |
| After restart | The play lives in the parent’s context | Ledger + inbox checkpoints survive |
| Parallelism | Many children at once from one parent | Many peers at once; each peer still one inbox item per turn |
Where the cost actually goes
No fake speedup numbers — the difference is where tokens and latency are spent.
| Cost | Sub-agent | Agent Bus |
|---|---|---|
| Prompt cache | Every spawn pays a cold prefix (system prompt, tools, instructions). | A specialist is a long-lived session. The next task is another user turn on a warm prefix. |
| Orchestrator context | Each child’s summary lands in the parent window. N jobs → parent context grows with N summaries. | The initiator gets a short inbox notice. The full report lives in the ledger (and on disk when large). get_task is on demand. |
| Time to first token | Session boot + first decode on a cold cache. | Idle live peer: next turn now, no new process. |
| Specialist memory | Dies with the child. Job 4 does not remember job 3 unless the parent stuffed it into the next spawn prompt. | The same coder session still has job 3 in its window (and files in the workspace). Handoffs carry the rest. |
| Throwaway explore | Use this. Isolated window, parent cache untouched. | Don’t. A peer session is a person on the team, not a sandbox. |
Rule of thumb: spawn a sub-agent to protect the caller’s context for a one-shot. Use the bus when the callee is a named teammate — with their own skills, MCP, plugins, and permissions — who will take the next job after this one.
Tools
| You want to… | Use |
|---|---|
| Ask a peer something that is not a job | send_note |
| Give one peer one deliverable to review | create_task |
| Run a multi-step plan in order | create_flow, then create_task with flow_id / dependencies |
| Finish / accept / rework / stop / ask back / move the job | report_task · settle_task · cancel_task · request_input · reassign_task |
| Pass context down the chain | submit_handoff |
| Fix an undispatched node, or look things up | edit_task · list_flows · list_tasks · get_task |
| See who is live, declare what you can do | list_peers · update_card |
Docs
docs/usage.md | Handbook (Chinese): tools, state machine, templates |
docs/v1.5-resilience-spec.md | Offline notes, reassign, offline grace |
docs/v1.4-event-driven-scheduling-spec.md | Event-driven dispatch, flows, handoffs |
docs/a2a-alignment.md | A2A task-state alignment |
License
MIT