dsh-evolve 架构设计(v0.1)

August 20, 2026 · View on GitHub

定位:Evidence-driven runtime optimization for DeepSeek Harness —— 把执行轨迹变成可测试的运行时策略。 核心边界:DeepSeek Harness 负责执行,dsh-evolve 负责基于执行轨迹进行优化。 插件旁挂,不接管;插件移除后 DSH 恢复原始行为。 前置阅读:docs/dsh-runtime-analysis.md(源码证据)。


1. 设计原则(来自需求 §二)

  1. 标准 DSH/Cordis 插件:以 bundle(dsh.bundle.patch → cordis.patch.yml)或 profile 依赖 + patch 行的形式安装;配置行级启用/禁用;无 monkey patch;无源码修改。
  2. 不替换官方 Agent Loop:v0.1 只观察 + 注入,loop 原样运行。
  3. Runtime 热路径极轻:无 LLM 调用、无 embedding、无向量检索、无每-step 数据库查询、无完整 session 重分析、无在线策略搜索。只做 collect → normalize → incremental update → deterministic detection → apply approved policy → record
  4. 不是新的 Runtime Authority:Session/Agent/Tool/Context/LLM/Sandbox/Lifecycle 全部归 DSH;dsh-evolve 只有 Optimization Policy / Trajectory Analysis / Experiment Results / Intervention Decision。
  5. 双平面:Runtime Plugin(轻、在线、安装在 DSH 内)与 Offline Lab(重、离线、分析+实验)。两者通过 Policy Recipe轨迹数据 交互。
  6. 污染最小化:不向 DSH durable Session 写插件自有状态(唯一例外是模型可见的策略重置消息,走 Core 词汇 user/message / agent/inbox/spliced)。

2. 双平面总图

                        dsh-evolve

         ┌──────────────────┴──────────────────┐
         ↓                                     ↓

   Runtime Plugin(安装在 DSH 内)        Offline Lab(独立进程/脚本)
   ───────────────────────────         ───────────────────────────
   Observe(tools/result, session/event)   Read trajectories(session JSONL)
   ↓                                      ↓
   Extract incremental features         Analyze(replay-style stuck 打分)
   ↓                                      ↓
   Detect stuck(StuckScore ≥ 阈值)       Tune / Sweep 参数(可选 v0.2)
   ↓                                      ↓
   Apply active policy(STRATEGY_RESET)   Run Eval(baseline vs treatment)
   ↓                                      ↓
   Record intervention evidence          Compare recipes → Promote/Reject
   ↓                                      ↓
   └───────────────┬──────────────────────┘

            Policy Recipe(versioned JSON)
            ~/.dsh/evolve/policies/{baseline.json, reset-v1.json, …}

交互契约:

  • Runtime → 数据:每次干预写 InterventionRecord;每个 session 结束时写一条 RunSummary(见 §5.4)。分析用原始 session 日志(DSH 持久化,~/.dsh/sessions/...)随时可读,不依赖插件状态。
  • Offline → Runtime:只通过 recipe。Runtime 永不自行修改配置;recipe 由人(v0.1)或 Optimizer(v0.2)生成,经 eval 后 promote。

3. Runtime Plugin 模块与文件树

按需求 §二十四的建议结构精简(去掉 eval/ 与 offline/——它们属于 Offline Lab 平面):

