Back to home

huxint

dsh-team

Agent teams for DeepSeek Harness: named long-lived teammates over ctx.subagents, a shared task list, a member-to-member mailbox, virtual workspaces, and a live team room in the conversation view

Stars
1
Language
TypeScript
Created
Aug 16, 2026
Updated
Aug 16, 2026

Introduction

dsh-team — DeepSeek Harness 的 Agent Team

团队协作室运行截图

给 dsh 加一支可以指挥的团队:主会话作为 leader,可以派生若干常驻队友(teammate),队友有自己的会话、记忆与工具;成员之间通过邮箱互发消息(消息成为收件人的下一个 turn),共享一份任务列表;会话视图环里多出一个 Agent 团队页签,把花名册、协作关系与消息流画成一间能看见的协作室。设计理念参考 Claude Code 的 agent team(共享任务列表 + 邮箱直连 + 成员自协调),实现完全走 dsh 的能力缝。

整个能力是一个包、一行装配:宿主半边(dsh-team)与浏览器半边(dsh-team/client)从同一个 package.json 构建。

能力一览

  • 具名常驻队友team_spawn 把一个 ctx.subagents 的 continuable 子代变成团队成员——会话、日志、冷恢复、活动驻留与中断都由 harness 负责,队友不占会话树。
  • 成员邮箱team_send 把消息变成收件人的下一个 turn;投递由 leader 权威执行,队友之间的转发有会话预算。
  • 共享任务列表team_task 建 / 改 / 结案,team_list 看花名册、任务与最近流量。
  • 虚拟工作区:共享黑板 + 每人一块私有便笺(team_note / team_board),落进 storage domain,跨重启、不占 turn。
  • 协作室页签:会话视图环里的第三个页签,实时展示成员座位、走动、消息流、工作区与任务板。

设计

队友 = continuable subagent

队友必须有会话(要记忆、要日志、要能恢复),所以问题不是"别建会话",而是"别建一个普通会话"。ctx.subagents.startContinuable() 建的子代天然满足:

  • childSessionMeta 打上 origin: 'subagent' → 会话树不展示,也不参与通用 Host 路由;
  • durable child id + descriptor 由缝持有 → dsh 重启后队友冷恢复,团队不丢;
  • 队友的 transcript 仍可读:内置的 subagent 目录里它们是 continuable 子代,标签是 名字 (角色),协作室里点一个成员就打开。

本插件在这之上只补三样 subagent 缝故意不提供的东西:具名成员成员之间的投递一份共享任务列表

投递权威永远是 leader 的

ctx.subagents.followup() 只认 durable 直接父级的权威,而 leader 正是每个队友的直接父级。所以 peer↔peer 消息也是"由 leader 权威执行、消息源里写明真实发送者"的一次投递:

  • relation 决定谁可以要求投递(managed 只能发给 leader,peer 可以直接发给任何成员),
  • 从不决定谁来执行投递(永远是 leader 权威)。

消息落到收件人自己的日志里,source 是 { kind: 'team-message', form: 'relay', senderSessionId, senderName, chainId, hop }——发送者归属与会话深度都随持久化一起留存。

队友之间不会聊到天荒地老

team_send 把消息变成对方的下一个 turn——两个 peer 互相礼貌回复就能永远转下去,而这一切不在用户的主对话里,没人踩刹车。所以投递带会话预算,是机械约束而不是提示词祈祷:

  • 一次对话(chain):leader 每发一条消息就开一条新链(hop = 0);队友发消息时继承它当前正在处理的那条投递的链,hop + 1。所以"leader 交办 → A 问 B → B 答 A"是同一次对话,而不是三条互不相干的消息。
  • 深度上限maxChainHops,默认 4):一次对话在队友之间最多转这么多手,超了 team_send 直接拒绝。
  • 同一有序对不许来回磨maxChainRoundTrips,默认 2):一条链里 A→B 最多这么多条。
  • 一字不差的重发直接拒:同一条链里同一有序对重复同样的内容,对收件人不产生任何新信息。
  • 出口永远开着:以上三条只管队友→队友。发给 leader 从不拒绝。所以预算不会把"有话要说"的成员困住,它只是把话逼回收敛点——拒绝语本身就是"settle it yourself and report to the leader"。
  • 为什么 leader 那一侧不设限:leader 的每个 turn 都在用户看得见、能中断的主对话里,链走到 leader 就已经收敛了。真正危险的是看不见的横向循环。
  • 深度是持久事实hop 写进投递的 team-message 源里,跟着收件人的日志一起存,所以协作室的气泡上能看到"第 n 跳"——第 3 跳起变警告色。深度不是只有拒绝时才存在的东西,它一直是可观测的。

