← 返回列表
⚠ 装前注意
@freepeak/dsh-feature-loop
基本兼容但装前注意:未发布到 npm registry,仅可从源码安装 · 最近上游提交 2026/9/25 · 已提供中文文档
DeepSeek Harness 的有界智能体循环:预算上限、廉价优先路由、循环卫生检测器,以及人在环路的运维仪表盘。
综合分
32.9
GitHub 分
32.9
用户评分
—
★ Stars
4
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add FreePeak/dsh-feature-loop未发布到 npm registry,仅可从源码安装,改用 GitHub 源安装
信任档位:仅索引本站尚未对其实装验证,仅收录元数据
- 是什么
- dsh 原生插件 · ui
- 装得上吗
- 静态安装检查有提示项,装前建议看一眼 README
- 安全吗
- 本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
- 还在维护吗
- 活跃:最近一次提交在 1 天前
档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →
数据截至 2026/9/26(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查⚠ 装前注意
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包@freepeak/dsh-feature-loop(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
未发布到 npm registry,仅可从源码安装
验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/23 19:03:14
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/dsh-typert-protocol@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-llm@deepseek-ai/dsh-tools用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
由 DeepSeek 最新模型翻译生成@freepeak/dsh-feature-loop
DeepSeek Harness 的有界智能体循环
预算上限 · 廉价优先路由 · 循环卫生 · 人在回路审查
= 22">
快速开始 ·
HITL 仪表盘 ·
书籍映射 ·
架构 ·
安装设置 ·
贡献指南
一个面向缺陷修复和小型功能的书籍式策略层:预算上限、廉价优先路由、步骤级审查门,以及一个本地评判器,用于决定哪些步骤值得你关注。
它托管在 @deepseek-ai/dsh-agent-loop 自身的扩展点(agent/pre-step、agent/request、tools/pre-execute)之上,而非对其进行分叉,另有一个独立运行器,可在没有 harness 构建的情况下执行相同的策略。
已去分叉化。 本包过去曾内嵌一份智能体循环的副本。现在不再如此——harness 为本包添加的每一项策略都发布了扩展点,因此约 3,064 行分叉代码被移除,并由 src/plugin.ts 取代。审计和验收标准见 docs/PRD.md。
功能特性
| | |
|---|---|
| 为循环设定预算 | 步数和美元上限会终止运行——是限制,而非账单 |
| 廉价优先路由 | 按步骤类型划分的模型阶梯(onegw/cheap → 执行) |
| 循环卫生 | 工具循环、错误级联、预算轨迹及相关检测器 |
| 故障关闭式审查门 | 按可逆性分级;评判器置信度低 → 询问人类 |
| HITL 运维仪表盘 | 工作区、活动、分组运行、允许 / 拒绝 + 可选反馈 |
| 一套策略,两条路径 | 独立运行器和 DSH 插件共享相同规则 |
HITL 运维仪表盘
审批界面是 DSH Web UI 中的一个页面——即 Feature Loop
侧边栏条目——而不是你访问的第二个应用。当某个工具触发审查门禁时,请求会出现在一个股票经纪风格的线程中,并带有可选反馈,而不是模态中断。实时运行按工作区分组;Activity 和 Runs 独立滚动。
你可以从该页面启动循环——描述任务后,它会作为普通轮次提交,因此上限、检测器和门禁会与在编辑器中输入的任何内容完全一样地生效。
该页面遵循 harness 调色板,因此它会跟随宿主的浅色/深色主题,而不是自带一套:
配置位于同一处:
它响应的是页面,而不是窗口
仪表盘渲染在 harness 的主列内,该列比窗口窄——侧边栏和右侧栏各占一部分。因此布局由容器查询驱动:当页面宽度约为 1000px 时,运行栏会移到线程下方;低于约 520px 时,所有内容变为单列,表单控件堆叠排列。
| 窗格 | 你将获得 |
|---|---|
| 左侧 | 实时工作区/会话树 + 可滚动的活动账本 |
| 中间 | 审批线程,包含审查简报、Allow once / Reject、可选反馈 |
| 右侧 | 按工作区分组的运行,包含步骤/花费计量和信号 |
实时更新是推送的:宿主发出合并后的变更事件,页面据此重新读取,因此新的运行或待处理的请求会立即出现,而不是靠轮询。较慢的间隔仅作为丢帧时的安全网保留。
只有当页面确实在监视时,请求才会被认领;否则它会落到编辑器面板,因此没人打开的仪表盘永远不会搁置一个决策。
对于无头和 CI 场景,仍有一个可选的环回页面——在补丁行中设置 dashboard.standalone: true——它会在 127.0.0.1 上以自带令牌提供相同的组件。详情:
docs/VERIFY-DASHBOARD.md。
演示可用
$ bash demo/run.sh
...
── step 10 · xiaomi/mimo-v2.5 · spent $0.0044
[signal:critical] error-cascade — 连续 3 个失败步骤 — 第一个失败才是需要阅读的那个
[review] 通过 signal 请求人工介入 — 连续 3 个失败步骤 — 第一个失败才是需要阅读的那个
── 步骤 12 · xiaomi/mimo-v2.5 · 花费 $0.0047
[judge] 审查价值 0/3
[model] 3 个失败,全部位于 percentile 方法中。文档注释说最近秩使用
ceil(p/100n) - 1,但代码使用的是 Math.floor((p/100) n)。当
p/100n 为整数时二者不同 — floor 给出该索引,但 ceil(n)-1 给出它前一个。
[review] 通过策略请求人工介入 — edit_file:可逆写入低于置信度阈值(0.00 add -w file:$PWD # 配置文件必须使用 -w
dsh --profile --dump-config | grep -A8 feature-loop # 验证组合
一旦运行起来,受门禁控制的步骤会在浏览器中提示你:编辑器会显示原因,并带有 Reject / Allow once 选项,你的回答决定该工具是否运行。这已被端到端观察到 —— 一个真实浏览器渲染了此插件自己的 REVIEW REQUESTED 原因,Allow once 写入了文件,而对同一提示选择 Reject 则没有写入任何内容:
docs/VERIFY-E2E-APPROVAL.md。你并不是在编写那个 UI —— 它作为 @deepseek-ai/dsh-client-ui-approval 随 DSH 一起发布;插件的职责是发出 ask,以便它被触达。
- docs/SETUP.md —— 安装并端到端驱动它。
- docs/RUNBOOK-SERVER.md —— 经验证的实时服务器:验收清单、启动命令、URL,以及会静默禁用审批提示的权限预设陷阱。
- docs/PRD.md —— 当前设计背后的审计。
- docs/VERIFY-APPROVAL.md —— 对所有五种审批结果的独立验证,附 file:line 证据。
- docs/VERIFY-INTEGRATION.md —— 在真实 DSH 上下文中执行的同样五种结果(5/5 通过),外加审批面板渲染的确切字符串。
- docs/VERIFY-DASHBOARD.md —— 审批仪表板:296 个单元测试 + 11 个集成测试全绿,一份实时 HTTP 记录(页面 200,token 401,批准 → allowed-once,409,403),通过 make e2e-dashboard 验证的浏览器点击,以及模型编写的审查简报。
- docs/VERIFY-E2E-APPROVAL.md — 在容器化部署上运行的真实浏览器:面板会显示此插件的原因,Allow once 会写入文件,Reject 会阻止它。
- docker/README.md — 一条命令即可运行容器。
书籍映射
| 书籍概念 | 模块 | 位置 |
|---|---|---|
| 8 维循环规范 | spec.ts | LoopSpec — 全部 8 项必填,加载时校验 |
| “命名终止条件” | spec.ts | TerminationSpec.successCommand |
| “为循环做预算,而不是为请求做预算” | budget.ts | LoopBudget.verdict() |
| “按步骤类型路由模型” | routing.ts | ModelLadder.forStep() |
| “不到 10% 的情况下才询问人类” | review.ts | AttentionRouter.route() |
| 审批门禁,按可逆性分级 | review.ts | ReviewGate.check() |
| 工具循环检测器(阈值 3) | signals.ts | trailingRepeat() |
| 错误级联检测器(连续 3 次) | signals.ts | detectSignals() |
| 预算警告(0.8) | budget.ts | warnAt 默认值 |
| 渐进式自主 | review.ts | DEFAULT_GATE_POLICIES |
| BUG_FIX_PROMPT(6 条规则) | prompts.ts | BUG_FIX_RULES |
| FEATURE_PROMPT(5 条规则) | prompts.ts | FEATURE_RULES |
| “将成功与停止分开” | runner.ts | 每一步之后检查成功 |
每个阈值都引自 BOOK_THRESHOLDS 中的 playbook,因为一个没有出处的阈值,不过是某个人喜欢的数字。
两条路径,一个策略层
策略是共享的。只有传输和会话状态不同。
| | 独立运行器(src/runner.ts、src/cli.ts) | DSH 插件(src/plugin.ts) |
|---|---|---|
| 传输 | src/llm.ts → onegw | harness llm 服务 |
| 预算上限 | ✅ 已接入 | ✅ 已接入(agent/pre-step、reject) |
| 廉价优先阶梯 | ✅ 已接入 | ✅ 已接入(agent/request) |
| 计量 | ✅ 已接入 | ⚠️ 见“已知限制” |
| 信号 | ✅ 已接入 | ✅ 已接入(agent/pre-step) |
| 审查门禁 | ✅ 已接入(阻止) | ✅ 已接入(tools/pre-execute,询问) |
| 浏览器中的人类审批 | ➖ 控制台提示 | ✅ Web UI 输入框提示 |
| HITL 审批仪表板 | ➖ | ✅ 运维控制台:工作区树、活动、分组运行、Allow/Reject + 反馈(dashboard: 配置) |
| 评判器 | ✅ 已接入 | ✅ 已接入(agent/pre-step,等待) |
| 操作员审查 | ✅ 已接入(阻止) | ✅ 提示,然后阻止 |
门禁在派发之前就已决定,并且由人类在浏览器中作出回答。
这曾经是两条路径之间唯一真实的差异,而且它是一个缺陷:旧分支从它自己的 executeToolCalls 副本中查询门禁,因此门禁引发的审查会晚一步到达——在工具已经运行之后。该分支的 README 将阻塞式审批列为一项未完成的改进。
托管在 harness 上解决了这个问题。tools/pre-execute 是一个一等公民的预派发钩子,返回 PreToolDecision(allow / deny / ask):
ctx.on('tools/pre-execute', async ({ agent, name }, next) => {
const gate = gateForTool(policyFor(agent), name)
if (gate.kind === 'proceed') return next()
return { kind: 'ask', reason: gate.reason } // 已应答,而非丢弃
})
ask 会路由到部署的审批通道——在 Web UI 中,即来自 @deepseek-ai/dsh-client-ui-approval 的对话编辑器提示。你批准后工具即派发;你拒绝后模型会被告知有人说了不。插件的工作在发出决策时结束;harness(@deepseek-ai/dsh-tools 中的 serviceAsk)将结果映射为允许或拒绝。
被阻止的调用是已应答的,而非被丢弃:助手的工具调用块必须收到一个结果,否则会话重放将失效。deny 会生成一个工具错误,模型可以读取并对其做出反应。
gateMode —— 如何询问人类
| 模式 | 行为 | 适用场景 |
|---|---|---|
| ask (默认)* | 提示审批通道。当未挂载任何通道,或结果为 unavailable 时,安全失败为拒绝。 | 交互式 Web UI 会话 |
| deny | 直接拒绝,从不提示。 | 无人值守运行和 CI |
ask 是默认值,因为当没有审批通道时,它会降级为与 deny 完全一致——它在不降低安全性的前提下严格更具能力。当没有人类在观察时,请设置 deny。
- id: feature-loop
config:
gateMode: ask # 或:deny(CI / 无人值守)
审批仪表盘
除了编辑器提示之外,插件还可以托管自己的回环网页(src/dashboard.ts + src/dashboard-page.ts,仅使用 Node 内置模块;UI 是一个从 web/app.tsx 构建、使用 assistant-ui 的 vendored React 包):待审批卡片携带门禁的 REVIEW REQUESTED 原因,并带有允许一次 / 拒绝按钮;通过 SSE 展示实时运行状态(步数与上限、花费、阶梯路由、评判分数、信号);以及门禁决策和审批结果的活动流。每个响应使用 nonce CSP(default-src 'none'、frame-ancestors 'none')来声明姿态:页面只运行自己的脚本和样式,别无其他。
- id: feature-loop
config:
dashboard:
enabled: true # 启动服务器;省略 = 仅编辑器
host: 127.0.0.1 # 127.0.0.1 | 0.0.0.0 —— 封闭集合
port: 8100 # Docker:容器内 0.0.0.0:8100,
发布为 127.0.0.1:3092(仅回环)
answers: true # false = 仅观察,编辑器继续应答
answerTimeoutMs: 600000 # 待处理的 ask 在此时间后安全失败
brief: # 模型撰写的审查简报,默认关闭
enabled: false
model: xiaomi/mimo-v2.5 # 启用时必填
maxTokens: 1024
timeoutMs: 15000
make up 会打印 URL + token(make dashboard 会重新打印):token 在每次启动时生成,且每个请求都需要它。在此功能之前已播种的现有 Docker 卷需要执行 FORCE_REINIT=1 make up 才能拾取该行。
optimize 块——基于实测历史的精炼轮次
optimize: 与 spec 并列,省略时不会改变任何东西。存在时会在加载时进行校验(loops: 30 会阻止插件加载,并指出字段名和 3–10 的范围):
- id: feature-loop
config:
optimize:
loops: 3 # 精炼轮次,整数 3–10(仅 CLI runRefined)
derive: true # 接受;CLI 的 --derive 推导包络,插件记录它们所依据的历史
history: .feature-loop/runs.jsonl # 运行历史文件:插件每关闭一个回合追加一行,并据此为 Metrics 提供数据
judge: chat # none | chat | laya — 由谁在各轮次间评分(仅 CLI)
totalBudgetUSD: 3.00 # 精炼预算;默认 derived × loops × 0.6(仅 CLI)
两半,各归其位:
- CLI 负责推导和建议。 每次 CLI 运行都会向历史文件追加一行(JSONL,仅使用 Node 内置模块)。使用 --derive 时,下一次运行的包络(maxSteps、costBudgetUSD)是该目标已记录运行的 P95 加上 30% 余量——显式的 --max-steps/--budget 是钉死的值,永远不会被覆盖,而配置的预算是推导可以收紧但绝不能提高的上限。随后运行会打印其 Metrics 汇总和 judge 的建议。历史太少(少于五次可用运行)时不会应用任何内容——下限不是测量值。每个数字的来源都会被打印出来。
- 插件负责记录和汇总。 在 session/event 的 turn/end 上——这是恰好一次的运行接缝,pre-step 拒绝也会通过它关闭——插件仅根据计量数字追加一条记录(steps/cost/未定价 steps 的预算快照,latencyKind: 'round-trip'),然后从该文件刷新仪表盘的 Metrics 面板。judge 测试组在这条路径上被刻意地 NOT 询问:一个热路径事件必须保持廉价,因此 Optimizations 建议在出现更廉价的节奏之前一直是 CLI 的事。无论仪表盘页面是否打开,记录都会被写入,因此带有 history: 的无头部署仍然能学习。
建议仅用于展示——应用一条建议意味着手动复制其代码片段;刻意没有端点让 judge 放松自己的上限。loops/totalBudgetUSD 在加载时校验,但仅由 CLI 的 runRefined 消费:迭代属于调用方,而不属于 step 瀑布,因此一个在 hook 内部授予轮次的插件配置会是一个戴着功能帽子的超时。
守卫就是安全属性。 仪表盘的应答器注册在所有其他 approval/request 监听器之前——这是必需的,因为当 Web UI 标签页附加时,harness 的远程转发器会持有请求而不调用 next()——但它仅在仪表盘标签页实际连接时才认领请求。没有标签页 → 它委托,而 composer 恰好应答
和之前这个功能存在时一样。每条失败路径(最后一个标签页关闭、请求撤回、超时、关闭)都会将待处理的请求结算为 unavailable,harness 会将其映射为拒绝。已在包括浏览器点击在内的每个层级验证:docs/VERIFY-DASHBOARD.md。
UI 是 assistant-ui;决策仍然是我们的。 仪表盘通过 assistant-ui 渲染:待处理的请求变成携带审批闸门的工具调用消息部分,而 onRespondToToolApproval 是将点击转化为请求的单一回调。该回调会向此插件自己的受保护端点(POST /api/approvals/:id)发送请求——与 composer 路径使用的是同一个端点、同一个令牌,以及同样的故障关闭行为。assistant-ui 决定如何请求决策;它从不决定某个决策是否有效。
映射逻辑位于 src/approval-bridge.ts,它不导入任何内容,因此可以在 CI 的无安装任务中测试。它被刻意收窄:只提供 allow-once 和 reject-once,绝不提供 allow-always/reject-always,因为浏览器点击不得放宽部署的闸门——那是 gatePolicies 和 actuator 的职责。未知选项会抛出错误,而不是解析为授权。
审查简报纯粹是建议性的。 启用 brief.enabled 后,每个被认领的请求还会触发一次模型调用,由模型撰写纯文本(src/brief.ts),并显示在卡片内。不再有模型编写的组件语言——在 OpenUI 下,模型发出组件调用,而解析器加允许列表让这变得可承受;assistant-ui 没有这样的语言,因此剩下的是始终承载关键作用的部分:边界。简报受节点数量、列表长度、单字符串长度和总大小限制,任何超过上限的简报都会被整体拒绝,而不是截断——半渲染的简报读起来像完整的简报,当文本是人类决策依据时,这正是要紧的失败。简报从不阻塞、延迟或结算请求:它要么解析为一张卡片,要么解析为“简报不可用”,要么什么都不解析。
内置的打包产物可审计且离线。 web/build.mjs 从 web/app.tsx 生成 assets/assistant-ui/dashboard.js(约 470 KB;gzip 后约 142 KB),并且如果输出包含 assistant-cloud、遥测上报器或回连行为,就会拒绝构建——@assistant-ui/react 依赖 assistant-cloud,后者附带互动/运行上报器,目前会被 tree-shaking 移除;构建将“目前”变成了受检查的不变量。页面在 default-src 'none' CSP 下从自身源加载该打包产物,并使用每次响应生成的 nonce,且该打包产物恰好包含一个 URL(https://react.dev)。
架构
src/
── 产品:纯策略,不导入 harness ──
spec.ts 本书的 8 个维度,在加载时验证
budget.ts 步骤/成本上限、USD 价格表、未定价步骤跟踪
routing.ts 廉价优先的阶梯,基于证据升级
signals.ts 6 个确定性检测器——循环、级联、支配、预算、步骤、质量
review.ts 可逆性门控 + 注意力路由器( assistant-ui 审批映射(纯函数)
explainer.ts 简要的模型调用(默认 NO_EXPLAINER)
── 独立证明:相同的策略,无测试框架 ──
runner.ts spec → budget → route → judge → review → model → tools
llm.ts 兼容 OpenAI 的客户端 + 用于测试的脚本化客户端
tools.ts 沙箱化的 read/write/edit/list/run_tests,路径受限
cli.ts 演示入口点
demo/
src/latency-window.ts 植入的 bug(最近秩差一错误)
test/ 14 个测试,其中 3 个在该 bug 上失败
README.md 提供给循环的 bug 报告
verify.sh 退出码 0 = 目标达成
reset.sh 重新植入 bug
run.sh reset + run,一条命令
TRANSCRIPT.txt 一次捕获的成功运行记录
评判器
路由器的问题——“这一步是否值得让人过目?”——是一个
分类问题,因此在一个接口背后有三种实现:
| 评判器 | 成本 | 延迟 | 状态 |
|---|---|---|---|
| OnegwJudge(Laya) | $0,本地 | 冷启动约 1–4 秒,热启动 <200 毫秒* | 客户端就绪;2026-09-23 针对本地 sidecar 实测验证(scripts/laya-sidecar.py、~/venvs/laya、端口 8091)——三个原语均有响应,完整测试组返回建议 |
| ChatJudge | 按量计费 | 约 10–40 秒 | 演示所使用的 |
| NO_JUDGE | $0 | 0 | 仅检测器,一种受支持的模式 |
\* Laya-sidecar 计时在本机测得:首次预测约 7 秒(冷权重),之后每次热调用约 0.9–4 秒——远高于 JEV 文档中的 73 毫秒(该数字是原始前向传播;我们的数字包括 HTTP + 路由 + 一个偏冷的进程)。但按挂钟时间计算仍比聊天评判器便宜 10 倍,且为 $0。
要将演示指向本地 Laya(:8091 上的容器化 sidecar,相同的
System One / Jev / TypeSafe 通信协议):
bash
bash demo/run.sh --judge laya --judge-base-url http://127.0.0.1:8091
或:SYSTEMONE_BASE_URL=http://127.0.0.1:8091 bash demo/run.sh --judge laya
--judge laya 会将评判器基础 URL 默认为 http://127.0.0.1:8091(可覆盖
在 --judge-base-url / SYSTEMONE_BASE_URL 下)。actor 仍然与 onegw 通信;
只有 judge 的 URL 被拆分。评分标准以有序数组的形式传递,这样 Laya 就能保留
人工标签;noul 的回答映射到 probability。
该演示使用 ChatJudge 搭配 xiaomi/mimo-v2.5,因为在 ~/.onegw/onegw.toml 中
没有配置 systemone provider。有两个实测事实支持 Laya 而不只是出于成本考虑:
1. 推理模型是不合格的 judge。 mimo-v2.5 总是会思考;在
max_tokens: 256 时,它返回 finish_reason: "length" 且内容为空,并且
在输出一个数字之前需要约 859 个思考 token。现在默认值是
2048,并且错误信息会指出原因,而不是说“没有数字”。
2. 它校准不良。 当被问及一次常规的 read_file 且没有检测器
触发时,它回答 SCORE=3 —— 量表的最高值。专用决策
引擎才是正确的工具;通用聊天模型只是后备。实时 Laya
对相同形态的评分是 1.409,而常规与危险对比为 1.465 —— 方向
正确但接近随机,这与 JEV 文档的警告一致,即基础检查点
需要专门化,然后它们的等级才能驱动策略。
Fork delta —— 已移除
这个包过去从 @deepseek-ai/dsh-agent-loop vendor 了九个文件,并将
每个自定义项标记为 FORK-DELTA(35 个标记),通过 upstream.lock
固定到 c291e796(v0.1.5-rc.2),并由 scripts/sync-upstream.sh 进行协调。
所有这些都已消失。 harness 为这个包添加的每一项策略都发布了扩展点,
因此 vendored 的 loop、同步脚本、lock 以及
cordis.patch.yml 都被删除,并由 src/plugin.ts 取代。
| 已移除 | 行数 | 上游差异 |
|---|---|---|
| src/agent.ts | 968 | 364 行变更 |
| src/index.ts | 1069 | 222 行变更 |
| src/tool-calls.ts | 328 | 46 行变更 |
| src/inbox.ts | 247 | 7 行变更 |
| src/runtime-context.ts | 159 | 0 —— 字节完全相同 |
| src/assistant-stream.ts | 140 | 0 —— 字节完全相同 |
| src/constants.ts | 6 | 0 —— 字节完全相同 |
| src/invariant.ts | 65 | 0 —— 字节完全相同,未被任何内容导入 |
| src/notices.ts | 82 | vendored inbox.ts 的包装器 |
| scripts/sync-upstream.sh、upstream.lock、cordis.patch.yml | 140 | fork 机制 |
约 3,064 行移除,换入一个 368 行的插件。完整审计见
docs/PRD.md。
在 fork 存在期间,其中发现了两个真实 bug,现在都已不再重要:
- import { FiberState } from '@deepseek-ai/cordis' 是一个硬性 SyntaxError ——
cordis 将其声明为环境 const enum,没有运行时导出。它在上游
能工作,只是因为打包器内联了这些值。(继承自上游。)
- 在 function 生成器内部读取的 this.policy 在运行时是 undefined,
因此 fork 的第五个构造函数参数从未到达 agent。
已知限制
- src/plugin.ts 在 CI 中未进行类型检查。 它导入的 @deepseek-ai/dsh-
版本是 CI 无法解析的(没有 lockfile,并且
@deepseek-ai/cordis@0.4.0 没有已发布的版本)。它在本地针对预构建的包进行类型检查。
- test/plugin-approval.test.ts 不在 CI 中运行。 它导入了
src/plugin.ts,其 @deepseek-ai/dsh- 导入了无安装的 test 作业
cannot resolve. Like src/plugin.ts it is run locally against the prebuilt
packages; the same handshake is covered by the integration suite
(test/integration/plugin-in-dsh.spec.ts), which is where the plugin's
behaviour is exercised against the real harness.
- Spend is metered on both paths. runner.ts prices every model result into
LoopBudget, and the plugin drains settled assistant/message events into
spend() from both hooks (cursor-deduped, assistant/attempt retries still
unpriced — marked ponytail: at the call site). The cost ceiling is
load-bearing: a run with a deliberately tiny costBudgetUSD stops on cost.
- The approval prompt depends on the session's permission preset, and the
user's settings win.* A fresh session's approval policy comes from
permission.defaultPreset in ~/.dsh/settings.yaml, which
permission-presets applies after any config default. On this machine it is
danger-full-access, whose preset is approval: never — so an ask is
refused with Error: the user rejected tool "X" *before any UI is
consulted, and no panel can appear. Three fixes: switch the preset in the
session's UI selector, set that key in settings.yaml (global, and relaxes the
sandbox), or run a server under a private DSH_HOME whose settings pin
workspace-write — the last changes nothing globally. Details in
docs/RUNBOOK-SERVER.md §2.5-2.6.
This is proven, not assumed: the same headless task returns rejected under
the default home and unavailable ("no approval channel is available") under a
workspace-write home. The second is the gate's ask reaching the approval
seam; the first is never refusing before any UI is consulted.
- gatePolicies / actuator keys must be the harness's real tool names
(read, write, edit, bash, glob, grep). The read_file / edit_file
/ run_tests names belong to this repo's standalone runner and match nothing
in a DSH session. A non-matching name is not an error — the tool falls through
to the unclassified irreversible default, so the gate still fires but with
always-approve rather than the policy you configured.
- An agent-less tool call cannot prompt. It is still gated (it no longer
bypasses the gate), but with no agent there is no session to audit to and no UI
to reach, so the harness resolves the ask to a refusal.
- The plugin path needs a build. It imports @deepseek-ai/dsh-; the
standalone runner does not.
- Review rate is 20% in the demo, not the book's <10%. Critical signals are
deliberately not rate-limited — safety is not subject to an attention budget —
so a run with an error cascade will exceed the target. The budget governs
judge-driven reviews only.
- run_tests timeouts kill the direct child, not grandchildren (marked
ponytail: in tools.ts; upgrade path is detached spawn + kill(-pid)).
- The price table is an estimate. mimo-v2.5 runs on a subscription plan,
so marginal cost is near zero; the rates in cli.ts are illustrative and
exist so the ceiling has something to measure against.
- The dashboard's rendered page click is verified via make e2e-dashboard
(headless Chromium clicks Allow/Reject against the real page and asserts the
ask settles). docs/VERIFY-DASHBOARD.md records
the transcript. Related: spend shown on the dashboard
inherits the metering gap above, and a Docker volume seeded before the
dashboard existed needs FORCE_REINIT=1 to re-seed its profile.
Phase status
- Phase 0 — policy layer ✅ tests, pure, no harness dependency
- Phase 1 — loop integration ✅ runner + CLI + tools + demo, working end to end
- Phase 1b — plugin compiles ✅ tsc --noEmit clean against prebuilt @deepseek-ai/dsh-
- Phase 2 — plugin review gate ✅ all six detectors, the judge, the router and
the gate are read by src/plugin.ts; the gate asks before dispatch.
- Phase 1c — de-fork ✅ the vendored loop is gone; the policies are hosted on
agent/pre-step / agent/request / tools/pre-execute. See
docs/PRD.md.
- Phase 1d — human approval in the Web UI ✅ the gate emits ask, DSH routes
it to @deepseek-ai/dsh-client-ui-approval, and a human approves or rejects in
the composer. gateMode: ask | deny. Verified live on a scratch profile with
Agent Teams.
- Phase 1e — HITL approval dashboard ✅ optional loopback web surface
(src/dashboard.ts + src/dashboard-page.ts): pending cards, live run state
over SSE, Allow/Reject over HTTP — guarded so a tab-less deployment behaves
byte-identically to the composer-only path. 296 unit + 11 integration tests;
the browser click verified via make e2e-dashboard
(docs/VERIFY-DASHBOARD.md).
- Phase 1f — review briefs ✅ model-authored brief per ask
(src/brief.ts + src/explainer.ts): plain prose, bounded, rendered inside
the approval card — purely advisory, never blocking. Disabled by default
(dashboard.brief.enabled).
- Phase 1g — assistant-ui dashboard ✅ the UI runs on assistant-ui
(web/app.tsx, vendored as assets/assistant-ui/); approvals ride
ToolCallMessagePart.approval and onRespondToToolApproval, mapped by the
pure src/approval-bridge.ts onto the existing guarded endpoint.
- Phase 2b — real spend accounting ✅ runner.ts prices every model result;
the plugin drains settled attempts from both hooks. The cost ceiling stops
runs; test/budget.test.ts asserts spend is non-zero after a priced attempt.
- Phase 2c — Laya-guided optimization ✅ optimize: block, run history,
derived envelopes, metrics, Laya-as-advisor proposals (display only),
refinement passes with the book's < 0.05 stop rule, and SSE metrics +
recommendations on the dashboard snapshot. Composition is wired both ways:
the CLI derives envelopes and prints metrics + proposals (--derive), the
plugin records one history line per closed turn and feeds Metrics from the
file (optimize.history); proposals stay CLI-only so the hot path stays cheap.
- Phase 3 — Laya ✅ verified live 2026-09-23 (all three primitives + full battery via scripts/laya-sidecar.py); remaining: a --judge-base-url flag so the CLI can point at the sidecar without onegw, and calibration before levels drive policy (routine-vs-dangerous discriminated by only +0.056)
Verifying the whole thing
bash
node --experimental-strip-types --test test/.test.ts # 296 pass
pnpm test:integration # 9 pass, in the real harness
tsc --noEmit # clean
bash demo/run.sh # goal-met
grep -rn "FORK-DELTA" src/ | wc -l # 0 — the fork is gone
pnpm test:integration is the one that matters for human approval: it mounts
this plugin into a real cordis context — real tool runtime, real approval
service, real session — and drives the actual dispatch path, asserting that
approving runs the write and rejecting stops it. See
docs/VERIFY-INTEGRATION.md.
Four gaps closed along the way
- The gate was one step late. The fork consulted its gate from its own copy of
executeToolCalls, so a gate-raised review arrived after the tool had already
run. Hosting the gate on tools/pre-execute decides the call before dispatch —
the fix the fork could not reach without forking.
- error-cascade could not fire in the plugin path. StepObservation.error
was never populated: per-call isError was internal to tool-calls.ts and
thrown away at the executeToolCalls boundary. Since error-cascade is one of
only two critical signals, the plugin gate was silently running on four
detectors instead of six. plugin.ts records the outcome from the tool
boundary, so the step's observation learns the result.
- tool-dominance fired on step 1. At one or two steps every tool is
trivially 100% of all steps, so the detector fired on the first step of every
run — a signal that always fires is noise that trains its reader to ignore the
real ones. It now needs a five-step floor before it is allowed to speak.
- The gate failed open for an agent-less call. policyFor returned
undefined for agent === undefined, so the handler delegated and the call
dispatched ungated. An agent-less call now gets a shared policy and is
gated like any other; the refusal happens downstream, where the harness denies
an agent-less ask. Caught by independent verification, not by CI.
Contributing
See CONTRIBUTING.md. Please open issues with the templates under
.github/ISSUE_TEMPLATE. Security reports:
SECURITY.md.
License
MIT © Linh Doan / FreePeak