🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

流式多智能体内核 rootkiller6788/dsh-flow

DeepSeek Harnessv0.0.1
rootkiller678828 天前有提交已提供中文文档扫描:未发现敏感能力

拖住长期或复杂工作的,通常不是「工具不够」。而是执行器换不了、状态没有唯一事实来源、边界没有断言——一个跑了几小时的任务…

46.6/100
综合分
32
GitHub 星标
87
npm 周下载
—/5
用户评分

数据截至 2026/10/7 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-flow
npm 源 · dsh-flow 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
dsh 原生插件 · dev
装得上吗
静态安装检查通过
未做真实安装
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 28 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-flow @ 0.0.1
✓Node 引擎要求 >=22.19.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/30 11:47:29

安全扫描自动 · 静态分析源码
v2.7.0
🟢 扫描:未发现敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描未发现敏感能力:最多只发网络请求、读取自身元数据或只读文件,未发现执行命令、文件写入或敏感信息读取。

扫描于 2026/11/3(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

界面预览

1 张 · 点击查看大图

功能亮点

  • 插件化微内核架构
  • 构建编排多智能体系统
  • 扩展多智能体系统

插件化微内核架构,用来在 DeepSeek Harness 里构建、编排和扩展多智能体系统。

README

由 DeepSeek 最新模型翻译生成
dsh-flow · English 拖住长期或复杂工作的,通常不是「工具不够」。而是执行器换不了、状态没有唯一事实来源、边界没有断言——一个跑了几小时的任务崩一次就说不出跑到哪了;想改工作的执行方式就得改核心,加一种团队来源也一样。 dsh-flow 在内核形态这一层把这三件事一起解决:rules/ 是纯核心(不做 IO,不碰 ctx),执行与来源都是声明式接缝,kernel.js 是唯一的组装根,由它决定这次部署接上哪个实现。 它和 dsh-agent-teams 是两种内核形态:能力面相同,内部形态相反。这份文档就从这种对立讲起。 智能体画布:需求时间线 + 团队层级 + 作品检查器 = 22.19.0"> 两种内核形态 用宿主自己的话说,区别在于插件是否把自己的内部拆成服务。
展开完整文档(README 剩余 1 节)
dsh-flow

· English

拖住长期或复杂工作的,通常不是「工具不够」。而是执行器换不了、状态没有唯一事实来源、边界没有断言——一个跑了几小时的任务崩一次就说不出跑到哪了;想改工作的执行方式就得改核心,加一种团队来源也一样。

dsh-flow 在内核形态这一层把这三件事一起解决:rules/ 是纯核心(不做 IO,不碰 ctx),执行与来源都是声明式接缝,kernel.js 是唯一的组装根,由它决定这次部署接上哪个实现。

它和 dsh-agent-teams 是两种内核形态:能力面相同,内部形态相反。这份文档就从这种对立讲起。

智能体画布:需求时间线 + 团队层级 + 作品检查器

= 22.19.0">

两种内核形态

用宿主自己的话说,区别在于插件是否把自己的内部拆成服务。宿主把这套说法概括为「一切皆插件」和「Plugins, not loop changes」,落到代码上就是 core / seam 的拆分:

| 常见说法 | 宿主自己的术语 | 证据 |
| 微内核 | 让 core 尽量小,把每项能力都挂到声明式接缝上;新行为加到扩展点,而不是塞进 core | dsh-flow:rules/ 纯核心(不做 IO,不碰 ctx)+ 三个服务 + 一个组装根 |
| 宏内核 | 没有接缝;能力全都住在 core 里 | dsh-agent-teams:src/ 是一整块平面(17 个 TS 模块 + client/ 下 13 个),彼此随意 import,没有任何机制约束模块边界 |

一条接缝由三个角色组成——服务定义 / 服务商 / 消费者——三者齐备才算完整。在 dsh-flow 里它们对得很清楚:runner/interface.js 是定义,manual.js 和 subagents.js 是两个服务商,tools/ 是消费者。

+------------------------------------------------------+
|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 也有这三种角色,但 Provider 放在注册表里,而不是二选一,因为这些实现确实可以共存。上图是 flowRunner,也就是只允许一个实现存活的那个接缝。

两者都是 DSH 插件,都挂在宿主提供的接缝上——区别在于插件自己有没有接缝。功能一旦拆成核心与接缝,「再加一种执行方式」就是加一个 Provider,「再加一种团队来源」就是调一次 register();而在扁平的 src/ 里,这些工作都等于改核心。

所以这是对立,不是吸收:能力面可以对齐(13 个工具与 agent_teams_ 一一对应,pnpm test:diff 里的 31 项差分检查守住这条线),但两种内核形态给不出同一套保证。

下面是另一个平面,从它的源码读出来,而不是从 README。它自己不提供任何服务——ctx.provide 出现零次——state.ts 干了我们 store/ 层的活,却没有一层可以落脚:

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 |
+------------------------------------------------------------------------------+

它的数据路径是单向的,从磁盘流向浏览器,读取时把实时智能体状态合并进来:

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 停止 / 恢复 |
|GET /plugins/dsh-agent-teams/assets 美术资源 |
| 以上四个都在 BrowserRequestGate(连接鉴权)之后 |
+------------------------------------------------------------------------------+
HTTP 边界---------------------------------|--------------------------------------------- v +------------------------------------------------------------------------------+
|BROWSER AgentTeams 卡片,位于原生聊天流内 |
|定时轮询 /state 拿画面 |
|写操作走回 /plan(批准、编辑)和 /halt(停止) |
+------------------------------------------------------------------------------+

每张图的最后一行都是真实的分叉,不是哪一方的缺陷:把节点类型注册进宿主的会话,就意味着继承宿主的聊天渲染;而自己拥有页面,就意味着自带引擎——正是这一点换来了 0 构建重载。

并排看,用数字说话:

| | dsh-flow | dsh-agent-teams |
| 模块数 | 66(25 个纯核心 + 29 个宿主,跨 5 层 + 12 个画布) | 17 个宿主 + 13 个客户端 |
| 行数 | rules 3715 / host 5089 / canvas 2898 | host 8277 / client 6101 |
| 内部导入边 | 逐层断言,只指向内层 | 34 条,无方向规则 |
| 自有 ctx 服务 | 3(1 个核心 + 2 个接缝) | 0 |
| 直接导入的宿主运行时包 | 0 个静态;1 个走动态 import()(@deepseek-ai/dsh-llm) | 12 个(cordis ×9、dsh-session ×6、dsh-agent ×6、dsh-llm ×5、dsh-subagent ×3 …) |
| 产物 | 源码即产物(main: index.js) | main: lib/index.js;构建 = clean-build + tsc ×2 + tsdown |

模块数是最没意思的一行——66 对 30,说明不了质量。这张表真正要看的是它上面那一行:自有服务 3 个对 0 个。自有服务为 0 的插件没法从外部扩展,因为没有声明任何可扩展的点;自有服务为 3 的插件,可以在其中恰好 2 个点上替换,替换发生在组合期,另外 64 个模块一行都不用改。

用起来是什么感觉

| | dsh-flow | dsh-agent-teams |
| 安装 | 克隆下来就能跑,什么都不用装 | 拉下一整棵依赖树(24 个 peer,包括 React 18) |
| 宿主升级 | 不声明宿主版本;运行时探测能力,探不到就走回退分支 | 在 peer 里钉死 4 个宿主版本;超出这些版本就跑不起来 |
| 改一行画布代码 | 保存 → 刷新页面(0 次构建,0 次重启) | 重新构建客户端 bundle → 重启宿主 |
| 停止执行,保留视图 | runner: manual —— 只看和编辑,什么都不跑 | 没有对应开关 |
| 一次任务的各次尝试结果如何 | 点击任务行查看尝试时间线:每次尝试如何结束、哪次被回滚 | 单调递增的尝试计数器,说不出任何一次是怎么结束的 |
| 读不进去的行 | 进入导入报告,在画布上按行号列出 | —— |
| 联系不上画布的团队 | 在工具栏里计数;不会悄悄消失 | —— |
| 批准计划 | 直接在画布上编辑并批准,走的是 flow_edit_plan / flow_approve 用的同一套校验 | 面板编辑 |
| 画布 | 会话时间线与团队层级合在一张图里(团队嵌在需求轮次之下) | 一个团队树面板 |
| 卸载另一个插件 | 一切照常工作 —— 从来就没有第二份记录 | —— |

上表里你能「感觉到」的东西,全都来自架构:改动刷新即生效,因为画布模块逐个走 HTTP 提供(src/ 按 mtime 失效 + ETag 重新校验),没有打包步骤;runner: manual 之所以存在,是因为执行是一道接缝,在组装时固定;卸载另一个插件毫无影响,因为团队记录是本插件自己的只追加日志,另一个插件只是来源注册表里的一条。改 client.js(标签页注册层)在两边仍然都要重启宿主 —— 那一层确实会进宿主的客户端 bundle。

实测数据

以下全是本仓库自己的数字,一条命令即可复现:

| 项目 | 数字 |
| 插件在宿主启动路径上的开销(import './index.js') | 4.3 ms |
| 组装根装配(import './kernel.js',冷启动) | 33 ms |
| 改动 src/canvas/ 生效 | 0 次构建,0 次重启* |
| 画布首屏 | 41 个请求 / 449 KB;之后每个请求都按 ETag 重新校验,未变的文件就是 304 |
| 层边界门禁(解析 107 个模块 + 断言 7 层 + 提供白名单) | 4.0 s |
| 全部 434 个测试 | 1.1 s |

为什么这里没有「快 N 倍」

跨实现比性能,前提是两边都在这台机器上跑。dsh-agent-teams 的完整流水线跑不起来:它没有 node_modules,也没有 lib/(它发布的产物),而它的 snapshot.ts 直接 import 宿主运行时包(@deepseek-ai/dsh-llm、@deepseek-ai/dsh-agent)。把这些换成桩再计时,量的是桩,不是它。它的状态读取层(src/state.ts)只依赖 Node 内置模块,原则上能在 Node 24 下直接跑,但那是另一个仓库的代码,要跑得你明确同意。

所以本仓库里任何「快 N 倍」都是编的。能给的只有上面这类差异 —— 不用跑任何东西就能确认 —— 再加上我自己可复现的实测数据。

长时间运行的工作

运行得越久,两件事越要命:崩溃了说不清跑到哪一步,状态分叉了却不告诉你。

- 唯一可信来源是只追加的 events.jsonl;state.json 只是它的一种读法。 两者不一致时以日志为准,而且 teamDiffEvents 会把这种不一致判为错误,而不是悄悄抹平——对账拒绝自己表达不了的差异,不会挑一个赢家。运行得越久,状态越可能分叉,而悄悄挑赢家正是错误累积的方式。
- 一个任务试了几次、每次怎么结束、哪次被回滚——点开任务行看尝试时间线。这些信息只存在于协议层:单调递增的尝试计数器说不出任何一次尝试是怎么结束的。
- 读不出来的行不会让解析停下,但会进导入报告,在画布上按行号列出(kind / member / line / reason)。一个自己跑坏了的文件要能修,靠的就是行号。
- 跑飞了可以保留视图、停掉工作:runner: manual 让你边看边改,同时什么都不执行。这是组合期的选择,不是运行时热切换——把这话说清楚,比留一个假开关强。
- 边界写明白:团队存储和 workspaces.json 都是单写者,跨进程锁是建议性的,两份打开的副本仍可能互相覆盖。

复杂工作

复杂工作会在判断之间断裂:画布显示的不是队长拍板的那份,编辑和审批各判一次,迁移期间两套记录还会打架。

- 判断住在纯核心里,所以画布和模型看到的是同一份。 K9 判定 / K10 阻塞项 / K11 覆盖矩阵都在快照里执行,和模型调用 flow_status 时看到的是同一套计算——你从画布上读到的结论,不会和队长拍板的结论漂移。
- 一张图承载层级:用户的需求轮次留在主时间线上,团队在下方长成嵌套区域,每个成员分到一个子区域,里面放他们的任务卡片(按依赖深度连线)和发言卡(按时间顺序排布)。归属用包含关系表达,依赖用箭头,对话流用跨区域的轮次链。
- 编辑和审批在画布上发生,但只判一次:暂存团队的检查器编辑任务 / 成员及其依赖并批准,走的是和 flow_edit_plan / flow_approve 同一套 applyTeamEdits / applyTeamApproval——K8 门只判一次,画布不是绕过工具层的后门。
- 迁移期间两套团队都可见:源注册表把本部署的日志和 .agent-teams 并列列出,所以切换只是改一行配置,而不是一次大爆炸式搬迁。
- 纯核心既不碰 IO,也不碰 ctx:rules/ 下的 25 个模块在普通 Node 下就能导入,434 个测试跑完只要 1.1 s——判断逻辑能被穷尽测试,是承载复杂规则的前提。

扩展

- 换一种执行方式 = 换一个服务商:runner/interface.js 定义接缝,manual.js / subagents.js 是它的两个服务商。核心和工具都不用改。
- 换一种团队来源 = 加一个 register():ctx.flowTeamSources 是注册表,因为各种实现确实会共存。
- 边界由目录表达,再由门禁逐层断言:七层,每条不变量都在 pnpm run build 里断言(纯核心不得碰 IO 或 ctx,画布不得碰 node: 或任何宿主层,宿主层不得进入 serve 白名单,依赖只能向内指)。没人断言的边界会随时间腐化,最后只剩下一个恰好摆在那儿的目录。
- 改画布一行:保存 → 刷新页面。 没有构建步骤;src/* 靠 mtime + ETag 重新校验来失效。
- 零运行时依赖:files 列的是 .js 而不是 lib/,所以克隆下来就能跑——没有「先编译」这一步。

快速开始

你需要一个支持 profile 插件机制的 DeepSeek Harness、Node.js >= 22.19.0,以及 web 配置档。

dsh plugin --profile web add github:rootkiller6788/dsh-flow
dsh web

这个插件没有发布到 npm:dsh-flow 这个名字在 registry 上属于另一个项目,所以请不要用 dsh plugin add dsh-flow。

启动后,对话区上方的标签栏会多出一个 Agent Canvas 标签页;你也可以直接打开 /dsh-flow/。

画布上有什么

一个页面,一个引擎,一张图。节点分两种:

| 节点 | 画什么 | 数据来源 |
| 会话轮次卡片 | 一轮对话(问题 + 回答),按 DSH 原生的 fork 关系接成分支树;从这里跟进 / 分支 / 归档。智能体事件轮次——成员转述、子代理通知——渲染成派生标签(论文手 → 队长),绝不渲染成原始协议文本 | 宿主的 sessions + workspaces 服务,投影到磁盘上的 flow/workspaces.json |
| 团队区域 | 团队标题栏之下,嵌套成员子区域:每个成员一格,放他们的任务芯片(按依赖深度接线)和发言卡片(按时间顺序排布)。归属靠包含关系表达,依赖靠箭头,对话流靠跨区域的轮次链 | 团队数据存在 dsh-flow 自己的存储里(<stateDir>/<teamId>/events.jsonl);部署里有 .agent-teams/ 时,它作为只读来源并列列出 |

多智能体对话

打开回合卡片,检查器会显示参与者气泡流:用户消息、成员中转消息(作品头像 + sender → recipient 方向)、子代理通知各自成块,工具调用折叠进过程记录。

agent-teams(或宿主)把成员消息中转进宿主会话,外面套一层信封,形如 Agent <uuid> sent a message:【sender → recipient】body。本插件在投影层把信封拆成结构(message.agent),渲染层对旧数据套用同一规则作为兜底——UUID 和协议文本都不会出现在画布上。

团队检查器

点击团队标题栏或成员子区域,右侧面板会显示整体编排:成员作品行(头像 + 角色 + 模型 + 进度 + 当前活动)、任务依赖列表(状态标签)、队长信箱(真实的成员 → 队长消息),外加三个判定面板:

- K9 结论 / K10 阻塞项 / K11 覆盖矩阵——与模型调用 flow_status 时看到的同一套计算,所以你从画布上读到的结果不会和队长的决策依据产生偏差
- 点击任务行可展开该任务的尝试时间线:尝试过几次、每次如何结束、哪次被回滚
- 暂存团队可以直接在检查器里编辑和批准,走的是 flow_edit_plan / flow_approve 用的同一套校验

团队数据里出现读不了的行时,团队卡片会长出 数据损坏 N 徽标,检查器逐条列出类型 / 成员 / 行号 / 原因——行号是让损坏文件可修复的唯一线索。完全进不了画布的团队,由工具栏上的全局计数兜底;否则它们在画布上根本不存在。

作品

assets/ 内置 15 张角色/动作图(9 种职业 + 6 种状态)。成员名和角色关键词会自动映射到作品(research/data → analyst,modelling/science → scientist,verify/review → QA,solve/implement → engineer,paper/writing → researcher,captain → captain……),匹配不上就退回色块加首字符。作品容器的背景跟随明暗主题。

团队数据存放位置

团队结构(成员 / 任务 / 依赖 / 信箱)存放在 dsh-flow 自己的存储里,每个团队一个目录:

<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 日志才是事实,检查点只是缓存。 两者不一致时以日志为准,teamDiffEvents 会把这种不一致判为错误,而不是悄悄抹平——对账拒绝自己无法表达的差异,不会挑一个赢家。读不出来的行不会中断解析,但会记进导入报告。

团队从哪来由来源注册表(ctx.flowTeamSources)决定:本部署自己的日志始终注册,当 .agent-teams/ 存在时,它同时作为只读来源列出。迁移期间你能看到两套数据,所以切换只需改一行配置,不用一次性大搬迁(canAppend 为 false 的来源无法从画布编辑)。

对话编织本身来自会话投影中的 relay 解析,不依赖任何外部插件。

用法

- /dsh-flow 命令:/dsh-flow [--profile <name>] <goal> 把当前会话设为队长并拉起一支团队。每个配置档还会得到一个别名 /dsh-flow-<name>(配置档名必须是小写字母数字加连字符才能寻址;像 bug fix 这样的名字不会生成别名,而不是猜一个规范化结果)。该命令只创建团队,停在待审计划——它绝不会在同一轮里批准,因为审核正是暂存存在的理由。
- 拖拽与记忆:卡片可以拖动;坐标只作为视觉元数据写入浏览器 localStorage——节点的身份始终是真实的东西(DSH 会话 / teamId / 成员名),位置从不决定身份。「重置」回到自动布局。
- 检查器:点击卡片(不要点到按钮)打开右侧检查器,它同时把当前 DSH 会话切过去——不用离开画布。Esc 关闭。
- 追问 / 分支:检查器底部和卡片一角会在画布上打开草稿卡片;输入发生在画布上,这是唯一的写入入口。快捷短语可编辑(最多 12 条,每条 16 个字符)。
- DSH 按钮:切回宿主的 Chat 标签页并锚定到那一轮;完整过程记录在原生会话里查看。
- 归档:卡片上的归档按钮把会话移出画布(记入 hiddenSessionIds);刷新 DSH 列表不会把它重建出来。
- 主题:浅色 / 深色跟随宿主(theme/change 事件 → data-theme),深色由同一套设计 token 驱动,画作容器背景也随之切换。
- 滚轮在卡片上滚动卡片自己的回答,在空白处缩放画布。

配置

在配置档的 cordis.patch.yml 里覆盖本插件的配置:

- insert:
- id: dsh-flow
name: dsh-flow
config:
dataFile: !!js dshHomePath('flow/workspaces.json')
autoProjection: true
projectionWorkspaceTitle: DSH 任务
trustedHosts: []

stateDir: .dsh-flow # where teams liveyaml
runner: subagents # manual | subagents (fixed at composition time)
memberProvider: spawn
maxMembers: 8

agentTeamsStateDir: .agent-teams # open this only while migrating

profiles: {...} # see below

| 键 | 默认值 | 含义 |
| dataFile | dshHomePath('flow/workspaces.json') | 画布图形的持久化路径。必填 |
| autoProjection | true | 是否自动把 DSH 会话投射到画布上(监听 session/created 和 session/event) |
| projectionWorkspaceTitle | DSH 任务 | 无法从 cwd 推导出工作区名称时的兜底标题 |
| trustedHosts | [] | Host 检查额外接受的 authority(localhost 和 127.0.0.1 始终放行) |
| stateDir | .dsh-flow | 团队日志和邮箱的根目录;相对路径按会话工作目录解析,这样两套部署的团队不会互相干扰 |
| runner | subagents | 组合期选项:subagents 是真正的内核,manual 表示「只能看和改,什么都不执行」 |
| memberProvider | spawn | 成员用哪个子代理服务商启动 |
| agentTeamsStateDir | 无 | 填了就把它指向的 .agent-teams 目录注册为只读来源;迁移期间两套团队都可见,而 flow_ 工具仍然只操作我们自己的那套 |
| maxMembers | — | 配置档名册以及名册产出的团队的人数上限 |
| profiles | 无 | 团队配置档表:具名名册 + 种子任务 + 评审策略 |

profiles 属于部署配置,不是协议——一套部署提供哪些团队是它自己的事,所以放在配置里,不写进代码。配置档的 taskPlanning 只有两个取值:seed(下面的任务定义整张图)或 captain(名册给定,图由 captain 设计)。名册里写的 role / reasoning_effort 决定成员的默认路由。

/dsh-flow 的路由不在 DSH /api 的浏览器信任围栏内,所以插件自己校验 Host 头,防 DNS rebinding;要从非本地地址访问,把主机名加进 trustedHosts。

架构

单包,零运行时依赖,无需构建。源码即产物:files 列的是 .js,不是 lib/——克隆下来就能跑,没有「先编译」这一步。

宿主的设计哲学是万物皆插件,它把服务分为 core / seam / bundle 三类。dsh-flow 也照这个思路划分自己:一个纯内核,两条接缝,一个组合根。

三个服务

| 服务 | 类别 | 形态 | 为什么必须是这个形态 |
| ctx.flowTeams | core | 单实例 | 这套部署只有一份团队记录,其他插件消费它 |
| ctx.flowTeamSources | 接缝 | 注册表 | 这些实现确实会共存:迁移期间,原生团队和 .agent-teams 团队必须同时可见 |
| ctx.flowRunner | 接缝 | 二选一,在组合时选定 | 两个实例会打架:两个调度器会抢占同一个任务 |

一个名字只能有一个服务商(同名下第二次 ctx.provide 会抛错),所以上面的区分不是风格选择,而是硬性约束——会共存的东西做成注册表,互斥的东西必须在组合时固定下来。

kernel.js is the composition root: the one place that puts store / runner / tools side by side. It implements no capability itself; it only decides which implementation this deployment uses and which seams they are attached to.

Seven layers, each layer's invariant asserted by the gate

Boundaries are expressed by directories and asserted layer by layer by pnpm run build — a boundary nobody asserts decays over time, and what is left is just a directory that happens to sit there.

NODE host process (plain JS, no build step, no runtime dependencies)

+--------------------------------------------+
|kernel.js the composition root |
|the one module that names store / sources / |
|runner / tools / config together |
+--------------------------------------------+

v v v v v
+----------+ +----------+ +----------+ +----------+ +----------+
|store/ | |sources/ | |runner/ | |tools/ | |config/ |
|(5) | |(3) | |(11) | |(8) | |(2) |
+----------+ +----------+ +----------+ +----------+ +----------+

v
+--------------------------------------------+
|rules/ (25) PURE CORE |
|no node: no ctx |
|plain Node can import it |
+--------------------------------------------+
^
| canvas imports rules/ and nothing else

BROWSER | served over HTTP, ETag revalidated
+--------------------------------------------------+
|src/canvas/ (12 modules) |
|canvas.js core.js html.js |
|markdown.js relay.js session.js |
|teams.js team-panels.js |
|scene.js view.js artwork.js |
|actions.js |
+--------------------------------------------------+

Every arrow is a dependency and every one of them points at rules/: 25 (the pure core) + 5+3+11+8+2 (the five host layers) + 12 (the canvas) = the 66 modules across the 7 layers the gate asserts.

| Layer | Modules | The invariant the gate asserts |
| rules/ | 25 | The pure core: no node:, no ctx — importable by plain Node |
| canvas/ | 12 | Browser side: no node:; it may read the pure core and nothing else, importing any host layer is a boundary crossed |
| store/ | 5 | Host side: must not enter the serve allowlist (entering it means shipping it to the browser) |
| sources/ | 3 | same as above |
| runner/ | 11 | same as above |
| tools/ | 8 | same as above |
| config/ | 2 | same as above |
| all layers | — | Dependencies point inward only; no module may import canvas/ (it is a leaf, not a library) |

dsh-flow (plain JS, no runtime dependencies)
├── src/
│ ├── rules/ # 25 — the pure core: K1–K14 + the event protocol + projection + reconcile │ │ # entities / gates / project / reconcile / coverage / delivery …
│ ├── store/ # 5 — team registry + append-only log + mailboxes + snapshot + import report │ │ # → ctx.flowTeams (core) │ ├── sources/ # 3 — the team source registry: this deployment's log / an imported .agent-teams │ │ # → ctx.flowTeamSources (seam, registry form) │ ├── runner/ # 11 — the executor seam: interface.js defines it, manual / subagents implement it │ │ # → ctx.flowRunner (seam, one of two at composition time) │ ├── tools/ # 8 — registration and implementation of the 13 flow_* tools │ ├── config/ # 2 — deployment configuration: the profile table + the two hooks turning config into events │ └── canvas/ # 12 — the canvas page (the only layer ever served over HTTP) │ ├── canvas.js # entry: host bridge, live replies, polling, boot │ ├── core.js # shared state, geometry, localStorage, host bridge, member colours │ ├── html.js # escaping and other pure helpers (split out so panel modules are testable in Node) │ ├── markdown.js # Markdown rendering (with ■ section normalisation) │ ├── relay.js # agent envelope parsing (relays / member messages / subagent notices) │ ├── session.js # session projection data layer (incremental merge + cursors) + turn cards + branch layout │ ├── teams.js # team polling (live → snapshot fallback) + hierarchical region layout │ ├── team-panels.js # pure rendering for the panels (K9/K10/K11, attempt timeline, damaged lines) │ ├── scene.js # scene assembly: requirement timeline + nested regions + typed edges │ ├── view.js # camera, virtualised mounting, node rendering, inspector, main render │ ├── artwork.js # artwork mapping (role keyword → profession image, state → action image) │ └── actions.js # interaction: draft / follow-up / branch / archive / quick phrases / select-and-ask ├── index.js # host-side entry: WorkspaceStore + session event projection + envelope parsing + routes ├── client.js # client: one conversation.view tab (an embedded iframe) + theme following + action relay ├── engine.js # canvas engine: camera / gestures / viewport culling / edge geometry / drag binding ├── theme.css # design tokens (one set of variables for light/dark) + all component styles ├── assets/ # 15 artwork images (9 professions + 6 states) ├── kernel.js # the composition root: the one place that puts store / runner / tools side by side ├── cordis.patch.yml # inserts the dsh-flow service and gives it its config └── package.json # dsh.bundle.patch + dsh.client.inject

Dependencies point inward only. rules depends on no layer; store / sources / runner / tools / config depend only on rules; canvas reads only rules (team data arrives over HTTP, not by import); the composition root depends on all of them. Every one of those edges is asserted in the gate.

Three rules break most quietly, and the gate handles each explicitly: a canvas module doing import 'node:fs' is green everywhere under pnpm test and only a browser would notice; a module missing from the serve allowlist is only discoverable through a 404; and a host module importing canvas/ backwards is green in Node too. So these are not conventions, they are assertions.

Here is how the two channels reach the canvas. Sessions arrive twice — live over postMessage and stored over map-api — while teams arrive only from the plugin's own store:

NODE host process <stateDir>/<teamId>/
ctx.sessions ctx.workspaces events.jsonl <- TRUTH, append-only
ctx.subagents ctx.llm state.json <- checkpoint, rebuildable
manifest.json mail/
| subscribe: session/created | append + fold
| session/event v
v
+----------------------+ +--------------------------+
|index.js | |store/ |
|session projection | |append-only log + snapshot|
+----------------------+ +--------------------------+
| flow/workspaces.json | GET /map-api/teams (1 Hz)
| POST /map-api/projection {cursors} | GET .../tasks/:k on demand

HTTP boundary | |
------------------------------+----------------
BROWSER v
+--------------------------------------+
|src/canvas/ the Agent Canvas tab |
|one page - one engine - one figure |
+--------------------------------------+
^ postMessage: live session state,
| theme, locale, RPC back to host

+----------------------------------+
|client.js |
|the conversation.view tab |
|one iframe + action relay |
+----------------------------------+

That the team channel has only one source is the point, not an omission: if team records came from the host session, this would be a mirror again.

How team activity is observed: through its own channel, without emitting session events. The source of truth is <stateDir>/<teamId>/events.jsonl (an append-only event log), projected to GET /dsh-flow/map-api/teams for the canvas to read.

No dsh-flow/ event is written into the session because the host does not accept one: KNOWN_SESSION_EVENT_TYPES is a closed set generated at build time, whose comment states outright that a downstream plugin's events are "outside this list by construction" and that the registration surface is "deferred until there is a real consumer"; and Session.append offers no way to set the envelope's ignorable flag — without it an unrecognised type is treated as required, and the reader would rather refuse to reconstruct the entire session. So such an event would either be dropped or damage the log it landed in. The full argument is in the comment at the end of kernel.js.

Reference

HTTP routes

All go through the Host check (localhost / 127.0.0.1 are allowed by default).

| Method | Path | Purpose |
| GET | /dsh-flow | 302 → /dsh-flow/ |
| GET | /dsh-flow/ | the canvas page |
| GET | /dsh-flow/engine.js · /dsh-flow/src/canvas/.js · /dsh-flow/src/rules/.js · /dsh-flow/theme.css | canvas assets |
| GET | /dsh-flow/assets/.png | artwork (only [a-z0-9-]+.png is allowed) |
| GET | /dsh-flow/map · /dsh-flow/map/ | 302 → /dsh-flow/ (old path) |
| * | /dsh-flow/map-api/ | see below |

Canvas API

| Method | Path | Purpose |
| POST | /map-api/reset | Clear every workspace and hide all current DSH sessions |
| GET | /map-api/workspaces | Workspace summary list |
| POST | /map-api/workspaces | Create a workspace { title } |
| GET | /map-api/workspaces/:id | A full workspace (with threads / messages) |
| POST | /map-api/workspaces/:id | Create a node in a workspace { title, parentId?, dshSessionId?, position?, color? } |
| POST | /map-api/threads/:id/branch | Branch from a node { title?, dshSessionId?, position?, color? } |
| POST | /map-api/threads/:id/messages | Append a message { text } |
| PATCH | /map-api/threads/:id | Change title / position |
| DELETE | /map-api/threads/:id | Delete a node and every descendant, and hide the matching DSH sessions |
| POST | /map-api/sessions/sync | Align the canvas with the host's session list { sessions, removedSessionIds } |
| POST | /map-api/projection | Incremental read: { sessionIds, cursors } → only the threads those sessions belong to, each carrying only messages whose rev exceeds the cursor; always returns threadIds so the client can prune archived nodes |
| GET | /map-api/teams | Team snapshot { teams, damaged }: every team currently visible in the source registry, each with its own warnings (the import report); damaged is a deployment-wide count — a team that cannot even reach the canvas is visible only here |
| GET | /map-api/profiles | The addressable profile list (those with a truthy profileCommandName) |
| GET | /map-api/teams/:teamId/tasks/:taskId | One task's attempt timeline (including rollbacks). Fetched on demand, never carried in the one-second poll |
| POST | /map-api/teams/:teamId/plan | Edit a staged plan from the canvas. Shares applyTeamEdits with flow_edit_plan, so K8 judges once |
| POST | /map-api/teams/:teamId/approve | Approve a staged plan from the canvas. Shares applyTeamApproval with flow_approve |

The last two are a person acting on their own team through their own canvas, authorised by the Host check rather than by a session identity — an HTTP request carries no identity, and inventing one would be the appearance of a guarantee rather than a guarantee. Two constraints keep them from being a way around the tool layer: they go through the same applyTeamEdits / applyTeamApproval, and they accept only a source whose canAppend is true (a team imported from .agent-teams is somebody else's record and must not be appended to).

postMessage protocol

Between the canvas page and the host client, only { source: 'dsh-flow', type, ...payload } is recognised, and the origin and event.source must both be the frame this plugin mounted.

| Direction | Message | Payload | Behaviour |
| canvas → host | flow:request-current | — | sends back the current workspace and session |
| canvas → host | flow:open-session | sessionId, seq? | sessions.open() + switch back to the Chat tab + anchor the scroll at seq |
| canvas → host | flow:activate-session | sessionId | sessions.open() without leaving the canvas |
| canvas → host | flow:fork-session | sessionId, atSeq?, requestId | sessions.fork() |
| canvas → host | flow:send-message | sessionId, text, requestId | session.prompt(text, 'queue') |
| canvas → host | flow:create-session | workspaceId?, cwd?, requestId | sessions.create() |
| host → canvas | flow:theme · flow:locale | dark / locale | theme and locale following |
| host → canvas | flow:workspaces · flow:current-session | workspaces / current session | the canvas' input |
| host → canvas | flow:live-reply | sessionId, running, text | the live reply while generating |
| host → canvas | flow:forked-session · flow:created-session · flow:message-sent · flow:bridge-error | requestId, … | settling an RPC the canvas started |

Calls carrying a requestId are awaited by the canvas' dshRpc(), with a 20s timeout; opening the canvas page directly in a browser (outside DSH) rejects immediately.

Local storage

| Location | Contents |
| <DSH home>/flow/workspaces.json (+ .lock) | Workspaces / nodes / projected messages, stored gzipped (plain JSON is readable too); single writer only |
| <stateDir>/<teamId>/events.jsonl | The team source of truth: an append-only event log (the default stateDir is .dsh-flow) |
| <stateDir>/<teamId>/state.json · manifest.json · mail/ | Checkpoint, metadata, and one mailbox per member |
| dsh-flow:map-card-positions:v3 | Session card coordinates |
| dsh-flow:cluster-positions:v1 | Team region card coordinates |
| dsh-flow:map-collapsed-cards:v1 | Collapsed state |
| dsh-flow:map-quick-phrases:v1 | Quick phrases |
| dsh-flow:map-branch-anchors | Branch anchors |

Everything stored in the browser is visual metadata only; the real session always lives in DSH — clearing the cache loses layout and nothing else.

Design decisions

One canvas, hierarchical arrangement. Sessions and teams are two views of the same piece of work: a team is raised by a session, and tasks are reported in the session. Splitting them into two pages would only mean two engines and two appearances. The engine (engine.js) handles only camera, gestures, culling and edges, and knows nothing about the domain; sessions and teams are both nodes and edges on top of it, with the team growing as a nested region beneath the requirement timeline — hierarchy by containment, time by columns.

Team data lives at home. Team structure lands in this plugin's own append-only log, and the canvas reads its own map-api, mirroring nobody's state. The direct consequence is that uninstalling agent-teams changes nothing: there is no "degrade to a frozen snapshot" middle state, because there was never a second record. When agent-teams is installed it is a read-only source in the registry — a switch, not a dependency.

Relay messages are structured at the projection layer. Envelope parsing lives host-side in index.js rather than in the render layer, because what lands on disk is already dirty data and washing late is worse than washing early; the render layer only applies the same rule to older data as a fallback.

Writes funnel through the draft card. Follow-up / branch / new-session on the canvas all go through that one entry point, and everything else is still done in the host's native conversation — the canvas does not build a second composer.

Artwork is identity. Member names and role keywords hash to the profession artwork in assets/: the member card, the inspector bubbles and the member rows all read from the same source, falling back to a colour block with the first character. The container background follows the light/dark theme.

Development
sh
pnpm test # 434 tests: the rules core, the store on a real filesystem, the scheduler, the tools, the whole kernel
pnpm run build # syntax + layer boundaries + serve allowlist + theme token discipline
pnpm test:diff # 31 differential checks against dsh-agent-teams (same functions, same inputs, conclusions compared)
pnpm test:rehearsal # rehearse against a real .agent-teams directory: everything on disk is reachable

dsh web

click the Agent Canvas tab above the conversation area

After changing anything under src/, always run pnpm run build. It is not only a syntax check: every layer's invariant is asserted there (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, every layer's entry must exist). What those mistakes have in common is being green under pnpm test — a canvas module doing import 'node:fs' is only discoverable in a browser, and a module missing from the allowlist only through a 404.

A boundary nobody asserts decays over time, and what is left is just a directory that happens to sit there.

Changing client.js requires restarting the host: the client bundle carries a rev hash and is only repacked on restart. engine.js / src/ / theme.css / assets/.png go through an in-memory cache keyed by mtime and are revalidated with cache-control: no-cache + ETag — every request confirms the file has not changed (and returns 200 with the new content if it has), so refreshing the page is enough.

Known limits

- The canvas' actual rendering has no automated tests: the 434 tests cover the host side (rules, store, scheduler, tools, kernel) and the pure rendering functions of the panel modules. The canvas' DOM behaviour — dragging, the interaction, clickable task rows, repainting after an edit — has to be walked through in a real browser, and that part has no CI behind it.
- The .agent-teams source is read-only: it can be listed and read, but not edited from the canvas (canAppend is false). It is somebody else's record, and appending would write a second copy of a team into our log under an id we do not own.
- There is no edge between a team and its session: the team record only says which session raised it; the dialogue chain on the canvas comes from the session projection itself.
- Both data locations are single-writer: workspaces.json and the team store each have a cross-process lock and a "modified by another instance" warning, but the lock is advisory and two open copies can still overwrite each other.
- The canvas' own text is not internationalised: tab names follow the host's language, the canvas' internal text is currently Chinese.
- The canvas page carries no navigation chrome: its only entry is the host tab bar. The tab only appears when there are sessions (the host returns null for a blank session). If the host's slots service is missing the tab is not registered, and only the direct link works.

License

MIT © 2026 rootkiller6788 — see LICENSE.
预览分享卡片长什么样 ↓rootkiller6788/dsh-flow
分享 · DeepSeek Harness Hub
rootkiller6788/dsh-flow

拖住长期或复杂工作的,通常不是「工具不够」。而是执行器换不了、状态没有唯一事实来源、边界没有断言——一个跑了几小时的任务…

这是你的仓库?认领条目

回到顶部