链的执行状态(谁在处理哪条链、每条边发过几次)是进程内的,随 leader 的 live team 一起建立与丢弃,最多记住最近 64 条链;重启后重新开始——重启本身就断链。

两个虚拟工作区

队友做完一件事,除了发消息告诉别人,还应该有个地方把它写下来。所以团队有两个虚拟工作区——它们不是文件,也不在用户的真实工作目录里:

  • 共享黑板:全队可读可写的具名条目(结论、约定、交接物)。
  • 私有便笺:每个成员一块只有自己能读写的板子,用来把自己的状态带过 turn 边界。

工具是 team_note(写/删,private: true 走自己的便笺)和 team_board(不带 key 读索引,带 key 读全文),leader 和队友都有。

为什么不落在会话日志里:队友的工具调用只落在它自己的日志里,leader 的折叠永远读不到——这正是"任务列表只能 leader 写"这条限制的根源。所以工作区落在 ctx.storageDomain@deepseek-ai/dsh-storage-domain):写入先落盘再更新内存,跨重启存活,队友可以直接写,而且不占任何人的一个 turn。

这也是防死循环的正向出口:留一条笔记不花 turn、不花会话预算;给同级发消息两样都花。提示里明确这么讲。

面板看到的是快照:durable 工作区不在任何会话日志里,所以协作室里的"共享工作区"是主会话最后一次读或写时的样子——team_note / team_board 的结果 meta 带着整份共享索引(只有索引与首行预览,不带正文;私有便笺从不进投影)。面板上写着快照时间,不假装自己是实时的。

工作区随团队走:解散整队清掉这支团队的全部区域,解雇一名队友清掉它的私有便笺(team/changed 带上 ended / removedMember)。没有 storage-domain 的部署照常用团队,只是这两个工具根本不会注册——没人会看到一个写不下去的工具。

leader 不在场时,队友不失联

主会话被卸载(用户关掉、进程重启、residency 回收)而队友还在跑,是常态而不是异常。投递确实停了——每次投递都跑在 leader 的父级权威上,没有 live 的 leader 就没有权威——但"投递停了"不等于"你没有团队":

  • 拒绝语分得清两件事LEADER_AWAY("团队还在,只是主会话没加载,把结果写进 team_note,leader 回来会读到")与 NO_TEAM("这里还没有团队,用 team_spawn 开一支")。
  • 身份提示段落同样分得清:花名册读不到时,段落会说"团队还在、主会话没加载、把活收个尾写进黑板";真正被解雇的成员才会看到"团队已经不在了"。段落每次组装都重算,所以 leader 一回来,下一步就自动恢复成完整花名册。
  • 工作区不受影响:队友的席位(leaderId / memberId / 名字)是组装时就捕获的,不需要每次调用回去问 leader。所以 team_note / team_board 在 leader 不在场时照常可写可读——这正是把它做成 durable 而不是会话日志的收益。

只折叠一次

rc.6 的 Session.append 无法把事件标成 ignorable,因此仓库外插件不能新增会话事件类型(否则卸载插件后旧日志会拒绝加载)。所以团队的每条持久事实都骑在 harness 已认识的词汇上:

  • 团队工具自己 tool/resultmetapresentationMeta整个实体,不是增量);
  • user/message 的消息源(team-message / 内置的 subagent-report / subagent-settled)。

src/fold.ts 是唯一的折叠实现,src/projection.ts 把它注册成 team session projection:宿主算一次,框架负责推给浏览器(历史尾巴基线 + session/projection 帧),客户端只决定"现在显示哪个会话的值"。

工具作用域

  • leader 侧:agent/created 时把 6 个工具注册进该 agent 自己的 ctx(普通 subagent 继承全局注册表,但不继承别人的 agent scope,所以看不到)。
  • 队友侧:registerContinuableSetup 在子代未发布的组装窗口里注册 team_sendteam_list、身份提示段落与(可选的)思考强度;不属于任何团队的子代拿到的是空的 disposer。

模型看到的工具

