中文 | English
dsh-superagent
DeepSeek Harness 的长程运行插件:输入一个 goal,由模型规划出可并行的 subtask,最后合成一份报告——而且宿主重启之后,这次 run 还能接着跑完。
这是什么,解决什么问题
单个 agent 循环只有一个上下文、一条执行序列。需要几条独立工作线的目标会在里面卡住,一次崩溃就会让这一轮什么都留不下、无从续跑。这个插件接收一个 goal,让模型把它拆成若干 subtask,最多同时跑三个子 agent,再把它们的报告合并成一个回答。run 的状态和花费都写到磁盘上,所以宿主重启之后 superagent_status 仍然能找到这个 run,superagent_resume 只把没做完的部分补完。每个 run 都有硬性的调用次数与 token 预算,会打印实测用量;一旦你为对应路由配好了价格,它还会打印花费。
以下情况不要用它:一次模型调用就能答完的时候——那样你只是在为 planner 和 aggregator 额外付钱。必须在一轮之内结束的工作、中间需要人工审批的步骤、需要嵌套委派的任务(子 agent 深度上限为 1)、以及想要宿主内嵌面板的人,也不适合;下文那个可选的工作区面板是回环地址上的网页,不是宿主 UI。子 agent 运行时用的是宿主的凭据和文件系统,这里不是沙箱。
它不添加宿主内 UI(工作区面板和对话控制台都是回环页面),也不修改宿主代码:对外的接口就是十二个 superagent_* 工具,另外还有六个可选的运维工具(终端面板、HTML 报告、安装自检、run 归档、git 同步、工作区面板),它们读的是同一批 run 文件。浏览器与桌面控制是独立的一层:设置 runtime.enabled 后会再注册十四个工具;默认值是 false,此时这些工具一个都不存在。
个人项目,与 DeepSeek 及 harness 维护方无关。MIT 许可,© 2026 Admire (LittleChenLiya);详见许可证。
安装
三种安装方式,dsh plugin add 和桌面应用的 Add plugin 对话框都接受:
- npm 包名:
@xiangyixie/dsh-superagent@0.14.4,从 npm 安装。请写明版本号,原因见下文。 - GitHub 仓库:
https://github.com/LittleChenLiya/dsh-superagent,从仓库安装。需要固定版本时加#v0.14.4。 - 本地目录:
file:/absolute/path/to/dsh-superagent,拷贝本地 checkout。改过源码之后要重新 add 一次。
最常用的一条命令:
dsh plugin --profile web add @xiangyixie/dsh-superagent@0.14.4
从插件索引安装(可选)
插件索引把包名、版本和固定的 commit 写在一个 JSON 里,桌面应用的「发现」页可以直接读取:
- 打开「发现」页,点「添加索引」,填入
https://raw.githubusercontent.com/LittleChenLiya/dsh-superagent/main/plugin-index.json。 - 在列表里找到「超级智能体」并安装;之后有新版本也在这里一键更新。
索引里的每个版本都绑定完整的 40 位 commit,所以一键更新拿到的是确定的那一份代码,而不是浮动的分支头。
升级:先卸载,再装新版
升级是 remove-then-add,不是原地覆盖:先卸掉旧的那份
(dsh plugin --profile web remove @xiangyixie/dsh-superagent),再装新版本。同一个 profile 里已有的其他插件也是同样的规则——要先卸载再重新安装,不要指望对话框或 add 会替你替换掉它们。
版本号要写清。不写的话,pnpm 的 minimum release age 策略会跳过最近几小时才发布的版本,装上一个"次新"的版本,结果是 add 成功了,但拿到的不是当前版本。如果你更想跟最新发布,就把它加进该 profile 的 pnpm-workspace.yaml 里的 minimumReleaseAgeExclude;否则请把上面的版本号改掉。用下面这条命令确认实际装上的是哪个版本:
npm ls --prefix <DSH_HOME>/profiles/web @xiangyixie/dsh-superagent
装完或更新完,必须重启 DSH 才生效:桌面端完全退出应用再打开,命令行宿主停掉 dsh web 再启动。插件只在宿主启动时加载一次,替换磁盘上的文件不会改变已经在跑的进程。superagent_status 会在「磁盘版本 ≠ 宿主已加载版本」时多打一行 note: ... restart DSH;但这个检查本身也要新版本代码在运行才存在,所以升级之后仍以重启为准。
离线机器或需要固定制品时,改装打好的 tarball——它与 registry 提供的内容逐字节一致:
npm pack # writes xiangyixie-dsh-superagent-<version>.tgz
dsh plugin --profile web add ./xiangyixie-dsh-superagent-<version>.tgz
需要执行命令时开启 commandWorkerPermission
在默认的 inherit 权限下,worker 跑不了 npm test 这类命令:宿主以管道 stdio 启动子进程时会被 workspace-write 沙箱拒绝(EPERM)。要让计划里声明了命令能力的 worker 真正执行命令,就打开 commandWorkerPermission(例如设为 danger-full-access)。它只影响声明了该能力的 worker,planner、aggregator、验收判定和其他子 agent 仍然继承原权限。完整的机制、边界和风险见按 worker 设置命令权限。
环境要求
- Node
>= 24.9.0。DeepSeek Harness 低于这个版本拒绝启动,所以本插件也一样。 - DeepSeek Harness
>= 0.2.2-rc.1 < 0.3.0,并带 base bundle(即tools、agents、subagents三个服务)。 - 一条能调用工具的模型路由,且该路由的 API key 已放进环境。默认 profile 使用
deepseek-official/deepseek-flash,读取DEEPSEEK_API_KEY。
快速开始
五分钟,从空机器做到一次跑完的 run。下面每条命令都能在干净机器上执行——这里也是从零起步的完整路径,包括为什么不要按名字安装 npm 上那个无关的 dsh 包。
0. 不要按名字从 npm 安装 dsh
# WRONG: npm's `dsh` is unrelated ("a shell written in JavaScript")
npm i -g dsh
# RIGHT: run the DeepSeek Harness by its scoped package, pinned
npx @deepseek-ai/dsh@0.2.2-rc.1 --version
本文其余部分直接用 dsh 指代那条命令;如果二进制不在你的 PATH 里,就换成 npx @deepseek-ai/dsh@0.2.2-rc.1。
# 1. install the plugin into a profile (web, headless, ... whatever you run)
dsh plugin --profile web add @xiangyixie/dsh-superagent@0.14.4
# 2. confirm the bundle row exists in the merged config
dsh --profile web --dump-config
桌面应用的 Add plugin 对话框接受同样这三种形式:
| 对话框中的栏目 | 填写值 | 作用 |
|---|---|---|
| 包名(Package name) | @xiangyixie/dsh-superagent@0.14.4 |
从 npm 安装。要写明版本号(见上文)。 |
| GitHub 仓库(GitHub repository) | https://github.com/LittleChenLiya/dsh-superagent |
从仓库安装。需要固定版本时加 #v0.14.4。 |
| 本地目录(Local directory) | file:/absolute/path/to/dsh-superagent |
拷贝本地 checkout。改过源码后需重新 add 一次。 |
安装的三种形式和升级方式见上文的安装一节。查一下 dump 里有没有下面这一段。如果没有,说明插件装了但没挂载,任何 superagent_* 工具都不会存在:
# == @xiangyixie/dsh-superagent
- id: superagent
name: @xiangyixie/dsh-superagent
# 3. start the host with the key in the environment that starts it
export DEEPSEEK_API_KEY=sk-... # PowerShell: $env:DEEPSEEK_API_KEY="sk-..."
dsh --profile web
这个 key 必须是该 profile 实际使用的那条路由的 key。指向 DashScope 的 profile 用 DASHSCOPE_API_KEY;指向自建网关的 profile 用它自己声明的变量。想新增一条路由,看其他路由。
在会话里发起一次 run:
Use superagent_start with goal "Explain the difference between process and thread in 300 words, with one concrete example" and wait_seconds 120.
回复长这样(数字仅作示意——一次真实 run 的完整记录在 docs/e2e-2026-11-14.md):
run 20261114T051233-a1b2 — completed
goal: Explain the difference between process and thread in 300 words, with one concrete example
plan: planned; subtasks 2/2 completed; max 3 in parallel; 2 attempt(s) each
elapsed: 18.4s
usage: input 4120 (+980 prompt tokens served from cache), output 455, calls 4 (deepseek-official/deepseek-flash: 4 call(s) 4120 in / 455 out)
cost: not computed — no price configured for deepseek-official/deepseek-flash; add a pricing entry in cordis.patch.yml to get an estimate
[1] completed (attempts 1) Processes vs threads
[2] completed (attempts 1) Concrete example
report:
...
默认的 DeepSeek 路由没有内置价格;想加上就参见花费上限与价格,之后下一次 run(或者对这次 run 的 resume)就会打印 cost: ...。cost 是在 run 落定时计算的,所以已经结束的 run 会一直保留 "not computed" 那一行。usage: not recorded 只会出现在一种已结束的 run 上:它的子 session 日志完全读不到——见故障排查。
如果 wait_seconds 用完时 run 还没结束,回复会以
next: superagent_status run_id="..." wait_seconds=120 结尾。再调一次就行;无论哪种情况,run 都在继续跑。
想验证恢复能力又不想真的崩一次:在 run 活跃时杀掉宿主,再启动它,superagent_status 会显示 interrupted,superagent_resume 会把它跑完。只有当没有任何活着的进程拥有这个 run 时,它才会是 interrupted:原宿主还在跑的时候,它拥有的 run 不会被动,下一次挂载会把它报成活跃,而不是 interrupted。
两条关闭路径行为不同,E2E 记录里都有:
- 硬杀(或崩溃)。 心跳只有几秒旧,但在写下这个 run 的那台机器上会先检查那个 pid 是否已死,所以下一次挂载会立即回收,并打印
reclaimed: owner pid … is not running and this state directory is local。 如果状态目录是另一台机器写的共享目录,就改为信任心跳,那里的回收最多要等 90 秒。 - 优雅关闭。 如果 run 还在飞的时候宿主消失(例如一次
wait_seconds: 0之后结束的无头单次运行),插件会发现subagents服务不在了,把没做完的 subtask 留在pending,并以interrupted落定这次 run,附上note: the host shut down while this run was active。不会把 attempt 浪费在一个已经无法应答的服务上。这种情况下宿主进程本身可能还会滞留,直到某个无视 abort 的子进程退出;那段等待属于宿主,不属于插件。
配置
在 <profile>/cordis.patch.yml 里写行配置:
- id: superagent
config:
maxSubtasks: 8
maxParallel: 4
maxActiveRuns: 2
| 配置项 | 默认值 | 含义 |
|---|---|---|
stateDir |
$DSH_HOME/state/superagent |
run 状态根目录。 |
provider |
spawn |
planner、worker、aggregator 使用的子 agent 后端。 |
childProvider |
跟随父级 | 可选的子 agent provider 路由覆盖。 |
childModel |
跟随父级 | 可选的子 agent 模型覆盖。 |
childPreset |
无 | 子 agent 使用的 agent preset。preset 决定子 agent 的工具集;这个宿主没有按 run 过滤工具的能力。 |
plannerPreset |
无 | 只给 planner 子 agent 用的 preset。请用无工具的 preset:planner 只读自己的 prompt 并返回 JSON,继承来的工具反而会引出一整轮工具调用而不是计划。需要宿主组合出 agentPresets 注册表。 |
plannerProvider |
跟随父级 | 可选,planner 子 agent 的 provider 路由覆盖。 |
plannerModel |
跟随父级 | 可选,planner 子 agent 的模型覆盖。规划只是一次 JSON 回合,所以这里换个更强的模型,通常比 worker 跑完之后再修计划便宜。 |
plannerTimeoutSeconds |
120 |
单次规划尝试的墙钟上限。超时后这次尝试被停止并记录;不会转而启动一个兜底 worker。 |
plannerMaxModelCalls |
20 |
规划阶段跨多次尝试可以花的模型调用数;0 表示不限制。花完就以"无法规划"失败,而不是把整个 run 的预算抽干。 |
maxSubagentDepth |
1 |
子 agent 还能往下委派几层。1 禁止孙代理;provider-managed 交给后端。 |
commandWorkerPermission |
inherit |
给"计划条目里声明了命令能力"的 worker(shell、bash、pwsh、…)使用的沙箱模式。danger-full-access 让这些 worker 在 workspace-write 宿主上真正执行命令;其他子 agent 保持继承。见按 worker 设置命令权限。 |
maxSubtasks |
8 |
计划中子任务数量的上限。工具参数只能调低,不能调高;硬上限 16。 |
maxParallel |
4 |
同时执行的子任务数。工具参数只能调低;硬上限 8。 |
maxActiveRuns |
2 |
每个状态目录的活跃 run 数,跨共享该目录的 profile 和宿主计算。超出后再 superagent_start 会被拒,除非带上 queue: true,那样会等一个空位。superagent_resume 不计入配额,配额满了也能用,因为它只是把已经存在的活儿干完。 |
maxQueuedRuns |
5 |
同时允许排队等空位的 run 数;硬上限 32。0 关闭排队,超过 maxActiveRuns 的启动重新一律被拒。 |
queueTimeoutMinutes |
30 |
排队的 run 等多久还没开始就失败;期间不花任何钱。 |
queueOnFull |
false |
让每个超过 maxActiveRuns 的启动都排队,而不只是显式传 queue: true 的那些。 |
attempts |
2 |
每个子任务的尝试次数,用完即标记失败。看起来是暂时性的失败(配额、限流、超时、5xx)会依次等 2s、4s、8s 再试。 |
deadlineMinutes |
120 |
墙钟预算;到期后 run 以 partial 或 failed 结束。 |
maxModelCallsPerRun |
80 |
每个 run 子 agent 模型调用次数的硬上限;0 关闭。 |
maxTokensPerRun |
2000000 |
每个 run 的 prompt + completion token 硬上限;0 关闭。 |
pricing |
空 | 每百万 token 的价格,键必须与用量日志里模型的名字完全一致(provider/model)。开箱状态下没有任何价格。 |
resultCapBytes |
32768 |
每个子任务报告保留的字节数。 |
notifyOnComplete |
true |
主动消息的总开关。关掉之后插件永远不会叫醒会话。 |
maxNotificationsPerHour |
3 |
每小时允许的主动消息数。超出上限的 run 会被折叠成一条摘要;0 取消上限。 |
notifyRetries |
2 |
宿主拒绝唤醒时的额外投递次数;同一个会话的消息按顺序投递。 |
notifyRetryDelayMs |
2000 |
上述重试的首次间隔,每次翻倍,上限 60s。 |
autoResumeRounds |
1 |
在报告失败之前,针对"看起来是暂时性"的失败再重试子任务的轮数;0 关闭。 |
childModelFallback |
无 | 这类重试(配额、限流、超时)时使用的模型。不设则沿用配置的路由。 |
memory |
true |
跨 run 的笔记本。见跨运行记忆。 |
memoryScope |
default |
按 scope 分文件,让不相关的项目彼此分开。run 的笔记写在这里;rules 和 profile 会被读进每一个 run。 |
memoryMaxEntries |
60 |
压缩阈值:一次会超过它的写入会把笔记本压缩到这个条数(400 行 / 128 KB 也是硬上限)。 |
memoryMaxAgeDays |
30 |
比这更旧的笔记被忽略,并在压缩时清掉。 |
memoryDigestEntries |
12 |
注入 prompt 的笔记条数。 |
memoryDigestChars |
1600 |
该 prompt 中笔记部分的字符预算。 |
memoryUserProfile |
false |
跨任务的用户画像(事实、偏好、决策),位于 <stateDir>/user-memory。关掉时不构建任何东西,也不创建目录。见用户画像。 |
keepRuns |
0 |
挂载时删除早于这么多天的终态 run;0 全部保留。 |
guard |
开 | worker 的路径与命令策略。每一项默认值和背后的理由见worker 安全护栏。 |
audit |
true |
只追加的审计轨迹。它只观察;关掉只会去掉轨迹,不会改变 run 的行为。 |
policyFile |
存在时为 <stateDir>/policy.json |
权限策略文件。不设置且文件不存在就表示没有策略,即旧行为。 |
auditDir |
<stateDir>/audit |
audit.jsonl 及其导出文件所在目录。 |
controlPort |
关 | 嵌入接口的回环端口;0 让系统挑一个空闲端口。见 docs/embedding.md。 |
workspaceIsolation |
false |
给每个 worker 一个自己的 git worktree,避免并行 worker 互相覆盖。见按 worker 隔离工作区。 |
workspaceRoot |
会话目录(见 workspaceBase) |
workspaceIsolation 打开时,worker 在其中被隔离的仓库。必须是至少有一次提交的 git 工作树。 |
workspaceBase |
session |
隔离的基准目录。session 用当前会话的目录(或 git 从它解析出的仓库根),workspaceRoot 只有指向同一目录时才生效;config 恢复旧行为,始终使用 workspaceRoot。两者不一致时 status 打印 workspace warning,并在日志里说明,不会静默改用另一个项目。 |
workbenchPort |
关 | 本地工作区面板的回环端口;0 让系统挑一个空闲端口。面板自己读写文件,不经过宿主 API。 |
workbenchRoot |
process.cwd() |
面板可以浏览和编辑的目录。它之外的任何路径都会被拒绝。 |
consolePort |
关 | 对话控制台的回环端口;0 让系统挑一个空闲端口。需要宿主的 sessionController 服务。 |
consoleRoot |
先取 workbenchRoot,再取宿主 cwd |
控制台的对话会话所加入的目录。 |
consolePreset |
宿主默认 | 控制台会话挂载的 agent preset id。工具跟着这个 preset 走,不跟本插件走。 |
boardPort |
关 | 任务看板的回环端口;0 让系统挑一个空闲端口。一个页面里放子任务卡片、待办收件箱和内置模板。见任务看板。 |
boardRoot |
先取 consoleRoot,再取 workbenchRoot |
看板自己的对话会话所加入的目录,也是从模板启动的 run 的工作目录。 |
schedulerTickSeconds |
30 |
检查活跃计划是否到点的频率,单位秒(1–3600)。没有计划之前不会武装任何东西,所以对没有定时任务的安装没有影响。 |
schedulerHumanPriority |
true |
计划 human_priority 的默认值:定时 run 不能占掉最后一个空闲并发位。 |
schedulerMaxConsecutiveFailures |
3 |
连续失败多少次后计划自动暂停并只通知一次(1–20)。 |
切换模型
模型选择是配置,不是代码。默认情况下子 agent 继承会话的模型,即 profile 的 agent-default-model。要把子 agent 钉在特定路由上,两个键都设,然后重启宿主:
- id: superagent
config:
childProvider: dashscope
childModel: qwen-flash
插件里没有编译进任何模型 id。API key 只从路由指定的环境变量读取;插件从不保存 key。默认 profile 的路由是 deepseek-official,读取 DEEPSEEK_API_KEY。
其他路由
上面的例子假设 profile 里已经装了 DashScope 路由。它不属于 base bundle:默认 profile 没有 dashscope provider,光有 DASHSCOPE_API_KEY 也不会变出一个来。先装好那条路由(见 harness 关于 LLM 插件的文档),再在 childProvider 里写它的名字。
子 agent 的工具与递归
maxSubagentDepth: 1 会作为 maxDepth 传给宿主,所以 worker 无法再创建自己的子 agent。这条限制由宿主强制执行,而不是靠 prompt 劝说;如果某个 provider 做不到,superagent_start 会直接报错失败,而不是悄悄无限制地跑下去。
在 dsh@0.2.2-rc.1 上没有强制的工具白名单。宿主的 subagent 启动请求里没有工具过滤字段,它自己的 subagent 工具也拒绝 toolFilter 配置,并指向 agent preset。因此 childPreset 才是收窄子 agent 工具集的受支持方式;没有专用 preset 时,worker 继承会话的工具。superagent_* 工具仍在它们的工具列表里,但管理 run 的那几个(superagent_start、superagent_resume、superagent_cancel、
superagent_followup、superagent_inject、superagent_answer)会拒绝从 worker 会话中运行;只有顶层 agent 能管理 run。
按 worker 设置命令权限
workspace-write 的宿主沙箱会拒绝以管道 stdio 启动子进程(EPERM)。所以需要跑 npm test 的 worker 在继承策略下做不到,而这个插件也不会为整台机器关掉沙箱。commandWorkerPermission 只对声明了该能力的 worker 抬高权限:
- id: superagent
config:
commandWorkerPermission: danger-full-access
这样设置之后,计划条目中 requires 带命令能力(shell、bash、sh、zsh、fish、terminal、pwsh、
powershell、cmd、run_command、exec_command)的 worker 获得完全访问权。planner、aggregator、验收判定,以及没有这类能力的 worker,仍然保持继承模式。
它的工作原理,以及为什么这是一种"委派技巧"而不是宿主特性:
- 宿主没有按子 agent 设置权限的字段。它有的是继承机制:
subagents.startActivation会在第一次await之前同步地捕获父会话最后的sandbox/mode事件,并把该模式播种给子会话。 - 对一个符合条件的 worker,插件先向父会话追加
sandbox/mode: danger-full-access,调用startActivation,再在finally里把先前的模式追加回去——整个过程在同一个同步 tick 内完成,中间没有await。因此并发调用永远看不到这个临时覆盖,而子会话仍然捕获到了它。 - 两个步骤各追加一条
sandbox/mode事件;插件从不用permissionPresets.set(),因为它的custom状态无法按名字恢复。 - 该子会话的审批策略被钉为
never,这是宿主自己的委派捕获行为,不是本插件做的。 - 每一步都会以
worker.escalate和worker.restore写进审计轨迹,带上runId、subtaskId和原因(所声明的能力)。audit关闭时不写审计行,但升级行为本身不变。
限制:这只适用于进程内、子会话由同一宿主进程中的父会话创建的那种 provider(provider: spawn)。如果上游改变了捕获时机,或者改变了委派继承本身,这里会静默退回继承模式,而不是让 run 失败。另外,当父会话没有显式覆盖时,这个临时事件也会把"部署默认模式"钉进父日志:它记录的是部署默认值,在该默认值变化之前行为完全一致。
按 worker 隔离工作区
并行 worker 是相互独立的 agent,但默认共用一个工作目录:两个子任务改同一个文件就会互相覆盖,最后写的赢。workspaceIsolation 通过给每个 worker 一份自己的 checkout 来解决这个问题。
- id: superagent
config:
workspaceIsolation: true
workspaceRoot: /home/me/projects/api # or C:\work\api on Windows
为什么用 git worktree 而不是复制目录。 隔离难的那一半是把结果合回去。worktree 免费提供了这件事:真正的三方合并、带冲突检测、两侧都保留为 ref,还留一条命令让用户手工收尾。递归复制要么需要 diff3 引擎(本包没有这个依赖),要么直接覆盖主 checkout——而那正是这个特性要防的 bug。worktree 还共享对象库和忽略规则,所以 worker 看到的是真实仓库,而不是它的一份完整拷贝。worktree 放在宿主启动目录下的 .superagent/worktrees/<runId>/w<n>——如果宿主就是在仓库里启动的,那就是仓库本身。宿主的 workspace-write 沙箱以会话 cwd 为可写边界,所以放在状态目录下、或者放在宿主并非在其中启动的仓库里,checkout 的每次写入都会被拒。这个 worktree 池自带 .gitignore,因此 worker 的 checkout 不会漏进用户的下一次提交,也不会让主 checkout 看起来是脏的。
run 期间发生什么。 每个 worker 拿到一个从主 checkout 的 HEAD 创建的
superagent/<runId>-w<n> 分支。它的 prompt 里写明绝对路径,并禁止碰其他任何东西、切分支、commit、merge 或 push。worker 结束时,run 会把它在 checkout 里留下的东西提交掉(所以忘了 commit 的模型也不会丢东西;git hook 被跳过,免得 pre-commit 钩子挡住合并),然后在所有子任务结束之后,逐个把分支合并回主 checkout。
合并冲突时你会看到什么。 合并被中止,主 checkout 精确恢复到原样,里面不会留下冲突标记。结果记录在 run 上,并由 superagent_status 打印:
workspace conflict: [2] src/routes.js, src/index.js — branch superagent/20261115T101500-ab12-w2
kept at <repository>/.superagent/worktrees/20261115T101500-ab12/w2; the main checkout was restored, so nothing
was lost. Resolve with: git -C "/home/me/projects/api" merge superagent/20261115T101500-ab12-w2
该 run 的唤醒消息会用一行说出同样的事。只要有一条分支没合回主 checkout(冲突,或者合并时主 checkout 是脏的),run 就不会报 completed:它降级为 partial,并往收件箱写一条 attention,写明分支、worktree 还在哪、为什么没合,以及手工收尾要跑的命令。什么都不会被丢弃:分支和它的 checkout 会一直保留,直到你合并或删除它们。另外两种情况也照样报告,而不是悄悄丢掉:
- 该合并时主 checkout 还有未提交的改动——所有分支都不合并,因为往脏树上合并会把你的改动和 worker 的混在一起,中止时还会恢复成错误的状态;
- 合并之前 run 被取消或撞上了 deadline——分支保留,resume 会复用同一批 worktree。
基准目录。 隔离默认以当前会话所在目录为准(workspaceBase: "session"),也就是沙箱边界、声明的交付物和合并目标都指向的那个目录;workspaceRoot 只有在指向同一目录时才生效,不一致时 superagent_status 会打印 workspace warning 并说明用了哪个目录,而不是默默在另一个项目里隔离。workspaceBase: "config" 恢复旧行为,始终使用 workspaceRoot。
无法隔离时。 如果基准目录不是 git 工作树、PATH 里没有 git,或者仓库还没有任何提交,run 不会失败。它退回共享目录(也就是旧行为),并且 superagent_status 和通知都会说明:
workspace: isolation was requested but is unavailable — /tmp/notes is not a git working tree,
so a per-worker worktree cannot be created; workers shared /tmp/notes, so their edits can
overwrite each other
直说限制。 dsh@0.2.2-rc.1 不给子 agent 提供 cwd 字段,所以插件无法在 spawn 时交给 worker 一个目录。隔离是一份 prompt 约定加一个真实分支:无视 prompt 的 worker 仍然能改主 checkout,但这种改动会在任何合并之前出现在 git status 里,也破坏不了兄弟分支。关掉隔离时,行为与这个选项存在之前完全一致。
worker 安全护栏
worker 继承会话的文件系统和凭据,所以 superagent 子 agent 的每次工具调用都会经过挂在宿主
tools/pre-execute 路径上的护栏(有 ctx.tools.guard 时走它)。护栏只约束本插件 spawn 的子 agent;父会话不受影响。
- id: superagent
config:
guard:
enabled: true
allowWriteRoots: [] # extra writable roots, absolute or ~-relative
denyPaths: [] # extra read+write denials
denyCommands: [] # [{ id, pattern, reason }] extra regex denials
allowCommands: # explicit overrides; every use is recorded
- id: clean-build
pattern: '^rm -rf build$'
reason: clean build output
maxEvents: 50
默认值,以及为什么这么设:
| 规则 | 默认值 | 理由 |
|---|---|---|
| 可写根目录 | 子会话的工作目录加上操作系统临时目录 | worker 需要自己的工作区和一处草稿区。其他一切都与任务无关,因此拒绝。状态目录故意不可写:worker 不能改写 run 记录或报告。 |
| 始终拒绝(读和写) | ~/.ssh、~/.aws、~/.gnupg、~/.netrc、~/.npmrc、~/.yarnrc、~/.pypirc、~/.git-credentials、~/.docker/config.json、~/.kube/config、~/.config/gcloud、~/.config/gh/hosts.yml、~/.azure、任何 .git/config 与 .git/hooks |
这些是凭据或代码执行入口。工作区再大也不会开放它们。 |
| 系统目录 | 一律不可写(C:\Windows、C:\Program Files、C:\ProgramData、/etc、/usr、/bin、/boot、/dev、/System、/Library、…),操作系统临时目录除外 |
读它们是正常的(win.ini、/etc/hosts);写它们不是开发任务。临时目录之所以例外,是因为服务账号的 %TEMP% 可能落在 C:\Windows\TEMP 下,而 worker 仍必须能在那里写草稿文件。 |
cmd/recursive-force-delete |
当目标是 /、盘符根、~、.、..、*、可写根之外,或包含其中之一时拒绝 |
rm -rf build 和 rm -rf node_modules 是正常的;rm -rf / 和 rm -rf . 不是。一刀切地封掉所有 rm -rf 会让日常清理没法做,所以由目标决定。 |
cmd/disk-format |
mkfs、diskpart、wipefs、format X:、dd of=/dev/sd* |
从来不是开发动作。 |
cmd/system-scheduler |
schtasks /create、Register-ScheduledTask、crontab -r、systemctl enable|disable|mask、launchctl load |
在工作区之外建立持久化。 |
cmd/system-registry |
针对 HKLM/HKCR 的 reg add/delete,以及任何 \Run 键 |
全机范围或开机自启的改动。 |
cmd/system-write |
目标为系统目录的重定向或写 cmdlet,包括 %SystemRoot%/$env:SystemRoot |
抓住藏在变量后面的写入。 |
cmd/permission-nuke |
chmod -R 777 /、递归 chown /、icacls … /grant Everyone /t |
摧毁机器的访问模型。 |
cmd/credential-access |
点名上述凭据位置、id_rsa、id_ed25519、… 的 shell 命令 |
worker 读 key 和写 key 一样有害。 |
拒绝是真实的工具错误,不是静默失败:worker 会收到
[guard] blocked <tool>: rule <id> — <reason>. The call did not run.,事件也会加到 run 上。superagent_status 打印
guard: N tool call(s) blocked; M explicit allow(s) recorded (rule ×count)
以及最多三条被拦下的命令,最终报告会多出一节 ## Safety guard,列出每一个保留的事件。即使 maxEvents 截断了列表,计数器也统计全部事件。
allowCommands 是唯一的越权通道。它胜过拒绝,但每一次命中都会以 decision: allowed 记录下来,并注明它覆盖了哪条规则,所以覆盖行为永远不会隐身。凭据、.git 控制项和系统目录的拒绝不可覆盖;要放行工作区之外的合法构建目录,请加进 allowWriteRoots。护栏基于词法和 realpath,所以工作区内指向被拒路径的符号链接照样会被抓住。
护栏是加在工具参数上的一层策略,不是操作系统沙箱。把写入委托给某个程序的命令(node build.js、一个编译器)仍然能写到那个程序能写的任何地方;要挡住它需要宿主级沙箱,而 dsh@0.2.2-rc.1 没有暴露。护栏拦住的是直接的 write/edit/
apply_patch 目标、shell 重定向,以及上面那些破坏性命令类型——这恰好覆盖了一个乱走的 worker 真正会造成的"误删 rm"和"写进 C:\Windows"。
花费上限与价格
两道硬预算,能在病态目标开始悄悄烧钱之前把它停住。两者都在每次 spawn 子 agent 之前检查,所以重试循环也绕不过去:
- id: superagent
config:
maxModelCallsPerRun: 40
maxTokensPerRun: 500000
pricing:
deepseek-official/deepseek-flash:
input: 0.28 # price per 1M uncached prompt tokens (example)
output: 0.42 # price per 1M completion tokens (example)
cacheRead: 0.028 # optional; defaults to the input price
currency: CNY
价格键会与用量日志里的模型 id 匹配,那个 id 就是
provider/model——superagent_status 会在 usage: 行打印它,从那里复制键即可。先精确匹配,再不济按裸模型名匹配,所以没有精确条目时 qwen-flash 也能给 dashscope/qwen-flash 定价。插件不为任何路由内置价格,因为过期价格比没有价格更糟。
预算触顶时不再启动新的子 agent,并让 abort controller 停掉还在跑的那个,于是花费停在帽子上,而不是再抽干一个 worker。已经完成的报告仍然会被合并。至少有一个子任务完成时 run 以 partial 结束,一个都没完成时以 failed 结束(没做完的子任务留在 pending)。superagent_resume 会用当前配置重建限额,保留已经花掉的部分。从那里继续——这也是提高上限的方式。在真实宿主上,把
maxModelCallsPerRun 从 2 提到 80 再 resume 一个停在 2/2 的 run,它跑完了,且没有丢掉先前的花费。
即使子会话日志读不到,一次 spawn 也算一次模型调用,所以调用预算在日志布局变了的宿主上仍然有效。token 记账仍然依赖日志;读不到时 run 会打印一行
usage note:,说明这些数字只是下界。
花费是根据上表估算的,绝不来自内置猜测。没列出的模型报为 unpriced 并排除在合计之外,所以你看到的数永远是下界:
cost: at least ¥0.0041 (estimate; no price for haoduo/gpt-6.1-sol)
金额按币种分别记账,绝不跨币种相加。同时用了 CNY 和 USD 路由的 run 会每种币种打一个小计:
cost: ¥0.0952 + $0.0248 (estimate; all models priced)
同样的规则适用于 superagent_list 的账目行、实时 watch 的页脚、导出的 README.md 以及 HTML 报告——混合窗口会得到每种币种一个数字,而不是一个单位不明的单一数字。
运行时控制
插件可以驱动浏览器,在 Windows 上还可以驱动桌面。两部分默认都关:runtime.enabled 为 false 时,这些工具一个都不注册,已有的 profile 看到的工具列表与之前完全一样。这里不需要任何 npm 依赖——浏览器通过 DevTools 协议、用 Node 自带的 WebSocket 对话,桌面则通过 powershell.exe 和 user32.dll 触达。
{
"@xiangyixie/dsh-superagent": {
"runtime": {
"enabled": true,
"browser": {
"headless": true,
"allowedHosts": ["localhost", "127.0.0.1"],
"executablePath": null,
"attachPort": null,
"userDataDir": null
},
"desktop": { "enabled": false, "allowActivation": false }
}
}
}
enabled 打开这一层。browser.enabled 和 desktop.enabled 决定哪一半注册;桌面单独设闸,因为它能碰机器上的任何应用。allowedHosts 替代回环默认值——开头的 *. 匹配任意子域。stopFile 和 shotDir 默认是
<stateDir>/runtime/STOP 和 <stateDir>/runtime/shots。
attachPort 和 userDataDir 是两种触达"需要登录的页面"的方式,代价各不相同:
attachPort(例如9222)借用你自己用--remote-debugging-port启动的浏览器。元素列举、点击、输入、截图全都走那条连接,标签页保持自己的窗口尺寸,而browser_close只是断开——它从不终止你的浏览器。如果端口没在监听,调用会失败并给出端口和启动命令;它绝不会背着你退回一个全新的 profile。因为那个 profile 已经登录了你用的东西,每个动作都会继承那份权限。userDataDir为本插件启动的浏览器保留一个专用 profile:登录一次(二维码、短信,站点要什么就什么),之后的 run 复用同一个 cookie jar。这个目录是磁盘上一份真实的凭据存储——请放在你愿意放密码文件的地方,不要指向日常浏览器的 profile。设置attachPort时它会被忽略。
默认仍然是 %TEMP%/dsh-runtime-profile-* 下的临时 profile,所以普通 run 看不到你日常浏览器的 cookie。
注册的工具
| 工具 | 作用 |
|---|---|
browser_open(url, headless?, width?, height?) |
用全新的或持久化的 profile 启动浏览器,或者在你设置了 attachPort 时附着到你的浏览器;打开 URL,返回 URL、标题、视口和一张截图路径。登录、CAPTCHA、短信或二维码页面会以 [attention] 返回。 |
browser_elements(text?, role?, limit?) |
列出可操作元素,带稳定索引、无障碍名、角色、包围盒以及是否在视口内。匹配依据是可见文本、ARIA 名或角色——不只靠 CSS。 |
browser_click(index?, css?, text?, role?, confirm?, screenshot?) |
把元素滚进视口,在它的中心派发真实的鼠标按下/抬起,然后截图。 |
browser_type(index?, css?, text?, value, submit?, screenshot?) |
聚焦输入框,通过浏览器输入管线插入文本,可选按回车,并报告页面现在的值(密码框会被遮蔽)。 |
browser_screenshot(path?, full_page?) |
页面 PNG,带路径、像素尺寸和字节大小。 |
browser_script(expression, await_promise?) |
求值一个表达式并返回其 JSON 值;用于读取,不用于点击。 |
browser_wait(text?, css?, url_contains?, milliseconds?, timeout_ms?) |
等待某个条件或固定时长。 |
browser_await_human(timeout_ms?, poll_ms?, screenshot?) |
等待人去完成登录、CAPTCHA、短信、双因素或二维码检查;可以继续自动化时返回 ok,仍在等待时返回 [attention]。它从不自己解这个检查。 |
browser_close() |
关闭本插件启动的浏览器。在 attach 连接上它只断开,你的浏览器继续运行。 |
desktop_screenshot(window?, path?) |
整个桌面,或用 PrintWindow 抓取某个标题的窗口(被遮挡的窗口也能拿到自己的像素)。 |
desktop_windows(filter?, limit?) |
可见的顶层窗口:句柄、标题、pid、屏幕矩形。 |
desktop_click(x, y, window?, activate?, right?, confirm?) |
按屏幕坐标点击。默认后台;前台点击必须 activate。 |
desktop_type(text, window?, activate?) |
向窗口输入文本,默认后台 WM_CHAR。 |
desktop_keys(keys, window?, activate?) |
按顺序按下具名键(enter、tab、f5、a、1 …)。 |
runtime_status() |
停止状态、已注册的部分、允许的 host、浏览器可执行文件、attach/启动模式、会话。 |
每次浏览器和桌面调用都会往既有审计轨迹(lib/audit.js)写一行,带 source: "runtime"、动作、目标、判定、
结果和耗时。被闸门拒绝的动作写
decision: "deny" 或 "confirm" 以及 result: "blocked",所以日志永远不会声称一次被拒的点击成功了。浏览器那几行还带 meta.session——
动作跑在你启动的浏览器上时是 "attach"(带 meta.port 和它复用的标签页),跑在本插件启动的浏览器上时是 "launch"(带
meta.profile 和 meta.persistentProfile)——因此轨迹永远不会把你的会话和一次性 profile 搞混。需要人介入的页面会写
kind: "attention"、decision: "attention" 和 result: "needs-human"。
闸门
- 紧急停止。 停止文件存在期间——或者设置了
DSH_RUNTIME_STOP=1——每个动作都被拒绝,且拒绝行为会被审计。创建该文件,这一层在下次调用时停下;删掉它即恢复。 - 导航。 只允许
http:和https:,且 host 在白名单内。file://被拒,因为能读本地文件的页面会绕过工作区护栏;javascript:、data:和chrome:同样被拒。 - 上传。
browser_click拒绝<input type="file">,因为随之而来的原生文件选择器无法被驱动,会一路落到用户自己的文件系统上。 - 不可逆动作。 标签里出现 submit、delete、pay、publish、transfer、提交、删除、支付 … 的目标需要
confirm: true。没有它点击不会发生,审计行记录decision: "confirm"。 - 人工检查。 要求登录、CAPTCHA、短信验证码、双因素确认或扫码的页面会在那一点停下动作,返回
[attention],说明页面要什么以及如何交接。检查内容是从页面文本里读的,所以一个带密码框但没有登录字样的普通设置页不会被误判;这里不会去解或去输入那个检查。在启动的窗口或 attach 的标签页里,人就在浏览器里完成,然后browser_await_human轮询到它消失,run 继续。等待中的检查是一次交接,不是失败的 run。 - 焦点。 后台点击或按键从不把窗口拉到前面。前台动作需要
activate: true并且runtime.desktop.allowActivation: true,每次激活都记录在meta.activated里。 - worker agent。
browser_*和desktop_*属于挂载插件的那个会话。护栏会以规则guard/runtime-tool拦下受绑定的子 agent 的调用,除非guard.allowRuntimeTools为 true。
它刻意做不到的事
- 顺手借用你的登录态。 启动的浏览器拿的是
%TEMP%/dsh-runtime-profile-*下的全新 profile,所以你的 cookie、保存的密码和已登录会话根本不在那里。触达需要认证的页面是一个明确选择:attach 到你自己用--remote-debugging-port启动的浏览器并设置browser.attachPort,或者把browser.userDataDir指向一个专用 profile 并在那里登录一次。两者都会让那份权限对之后每个动作可用,也都会出现在审计日志的meta.session里;attach 端口写错会失败并给出说明,而不是悄悄启动一个新 profile。 - 替你通过人机验证。 CAPTCHA、短信验证码、"确认你是真人"的中间页、扫码登录都会停下动作交还给人。它从不替你输码或扫码;它等你做完,然后报告 run 可以继续。
- 规避 bot 检测。 启动的 Chromium 能通过 CDP 特征被检测出来,封自动化站点的站会连它一起封。这里没有修补
navigator.webdriver,也没有伪装指纹。 - 往任何应用里输入。 后台
WM_CHAR能触达经典 Win32 控件;Chromium、Electron、Qt 和大多数游戏会忽略投递的消息。对它们,要么activate: true并接受焦点变化,要么改用browser_*驱动页面。 - 猜坐标。
desktop_*用的是物理屏幕像素。DPI 缩放混杂时,先desktop_screenshot,再从desktop_windows读矩形,而不是自己算。 - 控制 macOS 或 Linux 桌面。 桌面这一半只支持 Windows。浏览器那一半在任何有 Chromium 系二进制的地方都能用。
仓库里带了两份可直接跑的验证。node scripts/runtime-demo.mjs
启动一个本地页面,点一个按钮,从 DOM 读回计数器,往输入框里打字,在不可逆按钮和文件输入上被拒,翻转停止开关,并打印审计行和截图路径。
node scripts/runtime-desktop-demo.mjs 打开一个不激活的测试窗口,向它投递点击和文本,用目标进程自己的状态文件验证两者,
并抓取该窗口。
工具
如果目标有两个及以上可独立交付的产物,或含“先做 A 才能做 B”的前置依赖,就用 superagent_start,不要用宿主自带的 subagent 自己编排:只有 run 会留下计划、并行时序和一份合并报告。顶层会话的第一步会收到一条同样意思的简短提示,子会话不会重复收到。单一动作(一次查询、一次编辑、一条命令、一段短文本)不必起 run,直接做。
superagent_start(goal, mode?, queue?, max_subtasks?, max_parallel?, deadline_minutes?, allow_duplicate?, wait_seconds?)启动一次 run 并立刻返回 run id。mode: "direct"让一个 worker 直接跑这个 goal,不要 planner 也不要 aggregator,并把该 worker 的回复原样作为报告返回——适合一次自成一体的查询、修改、检索或短写作,不适合可以并行拆开的目标。默认的mode: "planned"走完整的 planner → worker → 合并报告流程。计划中的子任务可以声明dependsOn另一个子任务:依赖方会等它,并在自己的 prompt 里拿到它的报告,而彼此独立的子任务仍然并行。依赖成环会在任何工作开始前否掉整个计划,依赖一个计划从未声明的任务则以原因跳过。两种模式产出相同的 run 记录、配额、deadline、取消和用量记账。goal_mode: true增加一步验收,并在失败的轮次重新规划;见 Goal 模式。queue: true让maxActiveRuns占满时的启动改为等空位而不是失败:它返回status: "queued"以及queue.position/queue.of,某个活跃 run 结束时自动开始,等不到空位则在queueTimeoutMinutes之后失败。最多maxQueuedRuns个在等;排队的 run 在等待期间不计入maxActiveRuns,取消它会干净地把它从队列里移除。启动一个同会话中活跃 run 已经覆盖的 goal,会返回那个 run id 并标上deduped: true,而不是为第二个计划付钱:比较前会去掉首尾空白、折叠连续空白并忽略大小写,检查在一个按 goal 命名的 claim 锁下进行,并落在状态目录里,所以共享该目录的两个进程也只会启动一个 run。匹配是按会话的,因为两个会话同时要同一个泛化的 goal 是合理的。allow_duplicate: true可以再起一个。已结束的 run 从不阻止同一个 goal 再次启动。wait_seconds会阻塞到终态,并在结果里保留deduped标记。superagent_status(run_id, wait_seconds?)返回状态、里程碑、 每个子任务的结果、实测 token 用量、估算花费、报告文件路径,以及最终报告。superagent_list(limit?)列出 run,最新的在前,并给出状态目录的文件数与大小。superagent_cancel(run_id)停止 run,可从任何进程调用;见下文。superagent_resume(run_id, wait_seconds?, force?)继续一个interrupted、failed、partial或cancelled的 run;已完成的子任务保留。worker 会被告知什么失败了,并被要求检查之前那次尝试已经做了什么。只有启动该 run 的会话才能 resume 它。resume 不计入maxActiveRuns,所以配额满了也能用;force仍然被接受,但已不再需要。completed的 run 保持 completed,除非它持有一个从未进入任何 prompt 的答案——那时 resume 会重新打开它,否则那个决定就会被搁浅。superagent_followup(run_id, message)往已经在执行的 run 里加一条指令,而不是另起一个 run。这个工具明确是"下一个 prompt 生效":指令排在 run 上,下一次为它构建的 prompt 会带上它——下一个 worker 的 spawn 或聚合,哪个先到算哪个。已经在跑或已完成的子任务保留自己的结果,永远看不到它。回复会点名这个边界,status 也显示同样的东西;在最后一个边界之后到达的指令记录为never took effect,而不是看起来已投递。暂停的 run (interrupted、failed、partial、cancelled)会保留这条指令给下一轮,resume 时开始;completed的 run 会拒绝它。属于另一个进程的 run 会被拒绝,因为那个 owner 的下一次保存会覆盖它。只有启动该 run 的会话才能往里加。如果正在跑的 worker 必须立刻读到指令,用superagent_inject。superagent_inject(run_id, message, subtask_index?, wait_seconds?)把一条指令送进此刻正在运行的 worker。它使用宿主的实时引导通道(subagents.sendMessage,即 agent 循环在下一个 pre-step 排空的同一个收件箱),所以消息会成为子 agent 下一次模型请求的一部分。它不会打断已经在飞的模型调用:延迟就是当前那一步剩下的部分(那次模型响应加上它的工具批次)。一个已经决定收尾的活着的子 agent 会再跑一轮来读它,所以未被取消的子 agent 会消费掉它。回复报告从接受到子 agent 下一次请求的实测延迟,并在子 agent 被取消、 释放或在另一边界之前结束时,把消息标成missed——而不是 delivered。在真实dsh@0.2.2-rc.1宿主上实测最坏情况:子 agent 步长 8 s 时 6978 ms,步长 4 s 时 2986 ms,也就是说上界跟着步长走,不是跟着插件走。宿主没有暴露实时通道时,工具退化为 followup 队列并在回复里说明;它从不把那种情况称作实时。superagent_answer(run_id, attention_id?, answer, wait_seconds?)回答一条结构化的 attention 项,让 run 带着这个决定继续。活跃 run 在下一个边界应用答案;已结束的 run 会被重新打开,只重跑它的聚合,这次 prompt 里带着答案,所以报告反映该决定,而不是让用户另起一个 run。 同时有多个问题时attention_id必填。答案按 id 匹配,不按数组位置匹配:从未问过的 id、已经回答过的问题、以及被后续 resume 退役的问题都会被拒绝,而如果答案文本正好是另一个问题的选项,会指出那个 id 而不是猜。superagent_memory(action, key?, kind?, text?, scope?, query?, tags?, limit?)列出、搜索、添加、更正或遗忘笔记本里的笔记。set一个已存在的 key 会取代旧的;forget撤回它。scope指向rules(项目规则)、profile(用户偏好),或 run scope。search按query(当前 goal 效果不错)和/或精确tags给笔记排序,于是相关的老笔记能被找到,而不只是最新的几条;笔记在设置时可以携带tags。这个工具是增量的:另外十一个工具的参数和输出都不变。superagent_template(action, id?, params?, template?, limit?)列出、展示、展开、保存或删除一个工作流模板:一个固定的 goal 骨架,带必需参数、适用场景和不适用场景。expand返回一个普通 goal 字符串,superagent_start会像对待手打的 goal 一样规划它;缺失或为空的参数会按名字被拒绝,而不是取默认值。模板不能设置限额、预算、模式或覆盖项,所以用模板永远不会扩大 run 的权限。内置 id 只读;自定义模板放在状态目录的模板目录下,那里的失败会被记录,但不会让 run 工具失效。superagent_schedule(action, id?, name?, goal?, template_id?, params?, cron?, every_minutes?, timezone?, miss_policy?, goal_mode?, acceptance?, max_rounds?, max_wall_clock_minutes?, no_progress_rounds?, budget?, human_priority?)创建、列出、暂停、恢复、删除或立即运行一个定时计划。计划保存一个 goal(或展开后的模板)、一个时钟和它自己的限额;它能跨宿主重启存活,并启动没人手动发起的 run。没有计划之前不会武装任何东西。见定时运行。superagent_goal(action, goal_id?, goal?, phases?, cwd?, budget?, phase_budget?, max_parallel_phases?, max_attempts_per_phase?, auto_resume?, decision_id?, answer?, note?, limit?)把一个自然语言目标拆成有依赖关系的阶段,每个阶段是一个普通 run,按max_parallel_phases并行推进,跨宿主重启不丢。create在没给phases时先用一次 direct run 拆阶段;status/list给出阶段状态、run id、用量和待决策项;answer结算 阶段耗尽、验收证据冲突和停滞决策;pause/resume/cancel管住整个目标。阶段预算收紧到 目标剩余额度,目标预算合计所有阶段、重试和拆阶段 run。它扩展而不是替换 run: planner、子任务依赖图、预算、attention、持久化和 resume 还是同一套,见 docs/LONG-GOAL.md。superagent_audit(run_id?, subtask_id?, kind?, decision?, result?, target?, since?, until?, limit?, order?, format?, out_path?)读审计轨迹,或把匹配的事件导出成一个 JSONL 或 CSV 文件。见 权限与审计。
Goal 模式
普通 run 在子任务结束时结束。这和"目标达成了"不是一回事,一个计划完全可能在构建已经坏掉的情况下报告成功。Goal 模式是可选开启的(在 superagent_start 或 superagent_schedule 上设 goal_mode: true),它增加一步独立的验收,并让 run 一直跑到每个条件都通过、或者某个硬停点说明必须由人来决定为止。
- 验收条件。
acceptance必填,是{id?, text, check?}的列表。{"type":"command","command":"npm test"}或{"type":"file","path":"dist/app.js"}这样的check由插件自己执行,所以那个判定落在真实的退出码或真实存在的文件上,绝不落在模型的声称上。没有 check 的条件由另一个验收模型判定,superagent_status会注明每个判定的来源。最多接受 12 个条件;更长的列表说明目标本身没框好,那不是清单。 - 轮次。 失败的轮次不会重复同样的工作:下一轮的计划会拿到什么失败了、为什么。
max_rounds(默认 5,最大 20)、max_wall_clock_minutes(默认 60)和no_progress_rounds(默认 2)给它设界。当验收带有机器检查时,一个没有改动任何文件的轮次会立刻停下,而不是再付一轮钱。 - 硬停。 循环带着原因停下,而不是永远转下去:轮次上限、墙钟、run 的 deadline、预算停、有一个未决的 attention 项、两轮完全相同的失败,或者某一轮无法取得进展。停止报告列出每个条件及其最后结果,以及试过什么。
- 通知。 只有 goal 达成或循环停止时通知一次——轮次之间不通知。run 记录、状态行和报告都带轮次计数和每个条件的判定。
Goal 模式默认关闭,不新增自己的工具,也不会改变任何不带 goal_mode: true 的 run。
定时运行
superagent_schedule 保存一个计划——一个 goal 加一个时钟——并在没人调用 superagent_start 的情况下启动它。定时 run 是普通的 run:同样的 planner、同样的配额、同样的预算、取消、用量记账、报告和状态。调度器只是编排器的一个定时形状的调用方,不是第二套 run 引擎。
superagent_schedule(action: "create", name: "nightly audit",
goal: "Audit yesterday's error logs and list anything new",
cron: "0 3 * * *", timezone: "Asia/Shanghai",
budget: { max_total_runs: 30, max_total_tokens: 5000000 })
动作有 create、list、pause、resume、delete 和 run-now。
run-now 立刻开始一次执行,但不移动下一个计划窗口。计划可以用 template_id 和 params 代替字面 goal;模板在创建时展开,所以之后编辑模板不会悄悄改变已有计划。
计划保存它的时钟(cron 或 every_minutes,外加 timezone)、goal、
可选的 mode / goal_mode 与 acceptance、它自己的 budget,以及
human_priority。计划持久化到状态目录下的 schedules.json,并在挂载时重新加载,所以宿主重启不会丢。只有至少存在一个计划时定时器才启动;空状态目录下 init() 不会创建那个状态文件。
错过的窗口
宿主停机期间到点的窗口不会全部重放。默认策略 miss_policy: "latest" 只跑最新一个错过的窗口,把其他每个到点窗口记录为 missed,并带上计划时间和原因(那时宿主没在运行);随后计划在正常网格上推进到下一个窗口。执行那个最新窗口的 run 是一次追赶,计划会说明这一点:totalRuns 计入它,catchUpRuns 记录有多少次是追赶,started 历史条目会写明是哪个窗口、它代表了多少个错过的窗口。miss_policy: "none" 跳过宿主停机期间所有到点窗口,记录数量,并在下一个计划窗口恢复。追赶工作是有界的,所以一台关了一个月的笔记本不会排出几千个 run。
这些计数器故意分开:missedRuns 是从未运行的窗口数,catchUpRuns 是晚运行的次数,skippedRuns 是因为上一次 run 还活跃而被跳过的次数。list 会把三者都打印出来。
计划也永远不会和自己重叠:新窗口到点时如果上一次执行还活跃,该窗口不会启动,并记录为
skipped: previous run still active。下一个窗口正常评估。
完成必须有产物
worker 以 completed 结束自己的回合,本身并不等于子任务完成。当子任务声明了 deliverables(文件路径,相对于它的工作目录)时,列出的每个文件都必须存在、是文件、且非空,子任务才算数。没有声明文件时,回复文本就是产物,所以空回复或只有空白的回复会以
empty deliverable: ... 被拒绝,子任务像其他未完成的尝试一样重试或失败。报告被剥到什么都不剩的 run 会记录为
partial,永远不会是 completed,所以定时任务无法为空结果报告成功。
配额与人工优先
定时 run 与手动 run 共享 maxActiveRuns。设了
human_priority: true(默认值,也可用
schedulerHumanPriority 配置)时,定时 run 不能占用最后一个空闲位:只剩一个空位时它会等下一个 tick,而不是堵住一次
superagent_start。定时 run 也永远不进队列,所以人的
superagent_start(..., queue: true) 永远不会被定时器推后。配额满时,到点的窗口保持到点状态,在之后的 tick 重试,
期间不花任何东西。
预算与失败
budget.max_model_calls / max_tokens 限制单次执行;
budget.max_total_model_calls / max_total_tokens / max_total_runs 限制计划跨多次执行的总量。达到计划总量会暂停计划,并把原因记在计划和它的 run 报告里;达到单次 run 的上限则以手动 run 相同的方式结束那次执行。
失败的执行不会在每次重试时都通知。第一次失败只报告一次,
schedulerMaxConsecutiveFailures(默认 3)次连续失败会暂停计划并只报告这一次。成功会重置计数。除此之外,通知仅限于:目标达成、预算到顶、或者某个 run 需要决策。里程碑和普通进展留在
list / status 里,永远不会叫醒会话。
Cron 支持
Cron 解析是内置的,没有依赖。支持:标准五个字段(分 时 日 月 星期)、*、*/n、范围
(1-5)、列表(1,15,30)、月份与星期名(JAN、MON-FRI),以及星期日写成 0
或 7。日与星期同时出现时按经典 cron 的方式做 OR。
不支持,并在创建时以出错的字段序号拒绝:秒、年份字段、@daily 之类的宏,以及 L、W、#、?
扩展。六个字段的表达式会以"field 1 is not a minute"被拒,而不是被重新解释。时区用 IANA 名字,始终按表达式书写时所在的时区求值,绝不按宿主时区;春季 DST 跳变时不存在的时间被跳过,秋季二义的时间使用较早的那个偏移。
计划对宿主的要求
每个计划都属于创建它的会话(或显式的
session_id),而这个 owner 是在每个窗口解析的,不是只捕获一次。会话还活着就直接用。宿主重启后,插件请求宿主恢复持久化的会话(agents.resume,或在只有那个接口时用 Host Session API);如果会话被删除,它会创建一个替代者,计划采用新 id(记录为 owner-recovered)。因此计划能跨重启存活,而不依赖原进程或原对话保持打开。
当宿主完全没有提供恢复或创建会话的途径时,这个限制在 create 时就记录下来,而不是事后以一次暂停的形式被发现。响应里带一条警告,计划标记为
requires a resident session;窗口发现没有活着的 owner 时记为 blocked,只通告一次,计划保持启用。它不会在三个窗口之后被悄悄暂停。
在第一次 create 之前,不跑任何东西,也不写任何东西。没有任何计划的安装,行为与这个工具存在之前完全一样。
进度、决策与产物
- 计划的诚实性。 worker 开始之前,planner 会点出它无法映射到具体对象的东西:含糊的措辞进
ambiguities,只有用户能回答的问题进needsInput,于是 run 停下来问,而不是编一个看起来合理的名字。planner 确实做出的每个假设都记录在那个子任务上(assumptions)并打印在 status 和报告里,所以猜测是可见的而不是静默的。问题一起到达的计划是一个决策点,而不是一连串滴灌式的提示。 - 能力预检。 子任务可以声明
requires(shell、net.http、browser、gui、desktop、git、gh)。宿主会在 spawn 任何 worker 之前,把该列表与本会话实际拥有的工具比对。能力缺失的子任务是blocked,会写明缺哪项能力、什么能启用它;它从不开始,也从不被算作成功,它下游的一切都因这个原因跳过。宿主无法列出自己的工具时,预检记录unavailable并不阻挡任何东西,而不是猜一份清单。 - 里程碑。 每个 run 记录
plan_ready、running、aggregating和settled及时间戳,显示在superagent_status里。它们从不产生主动消息;进展保持可见但不会变成噪音。 - Attention。 worker 或 aggregator 可以追加一个围栏
attention块,提出一个只有用户能回答的问题。它从报告里剥掉,作为attention.items存在 run 上,在 status 里显示为needs your decision:,并在 run 结束时领衔通知。每一项都从 run 上的计数器获得稳定 id(q1、q2、…),所以同一个问题在多次保存和 resume 之间含义不变。问题、选项和原因会折叠成一行,并分别限制在 500 / 200 / 300 字符,所以这个块无法注入伪造的状态行或通知行。不是合法 JSON 或没有问题的块会留在报告里,status 打印attention: <reason>而不是把请求藏起来。 - 回答。
superagent_answer把决定记录在该项上,让 run 继续:活跃 run 把它带进下一个 prompt,终态 run 被重新打开以便带着答案重跑聚合。resume 让问题退役时,它是被归档而不是删除——这正是让迟到的答案能以expired at <time>被拒、而不是被挂到当前随便哪个问题上。 - run 期间的指令。 两个边界,保持区分。
superagent_followup追加到对话日志,由下一个为 run 构建的 prompt 携带:每条指令在某个 prompt 第一次携带它时记录appliedAt/appliedAtStage,run 先落定时记录missedAt。superagent_inject则引导活着的子 agent 的收件箱,并在它的下一个 pre-step 被认领;回执记录从接受到那次认领的latencyMs,或者在子 agent 先离开时记录missedReason。Status 两者都打印,所以排队的指令和实时的指令永远不会混淆。 - 报告文件。 终态 run 把报告写到
<stateDir>/reports/<runId>.md,用原子重命名,所以结果有一个可以打开或附加的路径。该目录带一个内容为*的.gitignore,路径记录在run.artifacts里并显示在 status 中。 - 子任务之间的依赖。 planner 可以给子任务一个 id 和
dependsOn列表;依赖方于是等待(waiting)而不是开始,它的 prompt 带上已完成的依赖方的报告。彼此独立的子任务仍然并行。未知引用是skipped并带原因,失败会跳过下游的一切而不是把它报成完成,成环则在任何子任务开始之前否掉计划。Status 和报告把等待与运行分开显示。
权限与审计
run 周围有两层互相独立的东西。策略决定某个动作能否发生;审计轨迹记录决定了什么、随后发生了什么。轨迹默认开启,因为它只观察。策略在存在之前不改变任何东西,而且它只能把插件自发的行为收得更紧。
策略
把 docs/policy.example.json 复制到
<stateDir>/policy.json,或用 policyFile 指定另一个文件。规则按这个顺序匹配:
paths.deny/commands.deny模式 ->deny(deny 永远优先);- 显式的
paths.allow/commands.allow条目 ->allow; confirm.actions里列出的动作类型 ->confirm;- run 的
runs.<id>.level,否则defaultLevel。
压过 confirm 或按 run 的 deny 的 allow 条目会带
meta.relaxedFrom 记录,所以人总能看出策略在哪里被放宽了。相对路径模式在任意目录边界匹配(src/** 覆盖
/anywhere/repo/src/a.js),但不匹配只是结尾相同的兄弟路径(xsrc/a.js)。不带 * 的命令条目是按整 token 的前缀
(git status 允许 git status --short,不允许 git statusx);复合行
(git status && curl ...)永远不能被前缀提升到 confirm 之上。
policy.json 可以包含 //、# 和 /* */ 注释。模式问题会带着文件、行、列报告:
policy.json: 1 problem in the permission policy
policy.json:3:19 [defaultLevel] must be one of allow | confirm | deny
"defaultLevel": "maybe",
^
不可读或无效的策略失败关闭:受管辖的动作被拒绝,superagent_audit 打印问题。被拒绝的 start 或 resume 在任何状态改变之前就被拒;被 confirm 拦下的那个需要在下一次调用时带
approve_policy: true,该批准会写成一条
approved 事件,点名被拦下的事件。批准不能解除 deny。
审计
<stateDir>/audit/audit.jsonl,每个事件一个 JSON 对象:
{"v":1,"id":"evt_...","at":"2026-11-15T10:04:11.238Z","runId":"20261115T100400-ab12cd34","subtaskId":"subtask-2","actor":"session:9f3c","kind":"path.write","action":"path.write","target":"C:/work/app/src/db.js","decision":"allow","result":"ok","durationMs":4,"rule":"paths.allow[0]","source":"policy","meta":{"relaxedFrom":"confirm","reason":"..."}}
每个事件都带 run、子任务、actor、时间戳、判定、
结果,以及检查耗时。更高层可能再加 tool.call 和
command.exec 事件。由插件自己触发的是:run.start、run.resume、
run.cancel、run.finish、attention,以及策略拒绝时的 blocked、人释放被拦动作时的
approved。
进程内的追加是串行的,跨进程由一个 wx 锁文件守护,每个事件一次追加写完,所以两个同时结束的 worker 不会把行拼在一起。崩溃留下的尾部碎片会报为
torn 并跳过;损坏的行计为 invalid,不会掩盖其余部分。
superagent_audit 可按 run、子任务、kind、decision、result、目标文本和时间窗读取。用 format: "jsonl" 或 format: "csv" 时,它写出一个可交付文件(默认
<stateDir>/audit/audit-export-<time>.<ext>)并返回路径。
路径/命令护栏可以挂上去,而不需要审计层知道它的规则:
createAuditor({ dir, policyFile }).attachGuard(request => ({ decision, reason }))。
护栏只能把策略产生的判定收得更紧;抛异常的护栏被视为阻断。返回 { blocked, rule, reason }——也就是工具调用护栏本来就产出的形状——的护栏同样能被理解。
跨运行记忆
run 是故意隔离的:planner 和 worker 都从空白开始。没有笔记本,每个 run 都会重复上一个已经做完的工作,并重新踩一遍同样的坑。插件保存两类不同的东西,而其中只有一类被存下来:
- 发生过什么 在规划时从 run 存储推导。最近六次 run 各渲染成一行(日期、状态、完成的 子任务数、goal、run id)。没有任何东西被复制,所以它不可能和 run 记录漂移。
- 学到了什么 是一个小的、经过整理的笔记本:决策、坑、下一步,以及哪些工作被委派出去了。aggregator 把它放在一个围栏
memory块里返回,这个块在用户看到之前就从报告里移除。没有产出块的 run 什么都不写;插件从不凭空造笔记。
存储。 <stateDir>/memory/<scope>.jsonl,一行一个 JSON 对象。一行的结构是
{v,id,ts,key,kind,text,runId,status};key 是一个短 slug(deploy-target),
kind 是 decision、pitfall、next、dispatch 或 note。该目录包含一个内容为 * 的 .gitignore,所以即便 stateDir 指向仓库内部,笔记本也不会被提交。它也从不进入包的 files
白名单,所以永远不会发布到 npm。
什么时候写。 每个 run 一次,在落定时,且只对报告里带块的终态 run。把同一个 run 再次落定的 resume 是一次 no-op,
因为存储记录了写入的 run id。写入失败记录在 run 上(memoryWrite.reason),从不让 run 失败。
什么时候读。 在规划之前,superagent_start 时读一次(superagent_resume 时再读一次,因为 aggregator 会再跑)。组合出的上下文作为 memory.digest 存在 run 上,注入 planner 和 aggregator
的 prompt,并在 superagent_status 里摘要显示。
分层。 按顺序读三个 scope:rules(项目规则)、profile
(用户偏好),然后 memoryScope(更早的 run 学到的东西)。前两个用
superagent_memory 整理,永远不由 run 写入,所以一条规则不会被模型的观察覆盖。每一层在 prompt 里都有标注,并各自设界;run 上的 memory.scopes 列出读了哪些。
体量。 每条笔记在脱敏之后上限 400 字符。一个 prompt 最多看到
memoryDigestEntries 条笔记、总计 memoryDigestChars 字符以内,再加六条
一行的 run 摘要,整体硬上限 2600 字符。当一次写入会把文件推过 memoryMaxEntries 条笔记、400 行
或 128 KB 时,文件被压缩;压缩保留最新的 memoryMaxEntries 条笔记。
上下文不会随 run 数量增长。
并发。 进程内,对同一 scope 的写入是串行的。跨进程,每一批作为一次小 write 追加(不对整个文件做读-改-写),压缩拿锁文件并从临时文件替换,被杀死进程留下的截断末行会被跳过。两个同时结束的 run 不会覆盖彼此的笔记;行的顺序可能不同,但合并依据时间戳,绝不依据位置。
搜索。 superagent_memory 的 action: "search" 按 query
关键词和精确 tags 给笔记排序,而不是只返回最新的那些;planner 的摘要也用同一套排序针对 run 的 goal。所以几周前一条相关的笔记在匹配时仍能进入 prompt,而不相关的笔记不会挤占预算。笔记在设置时可以携带 tags。
更正。 同一 key 的更新笔记取代旧的。显式 forget 写一条墓碑。早于 memoryMaxAgeDays 的笔记不再被注入,并在下一次压缩时丢弃。每条笔记都显示年龄,摘要也会告诉模型这些笔记可能过期、必须核实,所以一个错误结论可以被后续的 run 推翻,而不是变成永久的偏见。
隐私。 默认情况下笔记写在仓库之外
($DSH_HOME/state/superagent/memory)。存储之前,插件会脱敏 API key 形状(sk-…、Bearer …、ghp_…、AKIA…)、长的 hex/base64 串、邮箱
地址,以及绝对家目录路径;它把笔记压成一行并设上限。
aggregator 被指示不要记录密钥、客户数据或报告本身。stateDir 落在已安装包内时会禁用记忆,而不是冒把它发布出去的风险。如果你把 stateDir 指向仓库,嵌套的
.gitignore 仍然会让这些文件躲开默认的 git add。
用户画像
上面的笔记本回答的是"上一个 run 对这个任务学到了什么"。第二个可选的存储回答另一个问题——"这个人是谁、他想要什么、我们之前为什么那样选"——而且它必须比不相关的任务活得更久,所以它单独存放、单独读取。默认关闭
(memoryUserProfile: false):不构建任何东西、不创建目录,
run 笔记本的行为与之前完全一致。
- 三类,三个文件。
fact(持久的技术背景,例如技术栈或环境)、preference(用户希望工作怎么做)、 以及decision(选了什么、否决了什么、为什么、以及怎么撤销)。 一次搜索只打开一个类别的文件,所以一条事实不会仅仅因为词对得上就被当成偏好返回。 - 决策必须带理由。 没有理由的决策教不会下一个 run 任何东西,所以理由既是记录的一部分,也是每条摘要 行的一部分;光秃秃的"选了 X"会被拒绝。
- 读写。 带
layer: "user"和category的superagent_memory可以列出、搜索、添加、更正或遗忘画像记录。planner 的 prompt 会拿到匹配记录的短摘要,按 类别标注,字符预算与笔记本相同。 - 遗忘就是删除。 匹配的行在锁下被移除,文件被替换,不留墓碑,所以后续搜索不可能把值泄漏回来。遗忘必须点名 key、run id 或时间范围,所以光秃秃的"全部忘掉"不会误清整个画像。
- 隐私。 每条记录在写入前都会脱敏:凭据、
邮箱、电话号码,以及家目录路径里的用户名。画像位于
<stateDir>/user-memory,带与笔记本相同的嵌套.gitignore, 并且永远不在包的files白名单里。
观察与报告

一份由 40 次真实宿主 run 渲染出来的报告顶部。完整页面——时间线、 泳道图、调用与花费曲线、失败标注、run 表——在 docs/report-sample.html。
包里带五个运维工具,不需要会话,也不增加运行时依赖。watch、report、archive 和 sync
读的是与工具相同的持久 run 文件;doctor 检查安装本身。
从 profile 里已安装的那份副本运行:
PKG="${DSH_HOME:-$HOME/.dsh}/profiles/web/node_modules/@xiangyixie/dsh-superagent"
node "$PKG/bin/watch.js" # live panel, Ctrl-C to stop
node "$PKG/bin/report.js" --out report.html # self-contained HTML page
如果 profile 的链接器创建了 bin 链接(npm 会),同样的命令是
node_modules/.bin/dsh-superagent-watch、node_modules/.bin/dsh-superagent-report、
以及 node_modules/.bin/dsh-superagent-doctor。
watch、report 和 archive 都接受 --state-dir <path>;默认是
$DSH_HOME/state/superagent。当插件配置设置了自定义
stateDir 时要传它。
watch 在备用屏幕上为每个 run 画一行——id、状态、已用调用数对上限、已耗时、此刻在做什么,以及父会话——并在 Ctrl-C、SIGTERM 或管道关闭时恢复光标。状态变化立即重绘;空闲面板每秒只读一个小文件。同一时刻活着的 run 共享编号边栏,所以并行工作看起来是一组,而不是互不相关的行。等待中的行先显示占着空位的那个 run,再显示队列位置,因为窄单元格从右边截断,而占位者才是可操作的信息。--once 打印单帧纯文本,
docs/e2e/transcripts/watch-panel.log 捕获的就是这个,来自 40 次真实 run。
report 把记录下来的 run 渲染成一个 HTML 文件:内联 CSS、内联 SVG、
不含脚本、字体、图片,也不联网。它有时间线(每个 run 的开始、结束,以及推导出的规划/执行/合并阶段);泳道图,泳道数等于峰值并发;累计模型调用与花费曲线;明确的失败、取消、中断标注;以及一张 run 表。每个
worker 的重试链和它占 run 墙钟的比例列在结果旁边,所以一个昂贵而飘忽的子任务能被看见,而不只是它最终的失败。
docs/report-sample.html
是宿主 e2e 的 40-run 报告——直接打开即可,不请求任何东西——
而
docs/e2e/transcripts/report-cli.log
记录了命令本身,以及一次零外部引用的审计。
doctor 在宿主之前回答"装是装上了,为什么什么都不工作?"。它只读:不安装、不挂载、不修改 profile。每项检查一个块——导入的文件对 files 白名单、声明的文件对磁盘实况、已安装副本对同版本已发布的 tarball、profile 的 bundle 注册,以及注册的十二个工具——每个失败后面跟着能修好它的命令。
PKG="${DSH_HOME:-$HOME/.dsh}/profiles/web/node_modules/@xiangyixie/dsh-superagent"
node "$PKG/bin/doctor.js" --profile web
node "$PKG/bin/doctor.js" --profile web --offline # skip the registry hash check
没有检查失败时退出码是 0,有一项失败是 1,所以它能用来给部署设闸。npm test 跑的打包防护与
scripts/check-pack.mjs 相同:从入口点可达的每个文件都必须出现在 npm pack --dry-run 里,而这正是 0.9.x 版本缺的那项检查。
这些工具都不是安装路径:插件用 dsh plugin add 安装,绝不用 npm i -g。
archive 是第四个工具。它清理终态 run,并把一个 run 导出或导入成一个自包含的归档;见保留策略和
归档。
在多台机器间同步同一个状态目录
没有服务器,也没有账号。状态目录就是一堆文件,所以一个 git remote 就能承载它:dsh-superagent-sync 把那个目录变成仓库,把机器本地文件排除在外,并提供 pull、push 和
status。唯一的要求是 git——这个命令本身没有依赖。
PKG="${DSH_HOME:-$HOME/.dsh}/profiles/web/node_modules/@xiangyixie/dsh-superagent"
node "$PKG/bin/sync.js" init git@github.com:me/superagent-state.git # once per machine
node "$PKG/bin/sync.js" push # after runs finish
node "$PKG/bin/sync.js" pull # before starting work
node "$PKG/bin/sync.js" status
或者用 node_modules/.bin/dsh-superagent-sync。四个子命令都接受 --state-dir <path>(默认 $DSH_HOME/state/superagent),--json 打印原始结果。
init <remote> 创建仓库,往
.gitignore 写入一个受管块,设置 origin,并做第一次提交。重复执行会刷新远端 URL 和那个块,不会碰你自己加的条目。没有 ~/.gitconfig 的机器会得到一个仓库本地的提交身份,所以第一次提交不会仅因为这个失败。
pull 获取 origin,然后合并。未提交的本地改动会先被 stash
(stash@{0},主题 dsh-sync auto-stash …)并在合并后重新应用,所以 pull 绝不会悄悄覆盖一次编辑。冲突会让合并停下,两侧内容都还在文件里:路径被打印出来,不做任何取舍,stash 保持未应用并有一行说明。
push 提交所有东西并推送当前分支,首次推送时设置 upstream。被拒的 push 会被报告,不强制、也不自动合并:先 pull,解决它报告的问题,再 push。
status 打印 ahead/behind 计数、未提交文件、stash,以及等待决定的冲突路径。
什么东西不会离开本机
.owner、control.json(一个 bearer token)、notifications.json、*.lock、
*.cancel、*.corrupt-*、*.tmp、*.log,以及 .env / key 材料。笔记本是唯一值得知道的例外:memory/.gitignore 通常是一个光秃秃的 *,那会让每条笔记都留在本地,所以 init 会把这一处默认值改写成允许 <scope>.jsonl。你自定义过的文件不会被改动。
发生冲突时
run 记录是一个 run 一个文件(runs/<runId>.json),所以两台机器上普通的并发工作——不同的 run——永远不会碰到同一个路径。冲突需要以下之一:
- 同一个 run id 在两台机器上都被写:在 pull 第一台机器的状态之前,就在第二台上 resume 或 cancel 了它;
- 同一个记忆 scope 在两台机器上都被追加(
memory/<scope>.jsonl是 只追加的,两条不同的尾部会碰撞); - 同一个
reports/<runId>.md被用不同内容重新生成; .gitignore在两台机器上都被手工改过。
run 文件里只要还有冲突标记就不是合法 JSON,插件会在挂载时把它隔离为 <runId>.json.corrupt-<ts>。启动宿主之前先解决它:留下你要的那一侧,git add,git commit。
为什么说这是近似的
- 两台机器在同一时刻写入会冲突。合并停下,由你决定;不会有任何东西在你背后被合并。
- 没有实时同步,也没有跨机器锁。一台机器只有
pull之后才看到另一台机器的 run,而push不知道另一台此刻在做什么。同一时间只让一个宿主使用一个状态目录。 - key 不分发:每台机器仍然需要自己的模型凭据。
keepRuns清理会删文件,而删除要等到下一次pull才到达另一台机器。
通过本地 HTTP 嵌入
工具需要一个会话。如果另一个 UI 或脚本要在不导入本包的情况下启动并观察 run,就设置 controlPort:
- id: superagent
config:
controlPort: 0 # off by default; 0 = OS picks a free loopback port
插件随后写出 <stateDir>/control.json,含绑定的 URL 和每进程一个的 bearer token(权限 0600),并只在 127.0.0.1 上提供 GET /、GET /health、
GET /runs、GET /runs/:id、POST /runs、POST /runs/:id/cancel 和
POST /runs/:id/resume。每个回复都带与该工具会打印的相同文本
(路由索引和 /health 是两个例外:
它们描述的是 socket,不是 run)。每个 run 都走与工具启动的 run 相同的配额、
预算、锁和恢复流程。POST /runs 要求 session_id 是该宿主中活着的会话——run 需要一个真实的父
agent 来带它的子 agent。
STATE="${DSH_HOME:-$HOME/.dsh}/state/superagent"
URL=$(node -e "console.log(require('$STATE/control.json').url)")
TOKEN=$(node -e "console.log(require('$STATE/control.json').token)")
curl -s -H "Authorization: Bearer $TOKEN" "$URL/runs"
完整契约——路由、错误 id、安全模型、生命周期、客户端 示例——在 docs/embedding.md。它是可选开启的、只走回环, 没有远程模式,也没有免认证模式。
对话控制台
consolePort 增加一个本地页面,让你和真实的 Host Session 对话,而不是一个假聊天,也不是一个换了皮的 superagent_start(goal) 表单:
- id: superagent
config:
consolePort: 0 # off by default; 0 = OS picks a free loopback port
consoleRoot: /path/to/project
插件用宿主的 sessionController.create() 创建会话,用 prompt({ mode: 'queue' | 'steer' }) 发送每条消息,并把
follow({ assistantStream: true }) 加 observeState() 转发给页面。所以后续消息是在继续同一个对话;它们不是新会话。
右栏列出 parentSessionId 为该会话的 run,所以页面显示的就是这个对话派生出的那些子任务。attention 卡片
通过 orchestrator.answer 作答,与
superagent_answer 工具走同一条路径,run owner 就是对话 agent。
绑定的 URL 和每进程新生成的 token 写到
<stateDir>/console.json(权限 0600)。打开
http://127.0.0.1:<port>/?token=<token> 一次;页面把 token 存进
sessionStorage 并从地址栏移除。没有它,每个路由(包括
事件流)都返回 401,而每个带非回环
Host 头的请求返回 403。
第一次使用时,页面上的「首次使用向导」按顺序走:检查环境 → 验证 API key(真的请求端点,通过了才写任何东西)→ 预览并保存 profile patch → 在一个临时目录里跑一次真实的 trial run。每次保存前会留一份可回滚的 patch 备份(保留最近 10 次),密钥只交给宿主的凭据存储,patch 里只留引用;向导只改它自己那两个条目(llm-pi-ai、agent-default-model),其余行和注释原样不动。宿主没有提供向导接口时,按钮会说明本宿主不支持,而不是弹一个填了也没用的表单。
如果宿主没有挂载 sessionController(例如没有 web session API 的无头 profile),页面会说明这一点并拒绝创建会话。它不会假装成一个聊天。控制台在一个进程里也只保留一个活跃
对话;"新对话"会创建一个新的 Host Session,旧的保持原样。以宿主 pendingInput 形式出现的审批提示会显示在
状态行里,但不会由这个页面回答;只有 run 的 attention 问题会被回答。
这个对话能用的工具
控制台会话创建时没有自己的工具列表:它挂载宿主 agent preset 挂载的那些,与 Web 应用自己的
会话拿到的是同一组。插件不注册对话工具,也不过滤;它唯一能施加的过滤是给子 agent 调用的可选 guard。要看真实的列表,页面向宿主请求 ctx.tools.definitions(agent),并显示在"这个会话挂载的工具"下;宿主无法枚举时,页面会说明,而不是打印一个固定列表。想要 profile
默认之外的 preset 时,consolePreset 会把 agentPreset 传给
sessionController.create()。
浏览器与桌面控制不在这里实现。它们来自
feat/runtime-control,那个分支注册宿主级的 browser_* 和 desktop_*
工具;这些工具像其他宿主工具一样对控制台会话可见。在没有那个分支的构建上,页面把它们报成"未挂载"。两个分支都不带 3D 工具;当
preset 里有 bash 或 pwsh 时,无头 Blender 可以通过它驱动
(blender --background --python),这是控制台本来就能触达的脚本化路径。
工作区面板
workbenchPort 在一个目录上增加一个本地的、IDE 形状的面板。它有自己的文件树、编辑器、diff 预览和 run 改动视图;任何东西都不经过宿主代理。
npx dsh-superagent-workbench --root /path/to/project --port 8787
同一个服务器也可以跑在宿主内部,Runs 视图接到插件的 状态目录:
- id: superagent
config:
workbenchPort: 0 # off by default; 0 = OS picks a free loopback port
workbenchRoot: /path/to/project
独立命令提供同一个页面,其 run 状态默认是
<root>/.dsh-workbench;传 --state-dir <plugin state directory> 可以读已有的 run。两种形式都不会在你要求之前打开 socket。
绑定的 URL 和每进程新生成的 token 写到
<stateDir>/workbench.json(权限 0600)。打开 http://127.0.0.1:<port>/?token=<token>
一次;页面把 token 存进 sessionStorage 并从
地址栏去掉。没有 token 时每个路由都返回 401。
面板可以列出、读取和编辑 workbenchRoot 下的普通文本文件。它
拒绝绝对路径、.. 穿越、指向根之外的符号链接、插件
状态目录、二进制文件,以及大于 512 KB 的文件。保存时发送编辑器读到的哈希和
mtime;如果磁盘上任一已变,保存会以
external-change 被拒,而不是覆盖另一个进程的工作。保存确认之前,页面显示统一 diff。
workbenchPort 打开时,插件把每次 run 改动的文件记录进
该 run 的 JSON 记录(run.changes),这样 Runs 视图之后可以显示这些文件及其
针对 git HEAD 的 diff。
任务看板
boardPort 增加一个页面,给不想读 run 记录的人用:每条子任务是一张卡,
所有需要人介入的事收在一个收件箱里,四个内置模板填表就能起。
- id: superagent
config:
boardPort: 0 # 默认关闭;0 表示让系统挑一个空闲的回环端口
boardRoot: /path/to/project
打开 <stateDir>/board.json 里的 URL(权限 0600),或者用
http://127.0.0.1:<port>/?token=<token>;页面会把 token 存在浏览器里,再从
地址栏去掉。没有 token 时每个路由和事件流都返回 401,Host 不是回环地址时
返回 403。事件流第一帧是完整快照,所以刷新页面或断线重连会从磁盘上的 run
记录恢复看板,而不是留下一片空白。
看板。 每条子任务一张卡:状态、尝试次数、耗时、tokens、花费、依赖关系,
以及没跑起来时究竟是被哪条依赖卡住的。每张卡都能单独取消或单独重跑。
cancelSubtask 只中止那一个子 agent,run 的其余部分照常继续,依赖它的子任务
会以 not started because w2 was cancelled 跳过。rerunSubtask 只重置那一条
子任务(以及仅因为它被跳过的下游),然后恢复 run;已完成的兄弟任务不受影响。
如果剩下的活儿全被取消了,run 以 cancelled 结束,而不是 failed。
收件箱。 planner 的 needsInput 提问、交付物待验收(unverified)、
能力缺失、权限确认、需要人看的浏览器检查、被中断的 run,都在这里。每条都会
说明发生了什么、为什么需要你、有哪些选项、回答之后会怎样。作答走的是和
superagent_answer 同一条 orchestrator.answer 路径,actor 是 run 的归属
会话,审计里会记下谁答了什么。没有待办时,页面显示"当前没有需要你处理的事",
而不是留一列空白。
模板。 bugfix-verify、review-fix、docs-examples、research-report
四个模板是表单:用途、要填的参数、预计会起几个子任务。提交后展开模板,用看板
自己持久化的 Host Session 起一个 run,并跳到它的看板。
报错说人话。 能力预检被拦、计划不可解析、交付物缺失或类型不符、沙箱拒绝
写入、spawn EPERM、模型或端点不可用、预算或超时耗尽、定时计划的 owner 会话
已不在,各自对应一句中文说明加一个动作(重试、重跑、打开配置、复制诊断信息)。
原始堆栈折叠在"原始错误(给技术同学看)"下面。
看板从不占有 run。它通过宿主解析 run 的 parentSessionId,然后调用工具调用的
同一批 orchestrator 方法。没有 sessionController 时,它照样列出所有 run,
并用中文说明为什么按钮不可用;它不会伪造一段对话或一个 agent。
状态与恢复
一个 run 一个文件:$DSH_HOME/state/superagent/runs/<runId>.json,用
临时文件加重命名写出,所以保存过程中被 kill 也不会留下半写的 run。
状态有:queued、planning、running、aggregating、completed、
partial、failed、cancelled、interrupted。正在等空位的 run 状态也是 queued,但带 queue.waiting: true 和它在队列里的
位置;它不是活跃的,不计入 maxActiveRuns。
挂载时,插件只在 run 没有活着的 owner(见下文)时把它标成 interrupted,并把在飞的子任务重置为 pending。
superagent_resume 随后只重跑没完成的部分。planner 的计划被复用,所以 resume 的 run 不会为规划付两次钱。
一个状态目录可以被两个 profile 或两台机器共享。因此恢复从不只凭本进程的内存做决定:owner 还活着的 run——心跳新鲜、本地 pid 活着、或有活进程持有它的
锁——保持不动,cancel 标记也一样。两个进程重写同一个
run 会让还活着的子 agent 继续花钱,而文件却说
interrupted,之后一次 resume 会把同样的子任务跑两遍。
resume 的独占性也是同一个原因。superagent_resume 拿与 superagent_start 相同的
串行闸,占用 <runId>.lock(原子创建),
拒绝还有活着 owner 的 run,也拒绝不是
parentSessionId 中那个会话的调用者。它不计入 maxActiveRuns:resume 是在把已经存在的活儿干完,所以配额满了也永远不会挡住它。force
(工具)或 force / override_quota(HTTP)仍然为兼容而接受,
但不改变任何东西。
run 旁边还有三个附属文件:
| 文件 | 含义 |
|---|---|
<runId>.cancel |
某个进程请求了取消。owner 每秒轮询一次并停掉它的子 agent。run 结束时移除。 |
<runId>.lock |
允许调度这个 run 的进程。用 wx 创建,所以恰好一个进程胜出。本地已死 pid 留下的锁立即破除;活着的本地 pid 无论持有多久都保留;来自另一台机器的锁超过 10 分钟后破除,因为它的 pid 无法检查。 |
<runId>.json.corrupt-<ts> |
状态文件不是合法 JSON,被移到一边而不是删除。superagent_list 跳过该 run,并按 id 连同这个路径报告。 |
<anchor>/.superagent/worktrees/<runId>/w<n>/ |
workspaceIsolation 打开时某个 worker 的 git worktree。锚定在宿主的会话目录内(宿主就是在仓库里启动时就是仓库),好让沙箱允许写入,并被忽略所以从不出现在 git status 里。分支合并完成或没产生改动后移除;冲突留给用户合并时保留。 |
run 的文件损坏时,其他东西不会跟着坏:其他 run 正常列出,启动新 run 也照常工作。检查 .corrupt- 文件,然后删掉它。完全读不出来的文件(权限、打开文件过多、磁盘满)会如实报告,而不是显示成一个未知的 run。
孤儿运行与活跃运行配额
一个 run 在其状态是活跃期间计入 maxActiveRuns。如果拥有它的进程没有重启就消失了,这个 run 否则会永远占着那个空位。每次配额检查之前,插件都会寻找这类 run 并回收,规则有两种:
- 状态目录属于本机(
.owner里是我们的主机名): 本地已死的 pid 本身就够了——那是进程没了的直接证据,所以下一次挂载会立即回收,不管心跳。活着的 pid 能否决回收,即便心跳陈旧,因为一个卡住的进程仍可能恢复。 - 目录是共享的,或它的 owner 未知:pid 可能属于另一台机器,所以忽略它,只由心跳决定;心跳超过 90 秒后 run 被回收。网络共享或同步文件夹上的状态目录就属于这种情况。
两种情况下,活着的本地进程持有的锁也能否否决回收,挂载时恢复用的是完全相同的规则——只要还有任何人可能在干活,run 就绝不会被重写。
回收从不静默:run 变成 interrupted,它的 reclaimed
字段记录确切依据(pid 1234 is not running and its heartbeat stopped at ...,或 the state directory is shared with <host> ...),原因由 superagent_status 打印,另有一行进宿主日志。在飞的子任务变成 pending,所以 superagent_resume 能继续这个 run。
心跳由拥有进程每 30 秒刷新一次,所以健康的 run 永远不会被误认成孤儿。
与用户对话
除非用户必须知道什么,插件是安静的。这是代码里的规则,不只是这份文档里的规则:
- 允许打断: run 结束并交付了结果、run 失败、或者 run 半途停下(
partial、interrupted)。用户自己取消的 run 从不通告——调用方已经知道了。 - 从不推送: 进展、子任务开始/结束、重试,以及记忆写入。
它们留在 run 记录、
superagent_status、watch 面板和报告页里;用户可以去看,但不会有东西弹出来。 - 先自愈: 每个子任务有
attempts次尝试,看起来是暂时性的失败(配额、限流、超时、5xx)还会多autoResumeRounds轮,可选地在childModelFallback上跑。只有仍然失败的东西才被报告,消息里会说明试过什么。 - 噪音有界: 每小时最多
maxNotificationsPerHour条主动消息。超出上限的 run 被缓冲,并折叠进下一条消息作为一个摘要;superagent_list显示有多少在等。如果承载折叠批次的消息投递失败,批次留在缓冲区搭下一条;只有连续三次投递失败后才丢弃。 - 不要问已经知道的事: planner 和 aggregator 拿到笔记本,并被要求用它,而不是拿它已经持有的事实去问用户。
notifyOnComplete 是总开关。有两种情况本来就会报告而不是通知:
- 同一宿主进程里的调用方正在
superagent_status里,会自己收到终态报告(不发消息,避免重复噪音)。在另一个进程里的轮询者不会抑制消息:它可能在 run 结束前一刻超时,那样唯一的通知就丢了; - 拥有该 run 的会话已经没了(关掉了无头用户的会话):run 记录
notify.delivered = false及原因,superagent_status打印notify: not delivered — ...,superagent_list把该行标成(result not delivered to the session)。报告始终可以用superagent_status读。
唤醒会话是有界的(15 s),并且心跳会一直跑到终态写入为止,所以慢唤醒不会让另一个进程把活着的 run 当成孤儿。
保留策略
默认什么都不删。把 keepRuns 设成天数,挂载时删除早于该天数的终态 run;活跃 run 从不动,被删的 id 会记日志,对应的 reports/<runId>.md 产物随 run 一起删除。superagent_list 打印当前文件数和大小,
所以增长在变成问题之前就能看见。
要按需清理,dsh-superagent-archive prune 先规划、再删除:
node "$PKG/bin/archive.js" prune --older-than 30 # dry run
node "$PKG/bin/archive.js" prune --older-than 30 --status failed # dry run, one status
node "$PKG/bin/archive.js" prune --older-than 30 --confirm # delete
规划会打印每个 run 的 id、状态、结束时间和它占的字节数;摘要给出 run、文件、字节的合计。不带 --confirm 不删任何东西。只有终态 run(completed、partial、failed、cancelled、
interrupted)才可能成为候选:还在活跃的 run,或 .lock 被另一个进程持有的 run,被列为跳过并保持原样。窗口之内的 run、没有可解析时间戳的 run,以及被隔离的
*.corrupt-* 文件也同样保留。
归档
archive export 把一个 run 变成别人可以离线打开的凭据——
不需要宿主、插件或网络:
node "$PKG/bin/archive.js" export <run-id> --out ./run-archive # directory
node "$PKG/bin/archive.js" export <run-id> --out ./run.html --format html # one page
node "$PKG/bin/archive.js" export <run-id> --out ./run.json --format bundle
目录里有 README.md(摘要、合并报告、子任务表、文件索引)、report.md(markdown 形式的报告)、run.json(完整的持久记录)、subtasks/(每个子任务一个 markdown 页面和一个 JSON 文件)、guards.json / guards.md(worker 或 aggregator 停下的每一个决定)、usage.json(每个子 agent 的 token 合计,加上运维定价产生的花费)、以及 manifest.json(归档元数据、大小,以及每个文件的
SHA-256)。--format html 把同样的内容写成一个自包含
页面,包体嵌入其中;--format bundle 只写那个 JSON。
archive import <path> 把三种形式中的任意一种读回本地存储以便
查看,恢复 run 记录和它的报告产物。它从不覆盖:本地已有同 id 的 run 时,命令在写任何东西之前就失败并
说明。导入的 run 是终态的,所以它可列出、可读,但永远不会被 resume。
从另一个进程取消
即使 run 是由另一个宿主进程启动的(web 与无头并存,或另一个终端),superagent_cancel 也能工作。交汇点是 .cancel 标记文件,不是共享内存:
- 调用方写
<runId>.cancel; - 拥有进程在一秒内察觉,abort 子 agent 的 signal,
释放它们的 activation,并写
cancelled; - 调用方最多等 5 秒,然后报告实际发生了什么。
如果 owner 没有及时应答,回复说
cancel requested ... not confirmed,而不是 cancelled。那意味着拥有进程没了或卡住了;标记留在磁盘上,下一次挂载会在恢复时把 run 转成 cancelled。整个过程不涉及 socket、管道或 Windows
命名管道,所以在每个平台上、跨共享状态目录,行为都一样。
在真实宿主上实测:两个 worker 睡在一个无视 abort 的模型调用里,
superagent_cancel 在 1.99 s 内返回 cancelled,状态文件从那一刻起不再接受新的 spawn。宿主进程仍然花了 22.8 s 才退出,因为它在等自己的子会话;那段等待是宿主的收尾,不在插件控制之内。在交互式
会话里你根本注意不到;在一次性的无头回合里,这就是为什么 cancel 看起来会"卡住",直到那个无视 abort 的子 agent 返回。
故障排查
| 你看到的 | 含义 | 怎么办 |
|---|---|---|
superagent_* 工具不存在 |
bundle 行没挂上,或者宿主拒绝加载插件。 | 跑 --dump-config 查 @xiangyixie/dsh-superagent 块;用 dsh plugin --profile <p> add @xiangyixie/dsh-superagent@0.14.4 重装,并在宿主日志里找 [dsh-superagent] 行。 |
Cannot find module .../lib/control.js(或 watch.js / report.js / wizard.js),或宿主日志里的 [dsh-superagent] dsh-superagent failed to load because a file it imports is missing: ... |
那个 profile 里的副本不完整——被中断的 add 会留下残缺的解包结果。0.14.3 起的入口会直接点名缺哪个文件。已发布的包不是原因:每个版本都带全部三十四个 lib/ 文件。 |
删掉 profile 的 node_modules 下该插件的目录(带 scope 的 @xiangyixie/dsh-superagent,或旧 tarball 安装留下的无 scope 的 dsh-superagent),再用同一条 add 命令重装,并带上版本号。不要手工拷贝文件进去。 |
[error] the model provider rejected the API key |
启动宿主的那个进程里缺 key 环境变量。 | 导出 DEEPSEEK_API_KEY(默认路由)或你那条路由声明的变量,然后重启 dsh。 |
MISSING_CREDENTIAL ... no API key for provider route "..." |
profile 的路由在环境里没有凭据。 | 设置该路由对应的环境变量并重启。错误行里写了路由名,以它为准,不要照本文。 |
[error] the subagent service is unavailable |
profile 没有 subagents 服务,插件没有东西可 spawn。 |
用基于 base bundle 的 profile(原版镜像的 web 或 headless),或者给该 profile 加上 subagent 插件。 |
error: subagent provider "..." cannot enforce maxSubagentDepth |
该路由的子 agent 后端无法限制委派深度。 | 保持 provider: spawn,或者在你接受后端自身限制时设 maxSubagentDepth: provider-managed。 |
error: the Windows file sandbox could not start a child process |
宿主沙箱启动器失败(windows-acl-run: ...),不是你的 goal 或 key 的问题。子任务会重试一次。 |
重试这个 run;如果重复出现,重启宿主,或在 profile 里关掉 Windows ACL 沙箱,然后跑 diagnose-windows-sandbox-acl 技能。 |
error: the run was not started because the active-run limit is already in use |
maxActiveRuns 满了,包括共享该状态目录的其他 profile 的 run,而这次启动没要求排队(或者队列本身也满了)。 |
用 queue: true 重试、等一等、取消一个,或者调高 maxActiveRuns / maxQueuedRuns。 |
error: the run stopped because it reached its spending cap |
撞上了 maxModelCallsPerRun 或 maxTokensPerRun。 |
读已有的部分报告;在配置里调高上限,然后 superagent_resume 会用新限额继续。 |
usage: not recorded |
run 已结束,但读不到任何子会话日志(sessions 目录被移走,或者这个 run 创建于记账功能之前)。 |
旧 run 没什么可修;新 run 会记录用量。run 还在活跃时,这一行说的是记账尚未完成。 |
usage note: N child session log(s) could not be read |
部分用量读不出来;token 数字是下界。调用预算仍然计入了这些子 agent。 | 检查 $DSH_HOME/sessions 存在且宿主进程可读。 |
cost: not computed — no price configured for X |
该模型不在 pricing 表里。 |
在 pricing["provider/model"] 下加它的每百万 token 价格,id 从 usage: 行复制。 |
cost: at least ... (no price for X) |
有些子 agent 用了未定价的模型。 | 同上;那个数字只覆盖已定价的部分。 |
[error] a saved run file is damaged |
某个状态文件被截断或是非法 JSON。 | 字节被保存在 <runId>.json.corrupt-<ts>;检查后删除。 |
cancel requested ... not confirmed |
拥有进程在 5 s 内没有确认。 | 查 superagent_status;如果 owner 已经不在,重启宿主让恢复把 run 标成 cancelled,想让它继续再 superagent_resume。 |
[error] ... still owned by a live process |
另一个 profile 正在跑这个 run,在这里 resume 会把工作做两遍。 | 让它跑完,或者先 superagent_cancel。 |
missing or wrong bearer token(HTTP 401) |
嵌入方用了上一次宿主启动的 token,或者读错了状态目录的发现文件。 | 重新从 <stateDir>/control.json 读 token;它每次启动都会重新生成。以 UTF-8 读该文件。 |
the request did not name a live session(HTTP 409) |
POST /runs 或 /resume 调用时没有 session_id,或者会话已经关闭。 |
传一个在该宿主进程中打开的会话 id;从应该拥有它的那个会话里发起。 |
宿主日志里的 [dsh-superagent] control plane is not listening |
controlPort 已被占用、值非法,或状态目录不可写。 |
用 controlPort: 0、释放端口、修好状态目录,或删掉这个键;两种情况下工具都照常工作。 |
本身失败的工具调用会返回三行:[error] 一句说明、hint:
一个下一步、以及 detail: 原封不动的宿主消息,什么都不隐藏。
在被接受之后才失败的 run 保留它的报告,并在 run 块里显示 error:
(是 failed 时再加一行 hint:)。
已知限制
- 子 agent 不由本插件隔离:它们带着宿主的 凭据和文件系统运行,受宿主自身施加的沙箱策略约束。
- run 活在宿主进程里。杀掉进程会停掉它的子 agent;
run 在下一次挂载时变成
interrupted。 - 从另一个进程取消是协作式的:它依赖拥有进程还在运行。卡死的进程只能在下次挂载时清理, 或者在它的心跳过期、配额被回收时清理。
- 规划是一次模型调用。如果回复里没有可解析的
{"subtasks":[...]}对象,run 退化为一个子任务,并 标记plan.mode = "degraded"。 - 只有一个成功子任务时跳过聚合(报告就是该 子任务的文本加上 goal 标题),aggregator 失败或预算耗尽时退回到确定性的合并。
- 保留策略是可选开启的(
keepRuns);默认状态会累积,每个子任务最多 32 KB,外加每个 run 的报告。archive prune按需删除终态 run,不给--confirm时只是演练。 - HTTP 嵌入面只走回环、可选开启、需要 token 认证; 它无法暴露到远程,也没有 MCP server。以同一用户身份运行的本地进程可以读 token,这与能写 run 文件是同一条信任边界。
- 跨机器同步是一个 git 仓库,不是服务:离线优先、
冲突即停而非无冲突、没有实时通道,也
不在机器之间搬运凭据。两台机器同时写一个状态
目录时,仍然需要在它们之间
pull。
命名
包以 @xiangyixie/dsh-superagent 发布——npm 上已经有一个
dsh-super-agent,与无 scope 的名字太接近,所以用 scope 保住
名字,而不是给项目改名。工具名不变:
superagent_*(superagent_memory 在 0.10 中以增量方式加入)。早期的工作名是 dsh-longhaul,工具前缀 longhaul_*;
从未发布过,所以旧名字在任何地方都不存在,
也没有别名层。如果你找到调用 longhaul_start 的旧片段,
把前缀换掉即可——参数没有变。
开发
npm test # node:test, fake store and fake child spawner, no network
npm pack # inspect what would ship
"可以发布"意味着什么,写成了可勾选的行,在 docs/DONE.md;其中大部分条目指向的真实宿主运行记录在 docs/e2e-2026-11-14.md。
代码布局:lib/index.js(插件、工具 schema、run 格式化)、
lib/orchestrator.js(排序、重试、预算、取消、resume)、
lib/store.js(原子 run 文件、取消标记、隔离)、
lib/errors.js(可读的错误文本)、lib/usage.js(会话日志解析)、
lib/control.js(可选的本地 HTTP 接口)。
许可证
MIT。Copyright (c) 2026 Admire (LittleChenLiya);见 LICENSE。
npm scope @xiangyixie 与 GitHub 账号 LittleChenLiya 是同一位
作者。
No comments yet. Be the first to write one.