dsh-continual-evolve 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -11
- package/README.zh.md +19 -11
- package/lib/apply.js +17 -9
- package/lib/auto.d.ts +19 -4
- package/lib/auto.js +61 -8
- package/lib/command.d.ts +6 -0
- package/lib/command.js +190 -27
- package/lib/evolve-event.d.ts +4 -2
- package/lib/evolve-event.js +12 -2
- package/lib/fate.js +8 -2
- package/lib/index.d.ts +56 -0
- package/lib/index.js +29 -2
- package/lib/inject.d.ts +35 -9
- package/lib/inject.js +86 -36
- package/lib/llm-text.d.ts +23 -0
- package/lib/llm-text.js +50 -33
- package/lib/planner.d.ts +13 -1
- package/lib/planner.js +31 -7
- package/lib/prefix-cache.d.ts +78 -0
- package/lib/prefix-cache.js +168 -0
- package/lib/project.d.ts +28 -0
- package/lib/project.js +66 -0
- package/lib/review.d.ts +11 -0
- package/lib/review.js +23 -2
- package/lib/runtime.d.ts +24 -0
- package/lib/runtime.js +83 -0
- package/lib/service.d.ts +11 -1
- package/lib/service.js +24 -3
- package/lib/state.d.ts +8 -4
- package/lib/state.js +14 -7
- package/lib/store.d.ts +44 -0
- package/lib/store.js +99 -4
- package/lib/token-usage.d.ts +68 -0
- package/lib/token-usage.js +211 -0
- package/lib/tool.d.ts +13 -1
- package/lib/tool.js +118 -32
- package/lib/types.d.ts +14 -2
- package/lib/types.js +12 -0
- package/lib/validate.js +30 -0
- package/lib/wrapup-command.js +11 -2
- package/lib/wrapup.d.ts +7 -2
- package/lib/wrapup.js +21 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
|
|
12
12
|
Continual self-evolution for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): a versioned, auditable, rollback-safe harness state layer — prompt notes, memories, skills, subagent specs — refined from session trajectories.
|
|
13
13
|
|
|
@@ -17,7 +17,8 @@ Continual self-evolution for [DeepSeek Harness](https://github.com/deepseek-ai/d
|
|
|
17
17
|
|
|
18
18
|
Agents accumulate reusable experience (repeated failures, durable facts, reusable procedures) and forget it next session. This plugin turns that experience into first-class state:
|
|
19
19
|
|
|
20
|
-
- **
|
|
20
|
+
- **Three scopes** with merge semantics (global < project < local): **local** per-session staging, **project** per-workspace cross-session store, **global** cross-project — plus mechanical promotion guards so only portable, substantial, non-duplicate knowledge reaches global
|
|
21
|
+
- **Typed one-fact memories**: every memory entry carries a recall type (`user | feedback | project | reference`); pitfalls (`feedback`) must include Why + How to apply
|
|
21
22
|
- **Deterministic rollback**: inverse edits generated from applied results — no LLM re-guessing
|
|
22
23
|
- **Benchmark loop**: candidate refinements are evaluated against frozen cases by a separate scorer before acceptance (rubric encrypted at rest)
|
|
23
24
|
- **Store hygiene**: `/evolve consolidate` turns write-time conflict hints and zero-use staleness into one approved, fully reversible batch of archives — with `merge`, near-duplicate content folds into the surviving original
|
|
@@ -26,8 +27,8 @@ Agents accumulate reusable experience (repeated failures, durable facts, reusabl
|
|
|
26
27
|
|
|
27
28
|
1. **Sediment** — the model creates entries via `evolve_add`, or the automatic review gate proposes them from the session trajectory (turn-interval + compaction checkpoints).
|
|
28
29
|
2. **Guard** — code-enforced validation: edit schema, blast-radius/scope coherence, and the promotion policy (project-scoped markers, thin content, near-duplicate detection, credential screening keep the global store clean — secrets are rejected at every write sink, including mount materialization). Global creates that near-duplicate an existing entry are rejected at write time (≥0.8 similarity); moderate overlaps carry a `conflictHint` for later consolidation.
|
|
29
|
-
3. **Approve** — global writes require explicit human approval; local-fate proposals are consulted before they land.
|
|
30
|
-
4. **Apply & inject** — atomic apply with snapshot + audit event. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, contradicted entries demoted, zero tokens when empty); memories/skills appear as a capped directory index.
|
|
30
|
+
3. **Approve** — global and project writes require explicit human approval; local-fate proposals are consulted before they land.
|
|
31
|
+
4. **Apply & inject** — atomic apply with snapshot + audit event. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, contradicted entries demoted, zero tokens when empty); memories/skills appear as a relevance-ordered capped directory index (`- [memory:type:id] title` hooks, full text one `evolve_list` away).
|
|
31
32
|
5. **Validate & roll back** — benchmarks score candidates against frozen cases; rejected candidates roll back deterministically and are captured as draft regression cases (`auto_regression` benchmark).
|
|
32
33
|
|
|
33
34
|
## Install
|
|
@@ -49,7 +50,7 @@ Commands (in-session):
|
|
|
49
50
|
| Command | Effect |
|
|
50
51
|
|---|---|
|
|
51
52
|
| `/evolve` | help + current local store |
|
|
52
|
-
| `/evolve list · history · rollback <id>` | inspect and revert (add `global` for the cross-
|
|
53
|
+
| `/evolve list · history · rollback <id>` | inspect and revert (add `project` for this project's store, `global` for the cross-project store) |
|
|
53
54
|
| `/evolve plan [msg]` | run the LLM planner against the store |
|
|
54
55
|
| `/evolve wrapup` | assess this session's local entries: promote / archive / keep |
|
|
55
56
|
| `/evolve archive · unarchive · demote <id>` | hide from injection (data kept, restorable) — `demote` targets global noise |
|
|
@@ -60,12 +61,16 @@ Commands (in-session):
|
|
|
60
61
|
| `/evolve mount · unmount <skillId>` | hot-mount an executable skill as a live plugin |
|
|
61
62
|
| `/evolve goal [objective · done · block]` | round-driven auto-review goal |
|
|
62
63
|
| `/evolve benchmark …` | case lifecycle, runs, acceptance |
|
|
64
|
+
| `/evolve pause · resume · status` | pause/resume the auto-review gate (manual tools keep working), gate state |
|
|
65
|
+
| `/evolve usage` | per-entry injection counts + exact provider-reported tokens for direct review/planner/wrapup/fate calls (benchmark host subagents excluded) |
|
|
63
66
|
|
|
64
|
-
Model tools: `evolve_list / add / update / delete / rollback
|
|
67
|
+
Model tools: `evolve_list / add / update / delete / rollback` (`evolve_delete` takes `id` or a batch `ids` array — one refinement, one approval).
|
|
65
68
|
|
|
66
69
|
For third-party consumers: every applied evolution (gate or manual) appends a structured `evolve_complete` event to `reviews.jsonl` (`src/evolve-event.ts` defines the shape) alongside the human-readable audit records.
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
`/evolve usage` also reads `evolve/token-usage.jsonl`: exact provider-reported input/cache/output/total tokens for the plugin's direct review, planner, manual-wrapup, and automatic-fate calls. The report covers a retained tail rather than lifetime usage, distinguishes missing provider samples, and explicitly excludes host benchmark subagents, their agent-loop calls, and per-entry injection attribution.
|
|
72
|
+
|
|
73
|
+
Injection shape: prompt notes and delegation specs inject with content (≤6/kind × 180 chars, relevance-ranked). Memories and skills appear as a relevance-ordered directory index (`[memory:type:id] title` hooks, capped at 15 lines with a fold counter) — full text via `evolve_list`. Empty store = zero injected tokens.
|
|
69
74
|
|
|
70
75
|
## Configuration
|
|
71
76
|
|
|
@@ -77,7 +82,7 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
|
|
|
77
82
|
| `maxReviewInputChars` | `40000` | trajectory slice handed to the gate |
|
|
78
83
|
| `reviewBudgetTokens` | `4096` | output budget for the gate call |
|
|
79
84
|
| `notifyOnAutoReview` | `true` | visible follow-up notice after an applied gate run |
|
|
80
|
-
| `requireGlobalApproval` | `true` | global edits ask for explicit approval |
|
|
85
|
+
| `requireGlobalApproval` | `true` | global and project edits ask for explicit approval |
|
|
81
86
|
| `localFate` | `true` | gate audits local entries and proposes promote/archive (consulted, never silent) |
|
|
82
87
|
| `fateIntervalTurns` | follows `reviewIntervalTurns` | minimum turns between fate assessments |
|
|
83
88
|
| `goalBlockedWrapupTurns` | `3` | consecutive blocked-goal gate runs trigger one fate assessment (`0` disables) |
|
|
@@ -91,6 +96,9 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
|
|
|
91
96
|
| `autoRollbackOnReject` | `true` | deterministic rollback after a benchmark rejection |
|
|
92
97
|
| `autoCase` | `true` | failed evolution attempts are captured as draft regression cases (`auto_regression` benchmark) |
|
|
93
98
|
| `reviewModel` | agent's own | optional cheaper model for the gate (`"provider/model"`) |
|
|
99
|
+
| `plannerPrefixCache` | `auto` | Route A session-prefix input when cache evidence exists (`session` always, `off` legacy flat text) |
|
|
100
|
+
| `plannerPrefixMaxChars` | `12000` | session-prefix budget for Route A planning inputs (chars) |
|
|
101
|
+
| `historyRetain` | `{snapshots: 20, refinements: 500, reviews: 500, tokenUsage: 500}` | storage hygiene: snapshots per store, tail lines per store history, shared `reviews.jsonl` tail, and direct-call `token-usage.jsonl` tail |
|
|
94
102
|
|
|
95
103
|
Example profile patch:
|
|
96
104
|
|
|
@@ -105,7 +113,7 @@ Example profile patch:
|
|
|
105
113
|
|
|
106
114
|
```bash
|
|
107
115
|
pnpm install && pnpm build # deps + tsc -> lib/
|
|
108
|
-
pnpm test # vitest (
|
|
116
|
+
pnpm test # vitest (665 tests)
|
|
109
117
|
pnpm test:coverage # v8 coverage, thresholds enforced in CI
|
|
110
118
|
pnpm lint # oxlint src test
|
|
111
119
|
```
|
|
@@ -113,8 +121,8 @@ pnpm lint # oxlint src test
|
|
|
113
121
|
Project layout:
|
|
114
122
|
|
|
115
123
|
```
|
|
116
|
-
├── src/ # engine, tools, commands, gate, fate, benchmark, usage…
|
|
117
|
-
├── test/ # vitest suites (
|
|
124
|
+
├── src/ # engine, tools, commands, gate, fate, benchmark, injection + token usage…
|
|
125
|
+
├── test/ # vitest suites (41 files)
|
|
118
126
|
├── lib/ # build output (tsc)
|
|
119
127
|
├── docs/
|
|
120
128
|
│ ├── design.md # full design doc (hardening matrix)
|
package/README.zh.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
|
|
12
12
|
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的持续自进化插件:一套**版本化、可审计、可回滚**的 harness 状态层——提示词补充、记忆、技能、子代理规格——从会话轨迹中沉淀而来。
|
|
13
13
|
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
Agent 在每个会话里积累可复用经验(重复失败、持久事实、可复用流程),下个会话就忘掉。本插件把这些经验变成一等公民的持久状态:
|
|
19
19
|
|
|
20
|
-
- **
|
|
20
|
+
- **三作用域**与合并语义(global < project < local):**local** 本会话暂存、**project** 本项目跨会话库、**global** 跨项目——配合机械化晋升守卫,只有可携带、有分量、非重复的知识才能进全局
|
|
21
|
+
- **类型化单条记忆**:每条 memory 带召回类型(`user | feedback | project | reference`);踩坑(`feedback`)必须含 Why + How to apply
|
|
21
22
|
- **确定性回滚**:逆操作编辑由已应用结果生成——不靠 LLM 重新猜测
|
|
22
23
|
- **benchmark 闭环**:候选沉淀先经冻结用例 + 独立评分者评估再接受(rubric 加密落盘)
|
|
23
24
|
- **store 卫生**:`/evolve consolidate` 把写入时冲突提示与零使用陈旧条目变成一次批准、完全可逆的批量归档——加 `merge` 可将近重复内容并入幸存原条目
|
|
@@ -26,8 +27,8 @@ Agent 在每个会话里积累可复用经验(重复失败、持久事实、
|
|
|
26
27
|
|
|
27
28
|
1. **沉淀**——模型经 `evolve_add` 创建条目,或自动 review 门禁从会话轨迹提议(回合间隔 + 压缩检查点)。
|
|
28
29
|
2. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测 / 凭据筛查保持全局库干净——密钥类内容在所有写入出口被拒,含 mount 物化)。全局 create 与既有条目高度相似(≥0.8)时写入即拒;中等重叠带 `conflictHint` 供后续合并。
|
|
29
|
-
3.
|
|
30
|
-
4. **应用与注入**——原子应用带快照与审计事件。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);memory/skill
|
|
30
|
+
3. **审批**——全局与项目写入需明确人工批准;local 归宿提议先征询后落地。
|
|
31
|
+
4. **应用与注入**——原子应用带快照与审计事件。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子)。
|
|
31
32
|
5. **验证与回滚**——benchmark 用冻结用例为候选打分;被拒候选确定性回滚,并自动沉淀为 draft 回归用例(`auto_regression` 基准)。
|
|
32
33
|
|
|
33
34
|
## 安装
|
|
@@ -49,7 +50,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
49
50
|
| 命令 | 效果 |
|
|
50
51
|
|---|---|
|
|
51
52
|
| `/evolve` | 帮助 + 当前 local store |
|
|
52
|
-
| `/evolve list · history · rollback <id>` | 查看与回滚(加 `global`
|
|
53
|
+
| `/evolve list · history · rollback <id>` | 查看与回滚(加 `project` 操作本项目库,`global` 操作跨项目库) |
|
|
53
54
|
| `/evolve plan [msg]` | 对 store 运行 LLM 规划器 |
|
|
54
55
|
| `/evolve wrapup` | 收尾本会话 local 条目:晋升 / 归档 / 保留 |
|
|
55
56
|
| `/evolve archive · unarchive · demote <id>` | 从注入中隐藏(数据保留可恢复)——`demote` 针对全局噪声 |
|
|
@@ -60,12 +61,16 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
60
61
|
| `/evolve mount · unmount <skillId>` | 把可执行技能热挂载为 live 插件 |
|
|
61
62
|
| `/evolve goal [objective · done · block]` | 回合驱动的自进化目标 |
|
|
62
63
|
| `/evolve benchmark …` | 用例生命周期、运行、接受决策 |
|
|
64
|
+
| `/evolve pause · resume · status` | 暂停/恢复自动门禁(手动工具不受影响)、门禁状态 |
|
|
65
|
+
| `/evolve usage` | 每条目注入次数 + review/planner/wrapup/fate 直属调用的 provider 精确 token(不含 benchmark 宿主子代理) |
|
|
63
66
|
|
|
64
|
-
模型工具:`evolve_list / add / update / delete / rollback
|
|
67
|
+
模型工具:`evolve_list / add / update / delete / rollback`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement、一次审批)。
|
|
65
68
|
|
|
66
69
|
第三方消费:每次进化落地(门禁或手动)都会向 `reviews.jsonl` 追加结构化 `evolve_complete` 事件(shape 见 `src/evolve-event.ts`),与人类可读的审计记录并存。
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
`/evolve usage` 还会读取 `evolve/token-usage.jsonl`:插件直属 review、planner、手动 wrapup 与自动 fate 调用的 provider 精确 input/cache/output/total token。报告只覆盖保留尾部而非终身累计,单独显示 provider 未返回 usage 的调用,并明确排除宿主 benchmark 子代理、其 agent-loop 调用与逐条 memory 注入归因。
|
|
72
|
+
|
|
73
|
+
注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子,15 行封顶 + 折叠计数行)——全文经 `evolve_list` 获取。空 store = 零注入 token。
|
|
69
74
|
|
|
70
75
|
## 配置
|
|
71
76
|
|
|
@@ -77,7 +82,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
77
82
|
| `maxReviewInputChars` | `40000` | 交给门禁的轨迹切片 |
|
|
78
83
|
| `reviewBudgetTokens` | `4096` | 门禁调用输出预算 |
|
|
79
84
|
| `notifyOnAutoReview` | `true` | 门禁应用后发可见跟进通知 |
|
|
80
|
-
| `requireGlobalApproval` | `true` |
|
|
85
|
+
| `requireGlobalApproval` | `true` | 全局与项目写入需明确批准 |
|
|
81
86
|
| `localFate` | `true` | 门禁审计本地条目并提议晋升/归档(先征询,绝不静默) |
|
|
82
87
|
| `fateIntervalTurns` | 跟随 `reviewIntervalTurns` | 归宿评估的最小回合间隔 |
|
|
83
88
|
| `goalBlockedWrapupTurns` | `3` | 连续阻塞目标的门禁轮数触发一次归宿评估(`0` 关闭) |
|
|
@@ -91,6 +96,9 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
91
96
|
| `autoRollbackOnReject` | `true` | benchmark 拒绝后自动确定性回滚 |
|
|
92
97
|
| `autoCase` | `true` | 失败的进化尝试自动沉淀为 draft 回归用例(`auto_regression` 基准) |
|
|
93
98
|
| `reviewModel` | agent 自身 | 门禁可选更便宜的模型(`"provider/model"`) |
|
|
99
|
+
| `plannerPrefixCache` | `auto` | 有缓存证据时用会话前缀输入(`session` 总是前缀,`off` 保持旧扁平文本) |
|
|
100
|
+
| `plannerPrefixMaxChars` | `12000` | Route A 会话前缀预算(字符) |
|
|
101
|
+
| `historyRetain` | `{snapshots: 20, refinements: 500, reviews: 500, tokenUsage: 500}` | 存储卫生:每 store 快照数、每 store 历史尾行、共享 `reviews.jsonl` 尾行、直属调用 `token-usage.jsonl` 尾行 |
|
|
94
102
|
|
|
95
103
|
profile patch 示例:
|
|
96
104
|
|
|
@@ -105,7 +113,7 @@ profile patch 示例:
|
|
|
105
113
|
|
|
106
114
|
```bash
|
|
107
115
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
108
|
-
pnpm test # vitest(
|
|
116
|
+
pnpm test # vitest(665 例)
|
|
109
117
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
110
118
|
pnpm lint # oxlint src test
|
|
111
119
|
```
|
|
@@ -113,8 +121,8 @@ pnpm lint # oxlint src test
|
|
|
113
121
|
目录结构:
|
|
114
122
|
|
|
115
123
|
```
|
|
116
|
-
├── src/ # 引擎、工具、命令、门禁、fate、benchmark
|
|
117
|
-
├── test/ # vitest 测试套件(
|
|
124
|
+
├── src/ # 引擎、工具、命令、门禁、fate、benchmark、注入 + token 用量…
|
|
125
|
+
├── test/ # vitest 测试套件(41 个文件)
|
|
118
126
|
├── lib/ # 构建产物(tsc)
|
|
119
127
|
├── docs/
|
|
120
128
|
│ ├── design.md # 完整设计文档(硬化矩阵)
|
package/lib/apply.js
CHANGED
|
@@ -7,15 +7,23 @@ export function applyRefinementProposal(state, proposal, options) {
|
|
|
7
7
|
const now = new Date().toISOString();
|
|
8
8
|
for (const edit of proposal.edits) {
|
|
9
9
|
// Store-prefix hygiene: planner edits are written against the MERGED
|
|
10
|
-
// view, where colliding local ids carry a `
|
|
11
|
-
// A CREATE must never bake that view
|
|
12
|
-
// (observed: global entries literally
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
// view, where colliding project/local ids carry a `project:` /
|
|
11
|
+
// `local:` / `global:` prefix. A CREATE must never bake that view
|
|
12
|
+
// prefix into a permanent id (observed: global entries literally
|
|
13
|
+
// named "local:handoff_todo_…").
|
|
14
|
+
// Non-create edits address existing entries: the literal id wins —
|
|
15
|
+
// legacy stores contain entries whose literal id carries a colon
|
|
16
|
+
// prefix, and stripping unconditionally would resolve them to a
|
|
17
|
+
// sibling that does not exist ("entry not found", forcing file-level
|
|
18
|
+
// surgery). Only when the literal id is absent does the stripped
|
|
19
|
+
// form serve as the merged-view fallback.
|
|
20
|
+
// (Review audit 2026-08-28 S7 origin; literal-first fix 2026-09-24.)
|
|
21
|
+
const strippedId = edit.id?.replace(/^(?:local|project|global):/, "");
|
|
22
|
+
const requestedId = edit.action === "create"
|
|
23
|
+
? strippedId
|
|
24
|
+
: edit.id !== undefined && state.entries[edit.kind]?.[edit.id] !== undefined
|
|
25
|
+
? edit.id
|
|
26
|
+
: edit.id && strippedId;
|
|
19
27
|
const computedId = requestedId ?? (edit.action === "create" ? slug(edit.title ?? edit.kind, edit.kind) : undefined);
|
|
20
28
|
const id = computedId ?? "";
|
|
21
29
|
// Unknown kinds fail per-edit — a malformed proposal must never crash
|
package/lib/auto.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { HarnessState, RefinementEdit, RefinementProposal } from "./types.j
|
|
|
4
4
|
import type { EvolutionEngine } from "./service.js";
|
|
5
5
|
import { type AutoRefineReason } from "./review.js";
|
|
6
6
|
import type { PromotionPolicy } from "./promotion.js";
|
|
7
|
+
import type { PlannerPrefixCacheMode } from "./prefix-cache.js";
|
|
7
8
|
export interface AutoReviewConfig {
|
|
8
9
|
intervalTurns: number;
|
|
9
10
|
maxInputChars: number;
|
|
@@ -29,6 +30,12 @@ export interface AutoReviewConfig {
|
|
|
29
30
|
* When absent, the review gate uses the agent's own provider/model.
|
|
30
31
|
*/
|
|
31
32
|
reviewModel?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Prefix-cache routing for the gate input (see reviewAutoRefine).
|
|
35
|
+
* Absent mode → auto-detect; absent budget → the default prefix budget.
|
|
36
|
+
*/
|
|
37
|
+
prefixCacheMode?: PlannerPrefixCacheMode;
|
|
38
|
+
prefixMaxChars?: number;
|
|
32
39
|
/**
|
|
33
40
|
* Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
|
|
34
41
|
* observe the session goal in phase "blocked", run one local-fate
|
|
@@ -52,6 +59,12 @@ export interface AutoReviewConfig {
|
|
|
52
59
|
autoCase: boolean;
|
|
53
60
|
/** Resolved rubric key for the capture's encrypted scaffold rubric. */
|
|
54
61
|
rubricKey?: Buffer;
|
|
62
|
+
/**
|
|
63
|
+
* Storage hygiene (#20): tail lines kept in the shared reviews.jsonl
|
|
64
|
+
* audit trail. Absent → the store default (readers need a recent
|
|
65
|
+
* window, not the full past).
|
|
66
|
+
*/
|
|
67
|
+
reviewsRetain?: number;
|
|
55
68
|
}
|
|
56
69
|
export interface GateState {
|
|
57
70
|
turns: number;
|
|
@@ -103,14 +116,16 @@ export declare function parseReviewModel(reviewModel: string | undefined, fallba
|
|
|
103
116
|
} | undefined;
|
|
104
117
|
/**
|
|
105
118
|
* The state view the gate and planner judge: the session's local entries
|
|
106
|
-
* merged with the global
|
|
107
|
-
* the global half the gate cannot see that a topic is already
|
|
108
|
-
* cross-session and happily re-sediments a local duplicate of it.
|
|
119
|
+
* merged with the project and global stores, each entry carrying its real
|
|
120
|
+
* scope. Without the global half the gate cannot see that a topic is already
|
|
121
|
+
* covered cross-session and happily re-sediments a local duplicate of it.
|
|
109
122
|
*
|
|
110
123
|
* The merged view is read-only context — applying still targets the raw
|
|
111
124
|
* local state (baseline checks compare local entries only).
|
|
112
125
|
*/
|
|
113
|
-
export declare function loadGateHarnessView(engine: EvolutionEngine, sessionId: string
|
|
126
|
+
export declare function loadGateHarnessView(engine: EvolutionEngine, sessionId: string, opts?: {
|
|
127
|
+
projectKey?: string;
|
|
128
|
+
}): HarnessState;
|
|
114
129
|
/**
|
|
115
130
|
* D3 (goal blocked → wrap-up coupling, reverse direction): count consecutive
|
|
116
131
|
* gate runs whose goal is in phase "blocked"; when the streak reaches
|
package/lib/auto.js
CHANGED
|
@@ -25,8 +25,11 @@ import { notifyAutoReview } from "./notify.js";
|
|
|
25
25
|
import { runLocalFatePhase } from "./fate.js";
|
|
26
26
|
import { entrySourceOf } from "./source.js";
|
|
27
27
|
import { mergeHarnessStates } from "./state.js";
|
|
28
|
+
import { projectKeyOf } from "./project.js";
|
|
28
29
|
import { questionServiceOf } from "./approval.js";
|
|
29
30
|
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
31
|
+
import { DEFAULT_REVIEWS_RETAIN, pruneJsonlFile } from "./store.js";
|
|
32
|
+
import { isGatePaused } from "./runtime.js";
|
|
30
33
|
import { captureAutoCase } from "./autocase.js";
|
|
31
34
|
/** Turns a rejected skill candidate stays silent before being offered again. */
|
|
32
35
|
export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
|
|
@@ -41,6 +44,14 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
41
44
|
}
|
|
42
45
|
catch (cause) {
|
|
43
46
|
logger.warn(`failed to record auto-review: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Storage hygiene (#20): bound the audit trail at write time.
|
|
50
|
+
try {
|
|
51
|
+
pruneJsonlFile(reviewsPath, config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// ignored — the next record retries
|
|
44
55
|
}
|
|
45
56
|
};
|
|
46
57
|
// Turn counting uses `agent/turn-stopping` — empirically the only event
|
|
@@ -66,6 +77,12 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
66
77
|
const goalDriven = goalDrivesRounds(goalServiceOf(ctx)?.get(agent));
|
|
67
78
|
if (!goalDriven && state.turns - state.lastReviewAt < config.intervalTurns)
|
|
68
79
|
return;
|
|
80
|
+
// #21 P2 runtime switch: a paused gate stays fully dormant — no LLM
|
|
81
|
+
// calls, no fate assessments, no audit records. Checked after the
|
|
82
|
+
// cheap in-memory interval check so the paused path costs one file
|
|
83
|
+
// read at most. Manual tools/commands are unaffected (separate path).
|
|
84
|
+
if (isGatePaused(engine.baseDir))
|
|
85
|
+
return;
|
|
69
86
|
// Run the gate outside the listener turn: agent is idle, work is auxiliary.
|
|
70
87
|
// Every failure is durably recorded — nothing fails silently.
|
|
71
88
|
void runGate(ctx, engine, agent, config, state, "turn_interval", record).catch((cause) => {
|
|
@@ -98,6 +115,12 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
98
115
|
catch (cause) {
|
|
99
116
|
logger.warn(`failed to write armed marker: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
100
117
|
}
|
|
118
|
+
try {
|
|
119
|
+
pruneJsonlFile(reviewsPath, config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// ignored — the next record retries
|
|
123
|
+
}
|
|
101
124
|
ctx.on("session/event", (session, event) => {
|
|
102
125
|
if (event.type !== "compaction/start")
|
|
103
126
|
return;
|
|
@@ -106,7 +129,11 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
106
129
|
if (!agent)
|
|
107
130
|
return; // no live agent for that session (e.g. cold read)
|
|
108
131
|
const state = stateFor(perSession, agent.id);
|
|
109
|
-
// Compaction is unconditional
|
|
132
|
+
// Compaction is unconditional — unless the human paused the gate
|
|
133
|
+
// (#21): a paused gate skips even the compaction run, so "pause"
|
|
134
|
+
// is a true zero-LLM-call state.
|
|
135
|
+
if (isGatePaused(engine.baseDir))
|
|
136
|
+
return;
|
|
110
137
|
void runGate(ctx, engine, agent, config, state, "compact", record).catch((cause) => {
|
|
111
138
|
logger.warn(`auto-review failed at compaction for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
112
139
|
record({
|
|
@@ -153,15 +180,16 @@ function stateFor(map, sessionId) {
|
|
|
153
180
|
}
|
|
154
181
|
/**
|
|
155
182
|
* The state view the gate and planner judge: the session's local entries
|
|
156
|
-
* merged with the global
|
|
157
|
-
* the global half the gate cannot see that a topic is already
|
|
158
|
-
* cross-session and happily re-sediments a local duplicate of it.
|
|
183
|
+
* merged with the project and global stores, each entry carrying its real
|
|
184
|
+
* scope. Without the global half the gate cannot see that a topic is already
|
|
185
|
+
* covered cross-session and happily re-sediments a local duplicate of it.
|
|
159
186
|
*
|
|
160
187
|
* The merged view is read-only context — applying still targets the raw
|
|
161
188
|
* local state (baseline checks compare local entries only).
|
|
162
189
|
*/
|
|
163
|
-
export function loadGateHarnessView(engine, sessionId) {
|
|
164
|
-
|
|
190
|
+
export function loadGateHarnessView(engine, sessionId, opts) {
|
|
191
|
+
const projectState = opts?.projectKey ? engine.load("project", opts.projectKey) : undefined;
|
|
192
|
+
return mergeHarnessStates(engine.load("global", undefined), engine.load("local", sessionId), projectState ? { projectState } : undefined);
|
|
165
193
|
}
|
|
166
194
|
/**
|
|
167
195
|
* One gate run = review phase + local-fate phase (#11 P2). The review phase
|
|
@@ -226,6 +254,12 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
226
254
|
const sessionId = agent.id;
|
|
227
255
|
const turnsSinceLastReview = state.turns - state.lastReviewAt;
|
|
228
256
|
const logger = ctx.logger("continual-evolve");
|
|
257
|
+
const tokenUsage = {
|
|
258
|
+
baseDir: engine.baseDir,
|
|
259
|
+
sessionId,
|
|
260
|
+
retain: engine.retention.tokenUsage,
|
|
261
|
+
onError: (cause) => logger.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
262
|
+
};
|
|
229
263
|
const trajectory = await readTrajectory(ctx, agent, config.maxInputChars).catch((cause) => {
|
|
230
264
|
logger.warn(`auto-review skipped for ${sessionId}: trajectory unavailable: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
231
265
|
record({ sessionId, reason, turnsSinceLastReview, outcome: "failed", rationale: `trajectory unavailable: ${cause instanceof Error ? cause.message : String(cause)}` });
|
|
@@ -239,7 +273,8 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
239
273
|
// can recognize topics already covered globally and decline duplicates;
|
|
240
274
|
// applying still targets the raw local store.
|
|
241
275
|
const localState = engine.load("local", sessionId);
|
|
242
|
-
const
|
|
276
|
+
const projectKey = projectKeyOf(agent);
|
|
277
|
+
const harnessState = loadGateHarnessView(engine, sessionId, projectKey ? { projectKey } : undefined);
|
|
243
278
|
const history = engine.history("local", sessionId);
|
|
244
279
|
// Gap C1: resolve optional review model override.
|
|
245
280
|
const reviewRoute = parseReviewModel(config.reviewModel, agent.options.provider);
|
|
@@ -250,7 +285,16 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
250
285
|
trajectory,
|
|
251
286
|
context: { reason, turnsSinceLastReview },
|
|
252
287
|
budgetTokens: config.budgetTokens,
|
|
288
|
+
tokenUsage,
|
|
253
289
|
...(reviewRoute ? { overrideProvider: reviewRoute.provider, overrideModel: reviewRoute.model } : {}),
|
|
290
|
+
...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
|
|
291
|
+
? {
|
|
292
|
+
prefixCache: {
|
|
293
|
+
...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
|
|
294
|
+
...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
: {})),
|
|
254
298
|
});
|
|
255
299
|
state.lastReviewAt = state.turns;
|
|
256
300
|
if (!review.shouldRefine) {
|
|
@@ -267,6 +311,15 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
267
311
|
// Read the skill-creator template facts (fallback: builtin distilled
|
|
268
312
|
// guide) so skill proposals follow the standard.
|
|
269
313
|
skillsRoot: join(engine.baseDir, "skills"),
|
|
314
|
+
tokenUsage,
|
|
315
|
+
...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
|
|
316
|
+
? {
|
|
317
|
+
prefixCache: {
|
|
318
|
+
...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
|
|
319
|
+
...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
: {})),
|
|
270
323
|
});
|
|
271
324
|
// Skills are governed resources: an auto-created skill is OFFERED to the
|
|
272
325
|
// user for a decision (固化/不固化) before it lands — the gate never
|
|
@@ -314,7 +367,7 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
314
367
|
logger.info(`auto-review approved (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns; auto-refine ${result.id}: ${result.appliedEdits.filter((e) => e.applied).length} applied, ${result.appliedEdits.filter((e) => !e.applied).length} failed — ${review.rationale}`);
|
|
315
368
|
record({ sessionId, reason, turnsSinceLastReview, outcome: "approved", rationale: review.rationale, refinementId: result.id });
|
|
316
369
|
// Gap C4: emit structured evolve_complete event for third-party consumers.
|
|
317
|
-
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId));
|
|
370
|
+
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
318
371
|
// Visibility: tell the user what the gate just persisted. Only the
|
|
319
372
|
// turn-interval path notifies — a compaction-triggered gate must not wake
|
|
320
373
|
// the agent mid-compaction — and only when something was actually applied
|
package/lib/command.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface CommandRuntimeOptions {
|
|
|
17
17
|
autoCase: boolean;
|
|
18
18
|
/** Mechanical promotion guards for wrapup/fate (2026-08-22 policy). */
|
|
19
19
|
promotionPolicy: PromotionPolicy;
|
|
20
|
+
/**
|
|
21
|
+
* Static patch flag for the auto-review gate (#21 status display).
|
|
22
|
+
* Absent (older wiring/tests) renders as unknown — the runtime pause
|
|
23
|
+
* switch still works.
|
|
24
|
+
*/
|
|
25
|
+
autoReview?: boolean;
|
|
20
26
|
}
|
|
21
27
|
export declare function registerEvolveCommand(ctx: Context, engine: EvolutionEngine, opts: CommandGateOptions, runtime: CommandRuntimeOptions): void;
|
|
22
28
|
/**
|