工具谁能用作用
team_spawnleader派生队友:name / task / relation,可选 rolepersonamodelreasoning_effort
team_sendleader + 队友邮箱。收件人写队友名、成员 id 或 leader;消息成为对方的下一个 turn,不等回复
team_taskleader共享任务列表:不带 task_id 是新建,带则更新;assignee 支持名字或 id
team_relationleadermanagedpeer 升降级
team_dismissleader解雇一个队友;不给参数则解散整队(中断当前工作,transcript 仍可读)
team_listleader + 队友花名册(含 running / idle / ready 实时状态)、任务列表、最近邮箱流量
team_noteleader + 队友往共享黑板(或 private: true 的私有便笺)写一条笔记;不给 text 即删除
team_boardleader + 队友读工作区:不带 key 是索引,带 key 是全文

队友汇报用的是 harness 内置的 report@deepseek-ai/dsh-tool-subagent-report 在子代作用域里注册,且不受 toolFilter 影响)——汇报会以 subagent-report 源落进 leader 日志,被同一份折叠记进消息流。

每个写操作的工具卡片都有自己的标题(Spawn teammate Alice / Alice joined the team / Message Alice / New task: … / Team disbanded),失败时直接把拒绝原因写在卡片上;写操作一律 isConcurrencySafe: () => false,不会被并行调度打散。

模型与思考强度

  • model:走 AgentOptions.model,只影响这一个队友;省略则继承 leader 的模型。
  • reasoning_effort:不是 AgentOptions 的字段(它是请求头状态),所以队友作用域里挂一个 agent/request waterfall,把 reasoningEffort 钉在这个队友自己的每次请求上。派生时就用 ctx.llm.resolveModelInfo() 校验该模型是否提供这个强度,当场失败而不是等到队友第一次发请求。
  • 强度记在成员事实里(随折叠持久化),冷恢复后仍然生效——subagent descriptor 本身不存这个字段。

团队协作室(视图页签)

