dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
dsh-flow —— DeepSeek Harness 的统一智能体画布:把需求会话、多智能体团队、成员任务与发言、依赖 DAG 编织进同一张可交互时间轴,让 Agent 从“后台运行”变成“可视化协作”。
中文 · English
What stalls long or complex work is usually not "not enough tools". It is that the executor cannot be swapped, the state has no source of truth, and the boundaries have no assertions — a task that has run for hours crashes once and cannot say how far it got; changing how work is executed means changing the core, and so does adding a kind of team source.
dsh-flow solves all three at the level of kernel shape: rules/ is a pure core (no IO, no ctx), execution and sources are declared seams, and kernel.js is the single composition root that decides which implementation this deployment attaches.
It and dsh-agent-teams are two kernel shapes: the same capability surface, the opposite internal shape. That opposition is where this document starts.

In the host's own words, the difference is whether a plugin divides its own internals into services. The host frames this as "everything is a plugin" and "Plugins, not loop changes", which in code comes down to the core / seam split:
| Usual phrasing | The host's own terms | The evidence |
|---|---|---|
| microkernel | Keep core as small as possible and hang every capability off a declared seam; new behaviour goes on an extension point rather than into core | dsh-flow: a rules/ pure core (no IO, no ctx) + three services + one composition root |
| monolithic kernel | No seam; the capabilities all live inside core | dsh-agent-teams: src/ is one flat plane (17 TS modules + 13 under client/), importing each other freely, with no mechanism asserting module boundaries |
A seam comprises three roles — Service Definition / Provider / Consumer — and is only complete with all three. In dsh-flow they line up plainly: runner/interface.js is the definition, manual.js and subagents.js are the two Providers, and tools/ is the Consumer.
+------------------------------------------------------+
|the Service Definition |
|runner/interface.js |
+------------------------------------------------------+
| |
implements consumed by
v v
+----------------------+ +------------------------+
|manual.js | |tools/ |
|subagents.js | |the Consumer |
|the two Providers | |(13 flow_* tools) |
+----------------------+ +------------------------+
^
kernel.js mounts exactly one of these, at composition time
(two live schedulers would claim the same task)
ctx.flowTeamSources has the same three roles but the Providers sit in a registry rather than behind a pick, because those implementations genuinely coexist. The picture above is flowRunner, the seam where exactly one may be live.
Both are DSH plugins, and both attach to seams the host provides — the difference is whether the plugin has seams of its own. Once the functionality is split into core and seam, "add another way to execute" is adding a Provider and "add another kind of team source" is one register() call; on a flat src/, that work is changing the core.
So this is an opposition, not an absorption: the capability surfaces can be aligned (the 13 tools map one to one to agent_teams_*, and the 31 differential checks in pnpm test:diff guard that line), but the two kernel shapes cannot give the same set of guarantees.
Here is that other plane, read out of its source rather than its README. It provides no service of its own — ctx.provide appears zero times — and state.ts does the job our store/ layer does, without a layer to sit in:
NODE host process @nanmicoder/dsh-agent-teams
host seams it CONSUMES - it provides none of its own (ctx.provide: 0)
ctx.tools ctx.agents ctx.subagents ctx.llm ctx.commands ctx.systemPrompt
----------------------------------------+--------------------------------------
v
+------------------------------------------------------------------------------+
|index.ts 484 lines the entry |
|registers 13 agent_teams_* tools + 2 commands + 1 system-prompt section |
|and 4 HTTP routes under /plugins/dsh-agent-teams/ |
+-------------------------------------|----------------------------------------+
v
+-------------------------------------+----------------------------------------+
| the flat plane: 17 modules, 34 import edges between them, no direction rule |
| +--------------+ +--------------+ +--------------+ +--------------+ |
| |capabilities | |command | |profiles | |web-routes | |
| |110 | |141 | |750 | |98 | |
| +--------------+ +--------------+ +--------------+ +--------------+ |
| +--------------+ +--------------+ +--------------+ +--------------+ |
| |tools | |scheduler | |members | |snapshot | |
| |2456 | |523 | |728 | |267 | |
| +--------------+ +--------------+ +--------------+ +--------------+ |
| +--------------------------------------------------------------------------+ |
| |state.ts 1030 lines imported by 7 of the other 16 modules | |
| |readTeam / writeTeam / withTeamLock (node:fs, the de facto hub) | |
| +--------------------------------------------------------------------------+ |
| +--------------+ +--------------+ +--------------+ +--------------+ |
| |events | |harness-compat| |quality-gates | |types | |
| |78 | |198 | |987 | |249 | |
| +--------------+ +--------------+ +--------------+ +--------------+ |
+-------------------------------------|----------------------------------------+
HTTP boundary-----------------------------|---------------------------------------------
v
+------------------------------------------------------------------------------+
|BROWSER - the card renders INSIDE the native conversation |
| |
|registers a conversation NODE TYPE plus 3 slot injections: |
| ctx.uiConversation.events.register(agentTeamsCardDefinition) |
| slots: shell.overlay | conversation.chat.commandview | ...chat.node |
|polls GET /plugins/dsh-agent-teams/state on a timer |
+------------------------------------------------------------------------------+
Its data path is one-directional from disk to browser, with the live agent status merged in at read time:
NODE host process
<workspace path>/<stateDir>/<teamId>/ <- one directory per workspace
team.json the whole team, rewritten on every change
inbox/*.jsonl one mailbox per member
|
| readdir + readFile, folded on every request
v
+------------------------------------+ ctx.agents.get(sessionId)
|snapshot.ts | |
|collectTeamsActivity | | live subagent status
+------------------------------------+ v
| merged into the same snapshot
v
+------------------------------------------------------------------------------+
|GET /plugins/dsh-agent-teams/state ?archived=1 for the archive |
|GET /plugins/dsh-agent-teams/plan staging edits |
|GET /plugins/dsh-agent-teams/halt stop / resume |
|GET /plugins/dsh-agent-teams/assets artwork |
| all four behind BrowserRequestGate (connection auth) |
+------------------------------------------------------------------------------+
|
HTTP boundary-----------------------------|---------------------------------------------
v
+------------------------------------------------------------------------------+
|BROWSER the AgentTeams card, inside the native chat stream |
| |
|polls /state on a timer for the picture |
|writes go back through /plan (approve, edit) and /halt (stop) |
+------------------------------------------------------------------------------+
The last line of each diagram is a real fork, not a defect on either side: registering a node type into the host's conversation means inheriting the host's chat rendering, while owning a page means carrying your own engine — which is what buys the 0-build reload.
Side by side, in numbers:
| dsh-flow | dsh-agent-teams | |
|---|---|---|
| Modules | 66 (25 pure core + 29 host across 5 layers + 12 canvas) | 17 host + 13 client |
| Lines | rules 3715 / host 5089 / canvas 2898 | host 8277 / client 6101 |
| Internal import edges | asserted per layer, pointing inward only | 34, no direction rule |
Own ctx services | 3 (1 core + 2 seams) | 0 |
| Host runtime packages imported directly | 0 static; 1 by dynamic import() (@deepseek-ai/dsh-llm) | 12 (cordis ×9, dsh-session ×6, dsh-agent ×6, dsh-llm ×5, dsh-subagent ×3 …) |
| Artifact | source is the artifact (main: index.js) | main: lib/index.js; build = clean-build + tsc ×2 + tsdown |
The module count is the least interesting row — 66 against 30 says nothing about quality. What the table is for is the row before it: 3 services of its own against 0. A plugin with 0 cannot be extended from outside, because there is no declared point at which to extend it; a plugin with 3 can be replaced at exactly 2 of them, at composition time, without a line changing in the other 64 modules.
| dsh-flow | dsh-agent-teams | |
|---|---|---|
| Install | Clone it and it runs, nothing to install | Pulls a whole dependency tree (24 peers, including React 18) |
| Host upgrade | Declares no host version; probes capabilities at runtime and takes a fallback branch when it cannot | Pins 4 host versions in its peers; outside those it will not work |
| Change one canvas line | Save → refresh the page (0 builds, 0 restarts) | Rebuild the client bundle → restart the host |
| Stop execution, keep the view | runner: manual — look and edit, nothing runs | No equivalent switch |
| How a task's attempts went | Click a task row for the attempt timeline: how each attempt ended, which was rolled back | A monotonic attempt counter that cannot say how any one ended |
| Unreadable lines | Go into the import report, listed with line numbers on the canvas | — |
| Teams that cannot reach the canvas | Counted in the toolbar; they do not vanish silently | — |
| Approving a plan | Edit and approve directly on the canvas, through the same validation flow_edit_plan / flow_approve use | Panel editing |
| The canvas | Session timeline and team hierarchy in one figure (the team nests under the requirement turns) | A team tree panel |
| Uninstalling the other plugin | Everything keeps working — there was never a second record | — |
What you can "feel" in the table above all comes from the architecture: a change takes effect on refresh because canvas modules are served over HTTP individually (src/** invalidated by mtime + ETag revalidation), with no bundle step; runner: manual exists because execution is a seam, fixed at composition time; and uninstalling the other plugin changes nothing because team records are this plugin's own append-only log, with the other plugin only an entry in the source registry. Changing client.js (the tab registration layer) still requires a host restart on both sides — that layer really does go into the host's client bundle.
All of these are this repository's own numbers, reproducible with one command:
| Item | Number |
|---|---|
Cost of the plugin on the host's startup path (import './index.js') | 4.3 ms |
Composition root assembly (import './kernel.js', cold) | 33 ms |
A change to src/canvas/** taking effect | 0 builds, 0 restarts |
| Canvas first paint | 41 requests / 449 KB; every request afterwards revalidates by ETag, so unchanged files are a 304 |
| The layer-boundary gate (107 modules parsed + 7 layers asserted + serve allowlist) | 4.0 s |
| All 434 tests | 1.1 s |
A cross-implementation performance comparison requires both sides to run on this machine. dsh-agent-teams' full pipeline does not: it has no node_modules and no lib/ (its published artifact), and its snapshot.ts imports host runtime packages directly (@deepseek-ai/dsh-llm, @deepseek-ai/dsh-agent). Replacing those with stubs and then timing measures the stubs, not it. Its state-reading layer (src/state.ts) depends only on Node builtins and could in principle run directly under Node 24, but that is another repository's code, and running it needs your explicit consent.
So any "N times faster" in this repository would be invented. What can be given is the kind of difference above — confirmable without running anything — plus my own reproducible measurements.
The longer it runs, the more two things matter: a crash that cannot say how far it got, and a state that forks without telling you.
events.jsonl; state.json is only one reading of it. When the two disagree the log wins, and the disagreement is judged an error by teamDiffEvents rather than quietly smoothed over — reconciliation refuses a difference it cannot express instead of picking a winner. The longer it runs the likelier state forks, and silently picking a winner is how errors accumulate.runner: manual lets you look and edit while nothing executes. This is a composition-time choice, not runtime hot-swapping — saying that plainly is better than keeping a fake switch.workspaces.json are both single-writer, the cross-process lock is advisory, and two open copies can still overwrite each other.Complex work breaks between judgements: what the canvas shows is not what the captain decided on, editing and approval each judge once, and during a migration two sets of records fight.
flow_status — so the conclusion you read off the canvas cannot drift from the conclusion the captain decided on.applyTeamEdits / applyTeamApproval as flow_edit_plan / flow_approve — the K8 gate judges once, and the canvas is not a back door around the tool layer..agent-teams, so switching over is one config line rather than a big-bang move.ctx: the 25 modules under rules/ import under plain Node, and the 434 tests run in 1.1 s — that the judgements can be tested exhaustively is the precondition for carrying complex rules.runner/interface.js defines the seam, manual.js / subagents.js are its two Providers. Neither the core nor the tools change.register(): ctx.flowTeamSources is a registry, because the implementations genuinely coexist.pnpm run build (the pure core may not touch IO or ctx, the canvas may not touch node: or any host layer, host layers may not enter the serve allowlist, dependencies must point inward only). A boundary nobody asserts decays over time, and what is left is just a directory that happens to sit there.src/** is invalidated by mtime + ETag revalidation.files lists .js rather than lib/, so cloning it is enough to run it — there is no "compile first" step.You need a DeepSeek Harness that supports the profile plugin mechanism, Node.js >= 22.19.0, and the web profile.
dsh plugin --profile web add github:rootkiller6788/dsh-flow
dsh web
This plugin is not published to npm: the name
dsh-flowbelongs to another project on the registry, so please do not usedsh plugin add dsh-flow.
Once it starts, the tab bar above the conversation area gains an Agent Canvas tab; you can also open /dsh-flow/ directly.
One page, one engine, one figure. Nodes come in two kinds:
| Node | What it draws | Data source |
|---|---|---|
| Session turn card | One round of conversation (question + answer), wired into a branch tree by DSH's native fork relation; follow-up / branch / archive from here. Agent event turns — member relays, subagent notices — render as a derived label (论文手 → 队长), never as raw protocol text | The host's sessions + workspaces services, projected to disk at flow/workspaces.json |
| Team region | Under the team's title bar, nested member sub-regions: one cell per member, holding their task chips (wired by dependency depth) and speech cards (laid out in time order). Assignment is expressed by containment, dependency by arrows, dialogue flow by the turn chain crossing regions | Team data lives in dsh-flow's own storage (<stateDir>/<teamId>/events.jsonl); when the deployment has a .agent-teams/, it is listed alongside as a read-only source |
Open a turn card and the inspector shows a participant bubble flow: user messages, member relay messages (artwork avatar + sender → recipient direction), and subagent notices each as their own block, with tool calls folded into the process record.
agent-teams (or the host) relays member messages into the host session inside an envelope like Agent <uuid> sent a message:【sender → recipient】body. This plugin splits the envelope into structure at the projection layer (message.agent), and the render layer applies the same rule to older data as a fallback — no UUID and no protocol text ever reaches the canvas.
Click a team's title bar or a member sub-region and the right pane shows the whole arrangement: member artwork rows (avatar + role + model + progress + current activity), the task dependency list (state chips), the captain's mailbox (real member → captain messages), plus three judgement panels:
flow_status, so what you read off the canvas cannot drift from what the captain decided onflow_edit_plan / flow_approve useWhen team data contains lines that cannot be read, the team's card grows a 数据损坏 N badge and the inspector lists each one with its kind / member / line number / reason — the line number is the only thing that makes a damaged file fixable. Teams that cannot reach the canvas at all are covered by a deployment-wide count in the toolbar; otherwise they simply would not exist on the canvas.
assets/ ships 15 character/action images (9 professions + 6 states). Member names and role keywords map to artwork automatically (research/data → analyst, modelling/science → scientist, verify/review → QA, solve/implement → engineer, paper/writing → researcher, captain → captain …), falling back to a colour block with the first character. The artwork container's background follows the light/dark theme.
Team structure (members / tasks / dependencies / mailboxes) lives in dsh-flow's own storage, one directory per team:
<stateDir>/ # defaults to .dsh-flow
└── <teamId>/
├── events.jsonl # the append-only source of truth — everything the team did
├── state.json # a checkpoint: one reading of events.jsonl, discardable and rebuildable
├── manifest.json # metadata such as creation time and the initial goal
└── mail/ # one .jsonl mailbox per member
The log is the fact; the checkpoint is a cache. When the two disagree the log wins, and the disagreement is judged an error by teamDiffEvents rather than quietly smoothed over — reconciliation refuses a difference it cannot express instead of picking a winner. A line that cannot be read never stops parsing, but it does go into the import report.
Where teams come from is a source registry (ctx.flowTeamSources): this deployment's own log is always registered, and when a .agent-teams/ exists it is listed at the same time as a **read-only sourc