rootkiller6788/dsh-flow

dsh-flow

dsh-flow —— DeepSeek Harness 的统一智能体画布:把需求会话、多智能体团队、成员任务与发言、依赖 DAG 编织进同一张可交互时间轴,让 Agent 从“后台运行”变成“可视化协作”。

미분류agent-teamsagentic-workflowai-agents

dsh-flow

中文 · 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.

Agent canvas: requirement timeline + team hierarchy + artwork inspector

Two kernel shapes

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 phrasingThe host's own termsThe evidence
microkernelKeep core as small as possible and hang every capability off a declared seam; new behaviour goes on an extension point rather than into coredsh-flow: a rules/ pure core (no IO, no ctx) + three services + one composition root
monolithic kernelNo seam; the capabilities all live inside coredsh-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-flowdsh-agent-teams
Modules66 (25 pure core + 29 host across 5 layers + 12 canvas)17 host + 13 client
Linesrules 3715 / host 5089 / canvas 2898host 8277 / client 6101
Internal import edgesasserted per layer, pointing inward only34, no direction rule
Own ctx services3 (1 core + 2 seams)0
Host runtime packages imported directly0 static; 1 by dynamic import() (@deepseek-ai/dsh-llm)12 (cordis ×9, dsh-session ×6, dsh-agent ×6, dsh-llm ×5, dsh-subagent ×3 …)
Artifactsource 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.

What it is like to use

dsh-flowdsh-agent-teams
InstallClone it and it runs, nothing to installPulls a whole dependency tree (24 peers, including React 18)
Host upgradeDeclares no host version; probes capabilities at runtime and takes a fallback branch when it cannotPins 4 host versions in its peers; outside those it will not work
Change one canvas lineSave → refresh the page (0 builds, 0 restarts)Rebuild the client bundle → restart the host
Stop execution, keep the viewrunner: manual — look and edit, nothing runsNo equivalent switch
How a task's attempts wentClick a task row for the attempt timeline: how each attempt ended, which was rolled backA monotonic attempt counter that cannot say how any one ended
Unreadable linesGo into the import report, listed with line numbers on the canvas—
Teams that cannot reach the canvasCounted in the toolbar; they do not vanish silently—
Approving a planEdit and approve directly on the canvas, through the same validation flow_edit_plan / flow_approve usePanel editing
The canvasSession timeline and team hierarchy in one figure (the team nests under the requirement turns)A team tree panel
Uninstalling the other pluginEverything 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.

Measurements

All of these are this repository's own numbers, reproducible with one command:

ItemNumber
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 effect0 builds, 0 restarts
Canvas first paint41 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 tests1.1 s

Why there is no "N times faster" here

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.

Long-running work

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.

  • The source of truth is an append-only 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.
  • How many times a task was tried, how each ended, which was rolled back — click a task row for the attempt timeline. This information exists only at the protocol layer: a monotonic attempt counter cannot say how any single attempt ended.
  • A line that cannot be read never stops parsing, but it does go into the import report, listed on the canvas with its line number (kind / member / line / reason). The line number is the only thing that makes a file that ran itself damaged fixable.
  • If it runs away you can keep the view and stop the work: 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.
  • The boundary is written down: the team store and workspaces.json are both single-writer, the cross-process lock is advisory, and two open copies can still overwrite each other.

Complex work

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.

  • The judgements live in the pure core, so the canvas and the model see the same one. K9 verdict / K10 blockers / K11 coverage matrix are carried out in the snapshot, the same computation the model sees when it calls flow_status — so the conclusion you read off the canvas cannot drift from the conclusion the captain decided on.
  • One figure holds the hierarchy: the user's requirement turns stay on the main timeline, the team grows below them as a nested region, each member gets a sub-region 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.
  • Editing and approval happen on the canvas, but are judged once: a staged team's inspector edits tasks / members and their dependencies and approves them, going through the same 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.
  • During a migration both sets of teams are visible: the source registry lists this deployment's log alongside .agent-teams, so switching over is one config line rather than a big-bang move.
  • The pure core touches neither IO nor 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.

Extension

  • Another way to execute = another Provider: runner/interface.js defines the seam, manual.js / subagents.js are its two Providers. Neither the core nor the tools change.
  • Another kind of team source = one register(): ctx.flowTeamSources is a registry, because the implementations genuinely coexist.
  • Boundaries are expressed by directories and asserted layer by layer by the gate: seven layers, every invariant asserted in 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.
  • Changing one canvas line: save → refresh the page. There is no build step; src/** is invalidated by mtime + ETag revalidation.
  • Zero runtime dependencies: files lists .js rather than lib/, so cloning it is enough to run it — there is no "compile first" step.

Quick start

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-flow belongs to another project on the registry, so please do not use dsh 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.

What is on the canvas

One page, one engine, one figure. Nodes come in two kinds:

NodeWhat it drawsData source
Session turn cardOne 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 textThe host's sessions + workspaces services, projected to disk at flow/workspaces.json
Team regionUnder 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 regionsTeam 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

Multi-agent dialogue

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.

Team inspector

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:

  • K9 verdict / K10 blockers / K11 coverage matrix — the same computation the model sees when it calls flow_status, so what you read off the canvas cannot drift from what the captain decided on
  • Click a task row to expand that task's attempt timeline: how many times it was tried, how each attempt ended, which one was rolled back
  • Staged teams are editable and approvable right in the inspector, through the same validation flow_edit_plan / flow_approve use

When 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.

Artwork

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.

Where team data lives

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