dsh-evolve/            (npm 包:@dsh-evolve/plugin,或独立 scoped 包)
├ package.json          dsh: { bundle: { patch: ./cordis.patch.yml } }
│                       peerDeps: @deepseek-ai/cordis, @deepseek-ai/dsh-agent,
│                       @deepseek-ai/dsh-session, @deepseek-ai/dsh-llm, @deepseek-ai/dsh-tools
├ cordis.patch.yml      插入插件行:dsh-evolve(detector+intervention+storage)+ 每-agent preset 钩子(可选)
├ src/
│  ├ plugin/
│  │  ├ index.ts        插件入口:name/inject/Config/apply;组合各模块
│  │  └ lifecycle.ts    生命周期编排:注册/清理所有 listener/timer/service/状态
│  ├ collector/
│  │  └ trajectory-collector.ts   监听 session/event + tools/result,维护每-agent 增量轨迹
│  ├ features/
│  │  ├ feature-extractor.ts      由增量轨迹推进 TrajectoryFeatures(窗口 ring buffer)
│  │  ├ invocation-normalizer.ts  tool name + canonical args → 调用 key(deep key-sort JSON,参考 repeat-tool-reminder 实现)
│  │  ├ error-normalizer.ts       tool + exit-code-等价物(code) + 标准化错误文本 → 错误 signature
│  │  └ observation-fingerprint.ts tool/result content → 低成本哈希指纹(xxhash/fnv 风格,禁止 embedding)
│  ├ detector/
│  │  ├ stuck-detector.ts        窗口内特征 → 各信号计数/比例
│  │  └ stuck-score.ts           StuckScore 加权求值(recipe 参数驱动)
│  ├ intervention/
│  │  ├ controller.ts            cooldown/maxPerTurn/maxPerSession 门禁 + InterventionRecord 落盘
│  │  └ strategy-reset.ts        STRATEGY_RESET 消息构造(createUserMessage + plugin source)
│  ├ policy/
│  │  ├ recipe.ts                PolicyRecipe 类型 + 校验
│  │  ├ loader.ts                从 ~/.dsh/evolve/policies/ 读 active recipe(或 workspace 覆盖)
│  │  └ registry.ts              active recipe 解析/缓存/热重载(版本校验)
│  ├ storage/
│  │  ├ evolve-store.ts          ~/.dsh/evolve/{runs,policies,reports} 读写(JSONLines/JSON,原子写)
│  │  └ interleaved.ts           InterventionRecord / RunSummary 序列化
│  └ contracts/
│     ├ trajectory.ts  features.ts  intervention.ts   (类型 + 版本化 schema)
├ docs/  recipes/  tests/  reports/

依赖注入(inject):agents(必)、sessions(观察)、sessionPersistence(可选,定位 session 工件)。不依赖 agent-loop 具体实现(官方约定:扩展插件依赖 agent,不依赖 agent-loop,见 docs/subsystems/core.md:20)。


4. Trajectory Evidence 清单(正式参考)

docs/dsh-runtime-analysis.md §1——此处只列 v0.1 实际消费的字段与信号对应:

证据来源事件(持久/实时)信号
工具名 + 原始参数tool/call(持久)B 重复调用
工具成败tool/result.message[0].isError(持久);tools/result result(实时)A 连续失败
结构化错误tool/result.error.{name,code} + message 文本C 重复错误 signature
结果内容tool/result.message.content(文本块)D 无新观察(指纹)
是否写完/编辑类工具成功tool/call.name + tool/result.isError(持久);fs/observed(实时可选)E 工作区进展(stepsSinceWorkspaceChange
Step/轮次计数step/startstep/end(持久)totalSteps、窗口边界
人类干预user/message.source.kind==='user'(持久)重置窗口(防止跨干预计数)
Turn 结局turn/end.reason(持久)eval 结果标签

约束:不为了填特征而伪造数据——无法可靠获得的字段(如 exitCodecacheReadTokens 若 provider 不报)一律 optional 并用 recipe 开关门控。


5. Stuck 信号与打分(v0.1)

5.1 接口

interface TrajectoryFeatures {
  totalSteps: number                      // 自插件观察起(或 recipe 定义的重置点)累计 step
  consecutiveToolFailures: number         // 连续失败步数(A)
  repeatedToolRuns: number                // 当前最长的"同名同参连续调用"run 长度(B)
  repeatedErrorSignatureRuns: number      // 当前最长的"同 error signature 连续"run 长度(C)
  noNovelObservationSteps: number         // 距上一个"新观察"的步数(D,窗口内计数)
  stepsSinceWorkspaceChange?: number      // 距上一个"工作区变更"的步数(E,可选,默认关)
}

interface StuckReason { signal: 'consecutive-failures'|'repeated-actions'|'repeated-errors'|'no-novel-observation'|'no-workspace-change'; count: number }

窗口语义:windowSize 的滑动窗口(默认 6)。每个信号只统计窗口内的 run/计数;run 在收到"非同一 key 的调用"或"用户消息"时重置(与 repeat-tool-reminder 的 chain 语义一致,但更宽:错误 signature 与观察指纹也各自成链)。

5.2 信号定义(全部确定性、可离线复算)

  • A. consecutiveToolFailures:连续 step 中,一个或多个 tool/result.isError 且无成功 mutation 的步数。单次失败不算 stuck。
  • B. repeatedToolRuns:对 (toolName, canonicalArgsHash) 做 canonicalization(deep key-sort JSON → 哈希),窗口内最长连续相同 key 的调用数。关键防误判:B 只在与"该次调用前后无状态变化"联合时计分——实现上把 B 与 D/E 捆绑:若窗口内存在新观察或工作区变更,B 的贡献按 discount 衰减(recipe 参数 repeatedActions.noveltyDiscount,默认 0.5)。这样"改代码→测试→再改→测试"的循环不会被误伤。
  • C. repeatedErrorSignatureRuns(toolName, error.code, normalizedErrorTextHash) 的标准签名;窗口内最长连续相同签名的调用数。MODULE_NOT_FOUND ×3 比"连续失败 ×3"强得多。
  • D. noNovelObservationSteps:每个 tool/result 的 content 做低成本指纹(规范化文本 → 稳定哈希,如 FNV-1a 64 位或 sha256 前缀);"新观察" = 指纹与窗口内前 N 个不同。连续无新观察的步数。prune 掉纯噪声差异(recipe 参数:忽略长度 < L 的微小文本差异为"不新")。
  • E. stepsSinceWorkspaceChange(默认关):持久等价物 = "自上次成功的 mutation 类工具调用(recipe 配置的名单,如 write/edit/apply_patch/bash 执行成功)的步数";在线增强 = fs/observed。实测证明可靠后再默认开启。

5.3 StuckScore

stuckScore(step) =
    wA · fA(consecutiveFailures)      // fA = min(1, consecutiveFailures / thresholdA)
  + wB · fB(repeatedRuns)             // fB = min(1, repeatedRuns / thresholdB) · noveltyDiscount
  + wC · fC(repeatedErrorRuns)        // fC = min(1, repeatedErrorRuns / thresholdC)
  + wD · fD(noNovelSteps)             // fD = min(1, noNovelSteps / thresholdD)
  + wE · fE(stepsSinceChange)         // fE = min(1, stepsSinceChange / thresholdE),默认关闭
触发条件:stuckScore ≥ triggerScore 且最近 X 步内未触发(见 §6 cooldown)

初始参数(recipes/reset-v1.json,全部可被 recipe/Offline sweep 覆盖):

detector:
  windowSize: 6
  consecutiveFailures: { threshold: 3, weight: 0.35 }
  repeatedActions:    { threshold: 3, weight: 0.25, noveltyDiscount: 0.5 }
  repeatedErrors:     { threshold: 2, weight: 0.25 }
  noNovelObservation: { threshold: 4, weight: 0.15 }
  workspaceChange:    { enabled: false, threshold: 5, weight: 0.15 }
  triggerScore: 0.65

这些参数是初始实验值,不是结论。Offline Analyzer 的职责之一就是在真实轨迹上扫描这些参数的 precision/recall(Q1)。

5.4 证据记录

每次触发(无论是否真正注入)都记录:

interface InterventionRecord {
  sessionId: string
  step: number
  turn: number
  score: number
  reasons: StuckReason[]
  policyId: string
  policyVersion: string
  injected: boolean            // false = 达到 cooldown/cap 而记录但未注入
  timestamp: number            // epoch ms
}

每个运行结束(turn/end 全部收敛 + agent 静止,或在 Offline 从日志重建)写:

interface RunSummary {           // 供 Offline Lab 关联
  sessionId: string
  taskId?: string
  policyId?: string
  steps: number; toolCalls: number
  inputTokens?: number; outputTokens?: number   // 从 assistant/message.usage 聚合
  durationMs: number                              // session 创建→最后事件
  stuckEvents: number; interventions: number; injections: number
  ended: 'completed' | 'aborted' | 'error' | 'max-tokens' | 'interrupted' | 'user-stop'
}

6. Intervention(只实现 STRATEGY_RESET)

6.1 注入机制(官方 seam,见 runtime-analysis §2.2/§5)

const message = createUserMessage({
  content: [{ type: 'text', text: STRATEGY_RESET_TEXT }],
  source: { kind: 'plugin', plugin: 'dsh-evolve', form: 'notice', summary: 'strategy reset: stuck trajectory detected' },
})
switch (injectionPoint) {
  case 'step-end':  agent.inject(message); break   // 工具循环中 stuck(主路径)
  case 'turn-stopping': agent.steer(message); break // 模型提前停止(辅助路径)
}
  • step-end 注入(v0.1 唯一活跃路径):在 session/event(step/end)同步监听器内完成,agent.inject(message)。时序已被 agent-loop 循环结构保证(见 runtime-analysis §2.3):注入后 inbox 非空,循环继续下一 step。
  • turn-stopping 为去重守卫(v0.1 语义):step-end 已处理当前 step 时,turn-stopping 到来即为 no-op(lastHandledGlobalStep 去重),不重复记录、不 steer。原因:凡是 step-end 曾注入的 step,inbox 已非空,loop 根本不会进入 turn-stopping;未注入(预算/cooldown)的 step 在 turn-stopping 也应遵守相同门禁,因此该 seam 在 v0.1 实际不产生新注入——留作未来"模型提前停止"干预的接口位。
  • cooldown 时钟:使用单调的全局 step 计数(extractor.stepCount,跨 turn 递增),而非 turn 内 step 号(DSH 的 step 号每 turn 从 1 重新计数)。

6.2 消息模板(recipe 可配,领域无关)

The current execution trajectory appears to be producing little new progress.

Do not continue the same approach automatically.

Before taking the next action:
1. summarize only the concrete facts established so far,
2. identify the assumption or strategy that has failed,
3. explicitly abandon that approach,
4. choose a materially different next strategy,
5. continue execution.

Do not repeat previous tool calls unless the environment or relevant state has changed.

要求:strategy reset ≠ domain-specific instruction;不改任务目标;只打断低收益轨迹。

6.3 门禁(防干预死循环)

intervention:
  cooldownSteps: 5      # 距上次注入 >= 5 步才允许再次注入
  maxPerTurn: 1         # 每个 turn 至多注入 1 次
  maxPerSession: 3      # 每个 session 至多注入 3 次

超限行为:继续检测与记录(stuckEvents 继续累计),但不再注入。v0.1 不自动升级为其他策略。maxPerSession 计数随 agent/inbox/spliced(dsh-evolve 源消息)在恢复时重建(从 user/message.source.plugin==='dsh-evolve' 计数),实现持久一致。

6.4 边界(v0.1 明确不做)

model escalation / fork / subagent / tool restriction / context compaction / human escalation / automatic stop 一律不进 v0.1;只留类型与接口位(InterventionType = 'strategy-reset',未来扩展在 recipe 层面演进)。


7. Runtime Overhead 分析

目标:每 step 增量 O(1)~O(windowSize),不随 Session 长度恶化

环节复杂度说明
观察(session/event / tools/result 同步回调)O(1)仅拷贝必要字段(key/hash/flag),不深拷贝大 payload
指纹计算O(content
增量特征推进O(1) 均摊ring buffer(定长 windowSize)滚动;过期槽位直接覆盖
StuckScoreO(窗口) = O(windowSize)触发判定只遍历固定窗口
注入O(1) + 一次消息构造受 recipe/cooldown 门禁;注入本身一次 createUserMessage
持久化(InterventionRecord)罕见仅在触发时写一行 JSONL(原子 append)

总量:每个 step 常数级(几十微秒量级),且无任何网络/模型/DB/向量开销。内存:每 agent 一个固定大小窗口对象,agent/disposed 即释放。禁止的热路径操作清单见需求 §三.3,本设计逐条承诺不执行。


8. Persistence 与卸载安全

8.1 插件自有存储

~/.dsh/evolve/
  policies/   active.json(软链或内容) + baseline.json + reset-v1.json …
  runs/       <sessionId>.json         (RunSummary)
  evals/      <taskId>/<runId>.jsonl   (eval 过程数据,Offline 平面写)
  reports/    latest.md 等

或 workspace-local .dsh/evolve/(recipe storage.root 可配:harness-home(默认)| workspace)。写入全部原子(临时文件 + rename),损坏自愈(目录缺失视为空库)。

8.2 与 DSH Session 的关系

  • 不写任何插件私有事件进 Session(见 runtime-analysis §6)。
  • 唯一进入 Session 的产物是策略重置 user/message(Core 词汇)——它提高而非降低会话可读性(模型可见内容有完整记录)。
  • 卸载插件:dsh plugin --profile <n> remove @dsh-evolve/plugin → bundle 行移出层叠;Cordis 卸载该插件的所有 effect(listener/timer/service/agent 内存状态全部回收)。已有 Session 的原样读取/续跑不受影响(无新事件类型、无 schema 变化)。
  • 插件数据与学习数据的生命周期分离:卸载 = 停止优化,不删除 ~/.dsh/evolve/;重新启用重新读取(no data loss)。用户可自行删除目录。

9. 插件生命周期(Cordis)

  • 所有注册走 ctx.on / ctx.effect(返回 disposer);apply(ctx, config) 组合所有模块;AgentRegistry 类注册全部清理。
  • 每个 agent 的可变状态放进 WeakMap<Agent, State> 并在 agent/disposed 监听器中清理(或随 scope 卸载一并回收);插件 dispose 后:无 listener、无残留干预、无 monkey patch、无全局状态
  • 作用域选择:插件默认装在 root context(服务 ctx.evolve 可选);干预/检测按 agent 级状态隔离;与 DSH 官方多 agent(含 subagent)语义兼容(默认只干预 root,recipe 可配 scope)。
  • 热重载(HMR/Include refresh)期间:effect 卸载→重建,状态重建从 session.events 尾部增量恢复(窗口从 firstLiveSeq 起重放一次,成本 O(窗口) 量级,可接受;默认不重放历史也可以,只丢窗口前信号)。

10. Offline Lab(v0.1 最小形态)

命令(包内 bin:dsh-evolve,包装在 DSH 之外独立运行,不加载 DSH):

dsh-evolve analyze <sessionId> [--recipe reset-v1.json]
    # replay-style:读 ~/.dsh/sessions/<project>/<id>/session.* → Session.create + fold
    # 每 step 打印 stuck score / 信号明细 / 若启用该 recipe 会在哪些 step TRIGGER
    # 不调用模型;只回答"这个 policy 会在哪里触发",不回答"触发后会不会变好"

dsh-evolve eval --tasks tasks.yaml --profile-name eval-baseline|eval-treat [--runs N] ...
    # 编排:profile 层 + --patch overlay → dsh --profile <n> "task prompt" ×N
    # 收集 sessionID → grader(确定性命令/断言)→ metrics → reports/latest.md

Analyzer 与检测器共用同一套特征/打分代码路径(离线对同一窗口语义逐事件复算),保证"运行时触发 == 离线可复现触发"(这是 Q1 可信度的基础)。


11. Policy Recipe(契约)

interface PolicyRecipe {
  id: string                 // 'baseline' | 'reset-v1'
  version: string            // semver-ish
  detector: DetectorConfig   // §5.3
  intervention: {
    type: 'strategy-reset'
    messageTemplate?: string // 默认 §6.2 文案
    cooldownSteps: number
    maxPerTurn: number
    maxPerSession: number
  }
  metadata?: { createdAt?: string; sourceExperiment?: string; compatibleDshVersion?: string }
}

加载:~/.dsh/evolve/policies/active.json → 校验 schema → 失败则回退 baseline.json(baseline = 只观察不注入,是所有实验的对照组 recipe)。Runtime 只执行 active recipe,绝不自行改写。


12. 与 DSH 已有相近实现的边界(Q: 是否重复)

DSH 已有做什么dsh-evolve 差异关系
repeat-tool-reminder(base bundle 默认挂载)仅"同名同参连续调用"一信号,阈值 [3,5,8],注入轻提醒("analyze before repeat"),不评分、无兜底/上限、无 recipe、无 eval多信号融合打分、窗口化、cooldown/cap、全链路 recipe + 实验、干预为策略重置(明确要求放弃旧策略换新策略,而非提醒)不重复;实验须隔离(两臂都显式禁用该插件,见 experiment-design §3.4)
timeout-policy工具超时包装(TOOL_TIMEOUT)无关(不同层)无冲突
goal / Ralph外层轮转/续跑策略;明确没有 stuck detection / no-progress 启发目标不同:我们检测"轨迹浪费"并做一次策略重置可协作:reset 可作为 goal round 的"干预动作"(v0.2 之后)
compaction上下文压力处理信号面不同(token 压力 vs 行为停滞)可协作

结论:不存在必须停止开发的高度重复项目。 dsh-evolve 的差异点(确定性多信号 StuckScore + 可实验的 recipe 体系 + 离线 analyze/eval 工具链 + 诚实报告)是 DSH 生态中缺失的部分。