团队协作室是会话视图环里的第三个页签对话 / 轨迹 / Agent 团队src/client/)。点开是一间占满整个标签页的办公室:每个成员有自己的工位、自己的电脑,坐在自己的椅子上;要跟谁说话就站起来走过去。消息流、共享工作区、任务板收在右侧竖排的三扇门(dock)后面,点开哪扇,哪份账本就以一块磨砂玻璃抽屉盖在房间右侧展开。

  • 有团队才有页签:客户端跟随器发现当前会话的 team 投影里有成员,才把这一条 conversation.view 注册进去;团队解散就把注册撤回(未知的 view id 会回落到对话页,撤回不会把读者卡住)。普通会话的视图环完全不变。

  • 房间就是整个页面:协作室在屏幕上时,插件在 conversation.composer 链的最后一位交出一个空的输入区——房间不跟一张输入卡片分屏,也就不需要什么"剧场模式"。座位是引用计数的:离开页签立刻还回去,两次挂载重叠也只占一个。

  • 一人一座,座位是算出来的:leader 拿第一张桌,队友按花名册顺序往后排(1–3 排,靠后的一排画得小一点,房间就有了纵深)。桌位、过道、纵向通道、休息角全是 room.ts 里 0–100 的纯几何,不量 DOM,所以画面只是持久状态的一个函数。

  • 每张桌子都是一整套:桌面 + 键盘 + 该席位强调色的马克杯 + 一沓纸,桌上立着显示器(加大号的屏幕、支架、底座),桌前一把办公椅(SVG 画的网面椅背、腰靠、气杆、五爪脚轮,还会偶尔轻轻上下落一下)。全部是 token 画出来的形状,没有一张贴图。

  • 屏幕在左肩,人在中间:显示器立在工位左前侧、屏幕面向读者;成员背对房间坐在桌前,工作时不需要看清谁的脸。图层从近到远是椅背 → 坐着的成员 → 桌面和显示器——显示器站在成员左肩外侧,再大也不会挡住人,人也正好夹在桌子和椅子之间。成员一旦站起来走路、去休息角、或者正在跟人说话,才转过身来露脸。

  • 人是戴海兽头套的人:一具人形(鞋、裤子、衬衫、两条胳膊)+ 一头侧面的鲸鱼或鲨鱼当兜帽——只有侧着才像它。衣服按席位轮流换衬衫 / T恤 / 毛衣 / Polo / 卫衣 / 束腰外衣六种款式,鞋也轮流换运动鞋 / 短靴 / 乐福鞋,并且都跟着该席位的强调色走。leader 戴蓝鲸(喷水),队友按席位轮流拿到虎鲸(背鳍 + 白眼斑)、座头鲸(长鳍 + 头瘤)、独角鲸(长牙)、白鲸(额隆)、抹香鲸(方头)、鲨鱼(背鳍 + 鳃裂)。正面有眉、眼高光、腮红和微笑,背面有头发高光;同一具身体,差别只在头套和细节上。强调色是品牌 token 旋转出来的hue-rotate),不写死颜色。背身时头套朝另一边、后脑勺有头发、腿收进椅子里。

  • 走路是真的走routeBetween 把一次移动拆成横竖两种腿——先退到自己的过道,沿房间一侧的纵向通道过去,再从目标的过道拐进去,绕开家具而不是从桌子上飘过去walk.ts 每次只把一条腿交给样式表(left/top 线性补间 + 一个 setTimeout),所以浏览器负责动画、React 一条腿只渲染一次;朝向随这条腿的方向左右翻。

  • 传消息 = 走过去说:最新一条投递会让发信人从自己的座位走到收信人桌边(站在旁边一步,谁也不挡谁),到了才开口(头顶一句截短的话),收信人转头听(···);这趟差事过去之后它再走回自己的位置。走的是本人,不是复制体——房间里每个成员永远只有一个。

  • 站在哪儿,就是它此刻在做什么

    成员的状态房间里的样子
    正在跑一个 turn坐在自己桌前,屏幕亮着它那份预设画面
    刚收到一条消息留在自己桌前读(屏幕上就是这条消息)
    自己刚汇报完、名下没有未完成任务起身去休息角(沙发、茶几、绿植、饮水机)
    闲着没事坐着打盹(zZ
  • 屏幕上放的是预设画面:每个席位固定一种——leader 看仪表盘,队友轮流拿到代码、文档、邮件、表格、终端——像素条全部由主题 token 混出来。它此刻在做的那件事(进行中任务标题,或最近一条发给它的消息)也写在屏幕上;没活干的屏幕是暗的。

  • 不标区域,靠家具说话:后墙上有透着海水的舷窗、白板、挂钟和一组放满书的墙架;右手边是休息角的地毯、沙发、落地灯与饮水机。房间里没有一个"工作区""休息区"的字牌——看得出来就不用写。

  • 名字在脚下:名牌贴在成员脚边的地面上(不会飘到别人桌子上),未完成任务数挂在肩上,运行状态点在身侧。

  • 右侧三扇门,各有计数:信箱 / 工作区 / 任务板各带一枚数字徽标(消息条数、笔记条数、未完成任务数);抽屉关着的时候来了新投递,信箱那扇门会亮起呼吸圈,打开即消。同一时刻只开一扇,再点一次或点抽屉的 × 就收回去。整个房间的 sprite 被关在自己的 stacking context 里,dock 和抽屉永远浮在它们上面——哪怕有成员走到休息角,也不会挡住消息流的按钮或抽屉内容。

  • 信箱是日志,不是聊天(信箱抽屉里):顶上一条花名册,每人一行——头像、名字、此刻在跑还是闲着、以及它最新的一句话(截短);下面是流量本身,一条一行、越新越靠下,长内容截到一行、整句挂在 title 上。全队都写在同一侧:右边属于读者,而读者不在这里发言。汇报 走成功色边条,已收工 虚线弱化,第 3 跳起的 hop 徽标变警告色。鼠标停在一行上,对应成员的工位会亮起——同一份状态的两种投影互相指认,玻璃抽屉正好让你同时看见两边。

  • 共享工作区是软木板(工作区抽屉里):每条笔记是一张钉着图钉的便签,微微歪着,鼠标停上去才摆正;作者一栏带着它的小头像,悬停同样点亮本人;标题旁边写着这份快照的时间。

  • 任务是泳道(任务板抽屉里):待办 / 进行中 / 已完成三条泳道,道头一枚状态色圆点;卡片上指派人带小头像,结案备注跟在旁边。

  • 点任何一个成员开对应 transcript(走 durable 的 subagent 地址,目录没拉取过就刷新后重试),点 leader 回主会话,aria-current 标出你正在看的那个。

  • 解散就关灯:不带参数的 team_dismiss 折叠出 ended,团队视图折成空,房间和页签一起收走——你此刻正读着某个队友的 transcript 也一样。

  • 全部动效在 prefers-reduced-motion 下关闭(走路的成员直接站到终点,抽屉直接出现)。

导航进队友会话时页签不会消失:跟随器认得"当前会话是这支团队的成员",只把"你在这儿"的标记挪过去,同时改订 leader 的 team 投影——被读的那个队友自己折不出团队,只有盯着 leader,房间才既是活的、又能在团队解散的那一刻关掉。leader 本身没加载时,房间保留它最后看到的样子。

安装

克隆、构建,然后挂进 web profile:

git clone https://github.com/huxint/dsh-team.git
cd dsh-team
pnpm install
pnpm run build
dsh plugin --profile web add link:$PWD

包自带 cordis.patch.ymlpackage.json 里的 dsh.bundle.patch 指向它),plugin add 装进去即生效。web profile 的 base bundle(@deepseek-ai/dsh-base)已经带齐所需的 continuable subagent provider、session projection 与持久化;虚拟工作区还需要挂载 storage-domain(@deepseek-ai/dsh-web-app 已组合),没有它团队其余能力照常,只是 team_note / team_board 不会注册。

验证装配与启动:

dsh --profile web --dump-config | grep 'id: team'
dsh --profile web

在会话里让主会话调用 team_spawn 派生第一名队友,视图环里就会出现 Agent 团队 页签。

改完源码重跑 pnpm run build:宿主行要重启 dsh,客户端 bundle 刷新页面即可。卸载:

dsh plugin --profile web remove dsh-team

配置(cordis.patch.yml 可覆写)

默认含义
providerspawn派生队友用的 ctx.subagents provider(base bundle 提供 spawn
maxTeammates8单个 leader 的在册成员上限(1–64)
maxRecentMessages50折叠保留、协作室显示的邮箱条数上限(1–1000)
maxChainHops4一次队友间对话最多转手几次(1–64);发给 leader 不计入
maxChainRoundTrips2一条链里同一有序对最多几条消息(1–64)
maxWorkspaceEntries32单个工作区区域(共享黑板 / 一块私有便笺)的条目上限(1–500)
maxNoteChars4000单条笔记正文长度上限(200–200000)

已知限制

  • peer↔peer 的历史只在成员自己的会话里:leader 的折叠只看得见 leader 可见的流量,协作室同理——所以横向对话只有被预算拒绝后成员主动上报时才会进入 leader 的视野。
  • 任务列表由 leader 写:队友的工具调用落在自己的日志里,leader 的持久状态读不到;队友用 report 汇报,由 leader 记账。需要队友自己留下的东西,走共享工作区。
  • 协作室里的共享工作区是快照:队友直接写 durable 工作区,不经过任何会话日志,所以面板显示的是主会话最后一次读写时的索引(面板标了时间)。
  • 输入框是"接管"不是"删除":协作室在屏幕上时,插件在 conversation.composer 链的最后一位(优先级 100)交出一个空节点。这是链式槽的正门——不碰宿主 DOM——代价是它排在所有其它接管者之后:真有一个待批准的审批要接管输入框,座位仍然归它,你在这个页签里照样能回答被卡住的 agent。
  • 解散清得掉房间,清不掉宿主的子代理目录:整队解散后本插件的房间与页签一起消失(折叠出 ended,团队视图折成空),但 harness 自己的子代理面板("N 个子代理")是由 durable 会话喂出来的,ctx.subagents 与会话存储都没有"释放 / 遗忘一个子代"的接口——插件没有正门能抹掉那些条目。这也正是"解散之后 transcript 仍然可读"的另一面。
  • leader 没加载时冷恢复的队友拿不到团队工具:花名册是从 leader 的日志折出来的,组装那一刻没有 live leader 就没有花名册,这个子代只会当作普通 subagent 组装(已经组装好的队友不受影响,见上)。
  • Code Mode 下的团队调用不入折叠:折叠读的是团队工具自己的 tool/resultrun_code 里的嵌套调用不产生这些行(base bundle 默认不含 Code Mode)。
  • 工具成功但结果没落日志(极端故障)会留下一个孤儿成员:活的花名册有、折叠没有,重启后消失。
  • 不嵌套:队友不能再开自己的团队(NESTED_TEAM)。

开发

pnpm run typecheck   # 源码 + 测试
pnpm run test        # 折叠 / 投影 / 服务授权矩阵 / 工具契约 / 队友组装 / 会话预算 / 虚拟工作区 / 房间几何与走路 / 客户端跟随与页签 / 协作室与抽屉
pnpm run build       # 宿主 ESM + 浏览器闭包工厂(构建期强制客户端 bundle 纯净性)
pnpm run check       # 三件一起

构建与类型针对 npm 上的 @deepseek-ai/dsh@0.1.0-rc.6。遵循 harness 的插件纪律:注册即 effect、能力缝三角色、事件全 JSON 整值、模型可见即落日志、配置无硬编码。

License

MIT