dsh-continual-evolve 0.10.3 → 0.10.5
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 +13 -12
- package/README.zh.md +13 -12
- package/lib/command.js +18 -146
- package/lib/index.d.ts +36 -0
- package/lib/index.js +43 -1
- package/lib/inject.d.ts +14 -0
- package/lib/inject.js +21 -6
- package/lib/memory-guide.d.ts +19 -0
- package/lib/memory-guide.js +73 -0
- package/lib/memory-index.d.ts +114 -0
- package/lib/memory-index.js +197 -0
- package/lib/record-language.d.ts +8 -0
- package/lib/record-language.js +18 -1
- package/lib/tool.js +27 -27
- package/package.json +2 -1
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
|
[]()
|
|
13
13
|
[]()
|
|
@@ -34,7 +34,7 @@ Agents accumulate reusable experience (repeated failures, durable facts, reusabl
|
|
|
34
34
|
2. **Capability-aware auxiliary calls** — the memory loop, review, planner, wrapup, and fate resolve exact provider/model metadata through [`src/llm-text.ts`](src/llm-text.ts), use the lowest advertised enabled reasoning effort (falling back to a closing effort only when no enabled level exists), and forward the host session id for provider routing; models without reasoning metadata use their provider default.
|
|
35
35
|
3. **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.
|
|
36
36
|
4. **Approve** — global and project writes require explicit human approval; the dialog shows the bounded structured edit diff and conflict warnings, while malformed/lost responses remain retryable rather than counting as rejection.
|
|
37
|
-
5. **Apply & inject** — memory batches preflight every persistent approval, recheck abort before writes, and compensate earlier scope writes if a later batch fails; every successful scope still passes through snapshot + audit. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, contradicted entries demoted, zero tokens when empty);
|
|
37
|
+
5. **Apply & inject** — memory batches preflight every persistent approval, recheck abort before writes, and compensate earlier scope writes if a later batch fails; every successful scope still passes through snapshot + audit. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, contradicted entries demoted, zero tokens when empty); memory CONTENT injects at session start under a hard character budget, frozen per session, with the when_to_save guide riding along; skills still appear as a relevance-ordered capped directory index.
|
|
38
38
|
6. **Validate & roll back** — benchmarks score candidates against frozen cases; rejected candidates roll back deterministically and are captured as draft regression cases (`auto_regression` benchmark).
|
|
39
39
|
|
|
40
40
|
## Install
|
|
@@ -55,14 +55,11 @@ Commands (in-session):
|
|
|
55
55
|
|
|
56
56
|
| Command | Effect |
|
|
57
57
|
|---|---|
|
|
58
|
-
| `/evolve` | help + current local store |
|
|
58
|
+
| `/evolve` | short help + current local store (`/evolve help all` lists every subcommand) |
|
|
59
59
|
| `/evolve list · history · rollback <id>` | inspect and revert (add `project` for this project's store, `global` for the cross-project store) |
|
|
60
60
|
| `/evolve plan [msg]` | run the LLM planner against the store |
|
|
61
61
|
| `/evolve wrapup` | assess this session's local entries: promote / archive / keep |
|
|
62
|
-
| `/evolve archive · unarchive
|
|
63
|
-
| `/evolve recall [scope] <query…>` | targeted memory recall: full content with version, source, and staleness |
|
|
64
|
-
| `/evolve remember <type> [scope] <text…>` | immediately persist one typed memory (`user|feedback|project|reference`) |
|
|
65
|
-
| `/evolve forget [scope] <query…>` | locate one memory and archive it (restorable); ambiguous queries only list |
|
|
62
|
+
| `/evolve archive · unarchive <id>` | hide from injection (data kept, restorable) |
|
|
66
63
|
| `/evolve consolidate [apply] [merge]` | report (or apply) one batch archive of conflict-hinted + stale zero-use global entries; `merge` folds near-duplicate content into the survivors |
|
|
67
64
|
| `/evolve failures` | aggregated failure classes (gate + benchmark) |
|
|
68
65
|
| `/evolve log [tail N] [session <id>]` | plugin log |
|
|
@@ -73,13 +70,13 @@ Commands (in-session):
|
|
|
73
70
|
| `/evolve pause · resume · status` | pause/resume the auto-review gate (manual tools keep working), gate state |
|
|
74
71
|
| `/evolve usage` | per-entry injection counts + exact provider-reported tokens for direct memory/review/planner/wrapup/fate calls (benchmark host subagents excluded) |
|
|
75
72
|
|
|
76
|
-
Model tools: `evolve_list / add / update / delete / rollback / recall` (`evolve_delete` takes `id` or a batch `ids` array — one refinement, one approval; `evolve_recall` filters by query, kinds, scopes, memory types, and limit, and returns full content with version, source, and staleness).
|
|
73
|
+
Model tools: `evolve_list / add / update / delete / rollback / recall` (`evolve_delete` takes `id` or a batch `ids` array — one refinement, one approval; `evolve_recall` filters by query, kinds, scopes, memory types, and limit, and returns full content with version, source, and staleness). Memory reads and writes live in the conversation — the model carries the injected memory index plus `evolve_recall`/`evolve_add` — so there are no `/evolve remember · forget · recall` counterparts: ask in chat.
|
|
77
74
|
|
|
78
75
|
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.
|
|
79
76
|
|
|
80
77
|
`/evolve usage` also reads `evolve/token-usage.jsonl`: exact provider-reported input/cache/output/total tokens for the plugin's direct memory-agent, 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.
|
|
81
78
|
|
|
82
|
-
Injection shape: prompt notes and delegation specs inject with content (≤6/kind × 180 chars, relevance-ranked). Memories
|
|
79
|
+
Injection shape: prompt notes and delegation specs inject with content (≤6/kind × 180 chars, relevance-ranked). Memories inject as CONTENT at session start: the `evolve:memory-index` section (order 400) carries the when_to_save guide plus every memory body the budget allows, ranked `project > feedback > user > reference`, with overflow degraded to `[memory:type:id] title` hooks and the remainder counted (hard cap `memoryIndex.maxChars`, default 6000 chars; `evolve_recall` reads anything not shown). The section is computed once per session and reused byte-for-byte, so the system prompt stays stable and prompt-cache reads keep hitting — a memory written mid-session appears in the next session. Skills still appear as a relevance-ordered directory index (capped at 15 lines with a fold counter); memories no longer do. Every memory apply also refreshes a readable `MEMORY.md` index plus one fact file per entry in the store directory. With `memoryIndex.guide` off and an empty store, injection is zero tokens.
|
|
83
80
|
|
|
84
81
|
## Configuration
|
|
85
82
|
|
|
@@ -100,7 +97,11 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
|
|
|
100
97
|
| `promotionBlockPatterns` | POSIX paths, session ids, `~/.dsh` | content matching these is project-scoped and never promoted to global |
|
|
101
98
|
| `promotionMinChars` | `100` | whole promotions below this length stay local |
|
|
102
99
|
| `injectionDirectoryLines` | `15` | entry-directory lines per build before folding into a counter |
|
|
103
|
-
| `sectionOrder` | `118` | system-prompt section order |
|
|
100
|
+
| `sectionOrder` | `118` | system-prompt section order for the guidance + entries sections |
|
|
101
|
+
| `memoryIndex.enabled` | `true` | register the `evolve:memory-index` content section |
|
|
102
|
+
| `memoryIndex.guide` | `true` | include the when_to_save guide (with the store empty and this off, the section renders nothing) |
|
|
103
|
+
| `memoryIndex.order` | `400` | section order (upstream named slots start at `PLAN_POLICY=500`; the plugin's other sections sit at 118/119) |
|
|
104
|
+
| `memoryIndex.maxChars` | `6000` | hard character budget for the injected memories block |
|
|
104
105
|
| `skillsDir` | `<dshHome>/skills` | where skill entries materialize as SKILL.md bundles |
|
|
105
106
|
| `rubricKey` | auto-generated key file | AES-256-GCM passphrase for benchmark rubrics (`DSH_EVOLVE_RUBRIC_KEY` overrides) |
|
|
106
107
|
| `logToFile` / `logLevel` / `logMaxBytes` | `true` / `1` / 5 MiB | plugin-owned JSONL file log with rotation |
|
|
@@ -142,7 +143,7 @@ before aborting.
|
|
|
142
143
|
|
|
143
144
|
```bash
|
|
144
145
|
pnpm install && pnpm build # deps + tsc -> lib/
|
|
145
|
-
pnpm test # vitest (
|
|
146
|
+
pnpm test # vitest (1097 tests)
|
|
146
147
|
pnpm test:coverage # v8 coverage, thresholds enforced in CI
|
|
147
148
|
pnpm coverage:gaps # locate uncovered lines per file (read-only)
|
|
148
149
|
pnpm lint # oxlint src test
|
|
@@ -152,7 +153,7 @@ Project layout:
|
|
|
152
153
|
|
|
153
154
|
```
|
|
154
155
|
├── src/ # engine, tools, commands, memory agent, recall, projection, gate, fate, benchmark, injection + token usage…
|
|
155
|
-
├── test/ # vitest suites (
|
|
156
|
+
├── test/ # vitest suites (59 files)
|
|
156
157
|
├── lib/ # build output (tsc)
|
|
157
158
|
├── docs/
|
|
158
159
|
│ ├── 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
|
[]()
|
|
13
13
|
[]()
|
|
@@ -34,7 +34,7 @@ Agent 在每个会话里积累可复用经验(重复失败、持久事实、
|
|
|
34
34
|
2. **能力感知的辅助调用**——memory loop、review、planner、wrapup、fate 通过 [`src/llm-text.ts`](src/llm-text.ts) 解析精确 provider/model 能力并使用模型公布的最低开启 reasoning effort,同时转发 host session id 供 provider 路由;只有没有开启档时才回退关闭档,没有 reasoning 元数据时使用 provider 默认行为。
|
|
35
35
|
3. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测 / 凭据筛查保持全局库干净——密钥类内容在所有写入出口被拒,含 mount 物化)。全局 create 与既有条目高度相似(≥0.8)时写入即拒;中等重叠带 `conflictHint` 供后续合并。
|
|
36
36
|
4. **审批**——全局与项目写入需明确人工批准;弹窗展示有界结构化编辑 diff 与冲突提示,弹窗丢失/响应畸形会重试,不会被误记为拒绝。
|
|
37
|
-
5. **应用与注入**——memory 批次先完成所有持久化审批,写前重查 abort;后续 scope 失败时补偿回滚先前写入,成功 scope 仍保留快照与审计。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token
|
|
37
|
+
5. **应用与注入**——memory 批次先完成所有持久化审批,写前重查 abort;后续 scope 失败时补偿回滚先前写入,成功 scope 仍保留快照与审计。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);**memory 正文**在会话开场按硬预算注入(会话内冻结,when_to_save 指南随行);skill 仍以按相关性排序的目录索引出现。
|
|
38
38
|
6. **验证与回滚**——benchmark 用冻结用例为候选打分;被拒候选确定性回滚,并自动沉淀为 draft 回归用例(`auto_regression` 基准)。
|
|
39
39
|
|
|
40
40
|
## 安装
|
|
@@ -55,14 +55,11 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
55
55
|
|
|
56
56
|
| 命令 | 效果 |
|
|
57
57
|
|---|---|
|
|
58
|
-
| `/evolve` |
|
|
58
|
+
| `/evolve` | 简短帮助 + 当前 local store(`/evolve help all` 列全部子命令) |
|
|
59
59
|
| `/evolve list · history · rollback <id>` | 查看与回滚(加 `project` 操作本项目库,`global` 操作跨项目库) |
|
|
60
60
|
| `/evolve plan [msg]` | 对 store 运行 LLM 规划器 |
|
|
61
61
|
| `/evolve wrapup` | 收尾本会话 local 条目:晋升 / 归档 / 保留 |
|
|
62
|
-
| `/evolve archive · unarchive
|
|
63
|
-
| `/evolve recall [scope] <query…>` | 定向记忆召回:全文 + 版本 + 来源 + 过时信号 |
|
|
64
|
-
| `/evolve remember <type> [scope] <text…>` | 立即持久化一条类型化记忆(`user|feedback|project|reference`) |
|
|
65
|
-
| `/evolve forget [scope] <query…>` | 按 query 定位一条记忆并归档(可恢复);多义只列候选不写 |
|
|
62
|
+
| `/evolve archive · unarchive <id>` | 从注入中隐藏(数据保留可恢复) |
|
|
66
63
|
| `/evolve consolidate [apply] [merge]` | 报告(或应用)冲突提示 + 零使用陈旧全局条目的批量归档;`merge` 将近重复内容并入幸存原条目 |
|
|
67
64
|
| `/evolve failures` | 失败类聚合(门禁 + benchmark) |
|
|
68
65
|
| `/evolve log [tail N] [session <id>]` | 插件日志 |
|
|
@@ -73,13 +70,13 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
73
70
|
| `/evolve pause · resume · status` | 暂停/恢复自动门禁(手动工具不受影响)、门禁状态 |
|
|
74
71
|
| `/evolve usage` | 每条目注入次数 + memory/review/planner/wrapup/fate 直属调用的 provider 精确 token(不含 benchmark 宿主子代理) |
|
|
75
72
|
|
|
76
|
-
模型工具:`evolve_list / add / update / delete / rollback / recall`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement、一次审批;`evolve_recall` 按 query/kind/scope/memoryType/limit 过滤,返回全文 + 版本 + 来源 +
|
|
73
|
+
模型工具:`evolve_list / add / update / delete / rollback / recall`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement、一次审批;`evolve_recall` 按 query/kind/scope/memoryType/limit 过滤,返回全文 + 版本 + 来源 + 过时信号)。记忆的读写都在对话里完成——模型手里有注入的记忆索引 + `evolve_recall`/`evolve_add`——因此不再有 `/evolve remember · forget · recall` 对应命令:直接在对话里说。
|
|
77
74
|
|
|
78
75
|
第三方消费:每次进化落地(门禁或手动)都会向 `reviews.jsonl` 追加结构化 `evolve_complete` 事件(shape 见 `src/evolve-event.ts`),与人类可读的审计记录并存。
|
|
79
76
|
|
|
80
77
|
`/evolve usage` 还会读取 `evolve/token-usage.jsonl`:插件直属 memory Agent、review、planner、手动 wrapup 与自动 fate 调用的 provider 精确 input/cache/output/total token。报告只覆盖保留尾部而非终身累计,单独显示 provider 未返回 usage 的调用,并明确排除宿主 benchmark 子代理、其 agent-loop 调用与逐条 memory 注入归因。
|
|
81
78
|
|
|
82
|
-
注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory
|
|
79
|
+
注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory 以**正文**在会话开场注入:`evolve:memory-index` section(order 400)承载 when_to_save 指南 + 预算内的全部记忆正文,排序 `project > feedback > user > reference`,放不下的降级为 `[memory:type:id] 标题`钩子、其余折叠计数(硬上限 `memoryIndex.maxChars`,默认 6000 字符;未展示的用 `evolve_recall` 读)。该 section 每会话只算一次并逐字节复用,system prompt 因此稳定、prompt cache 持续命中——会话中途写入的记忆下个会话生效。skill 仍以按相关性排序的目录索引出现(15 行封顶 + 折叠计数行),memory 不再列入。每次 memory 落盘同时刷新 store 目录下的可读 `MEMORY.md` 索引 + 单事实文件。`memoryIndex.guide` 关闭且 store 为空时 = 零注入 token。
|
|
83
80
|
|
|
84
81
|
## 配置
|
|
85
82
|
|
|
@@ -100,7 +97,11 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
100
97
|
| `promotionBlockPatterns` | POSIX 路径、session id、`~/.dsh` | 内容命中即判定项目专属,永不晋升全局 |
|
|
101
98
|
| `promotionMinChars` | `100` | 低于此长度的整体晋升留在本地 |
|
|
102
99
|
| `injectionDirectoryLines` | `15` | 每次构建的目录行数上限,超出折叠为计数行 |
|
|
103
|
-
| `sectionOrder` | `118` |
|
|
100
|
+
| `sectionOrder` | `118` | 指南与条目两个 section 的系统提示词顺序 |
|
|
101
|
+
| `memoryIndex.enabled` | `true` | 是否注册 `evolve:memory-index` 内容 section |
|
|
102
|
+
| `memoryIndex.guide` | `true` | 是否注入 when_to_save 指南(关闭且 store 为空时该 section 不渲染) |
|
|
103
|
+
| `memoryIndex.order` | `400` | section 顺序(上游命名槽位自 `PLAN_POLICY=500` 起;本插件另两段在 118/119) |
|
|
104
|
+
| `memoryIndex.maxChars` | `6000` | 注入记忆块的硬字符预算 |
|
|
104
105
|
| `skillsDir` | `<dshHome>/skills` | 技能条目物化为 SKILL.md 的根目录 |
|
|
105
106
|
| `rubricKey` | 自动生成本地密钥文件 | benchmark rubric 的 AES-256-GCM 口令(`DSH_EVOLVE_RUBRIC_KEY` 可覆盖) |
|
|
106
107
|
| `logToFile` / `logLevel` / `logMaxBytes` | `true` / `1` / 5 MiB | 插件自带 JSONL 文件日志带轮转 |
|
|
@@ -126,7 +127,7 @@ profile patch 示例:
|
|
|
126
127
|
|
|
127
128
|
```bash
|
|
128
129
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
129
|
-
pnpm test # vitest(
|
|
130
|
+
pnpm test # vitest(1097 例)
|
|
130
131
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
131
132
|
pnpm coverage:gaps # 定位各文件未覆盖行(只读)
|
|
132
133
|
pnpm lint # oxlint src test
|
|
@@ -136,7 +137,7 @@ pnpm lint # oxlint src test
|
|
|
136
137
|
|
|
137
138
|
```
|
|
138
139
|
├── src/ # 引擎、工具、命令、memory Agent、召回、投影、门禁、fate、benchmark、注入 + token 用量…
|
|
139
|
-
├── test/ # vitest 测试套件(
|
|
140
|
+
├── test/ # vitest 测试套件(59 个文件)
|
|
140
141
|
├── lib/ # 构建产物(tsc)
|
|
141
142
|
├── docs/
|
|
142
143
|
│ ├── design.md # 完整设计文档(硬化矩阵)
|
package/lib/command.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { ARCHIVED_AT_KEY
|
|
1
|
+
import { ARCHIVED_AT_KEY } from "./types.js";
|
|
2
2
|
import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
|
|
3
|
-
import { compactText } from "./render.js";
|
|
4
3
|
import { planWithLlm } from "./planner.js";
|
|
5
4
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
5
|
import { join } from "node:path";
|
|
@@ -19,9 +18,8 @@ import { projectKeyOf } from "./project.js";
|
|
|
19
18
|
import { loadUsage, getUsageCount } from "./usage.js";
|
|
20
19
|
import { loadGateRuntime, saveGateRuntime } from "./runtime.js";
|
|
21
20
|
import { planConsolidation } from "./consolidate.js";
|
|
22
|
-
import { formatRecallResult, recallMemories, RECALL_MEMORY_TYPES } from "./recall.js";
|
|
23
21
|
import { loadTokenUsage, renderTokenUsageReport } from "./token-usage.js";
|
|
24
|
-
const
|
|
22
|
+
const USAGE_COMMON = `Usage:
|
|
25
23
|
/evolve show this help and the current local store
|
|
26
24
|
/evolve list [project|global] list entries (default local staging; "project" = this project's cross-session store, "global" = cross-project store)
|
|
27
25
|
/evolve history [global] show applied refinements (rollback ids)
|
|
@@ -31,19 +29,16 @@ const USAGE = `Usage:
|
|
|
31
29
|
to the global store (approval required), archive one-offs
|
|
32
30
|
/evolve archive <id> [global] hide an entry from injection (data kept, restorable)
|
|
33
31
|
/evolve unarchive <id> [global] restore an archived entry
|
|
34
|
-
/evolve
|
|
35
|
-
/evolve
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
immediately persist one memory (type = user|feedback|project|reference;
|
|
39
|
-
feedback/project text must carry Why + How; default scope local)
|
|
40
|
-
/evolve forget [scope] <query...> locate one memory by query and archive it (restorable via /evolve unarchive)
|
|
32
|
+
/evolve pause | resume pause/resume the automatic Memory Agent (manual tools and commands keep working)
|
|
33
|
+
/evolve status automatic Memory Agent state plus store entry counts
|
|
34
|
+
/evolve help all every subcommand, including the advanced ones below`;
|
|
35
|
+
const USAGE_ADVANCED = `Advanced:
|
|
41
36
|
/evolve consolidate [apply] [merge]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/evolve log [tail N] show the recent plugin log (default 50 lines)
|
|
37
|
+
report (or apply) a batch archive of conflict-hinted
|
|
38
|
+
and stale zero-use global entries; "merge" folds
|
|
39
|
+
near-duplicate content into the surviving original
|
|
46
40
|
/evolve failures aggregated failure counts (gate + benchmark, by class)
|
|
41
|
+
/evolve log [tail N] show the recent plugin log (default 50 lines)
|
|
47
42
|
/evolve export [global] <path> backup a store to a JSON file
|
|
48
43
|
/evolve import [global] <path> restore a store from an export file
|
|
49
44
|
/evolve mount <skillId> hot-mount a skill entry as a live cordis plugin
|
|
@@ -52,14 +47,15 @@ const USAGE = `Usage:
|
|
|
52
47
|
/evolve goal show the evolution goal (round-driven auto-review)
|
|
53
48
|
/evolve goal <objective> create/update the evolution goal
|
|
54
49
|
/evolve goal done complete the evolution goal
|
|
55
|
-
/evolve
|
|
56
|
-
/evolve status automatic Memory Agent state plus store entry counts
|
|
50
|
+
/evolve benchmark ... case lifecycle, runs, acceptance
|
|
57
51
|
/evolve usage injection counts + exact direct-call token usage (benchmark subagents excluded)`;
|
|
52
|
+
/** Default help (`/evolve`, `/evolve help`): the commands worth knowing. */
|
|
53
|
+
const USAGE = USAGE_COMMON;
|
|
58
54
|
export function registerEvolveCommand(ctx, engine, opts, runtime) {
|
|
59
55
|
ctx.commands.register({
|
|
60
56
|
name: "evolve",
|
|
61
57
|
description: "自进化:检查并演进 harness 状态(记忆/技能/提示词/子代理) | Inspect and evolve the continual harness state (memories, skills, prompt notes, subagent specs)",
|
|
62
|
-
input: { hint: "[list [global] | history [global] | rollback <id> [global] | plan [msg]]" },
|
|
58
|
+
input: { hint: "[list [global] | history [global] | rollback <id> [global] | plan [msg] | help all]" },
|
|
63
59
|
handler: (invocation) => executeEvolveCommand(ctx, engine, invocation, opts, runtime),
|
|
64
60
|
});
|
|
65
61
|
}
|
|
@@ -162,7 +158,9 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
162
158
|
switch (sub) {
|
|
163
159
|
case "":
|
|
164
160
|
case "help":
|
|
165
|
-
|
|
161
|
+
// Layered help (2026-10-01): the default listing stays short;
|
|
162
|
+
// `help all` expands the advanced half.
|
|
163
|
+
return success(`${rest[0] === "all" ? `${USAGE_COMMON}\n\n${USAGE_ADVANCED}` : USAGE_COMMON}\n\n${formatHarnessStateForPrompt(engine.load("local", sessionId))}`);
|
|
166
164
|
case "list": {
|
|
167
165
|
const { scope } = scopeArg(rest);
|
|
168
166
|
return success(formatHarnessStateForPrompt(engine.load(scope, storeIdForCommand(scope, invocation))));
|
|
@@ -182,16 +180,12 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
182
180
|
return success(renderResult(result));
|
|
183
181
|
}
|
|
184
182
|
case "archive":
|
|
185
|
-
case "unarchive":
|
|
186
|
-
case "demote": {
|
|
183
|
+
case "unarchive": {
|
|
187
184
|
const { scope, rest: after } = scopeArg(rest);
|
|
188
185
|
const id = stripAngleBrackets(after[0] ?? "");
|
|
189
186
|
if (!id) {
|
|
190
187
|
return error(`${sub} requires an entry id.\n${USAGE}`);
|
|
191
188
|
}
|
|
192
|
-
if (sub === "demote") {
|
|
193
|
-
return demoteEntry(engine, id, sessionId, projectKeyOf(invocation.agent));
|
|
194
|
-
}
|
|
195
189
|
const state = engine.load(scope, storeIdForCommand(scope, invocation));
|
|
196
190
|
const found = findEntryById(state, id);
|
|
197
191
|
if (!found) {
|
|
@@ -214,90 +208,6 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
214
208
|
}, { scope });
|
|
215
209
|
return success(renderResult(result));
|
|
216
210
|
}
|
|
217
|
-
case "recall": {
|
|
218
|
-
// Human counterpart of the evolve_recall tool: same engine,
|
|
219
|
-
// same filters (memory kind, one scope or all three).
|
|
220
|
-
const { scope, rest: after } = scopeArg(rest);
|
|
221
|
-
const explicitScope = rest.length > 0 && (rest[0] === "global" || rest[0] === "project" || rest[0] === "local");
|
|
222
|
-
const query = after.join(" ");
|
|
223
|
-
const result = recallMemories(engine, { sessionId, projectKey: projectKeyOf(invocation.agent) ?? undefined }, { kinds: ["memory"], ...(explicitScope ? { scopes: [scope] } : {}), ...(query ? { query } : {}), limit: 10 });
|
|
224
|
-
return success(formatRecallResult(result, query || undefined));
|
|
225
|
-
}
|
|
226
|
-
case "remember": {
|
|
227
|
-
// Immediate manual memory path (P1 §6.6): one fact, explicit
|
|
228
|
-
// type, straight into the engine — validation, snapshot,
|
|
229
|
-
// versioning, and approval behave exactly like any other
|
|
230
|
-
// write. feedback/project text must already carry Why + How;
|
|
231
|
-
// the engine rejects it loudly otherwise.
|
|
232
|
-
const type = rest[0] ?? "";
|
|
233
|
-
if (!RECALL_MEMORY_TYPES.includes(type)) {
|
|
234
|
-
return error(`remember requires a memory type: user|feedback|project|reference.\nExample: /evolve remember feedback local 对方偏好深色主题。Why:…… How to apply:……\n${USAGE}`);
|
|
235
|
-
}
|
|
236
|
-
const maybeScope = rest[1] ?? "";
|
|
237
|
-
const scoped = maybeScope === "global" || maybeScope === "project" || maybeScope === "local";
|
|
238
|
-
const scope = scoped ? maybeScope : "local";
|
|
239
|
-
const text = (scoped ? rest.slice(2) : rest.slice(1)).join(" ").trim();
|
|
240
|
-
if (!text) {
|
|
241
|
-
return error(`remember requires the memory text after the type.\n${USAGE}`);
|
|
242
|
-
}
|
|
243
|
-
const storeId = storeIdForCommand(scope, invocation);
|
|
244
|
-
if ((scope === "global" || scope === "project") && opts.requireGlobalApproval) {
|
|
245
|
-
await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve remember ${type} → ${scope} store: ${compactText(text, 120)}`);
|
|
246
|
-
}
|
|
247
|
-
const result = engine.apply(scope, storeId, {
|
|
248
|
-
summary: `remember: ${compactText(text, 80)}`,
|
|
249
|
-
rationale: "Human-invoked immediate memory via /evolve remember.",
|
|
250
|
-
expectedOutcome: "The fact is persisted as a typed memory entry.",
|
|
251
|
-
edits: [{
|
|
252
|
-
action: "create",
|
|
253
|
-
kind: "memory",
|
|
254
|
-
title: compactText(text, 80),
|
|
255
|
-
content: text,
|
|
256
|
-
metadata: { [MEMORY_TYPE_KEY]: type },
|
|
257
|
-
}],
|
|
258
|
-
}, { scope });
|
|
259
|
-
return success(renderResult(result));
|
|
260
|
-
}
|
|
261
|
-
case "forget": {
|
|
262
|
-
// Manual forget path (P1 §6.6): locate by query, archive the
|
|
263
|
-
// single match (restorable via unarchive). An ambiguous query
|
|
264
|
-
// lists candidates instead of archiving — a silent multi-
|
|
265
|
-
// archive would look like success while hiding too much.
|
|
266
|
-
const { scope, rest: after } = scopeArg(rest);
|
|
267
|
-
const explicitScope = rest.length > 0 && (rest[0] === "global" || rest[0] === "project" || rest[0] === "local");
|
|
268
|
-
const query = after.join(" ").trim();
|
|
269
|
-
if (!query) {
|
|
270
|
-
return error(`forget requires a query to locate the memory.\n${USAGE}`);
|
|
271
|
-
}
|
|
272
|
-
const recalled = recallMemories(engine, { sessionId, projectKey: projectKeyOf(invocation.agent) ?? undefined }, { query, kinds: ["memory"], ...(explicitScope ? { scopes: [scope] } : {}), limit: 6, includeArchived: false });
|
|
273
|
-
if (recalled.hits.length === 0) {
|
|
274
|
-
return error(`no memory matches "${query}"${recalled.notes.length > 0 ? ` (${recalled.notes.join("; ")})` : ""} — try fewer words, or /evolve list to browse.`);
|
|
275
|
-
}
|
|
276
|
-
if (recalled.hits.length > 1) {
|
|
277
|
-
const list = recalled.hits.map((hit) => `- [${hit.scope}:${hit.id}] ${hit.title}`).join("\n");
|
|
278
|
-
return success(`"${query}" matches ${recalled.hits.length} memories — refine the query so exactly one matches, then forget archives it:\n${list}`);
|
|
279
|
-
}
|
|
280
|
-
const hit = recalled.hits[0];
|
|
281
|
-
const state = engine.load(hit.scope, hit.scope === "local" ? sessionId : hit.scope === "project" ? (projectKeyOf(invocation.agent) ?? undefined) : undefined);
|
|
282
|
-
const entry = state.entries.memory[hit.id];
|
|
283
|
-
if (!entry) {
|
|
284
|
-
return error(`memory ${hit.scope}:${hit.id} vanished since recall — run /evolve forget again.`);
|
|
285
|
-
}
|
|
286
|
-
const result = engine.apply(hit.scope, hit.scope === "local" ? sessionId : hit.scope === "project" ? (projectKeyOf(invocation.agent) ?? undefined) : undefined, {
|
|
287
|
-
summary: `forget: archive memory ${hit.scope}:${hit.id}`,
|
|
288
|
-
rationale: `Human-invoked forget via /evolve forget "${query}".`,
|
|
289
|
-
expectedOutcome: "The entry is hidden from injection (data kept; restorable via /evolve unarchive).",
|
|
290
|
-
edits: [{
|
|
291
|
-
action: "update",
|
|
292
|
-
kind: "memory",
|
|
293
|
-
id: hit.id,
|
|
294
|
-
title: entry.title,
|
|
295
|
-
content: entry.content,
|
|
296
|
-
metadata: { ...entry.metadata, [ARCHIVED_AT_KEY]: new Date().toISOString() },
|
|
297
|
-
}],
|
|
298
|
-
}, { scope: hit.scope });
|
|
299
|
-
return success(`forgot [${hit.scope}:${hit.id}] ${hit.title} (archived — restore with /evolve unarchive ${hit.id}${hit.scope === "local" ? "" : ` ${hit.scope}`})\n${renderResult(result)}`);
|
|
300
|
-
}
|
|
301
211
|
case "consolidate": {
|
|
302
212
|
// R3: deterministic global-store hygiene. Report by default;
|
|
303
213
|
// `apply` re-scans fresh state and lands the whole batch as ONE
|
|
@@ -511,44 +421,6 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
511
421
|
return error(cause instanceof Error ? cause.message : String(cause));
|
|
512
422
|
}
|
|
513
423
|
}
|
|
514
|
-
/**
|
|
515
|
-
* Demote (2026-08-22): hide an entry from injection WITHOUT deleting it —
|
|
516
|
-
* the one-command remedy for store pollution. Searches the global store
|
|
517
|
-
* first (the primary target: cross-project noise), then the project store,
|
|
518
|
-
* then the session's local store. The data stays; `/evolve unarchive` restores it.
|
|
519
|
-
*/
|
|
520
|
-
function demoteEntry(engine, id, sessionId, projectKey) {
|
|
521
|
-
const targets = [
|
|
522
|
-
{ scope: "global", storeId: undefined },
|
|
523
|
-
...(projectKey ? [{ scope: "project", storeId: projectKey }] : []),
|
|
524
|
-
{ scope: "local", storeId: sessionId },
|
|
525
|
-
];
|
|
526
|
-
for (const { scope, storeId } of targets) {
|
|
527
|
-
const state = engine.load(scope, storeId);
|
|
528
|
-
const found = findEntryById(state, id);
|
|
529
|
-
if (!found)
|
|
530
|
-
continue;
|
|
531
|
-
const [kind, entry] = found;
|
|
532
|
-
const result = engine.apply(scope, storeId, {
|
|
533
|
-
summary: `demote: archive ${kind}:${id} from the ${scope} store`,
|
|
534
|
-
rationale: "Human-invoked demote via the /evolve command.",
|
|
535
|
-
expectedOutcome: "The entry is hidden from injection in every scope it touched; data is kept and restorable.",
|
|
536
|
-
edits: [
|
|
537
|
-
{
|
|
538
|
-
action: "update",
|
|
539
|
-
kind,
|
|
540
|
-
id,
|
|
541
|
-
title: entry.title,
|
|
542
|
-
content: entry.content,
|
|
543
|
-
metadata: { ...entry.metadata, [ARCHIVED_AT_KEY]: new Date().toISOString() },
|
|
544
|
-
},
|
|
545
|
-
],
|
|
546
|
-
}, { scope });
|
|
547
|
-
const restoreScope = scope === "global" ? " global" : scope === "project" ? " project" : "";
|
|
548
|
-
return success(`demoted ${kind}:${id} from the ${scope} store (archived — restore with /evolve unarchive ${id}${restoreScope})\n${renderResult(result)}`);
|
|
549
|
-
}
|
|
550
|
-
return error(`entry ${id} not found in the global, project, or local store`);
|
|
551
|
-
}
|
|
552
424
|
/**
|
|
553
425
|
* Status answers whether the automatic listener is wired and what
|
|
554
426
|
* stores/retention it currently sees.
|
package/lib/index.d.ts
CHANGED
|
@@ -98,6 +98,24 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
98
98
|
promotionMinChars: z<number, number, "defined">;
|
|
99
99
|
/** Entry-directory lines injected per build before folding into a counter. */
|
|
100
100
|
injectionDirectoryLines: z<number, number, "defined">;
|
|
101
|
+
/**
|
|
102
|
+
* evolve v2 memory index: the section that injects memory CONTENT (not just
|
|
103
|
+
* an id/title directory) at session start, frozen per session to keep the
|
|
104
|
+
* system prompt byte-stable (prompt cache). `enabled`/`guide`/`order`/
|
|
105
|
+
* `maxChars` default to true/true/400/6000; with `guide` off and an empty
|
|
106
|
+
* store the section renders to "" and costs no tokens.
|
|
107
|
+
*/
|
|
108
|
+
memoryIndex: z<Schemastery.ObjectS<NoInfer<{
|
|
109
|
+
enabled: z<boolean, boolean, "plain">;
|
|
110
|
+
guide: z<boolean, boolean, "plain">;
|
|
111
|
+
order: z<number, number, "plain">;
|
|
112
|
+
maxChars: z<number, number, "plain">;
|
|
113
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
114
|
+
enabled: z<boolean, boolean, "plain">;
|
|
115
|
+
guide: z<boolean, boolean, "plain">;
|
|
116
|
+
order: z<number, number, "plain">;
|
|
117
|
+
maxChars: z<number, number, "plain">;
|
|
118
|
+
}>>, "plain">;
|
|
101
119
|
/**
|
|
102
120
|
* Storage hygiene (#20): how much append-only past each write keeps.
|
|
103
121
|
* `snapshots` = full-state copies per store, `refinements` = tail lines
|
|
@@ -210,6 +228,24 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
210
228
|
promotionMinChars: z<number, number, "defined">;
|
|
211
229
|
/** Entry-directory lines injected per build before folding into a counter. */
|
|
212
230
|
injectionDirectoryLines: z<number, number, "defined">;
|
|
231
|
+
/**
|
|
232
|
+
* evolve v2 memory index: the section that injects memory CONTENT (not just
|
|
233
|
+
* an id/title directory) at session start, frozen per session to keep the
|
|
234
|
+
* system prompt byte-stable (prompt cache). `enabled`/`guide`/`order`/
|
|
235
|
+
* `maxChars` default to true/true/400/6000; with `guide` off and an empty
|
|
236
|
+
* store the section renders to "" and costs no tokens.
|
|
237
|
+
*/
|
|
238
|
+
memoryIndex: z<Schemastery.ObjectS<NoInfer<{
|
|
239
|
+
enabled: z<boolean, boolean, "plain">;
|
|
240
|
+
guide: z<boolean, boolean, "plain">;
|
|
241
|
+
order: z<number, number, "plain">;
|
|
242
|
+
maxChars: z<number, number, "plain">;
|
|
243
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
244
|
+
enabled: z<boolean, boolean, "plain">;
|
|
245
|
+
guide: z<boolean, boolean, "plain">;
|
|
246
|
+
order: z<number, number, "plain">;
|
|
247
|
+
maxChars: z<number, number, "plain">;
|
|
248
|
+
}>>, "plain">;
|
|
213
249
|
/**
|
|
214
250
|
* Storage hygiene (#20): how much append-only past each write keeps.
|
|
215
251
|
* `snapshots` = full-state copies per store, `refinements` = tail lines
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* initial default when `evolve/runtime.json` does not exist yet, and
|
|
9
9
|
* `/evolve pause|resume` own it afterwards. Generic review/planner/fate are
|
|
10
10
|
* not reachable from this listener.
|
|
11
|
+
*
|
|
12
|
+
* evolve v2 registers a third section (`evolve:memory-index`) that injects
|
|
13
|
+
* memory content plus the when_to_save guide, frozen per session — see
|
|
14
|
+
* memory-index.ts.
|
|
11
15
|
*/
|
|
12
16
|
import { join } from "node:path";
|
|
13
17
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -18,6 +22,7 @@ import { registerEvolveCommand } from "./command.js";
|
|
|
18
22
|
import { registerAutoReview } from "./auto.js";
|
|
19
23
|
import { syncSkillsFromResult } from "./skill.js";
|
|
20
24
|
import { entriesSectionText } from "./inject.js";
|
|
25
|
+
import { createFrozenMemorySection, DEFAULT_MEMORY_INDEX_MAX_CHARS, DEFAULT_MEMORY_SECTION_ORDER, MEMORY_SECTION_NAME, memoryIndexSectionText, } from "./memory-index.js";
|
|
21
26
|
import { resolveRubricKey } from "./rubric.js";
|
|
22
27
|
import { restoreMounted } from "./mount.js";
|
|
23
28
|
import { registerFileLogger } from "./logfile.js";
|
|
@@ -119,6 +124,19 @@ export const Config = z.object({
|
|
|
119
124
|
promotionMinChars: z.natural().default(100),
|
|
120
125
|
/** Entry-directory lines injected per build before folding into a counter. */
|
|
121
126
|
injectionDirectoryLines: z.natural().default(15),
|
|
127
|
+
/**
|
|
128
|
+
* evolve v2 memory index: the section that injects memory CONTENT (not just
|
|
129
|
+
* an id/title directory) at session start, frozen per session to keep the
|
|
130
|
+
* system prompt byte-stable (prompt cache). `enabled`/`guide`/`order`/
|
|
131
|
+
* `maxChars` default to true/true/400/6000; with `guide` off and an empty
|
|
132
|
+
* store the section renders to "" and costs no tokens.
|
|
133
|
+
*/
|
|
134
|
+
memoryIndex: z.object({
|
|
135
|
+
enabled: z.boolean(),
|
|
136
|
+
guide: z.boolean(),
|
|
137
|
+
order: z.natural(),
|
|
138
|
+
maxChars: z.natural(),
|
|
139
|
+
}),
|
|
122
140
|
/**
|
|
123
141
|
* Storage hygiene (#20): how much append-only past each write keeps.
|
|
124
142
|
* `snapshots` = full-state copies per store, `refinements` = tail lines
|
|
@@ -162,11 +180,35 @@ export function apply(ctx, config) {
|
|
|
162
180
|
// entries real delegation specs. The text is a provider evaluated at every
|
|
163
181
|
// assembly with the assembling agent; a store without prompt/subagent
|
|
164
182
|
// entries renders to "" and the prompt renderer drops the section.
|
|
183
|
+
//
|
|
184
|
+
// evolve v2: when the memory index section carries memory content, the
|
|
185
|
+
// directory stops listing memories so the same entries are never paid for
|
|
186
|
+
// twice (one fact, one home).
|
|
187
|
+
const memoryIndexEnabled = config.memoryIndex?.enabled ?? true;
|
|
165
188
|
ctx.systemPrompt.section({
|
|
166
189
|
name: "tool:continual-evolve:entries",
|
|
167
190
|
order: (config.sectionOrder ?? 118) + 1,
|
|
168
|
-
text: (context) => entriesSectionText(engine, context.agent, undefined, {
|
|
191
|
+
text: (context) => entriesSectionText(engine, context.agent, undefined, {
|
|
192
|
+
directoryLines: config.injectionDirectoryLines ?? 15,
|
|
193
|
+
includeMemoryDirectory: !memoryIndexEnabled,
|
|
194
|
+
}),
|
|
169
195
|
});
|
|
196
|
+
// evolve v2 passive read path: memory content in the prompt at session
|
|
197
|
+
// start, frozen per session (see memory-index.ts) so the whole system
|
|
198
|
+
// prompt stays byte-stable within a session and prompt-cache reads keep
|
|
199
|
+
// hitting. Registered in the empty 1–499 order slot — the two sections
|
|
200
|
+
// above sit at 118/119, upstream's named slots start at PLAN_POLICY=500.
|
|
201
|
+
if (memoryIndexEnabled) {
|
|
202
|
+
const frozenMemories = createFrozenMemorySection();
|
|
203
|
+
const memoryIndexOrder = config.memoryIndex?.order ?? DEFAULT_MEMORY_SECTION_ORDER;
|
|
204
|
+
const memoryIndexMaxChars = config.memoryIndex?.maxChars ?? DEFAULT_MEMORY_INDEX_MAX_CHARS;
|
|
205
|
+
const memoryIndexGuide = config.memoryIndex?.guide ?? true;
|
|
206
|
+
ctx.systemPrompt.section({
|
|
207
|
+
name: MEMORY_SECTION_NAME,
|
|
208
|
+
order: memoryIndexOrder,
|
|
209
|
+
text: (context) => frozenMemories.textFor(context.agent, (agent) => memoryIndexSectionText(engine, agent, { maxChars: memoryIndexMaxChars, guide: memoryIndexGuide })),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
170
212
|
const gate = { requireGlobalApproval: config.requireGlobalApproval ?? true };
|
|
171
213
|
const promotionPolicy = resolvePromotionPolicy({
|
|
172
214
|
blockPatterns: config.promotionBlockPatterns,
|
package/lib/inject.d.ts
CHANGED
|
@@ -158,6 +158,16 @@ export declare function rankedDirectoryEntries(maxLines: number, query: string |
|
|
|
158
158
|
* beyond the nearest carrying store).
|
|
159
159
|
*/
|
|
160
160
|
export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, agent: AgentLike): HarnessState | undefined;
|
|
161
|
+
/**
|
|
162
|
+
* The single three-store merge every injection path reads from: the global
|
|
163
|
+
* store merged with this project's store (when the session cwd resolves one)
|
|
164
|
+
* and the nearest carrying local store (precedence global < project < local —
|
|
165
|
+
* see {@link mergeHarnessStates}). One home, so the memory section and the
|
|
166
|
+
* entry directory can never disagree about what the model can see.
|
|
167
|
+
*/
|
|
168
|
+
export declare function mergedInjectionState(engine: EvolutionEngine, agent: AgentLike, opts?: {
|
|
169
|
+
projectKey?: string;
|
|
170
|
+
}): HarnessState;
|
|
161
171
|
/**
|
|
162
172
|
* Compose the full injected block for one assembling agent: global entries
|
|
163
173
|
* merged with this project's store (when the session cwd resolves one) and
|
|
@@ -171,6 +181,9 @@ export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, ag
|
|
|
171
181
|
* `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle);
|
|
172
182
|
* `opts.projectKey` pins the project layer explicitly (tests, tools) —
|
|
173
183
|
* otherwise it is derived from the agent's session cwd, best-effort.
|
|
184
|
+
* `opts.includeMemoryDirectory` (default true) drops memory lines from the
|
|
185
|
+
* directory when the memory section already carries them (evolve v2) — one
|
|
186
|
+
* fact, one home, so the same memories are never paid for twice.
|
|
174
187
|
* Usage recording covers ALL kinds — memories and skills appear as directory
|
|
175
188
|
* lines, prompts/subagents as content — and is deduped per session so the
|
|
176
189
|
* counts read "how many sessions saw this", not "how many prompt builds".
|
|
@@ -178,5 +191,6 @@ export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, ag
|
|
|
178
191
|
export declare function entriesSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, query?: string, opts?: {
|
|
179
192
|
directoryLines?: number;
|
|
180
193
|
projectKey?: string;
|
|
194
|
+
includeMemoryDirectory?: boolean;
|
|
181
195
|
}): string;
|
|
182
196
|
//# sourceMappingURL=inject.d.ts.map
|
package/lib/inject.js
CHANGED
|
@@ -324,6 +324,20 @@ export function nearestLocalStateWithEntries(engine, agent) {
|
|
|
324
324
|
}
|
|
325
325
|
return undefined;
|
|
326
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* The single three-store merge every injection path reads from: the global
|
|
329
|
+
* store merged with this project's store (when the session cwd resolves one)
|
|
330
|
+
* and the nearest carrying local store (precedence global < project < local —
|
|
331
|
+
* see {@link mergeHarnessStates}). One home, so the memory section and the
|
|
332
|
+
* entry directory can never disagree about what the model can see.
|
|
333
|
+
*/
|
|
334
|
+
export function mergedInjectionState(engine, agent, opts) {
|
|
335
|
+
const globalState = engine.load("global", undefined);
|
|
336
|
+
const projectKey = opts?.projectKey ?? projectKeyOf(agent);
|
|
337
|
+
const projectState = projectKey ? engine.load("project", projectKey) : undefined;
|
|
338
|
+
const localState = nearestLocalStateWithEntries(engine, agent);
|
|
339
|
+
return mergeHarnessStates(globalState, localState, projectState ? { projectState } : undefined);
|
|
340
|
+
}
|
|
327
341
|
/**
|
|
328
342
|
* Compose the full injected block for one assembling agent: global entries
|
|
329
343
|
* merged with this project's store (when the session cwd resolves one) and
|
|
@@ -337,6 +351,9 @@ export function nearestLocalStateWithEntries(engine, agent) {
|
|
|
337
351
|
* `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle);
|
|
338
352
|
* `opts.projectKey` pins the project layer explicitly (tests, tools) —
|
|
339
353
|
* otherwise it is derived from the agent's session cwd, best-effort.
|
|
354
|
+
* `opts.includeMemoryDirectory` (default true) drops memory lines from the
|
|
355
|
+
* directory when the memory section already carries them (evolve v2) — one
|
|
356
|
+
* fact, one home, so the same memories are never paid for twice.
|
|
340
357
|
* Usage recording covers ALL kinds — memories and skills appear as directory
|
|
341
358
|
* lines, prompts/subagents as content — and is deduped per session so the
|
|
342
359
|
* counts read "how many sessions saw this", not "how many prompt builds".
|
|
@@ -345,11 +362,7 @@ export function entriesSectionText(engine, agent, query, opts) {
|
|
|
345
362
|
if (!agent) {
|
|
346
363
|
return "";
|
|
347
364
|
}
|
|
348
|
-
const
|
|
349
|
-
const projectKey = opts?.projectKey ?? projectKeyOf(agent);
|
|
350
|
-
const projectState = projectKey ? engine.load("project", projectKey) : undefined;
|
|
351
|
-
const localState = nearestLocalStateWithEntries(engine, agent);
|
|
352
|
-
const merged = mergeHarnessStates(globalState, localState, projectState ? { projectState } : undefined);
|
|
365
|
+
const merged = mergedInjectionState(engine, agent, opts);
|
|
353
366
|
const promptEntries = Object.values(merged.entries.prompt);
|
|
354
367
|
const subagentEntries = Object.values(merged.entries.subagent);
|
|
355
368
|
const relevanceQuery = (query ?? recentUserText(agent)).trim();
|
|
@@ -371,7 +384,9 @@ export function entriesSectionText(engine, agent, query, opts) {
|
|
|
371
384
|
const directoryLines = opts?.directoryLines ?? DEFAULT_DIRECTORY_LINES;
|
|
372
385
|
const promptKind = Object.values(merged.entries.prompt);
|
|
373
386
|
const subagentKind = Object.values(merged.entries.subagent);
|
|
374
|
-
|
|
387
|
+
// v2: memories live in their own content-carrying section, so the directory
|
|
388
|
+
// stops listing them (passing [] keeps the positional contract intact).
|
|
389
|
+
const memoryKind = (opts?.includeMemoryDirectory ?? true) ? Object.values(merged.entries.memory) : [];
|
|
375
390
|
const skillKind = Object.values(merged.entries.skill);
|
|
376
391
|
const directoryText = formatEntriesDirectoryRanked(directoryLines, relevanceQuery, promptKind, subagentKind, memoryKind, skillKind);
|
|
377
392
|
// Directory-visible keys count too: a memory's injection IS its directory
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The persistent-memory guide injected as part of the `evolve:memory-index`
|
|
3
|
+
* system-prompt section (evolve v2, change two).
|
|
4
|
+
*
|
|
5
|
+
* This text is the write-path half of the v2 plan: it teaches the model when a
|
|
6
|
+
* durable memory is worth creating, which recall type it belongs to, what body
|
|
7
|
+
* structure each type owes, and that the main session may write directly with
|
|
8
|
+
* `evolve_add` instead of waiting for the background pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Provenance: `docs/research/evolve-v2-passive-read-write-plan.md` §改动二
|
|
11
|
+
* (Chinese full text, adapted from ZCode's persistent-memory prompt). Kept in
|
|
12
|
+
* its own module so the wording is a single home — `memory-index.ts` only
|
|
13
|
+
* composes it with the `<memories>` block.
|
|
14
|
+
*/
|
|
15
|
+
/** Heading and framing before the `<memories>` block. */
|
|
16
|
+
export declare const MEMORY_GUIDE_INTRO = "# \u6301\u4E45\u8BB0\u5FC6\n\n\u4F60\u6709\u4E00\u4E2A\u8DE8\u4F1A\u8BDD\u7684\u6301\u4E45\u8BB0\u5FC6\u7CFB\u7EDF\u3002\u4EE5\u4E0B\u662F\u5F53\u524D\u8BB0\u5FC6\uFF08\u5DF2\u76F4\u63A5\u6CE8\u5165\uFF0C\u65E0\u9700\u67E5\u8BE2\u5373\u53EF\u4F7F\u7528\uFF1B\u9700\u8981\u68C0\u7D22\u5B8C\u6574\u5185\u5BB9\u6216\u786E\u8BA4\u662F\u5426\u6709\u76F8\u5173\u8BB0\u5FC6\u65F6\uFF0C\u8C03\u7528 evolve_recall\uFF09\u3002\u672C\u4F1A\u8BDD\u5185\u53EF\u4EE5\u76F4\u63A5\u7528 evolve_add \u843D\u76D8\uFF0C\u4E0D\u5FC5\u7B49\u540E\u53F0\u6D41\u7A0B\u2014\u2014\u9ED8\u8BA4\u5199 local\uFF0C\u53EA\u6709\u7A33\u5B9A\u7684\u8DE8\u9879\u76EE\u6559\u8BAD\u624D\u5199 global\uFF08global \u5199\u5165\u4F1A\u5F39\u4EBA\u5DE5\u6279\u51C6\uFF09\u3002";
|
|
17
|
+
/** The when_to_save / how_to_use rules and maintenance discipline. */
|
|
18
|
+
export declare const MEMORY_GUIDE_RULES = "## \u4F55\u65F6\u5199\u5165\u8BB0\u5FC6\n\n\u7528\u6237\u660E\u786E\u8981\u6C42\u8BB0\u4F4F\u65F6\uFF0C\u7ACB\u5373\u5B58\u4E3A\u6700\u5408\u9002\u7684\u7C7B\u578B\uFF1B\u8981\u6C42\u5FD8\u8BB0\u65F6\uFF0C\u627E\u5230\u5E76\u5220\u9664\u76F8\u5173\u6761\u76EE\u3002\n\u9664\u6B64\u4E4B\u5916\uFF0C\u5728\u5BF9\u8BDD\u4E2D\u81EA\u7136\u9047\u5230\u4EE5\u4E0B\u4FE1\u53F7\u65F6\u4E3B\u52A8\u5199\u5165\u3002\u5B58\u4E4B\u524D\u5148\u8FC7\u4E00\u9053\u7B5B\u5B50\uFF1A\n**\u80FD\u4ECE\u4EE3\u7801\u3001git \u5386\u53F2\u3001\u4ED3\u5E93\u6587\u4EF6\u91CC\u91CD\u65B0\u63A8\u5BFC\u51FA\u6765\u7684\u4E8B\u5B9E\u4E0D\u5B58**\u2014\u2014\u53EA\u5B58\u91CD\u63A8\u5BFC\u4E0D\u51FA\u6765\u7684\n\u80CC\u666F\u3001\u51B3\u7B56\u548C\u504F\u597D\u3002\u76F8\u5BF9\u65E5\u671F\u4E00\u5F8B\u8F6C\u6210\u7EDD\u5BF9\u65E5\u671F\uFF08\"\u5468\u56DB\"\u2192\"2026-10-08\"\uFF09\uFF0C\u5426\u5219\u672A\u6765\n\u7684\u4F1A\u8BDD\u65E0\u6CD5\u89E3\u8BFB\u3002\u4E00\u6761\u4E8B\u5B9E\u4E00\u4E2A\u6761\u76EE\uFF1B\u4E0D\u786E\u5B9A\u503C\u4E0D\u503C\u5F97\u5B58\u65F6\uFF0C\u95EE\u81EA\u5DF1\"\u8FD9\u6761\u7684\u4FE1\u606F\u91CF\n\u662F\u5426\u5728'\u4E3A\u4EC0\u4E48'\u91CC\uFF0C\u800C\u4E0D\u5728'\u662F\u4EC0\u4E48'\u91CC\"\u3002\n\n<type name=\"user\">\n <when_to_save>\u5F97\u77E5\u7528\u6237\u7684\u89D2\u8272\u3001\u6280\u672F\u6808\u3001\u7ECF\u9A8C\u6C34\u5E73\u3001\u504F\u597D\u6216\u77E5\u8BC6\u80CC\u666F\u7684\u4EFB\u4F55\u7EC6\u8282\u65F6\u3002\n \u4F8B\u5982\u7528\u6237\u81EA\u8FF0\"\u6211\u662F\u6570\u636E\u79D1\u5B66\u5BB6\uFF0C\u5728\u67E5\u65E5\u5FD7\u7CFB\u7EDF\"\u2014\u2014\u5B58\uFF1A\u7528\u6237\u662F\u6570\u636E\u79D1\u5B66\u5BB6\uFF0C\n \u5F53\u524D\u5173\u6CE8\u53EF\u89C2\u6D4B\u6027/\u65E5\u5FD7\u3002</when_to_save>\n <how_to_use>\u540E\u7EED\u89E3\u91CA\u548C\u5EFA\u8BAE\u6309\u7528\u6237\u7684\u80CC\u666F\u5B9A\u5236\u6DF1\u6D45\u4E0E\u7C7B\u6BD4\uFF1B\u5BF9\u8D44\u6DF1\u5DE5\u7A0B\u5E08\n \u4E0D\u89E3\u91CA\u57FA\u7840\u6982\u5FF5\uFF0C\u5BF9\u67D0\u9886\u57DF\u7684\u65B0\u624B\u7528\u5176\u719F\u6089\u9886\u57DF\u7684\u7C7B\u6BD4\u5207\u5165\u3002</how_to_use>\n</type>\n\n<type name=\"feedback\">\n <when_to_save>\u4E24\u6761\u4FE1\u53F7\uFF0C\u7F3A\u4E00\u4E0D\u53EF\uFF1A\n \uFF081\uFF09\u7EA0\u6B63\u2014\u2014\u7528\u6237\u8BF4\"\u4E0D\u662F\u8FD9\u6837\"\"\u522B\"\"\u505C\u4E0B\"\u65F6\uFF0C\u5B58\u88AB\u7EA0\u6B63\u7684\u505A\u6CD5\u4E0E\u6B63\u786E\u505A\u6CD5\uFF1B\n \uFF082\uFF09\u786E\u8BA4\u2014\u2014\u8FD9\u662F\u66F4\u5B89\u9759\u3001\u66F4\u5BB9\u6613\u6F0F\u7684\u4FE1\u53F7\uFF1A\u7528\u6237\u63A5\u53D7\u4E86\u4F60\u4E0D\u5BFB\u5E38\u7684\u9009\u62E9\u800C\u6CA1\u6709\u5F02\u8BAE\uFF0C\n \u6216\u660E\u786E\u8BF4\"\u5BF9\uFF0C\u5C31\u8FD9\u6837\"\"\u4FDD\u6301\u8FD9\u6837\"\u3002\u786E\u8BA4\u8FC7\u7684\u5224\u65AD\u662F\u5DF2\u9A8C\u8BC1\u7684\u65B9\u6CD5\u8BBA\uFF0C\u53EA\u5B58\u7EA0\u9519\n \u4F1A\u8BA9\u4F60\u7684\u884C\u4E3A\u8D8A\u6765\u8D8A\u4FDD\u5B88\u3001\u504F\u79BB\u7528\u6237\u5DF2\u8BA4\u53EF\u7684\u8DEF\u3002\n \u4F8B\uFF1A\"\u6D4B\u8BD5\u522B mock \u6570\u636E\u5E93\uFF0C\u4E0A\u6B21 mock \u8FC7\u4E86\u7EBF\u4E0A\u8FC1\u79FB\u6302\u4E86\"\u2014\u2014\u5B58\u89C4\u5219\u5E76\u5E26\u4E0A\u4E8B\u6545\u539F\u56E0\u3002\n \u4F8B\uFF1A\"\u7ED3\u5C3E\u4E0D\u7528\u603B\u7ED3\uFF0Cdiff \u6211\u81EA\u5DF1\u4F1A\u770B\"\u2014\u2014\u5B58\uFF1A\u8BE5\u7528\u6237\u4E0D\u8981\u5C3E\u968F\u6027\u603B\u7ED3\u3002</when_to_save>\n <how_to_use>\u8BA9\u7528\u6237\u4E0D\u5FC5\u628A\u540C\u4E00\u6761\u6307\u5BFC\u8BF4\u7B2C\u4E8C\u904D\u3002</how_to_use>\n <body_structure>\u89C4\u5219\u5148\u884C\uFF0C\u968F\u540E\u5FC5\u5E26\u4E24\u884C\uFF1A**Why:**\uFF08\u7528\u6237\u7ED9\u7684\u7406\u7531\uFF0C\u5E38\u662F\u67D0\u6B21\n \u4E8B\u6545\u6216\u5F3A\u504F\u597D\u2014\u2014\u77E5\u9053\u4E3A\u4EC0\u4E48\u624D\u80FD\u5224\u65AD\u8FB9\u7F18\u60C5\u51B5\u8BE5\u4E0D\u8BE5\u7834\u4F8B\uFF09\u548C **How to apply:**\n \uFF08\u4F55\u65F6\u4F55\u5730\u751F\u6548\uFF09\u3002feedback \u7C7B\u578B\u7F3A\u8FD9\u4E24\u884C\u89C6\u4E3A\u4E0D\u5B8C\u6574\u3002</body_structure>\n</type>\n\n<type name=\"project\">\n <when_to_save>\u5F97\u77E5\u8FDB\u884C\u4E2D\u7684\u5DE5\u4F5C\u3001\u76EE\u6807\u3001\u51B3\u7B56\u3001\u622A\u6B62\u671F\u6216\u4E8B\u6545\u7684\u80CC\u666F\u52A8\u673A\u65F6\u2014\u2014\n \u51E1\u662F\"\u8C01\u5728\u505A\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u3001\u5230\u4EC0\u4E48\u65F6\u5019\"\u4E14\u65E0\u6CD5\u4ECE\u4EE3\u7801\u6216 git \u63A8\u5BFC\u7684\u4FE1\u606F\u3002\n \u8FD9\u7C7B\u8BB0\u5FC6\u8870\u51CF\u5FEB\uFF0C\u72B6\u6001\u53D8\u5316\u65F6\u66F4\u65B0\u5B83\uFF0C\u522B\u5806\u53E0\u65B0\u6761\u76EE\u3002</when_to_save>\n <how_to_use>\u7528\u5B83\u8865\u5168\u8BF7\u6C42\u80CC\u540E\u7684\u4E0A\u4E0B\u6587\u4E0E\u52A8\u673A\uFF0C\u8BA9\u5EFA\u8BAE\u8D34\u4F4F\u771F\u5B9E\u7EA6\u675F\u3002\n \u4F8B\uFF1A\"\u5468\u56DB\u8D77\u51BB\u7ED3\u5408\u5E76\"\u2014\u2014\u5B58\uFF1A2026-03-05 \u8D77\u51BB\u7ED3\u5408\u5E76\uFF0C\u975E\u5173\u952E PR \u63D0\u524D\u6807\u8BB0\u3002</how_to_use>\n <body_structure>\u4E8B\u5B9E/\u51B3\u7B56\u5148\u884C + **Why:**\uFF08\u52A8\u673A\uFF1A\u7EA6\u675F\u3001deadline\u3001\u5E72\u7CFB\u4EBA\u8981\u6C42\uFF09\n + **How to apply:**\uFF08\u8FD9\u6761\u5982\u4F55\u5F71\u54CD\u540E\u7EED\u5EFA\u8BAE\uFF09\u3002</body_structure>\n</type>\n\n<type name=\"reference\">\n <when_to_save>\u7528\u6237\u63D0\u5230\u672A\u6765\u4F1A\u7528\u5230\u7684\u5916\u90E8\u8D44\u6E90\u6307\u9488\uFF1AURL\u3001\u4EEA\u8868\u76D8\u3001\u5DE5\u5355\u3001\n \u6587\u6863\u4F4D\u7F6E\u3002</when_to_save>\n <how_to_use>\u9700\u8981\u65F6\u76F4\u63A5\u53D6\u7528\uFF0C\u4E0D\u91CD\u65B0\u68C0\u7D22\u3002</how_to_use>\n</type>\n\n## \u5982\u4F55\u7EF4\u62A4\n\n- \u66F4\u65B0\u4F18\u5148\u4E8E\u65B0\u5EFA\uFF1A\u72B6\u6001\u53D8\u4E86\u6539\u539F\u6761\u76EE\uFF0C\u4E0D\u53E6\u8D77\u4E00\u6761\uFF08\u65B0\u65E7\u5E76\u5B58\u6BD4\u6CA1\u6709\u8BB0\u5FC6\u66F4\u7CDF\uFF09\u3002\n- \u6BCF\u6761\u7684 description \u662F\u7ED9\u672A\u6765\u4F1A\u8BDD\u7684\u76F8\u5173\u6027 hook\u2014\u2014\u5199\"\u51B3\u5B9A\u4E86\u4E00\u6761\u8BB0\u5FC6\u4F1A\u4E0D\u4F1A\n \u88AB\u60F3\u8D77\"\u7684\u4E00\u53E5\u8BDD\uFF0C\u4E0D\u5199\u6D41\u6C34\u8D26\u3002\n- [[\u540D\u5B57]] \u4E92\u76F8\u5F15\u7528\u76F8\u5173\u8BB0\u5FC6\uFF1B\u5F15\u7528\u8FD8\u4E0D\u5B58\u5728\u7684\u540D\u5B57\u662F\u5408\u6CD5\u7684\uFF0C\u5B83\u6807\u8BB0\"\u503C\u5F97\u5C06\u6765\u8865\u5199\"\u3002";
|
|
19
|
+
//# sourceMappingURL=memory-guide.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The persistent-memory guide injected as part of the `evolve:memory-index`
|
|
3
|
+
* system-prompt section (evolve v2, change two).
|
|
4
|
+
*
|
|
5
|
+
* This text is the write-path half of the v2 plan: it teaches the model when a
|
|
6
|
+
* durable memory is worth creating, which recall type it belongs to, what body
|
|
7
|
+
* structure each type owes, and that the main session may write directly with
|
|
8
|
+
* `evolve_add` instead of waiting for the background pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Provenance: `docs/research/evolve-v2-passive-read-write-plan.md` §改动二
|
|
11
|
+
* (Chinese full text, adapted from ZCode's persistent-memory prompt). Kept in
|
|
12
|
+
* its own module so the wording is a single home — `memory-index.ts` only
|
|
13
|
+
* composes it with the `<memories>` block.
|
|
14
|
+
*/
|
|
15
|
+
/** Heading and framing before the `<memories>` block. */
|
|
16
|
+
export const MEMORY_GUIDE_INTRO = `# 持久记忆
|
|
17
|
+
|
|
18
|
+
你有一个跨会话的持久记忆系统。以下是当前记忆(已直接注入,无需查询即可使用;需要检索完整内容或确认是否有相关记忆时,调用 evolve_recall)。本会话内可以直接用 evolve_add 落盘,不必等后台流程——默认写 local,只有稳定的跨项目教训才写 global(global 写入会弹人工批准)。`;
|
|
19
|
+
/** The when_to_save / how_to_use rules and maintenance discipline. */
|
|
20
|
+
export const MEMORY_GUIDE_RULES = `## 何时写入记忆
|
|
21
|
+
|
|
22
|
+
用户明确要求记住时,立即存为最合适的类型;要求忘记时,找到并删除相关条目。
|
|
23
|
+
除此之外,在对话中自然遇到以下信号时主动写入。存之前先过一道筛子:
|
|
24
|
+
**能从代码、git 历史、仓库文件里重新推导出来的事实不存**——只存重推导不出来的
|
|
25
|
+
背景、决策和偏好。相对日期一律转成绝对日期("周四"→"2026-10-08"),否则未来
|
|
26
|
+
的会话无法解读。一条事实一个条目;不确定值不值得存时,问自己"这条的信息量
|
|
27
|
+
是否在'为什么'里,而不在'是什么'里"。
|
|
28
|
+
|
|
29
|
+
<type name="user">
|
|
30
|
+
<when_to_save>得知用户的角色、技术栈、经验水平、偏好或知识背景的任何细节时。
|
|
31
|
+
例如用户自述"我是数据科学家,在查日志系统"——存:用户是数据科学家,
|
|
32
|
+
当前关注可观测性/日志。</when_to_save>
|
|
33
|
+
<how_to_use>后续解释和建议按用户的背景定制深浅与类比;对资深工程师
|
|
34
|
+
不解释基础概念,对某领域的新手用其熟悉领域的类比切入。</how_to_use>
|
|
35
|
+
</type>
|
|
36
|
+
|
|
37
|
+
<type name="feedback">
|
|
38
|
+
<when_to_save>两条信号,缺一不可:
|
|
39
|
+
(1)纠正——用户说"不是这样""别""停下"时,存被纠正的做法与正确做法;
|
|
40
|
+
(2)确认——这是更安静、更容易漏的信号:用户接受了你不寻常的选择而没有异议,
|
|
41
|
+
或明确说"对,就这样""保持这样"。确认过的判断是已验证的方法论,只存纠错
|
|
42
|
+
会让你的行为越来越保守、偏离用户已认可的路。
|
|
43
|
+
例:"测试别 mock 数据库,上次 mock 过了线上迁移挂了"——存规则并带上事故原因。
|
|
44
|
+
例:"结尾不用总结,diff 我自己会看"——存:该用户不要尾随性总结。</when_to_save>
|
|
45
|
+
<how_to_use>让用户不必把同一条指导说第二遍。</how_to_use>
|
|
46
|
+
<body_structure>规则先行,随后必带两行:**Why:**(用户给的理由,常是某次
|
|
47
|
+
事故或强偏好——知道为什么才能判断边缘情况该不该破例)和 **How to apply:**
|
|
48
|
+
(何时何地生效)。feedback 类型缺这两行视为不完整。</body_structure>
|
|
49
|
+
</type>
|
|
50
|
+
|
|
51
|
+
<type name="project">
|
|
52
|
+
<when_to_save>得知进行中的工作、目标、决策、截止期或事故的背景动机时——
|
|
53
|
+
凡是"谁在做什么、为什么、到什么时候"且无法从代码或 git 推导的信息。
|
|
54
|
+
这类记忆衰减快,状态变化时更新它,别堆叠新条目。</when_to_save>
|
|
55
|
+
<how_to_use>用它补全请求背后的上下文与动机,让建议贴住真实约束。
|
|
56
|
+
例:"周四起冻结合并"——存:2026-03-05 起冻结合并,非关键 PR 提前标记。</how_to_use>
|
|
57
|
+
<body_structure>事实/决策先行 + **Why:**(动机:约束、deadline、干系人要求)
|
|
58
|
+
+ **How to apply:**(这条如何影响后续建议)。</body_structure>
|
|
59
|
+
</type>
|
|
60
|
+
|
|
61
|
+
<type name="reference">
|
|
62
|
+
<when_to_save>用户提到未来会用到的外部资源指针:URL、仪表盘、工单、
|
|
63
|
+
文档位置。</when_to_save>
|
|
64
|
+
<how_to_use>需要时直接取用,不重新检索。</how_to_use>
|
|
65
|
+
</type>
|
|
66
|
+
|
|
67
|
+
## 如何维护
|
|
68
|
+
|
|
69
|
+
- 更新优先于新建:状态变了改原条目,不另起一条(新旧并存比没有记忆更糟)。
|
|
70
|
+
- 每条的 description 是给未来会话的相关性 hook——写"决定了一条记忆会不会
|
|
71
|
+
被想起"的一句话,不写流水账。
|
|
72
|
+
- [[名字]] 互相引用相关记忆;引用还不存在的名字是合法的,它标记"值得将来补写"。`;
|
|
73
|
+
//# sourceMappingURL=memory-guide.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* evolve v2 — the passive read path: the `evolve:memory-index` system-prompt
|
|
3
|
+
* section that puts memory CONTENT in front of the model at session start.
|
|
4
|
+
*
|
|
5
|
+
* Why not just hand out a path: the memory store lives in `<dshHome>/evolve/`,
|
|
6
|
+
* outside the workspace sandbox, so the model's read tool cannot open it.
|
|
7
|
+
* The section therefore carries the entries themselves — full body while the
|
|
8
|
+
* character budget allows, then one-line index rows, then a `evolve_recall`
|
|
9
|
+
* pointer for the rest. Budget is hard: a bounded prompt beats an unbounded one.
|
|
10
|
+
*
|
|
11
|
+
* Session-freeze contract (the prompt-cache guard): the section is computed
|
|
12
|
+
* once per session and reused byte-for-byte afterwards, so the system prompt
|
|
13
|
+
* stays stable within a session and cache reads keep hitting. A memory written
|
|
14
|
+
* mid-session becomes visible in the NEXT session; `evolve_recall` covers the
|
|
15
|
+
* immediate need. See `.agents/notes/proposed/feature/2026-10-01-evolve-v2-passive-read-write.md`.
|
|
16
|
+
*/
|
|
17
|
+
import type { HarnessEntry } from "./types.js";
|
|
18
|
+
import { type MemoryRecallType } from "./types.js";
|
|
19
|
+
import type { EvolutionEngine } from "./service.js";
|
|
20
|
+
import { type AgentLike } from "./inject.js";
|
|
21
|
+
/** Hard character budget for the `<memories>` block. */
|
|
22
|
+
export declare const DEFAULT_MEMORY_INDEX_MAX_CHARS = 6000;
|
|
23
|
+
/** Prompt-section order of the memory index (empty 1–499 slot, see ADR). */
|
|
24
|
+
export declare const DEFAULT_MEMORY_SECTION_ORDER = 400;
|
|
25
|
+
/** Sessions whose frozen section text is kept before LRU eviction. */
|
|
26
|
+
export declare const DEFAULT_FROZEN_SESSIONS = 32;
|
|
27
|
+
/** Name of the injected section (unique — duplicate names throw upstream). */
|
|
28
|
+
export declare const MEMORY_SECTION_NAME = "evolve:memory-index";
|
|
29
|
+
/**
|
|
30
|
+
* Recall-type priority for the budget: durable, behavior-shaping types first.
|
|
31
|
+
* `project` (current work context) > `feedback` (how to behave) > `user` (who
|
|
32
|
+
* the user is) > `reference` (external pointers). Entries without a valid
|
|
33
|
+
* memoryType sink below every typed entry.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MEMORY_TYPE_PRIORITY: Record<MemoryRecallType, number>;
|
|
36
|
+
/** The entry's recall type, or undefined when the metadata key is absent/invalid. */
|
|
37
|
+
export declare function memoryTypeOf(entry: HarnessEntry): MemoryRecallType | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Order memories for injection: recall-type priority first
|
|
40
|
+
* ({@link MEMORY_TYPE_PRIORITY}), then recency (newest first), then the stable
|
|
41
|
+
* dictionary order — deterministic for equal inputs, so a frozen section is
|
|
42
|
+
* reproducible across builds. The input is never mutated.
|
|
43
|
+
*/
|
|
44
|
+
export declare function rankMemories(entries: readonly HarnessEntry[], now?: number): HarnessEntry[];
|
|
45
|
+
/** One memory rendered in full, as a list item with the body indented. */
|
|
46
|
+
export declare function memoryFullLine(entry: HarnessEntry): string;
|
|
47
|
+
/** What a budget-bounded memory block shows. */
|
|
48
|
+
export interface MemorySelection {
|
|
49
|
+
/** Entries rendered with their full body, in display order. */
|
|
50
|
+
full: HarnessEntry[];
|
|
51
|
+
/** Entries degraded to a one-line index row (`id` + title hook). */
|
|
52
|
+
indexed: HarnessEntry[];
|
|
53
|
+
/** Entries that fit neither and are only counted. */
|
|
54
|
+
dropped: number;
|
|
55
|
+
/** Entries actually shown (full + indexed), in display order. */
|
|
56
|
+
shown: HarnessEntry[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Greedy budget fill over {@link rankMemories}: each entry takes the full body
|
|
60
|
+
* while it fits, otherwise degrades to an index row, otherwise is dropped —
|
|
61
|
+
* and because the ranking is fixed, the first entry that fits neither means
|
|
62
|
+
* every later one is dropped too (their rows are no smaller).
|
|
63
|
+
*/
|
|
64
|
+
export declare function selectMemoryInjection(entries: readonly HarnessEntry[], maxChars?: number): MemorySelection;
|
|
65
|
+
/** The `evolve_recall` pointer appended when rows had to be degraded or dropped. */
|
|
66
|
+
export declare const MEMORY_OVERFLOW_HINT = "- \u66F4\u591A\u8BB0\u5FC6\u53EF\u7528 evolve_recall \u6309\u9700\u8BFB\u53D6";
|
|
67
|
+
/**
|
|
68
|
+
* Render a selection as the `<memories>` block, or "" when nothing is shown.
|
|
69
|
+
* Full bodies come first in display order, then the degraded index rows, then
|
|
70
|
+
* the `evolve_recall` pointer with the degradation/drop counts.
|
|
71
|
+
*/
|
|
72
|
+
export declare function renderMemorySelection(selection: MemorySelection): string;
|
|
73
|
+
/** {@link renderMemorySelection} over a fresh selection (convenience). */
|
|
74
|
+
export declare function formatMemoriesBlock(entries: readonly HarnessEntry[], maxChars?: number): string;
|
|
75
|
+
/** Options for {@link memoryIndexSectionText}. */
|
|
76
|
+
export interface MemoryIndexOptions {
|
|
77
|
+
/** Hard budget for the memories block (default 6000). */
|
|
78
|
+
maxChars?: number;
|
|
79
|
+
/** Include the when_to_save guide (default true). */
|
|
80
|
+
guide?: boolean;
|
|
81
|
+
/** Pin the project layer explicitly (tests/tools); otherwise derived from cwd. */
|
|
82
|
+
projectKey?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compose the full `evolve:memory-index` section for one assembling agent:
|
|
86
|
+
* the persistent-memory guide (when to save / how to maintain) wrapped around
|
|
87
|
+
* the budget-bounded memories block. With the guide off and no memories the
|
|
88
|
+
* result is "" — the prompt renderer then drops the section, so an empty store
|
|
89
|
+
* keeps costing zero tokens.
|
|
90
|
+
*
|
|
91
|
+
* Injection usage is recorded for everything shown (best-effort; a failure
|
|
92
|
+
* never blocks injection), deduped per session by {@link recordInjection}.
|
|
93
|
+
*/
|
|
94
|
+
export declare function memoryIndexSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, opts?: MemoryIndexOptions): string;
|
|
95
|
+
/** A per-session frozen view of the memory section text. */
|
|
96
|
+
export interface FrozenMemorySection {
|
|
97
|
+
/**
|
|
98
|
+
* The section text for this agent: built on first use per `agent.id` and
|
|
99
|
+
* reused byte-for-byte afterwards. `undefined` agent → "" (unbuilt).
|
|
100
|
+
*/
|
|
101
|
+
textFor(agent: AgentLike | undefined, build: (agent: AgentLike) => string): string;
|
|
102
|
+
/** Sessions currently frozen (test/observability hook). */
|
|
103
|
+
size(): number;
|
|
104
|
+
/** Drop every frozen session (tests; a restart rebuilds naturally). */
|
|
105
|
+
clear(): void;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Freeze the section text per session id. The frozen value is what keeps the
|
|
109
|
+
* system prompt stable across a session's assemblies (prompt-cache guard);
|
|
110
|
+
* bounded by `maxSessions` with insertion-order (LRU) eviction, so a long-lived
|
|
111
|
+
* process cannot grow without limit.
|
|
112
|
+
*/
|
|
113
|
+
export declare function createFrozenMemorySection(maxSessions?: number): FrozenMemorySection;
|
|
114
|
+
//# sourceMappingURL=memory-index.d.ts.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { MEMORY_TYPE_KEY, isArchived, isMemoryType } from "./types.js";
|
|
2
|
+
import { recordInjection } from "./usage.js";
|
|
3
|
+
import { MEMORY_GUIDE_INTRO, MEMORY_GUIDE_RULES } from "./memory-guide.js";
|
|
4
|
+
import { directoryLine, mergedInjectionState, recencyScore } from "./inject.js";
|
|
5
|
+
/** Hard character budget for the `<memories>` block. */
|
|
6
|
+
export const DEFAULT_MEMORY_INDEX_MAX_CHARS = 6000;
|
|
7
|
+
/** Prompt-section order of the memory index (empty 1–499 slot, see ADR). */
|
|
8
|
+
export const DEFAULT_MEMORY_SECTION_ORDER = 400;
|
|
9
|
+
/** Sessions whose frozen section text is kept before LRU eviction. */
|
|
10
|
+
export const DEFAULT_FROZEN_SESSIONS = 32;
|
|
11
|
+
/** Name of the injected section (unique — duplicate names throw upstream). */
|
|
12
|
+
export const MEMORY_SECTION_NAME = "evolve:memory-index";
|
|
13
|
+
/**
|
|
14
|
+
* Recall-type priority for the budget: durable, behavior-shaping types first.
|
|
15
|
+
* `project` (current work context) > `feedback` (how to behave) > `user` (who
|
|
16
|
+
* the user is) > `reference` (external pointers). Entries without a valid
|
|
17
|
+
* memoryType sink below every typed entry.
|
|
18
|
+
*/
|
|
19
|
+
export const MEMORY_TYPE_PRIORITY = {
|
|
20
|
+
project: 0,
|
|
21
|
+
feedback: 1,
|
|
22
|
+
user: 2,
|
|
23
|
+
reference: 3,
|
|
24
|
+
};
|
|
25
|
+
/** Priority of an entry whose metadata carries no valid recall type. */
|
|
26
|
+
const UNTYPED_PRIORITY = 4;
|
|
27
|
+
/** The entry's recall type, or undefined when the metadata key is absent/invalid. */
|
|
28
|
+
export function memoryTypeOf(entry) {
|
|
29
|
+
const value = entry.metadata[MEMORY_TYPE_KEY];
|
|
30
|
+
return isMemoryType(value) ? value : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Deterministic final tiebreak (matches `inject.ts` ranking). */
|
|
33
|
+
function stableCompare(a, b) {
|
|
34
|
+
return [a.path, a.title, a.id].join("\0").localeCompare([b.path, b.title, b.id].join("\0"));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Order memories for injection: recall-type priority first
|
|
38
|
+
* ({@link MEMORY_TYPE_PRIORITY}), then recency (newest first), then the stable
|
|
39
|
+
* dictionary order — deterministic for equal inputs, so a frozen section is
|
|
40
|
+
* reproducible across builds. The input is never mutated.
|
|
41
|
+
*/
|
|
42
|
+
export function rankMemories(entries, now = Date.now()) {
|
|
43
|
+
return [...entries].sort((a, b) => {
|
|
44
|
+
const aType = memoryTypeOf(a);
|
|
45
|
+
const bType = memoryTypeOf(b);
|
|
46
|
+
const priorityDelta = (aType ? MEMORY_TYPE_PRIORITY[aType] : UNTYPED_PRIORITY) - (bType ? MEMORY_TYPE_PRIORITY[bType] : UNTYPED_PRIORITY);
|
|
47
|
+
if (priorityDelta !== 0) {
|
|
48
|
+
return priorityDelta;
|
|
49
|
+
}
|
|
50
|
+
const recencyDelta = recencyScore(b, now) - recencyScore(a, now);
|
|
51
|
+
if (recencyDelta !== 0) {
|
|
52
|
+
return recencyDelta;
|
|
53
|
+
}
|
|
54
|
+
return stableCompare(a, b);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** One memory rendered in full, as a list item with the body indented. */
|
|
58
|
+
export function memoryFullLine(entry) {
|
|
59
|
+
const type = memoryTypeOf(entry);
|
|
60
|
+
const body = entry.content.trim().replace(/\n/g, "\n ");
|
|
61
|
+
const head = `- [memory${type ? `:${type}` : ""}:${entry.id}] ${entry.title}`;
|
|
62
|
+
return body.length > 0 ? `${head}\n ${body}` : head;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Greedy budget fill over {@link rankMemories}: each entry takes the full body
|
|
66
|
+
* while it fits, otherwise degrades to an index row, otherwise is dropped —
|
|
67
|
+
* and because the ranking is fixed, the first entry that fits neither means
|
|
68
|
+
* every later one is dropped too (their rows are no smaller).
|
|
69
|
+
*/
|
|
70
|
+
export function selectMemoryInjection(entries, maxChars = DEFAULT_MEMORY_INDEX_MAX_CHARS) {
|
|
71
|
+
const visible = entries.filter((entry) => !isArchived(entry));
|
|
72
|
+
const ranked = rankMemories(visible);
|
|
73
|
+
const budget = Math.max(0, maxChars);
|
|
74
|
+
const full = [];
|
|
75
|
+
const indexed = [];
|
|
76
|
+
const shown = [];
|
|
77
|
+
let used = 0;
|
|
78
|
+
let dropped = 0;
|
|
79
|
+
for (const entry of ranked) {
|
|
80
|
+
const fullLine = memoryFullLine(entry);
|
|
81
|
+
if (used + fullLine.length <= budget) {
|
|
82
|
+
full.push(entry);
|
|
83
|
+
shown.push(entry);
|
|
84
|
+
used += fullLine.length;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const indexLine = directoryLine(entry);
|
|
88
|
+
if (used + indexLine.length <= budget) {
|
|
89
|
+
indexed.push(entry);
|
|
90
|
+
shown.push(entry);
|
|
91
|
+
used += indexLine.length;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
dropped = ranked.length - shown.length;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
return { full, indexed, dropped, shown };
|
|
98
|
+
}
|
|
99
|
+
/** The `evolve_recall` pointer appended when rows had to be degraded or dropped. */
|
|
100
|
+
export const MEMORY_OVERFLOW_HINT = "- 更多记忆可用 evolve_recall 按需读取";
|
|
101
|
+
/**
|
|
102
|
+
* Render a selection as the `<memories>` block, or "" when nothing is shown.
|
|
103
|
+
* Full bodies come first in display order, then the degraded index rows, then
|
|
104
|
+
* the `evolve_recall` pointer with the degradation/drop counts.
|
|
105
|
+
*/
|
|
106
|
+
export function renderMemorySelection(selection) {
|
|
107
|
+
const { full, indexed, dropped, shown } = selection;
|
|
108
|
+
if (shown.length === 0) {
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
const lines = [];
|
|
112
|
+
for (const entry of full) {
|
|
113
|
+
lines.push(memoryFullLine(entry));
|
|
114
|
+
}
|
|
115
|
+
for (const entry of indexed) {
|
|
116
|
+
lines.push(directoryLine(entry));
|
|
117
|
+
}
|
|
118
|
+
const omitted = [];
|
|
119
|
+
if (indexed.length > 0) {
|
|
120
|
+
omitted.push(`${indexed.length} 条仅列索引`);
|
|
121
|
+
}
|
|
122
|
+
if (dropped > 0) {
|
|
123
|
+
omitted.push(`${dropped} 条未列出`);
|
|
124
|
+
}
|
|
125
|
+
lines.push(omitted.length > 0 ? `${MEMORY_OVERFLOW_HINT}(${omitted.join(",")})` : MEMORY_OVERFLOW_HINT);
|
|
126
|
+
return `<memories>\n${lines.join("\n")}\n</memories>`;
|
|
127
|
+
}
|
|
128
|
+
/** {@link renderMemorySelection} over a fresh selection (convenience). */
|
|
129
|
+
export function formatMemoriesBlock(entries, maxChars = DEFAULT_MEMORY_INDEX_MAX_CHARS) {
|
|
130
|
+
return renderMemorySelection(selectMemoryInjection(entries, maxChars));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Compose the full `evolve:memory-index` section for one assembling agent:
|
|
134
|
+
* the persistent-memory guide (when to save / how to maintain) wrapped around
|
|
135
|
+
* the budget-bounded memories block. With the guide off and no memories the
|
|
136
|
+
* result is "" — the prompt renderer then drops the section, so an empty store
|
|
137
|
+
* keeps costing zero tokens.
|
|
138
|
+
*
|
|
139
|
+
* Injection usage is recorded for everything shown (best-effort; a failure
|
|
140
|
+
* never blocks injection), deduped per session by {@link recordInjection}.
|
|
141
|
+
*/
|
|
142
|
+
export function memoryIndexSectionText(engine, agent, opts) {
|
|
143
|
+
if (!agent) {
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
const guide = opts?.guide ?? true;
|
|
147
|
+
const maxChars = opts?.maxChars ?? DEFAULT_MEMORY_INDEX_MAX_CHARS;
|
|
148
|
+
const merged = mergedInjectionState(engine, agent, opts);
|
|
149
|
+
const selection = selectMemoryInjection(Object.values(merged.entries.memory), maxChars);
|
|
150
|
+
const block = renderMemorySelection(selection);
|
|
151
|
+
if (selection.shown.length > 0) {
|
|
152
|
+
try {
|
|
153
|
+
recordInjection(engine.baseDir, selection.shown.map((entry) => `${entry.kind}:${entry.id}`), agent.id);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Usage recording is diagnostic; never interrupt the injection path.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!guide) {
|
|
160
|
+
return block;
|
|
161
|
+
}
|
|
162
|
+
return [MEMORY_GUIDE_INTRO, block.length > 0 ? block : "<memories>\n(暂无记忆)\n</memories>", MEMORY_GUIDE_RULES].join("\n\n");
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Freeze the section text per session id. The frozen value is what keeps the
|
|
166
|
+
* system prompt stable across a session's assemblies (prompt-cache guard);
|
|
167
|
+
* bounded by `maxSessions` with insertion-order (LRU) eviction, so a long-lived
|
|
168
|
+
* process cannot grow without limit.
|
|
169
|
+
*/
|
|
170
|
+
export function createFrozenMemorySection(maxSessions = DEFAULT_FROZEN_SESSIONS) {
|
|
171
|
+
const limit = Math.max(1, maxSessions);
|
|
172
|
+
const frozen = new Map();
|
|
173
|
+
return {
|
|
174
|
+
textFor(agent, build) {
|
|
175
|
+
if (!agent) {
|
|
176
|
+
return "";
|
|
177
|
+
}
|
|
178
|
+
const cached = frozen.get(agent.id);
|
|
179
|
+
if (cached !== undefined) {
|
|
180
|
+
return cached;
|
|
181
|
+
}
|
|
182
|
+
const text = build(agent);
|
|
183
|
+
frozen.set(agent.id, text);
|
|
184
|
+
while (frozen.size > limit) {
|
|
185
|
+
const oldest = frozen.keys().next();
|
|
186
|
+
if (oldest.done === true) {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
frozen.delete(oldest.value);
|
|
190
|
+
}
|
|
191
|
+
return text;
|
|
192
|
+
},
|
|
193
|
+
size: () => frozen.size,
|
|
194
|
+
clear: () => frozen.clear(),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=memory-index.js.map
|
package/lib/record-language.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ export declare function detectLanguageFromText(text: string): RecordLanguage | u
|
|
|
39
39
|
* service. Fail-open by contract: a missing service, a throwing read, an
|
|
40
40
|
* async-shaped reply, or an unknown value all yield `undefined`.
|
|
41
41
|
*
|
|
42
|
+
* The service is resolved through `ctx.get("settings")` (the global registry),
|
|
43
|
+
* which sees services provided by sibling plugin entries. A direct
|
|
44
|
+
* `ctx.settings` walks only the caller's fiber ancestor chain and throws
|
|
45
|
+
* "cannot get property \"settings\" without inject" — the same failure mode
|
|
46
|
+
* documented in `goal.ts` — and a throw here would abort the whole snapshot
|
|
47
|
+
* gate, so the property access itself is guarded. Plain-object contexts
|
|
48
|
+
* (tests, non-Cordis hosts) fall back to the direct property.
|
|
49
|
+
*
|
|
42
50
|
* @param ctx Cordis context that may carry the host settings service.
|
|
43
51
|
* @returns The stored preference (`zh`/`en`, subtag-tolerant) or `undefined`.
|
|
44
52
|
*/
|
package/lib/record-language.js
CHANGED
|
@@ -44,13 +44,30 @@ export function detectLanguageFromText(text) {
|
|
|
44
44
|
* service. Fail-open by contract: a missing service, a throwing read, an
|
|
45
45
|
* async-shaped reply, or an unknown value all yield `undefined`.
|
|
46
46
|
*
|
|
47
|
+
* The service is resolved through `ctx.get("settings")` (the global registry),
|
|
48
|
+
* which sees services provided by sibling plugin entries. A direct
|
|
49
|
+
* `ctx.settings` walks only the caller's fiber ancestor chain and throws
|
|
50
|
+
* "cannot get property \"settings\" without inject" — the same failure mode
|
|
51
|
+
* documented in `goal.ts` — and a throw here would abort the whole snapshot
|
|
52
|
+
* gate, so the property access itself is guarded. Plain-object contexts
|
|
53
|
+
* (tests, non-Cordis hosts) fall back to the direct property.
|
|
54
|
+
*
|
|
47
55
|
* @param ctx Cordis context that may carry the host settings service.
|
|
48
56
|
* @returns The stored preference (`zh`/`en`, subtag-tolerant) or `undefined`.
|
|
49
57
|
*/
|
|
50
58
|
export function durableLocalePreference(ctx) {
|
|
51
59
|
if (typeof ctx !== "object" || ctx === null)
|
|
52
60
|
return undefined;
|
|
53
|
-
|
|
61
|
+
let settings;
|
|
62
|
+
try {
|
|
63
|
+
const get = ctx.get;
|
|
64
|
+
settings = typeof get === "function" ? get.call(ctx, "settings") : undefined;
|
|
65
|
+
if (settings === undefined)
|
|
66
|
+
settings = ctx.settings;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
54
71
|
if (typeof settings !== "object" || settings === null)
|
|
55
72
|
return undefined;
|
|
56
73
|
const describe = settings.describe;
|
package/lib/tool.js
CHANGED
|
@@ -83,12 +83,12 @@ function textResult(text) {
|
|
|
83
83
|
export function registerEvolveTools(ctx, engine, opts) {
|
|
84
84
|
ctx.tools.register(defineTool({
|
|
85
85
|
name: "evolve_list",
|
|
86
|
-
description: "List the
|
|
86
|
+
description: "List the harness state (prompt notes, memories, skills, subagent specs) for one store.",
|
|
87
87
|
parameters: {
|
|
88
88
|
scope: {
|
|
89
89
|
type: "string",
|
|
90
90
|
enum: SCOPES,
|
|
91
|
-
description: "
|
|
91
|
+
description: "'local' (default), 'project', or 'global'.",
|
|
92
92
|
},
|
|
93
93
|
},
|
|
94
94
|
output: {
|
|
@@ -118,25 +118,25 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
118
118
|
}));
|
|
119
119
|
ctx.tools.register(defineTool({
|
|
120
120
|
name: "evolve_recall",
|
|
121
|
-
description: "
|
|
121
|
+
description: "Read-only targeted recall across the harness stores: filter by query, kind, scope, and memory type; returns full content with version, source, and staleness signals. Prefer this over evolve_list when looking for something specific.",
|
|
122
122
|
parameters: {
|
|
123
|
-
query: { type: "string", description: "Free-text relevance query. Empty
|
|
123
|
+
query: { type: "string", description: "Free-text relevance query. Empty → most-recently-updated first." },
|
|
124
124
|
kinds: {
|
|
125
125
|
type: "array",
|
|
126
126
|
items: { type: "string" },
|
|
127
|
-
description: "Entry kinds
|
|
127
|
+
description: "Entry kinds (default memory only).",
|
|
128
128
|
},
|
|
129
129
|
scopes: {
|
|
130
130
|
type: "array",
|
|
131
131
|
items: { type: "string" },
|
|
132
|
-
description: "Stores to search (default
|
|
132
|
+
description: "Stores to search (default all three).",
|
|
133
133
|
},
|
|
134
134
|
memoryTypes: {
|
|
135
135
|
type: "array",
|
|
136
136
|
items: { type: "string" },
|
|
137
|
-
description: "Memory
|
|
137
|
+
description: "Memory types: user, feedback, project, reference.",
|
|
138
138
|
},
|
|
139
|
-
limit: { type: "number", description: "
|
|
139
|
+
limit: { type: "number", description: "Max hits (default 10, max 50)." },
|
|
140
140
|
includeArchived: { type: "boolean", description: "Include archived entries (default false)." },
|
|
141
141
|
},
|
|
142
142
|
output: {
|
|
@@ -157,18 +157,18 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
157
157
|
}));
|
|
158
158
|
ctx.tools.register(defineTool({
|
|
159
159
|
name: "evolve_add",
|
|
160
|
-
description: "Create one harness entry (prompt/memory/skill/subagent).
|
|
160
|
+
description: "Create one harness entry (prompt/memory/skill/subagent). Snapshots, versioning, and audit are automatic.",
|
|
161
161
|
parameters: {
|
|
162
162
|
kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true, description: "Entry kind." },
|
|
163
163
|
title: { type: "string", required: true, description: "Stable title." },
|
|
164
164
|
content: { type: "string", required: true, description: "Entry body." },
|
|
165
165
|
path: { type: "string", description: "Optional grouping path." },
|
|
166
|
-
skill_kind: { type: "string", enum: ["executable", "guidance"], description: "
|
|
167
|
-
reference: { type: "object", additionalProperties: true, description: "
|
|
168
|
-
arguments: { type: "object", additionalProperties: true, description: "
|
|
169
|
-
memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Required for memory
|
|
170
|
-
scope: { type: "string", enum: SCOPES, description: "
|
|
171
|
-
global: { type: "boolean", description: "
|
|
166
|
+
skill_kind: { type: "string", enum: ["executable", "guidance"], description: "Skills: executable (default, needs reference) or guidance (SKILL.md, no reference)." },
|
|
167
|
+
reference: { type: "object", additionalProperties: true, description: "Executable skills: {type:'python', import, callable}." },
|
|
168
|
+
arguments: { type: "object", additionalProperties: true, description: "Executable skills: accepted input contract." },
|
|
169
|
+
memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Required for memory (feedback/project need Why + How to apply)." },
|
|
170
|
+
scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
|
|
171
|
+
global: { type: "boolean", description: "Shorthand for scope='global' (requires human approval)." },
|
|
172
172
|
},
|
|
173
173
|
output: {
|
|
174
174
|
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
|
@@ -216,9 +216,9 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
216
216
|
id: { type: "string", required: true, description: "Existing entry id." },
|
|
217
217
|
title: { type: "string" },
|
|
218
218
|
content: { type: "string" },
|
|
219
|
-
memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "
|
|
220
|
-
scope: { type: "string", enum: SCOPES, description: "
|
|
221
|
-
global: { type: "boolean", description: "
|
|
219
|
+
memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Memory: (re)classify the recall type." },
|
|
220
|
+
scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
|
|
221
|
+
global: { type: "boolean", description: "Shorthand for scope='global' (requires human approval)." },
|
|
222
222
|
},
|
|
223
223
|
output: {
|
|
224
224
|
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
|
@@ -242,17 +242,17 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
242
242
|
}));
|
|
243
243
|
ctx.tools.register(defineTool({
|
|
244
244
|
name: "evolve_delete",
|
|
245
|
-
description: "Delete harness entries
|
|
245
|
+
description: "Delete harness entries: `id` for one, `ids` for a batch (one refinement, one approval). Ids are addressed verbatim, scope-prefixed or not.",
|
|
246
246
|
parameters: {
|
|
247
247
|
kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
|
|
248
|
-
id: { type: "string", description: "
|
|
248
|
+
id: { type: "string", description: "One entry id (or use ids for a batch)." },
|
|
249
249
|
ids: {
|
|
250
250
|
type: "array",
|
|
251
251
|
items: { type: "string" },
|
|
252
|
-
description: "
|
|
252
|
+
description: "Entry ids for one batch delete.",
|
|
253
253
|
},
|
|
254
|
-
scope: { type: "string", enum: SCOPES, description: "
|
|
255
|
-
global: { type: "boolean", description: "
|
|
254
|
+
scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
|
|
255
|
+
global: { type: "boolean", description: "Shorthand for scope='global' (requires human approval)." },
|
|
256
256
|
},
|
|
257
257
|
output: {
|
|
258
258
|
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
|
@@ -271,11 +271,11 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
271
271
|
}));
|
|
272
272
|
ctx.tools.register(defineTool({
|
|
273
273
|
name: "evolve_rollback",
|
|
274
|
-
description: "Deterministically revert a previous refinement by its id (from evolve_list history or
|
|
274
|
+
description: "Deterministically revert a previous refinement by its id (from evolve_list history or /evolve).",
|
|
275
275
|
parameters: {
|
|
276
|
-
refinementId: { type: "string", required: true, description: "
|
|
277
|
-
scope: { type: "string", enum: SCOPES, description: "
|
|
278
|
-
global: { type: "boolean", description: "
|
|
276
|
+
refinementId: { type: "string", required: true, description: "Refinement id to roll back." },
|
|
277
|
+
scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
|
|
278
|
+
global: { type: "boolean", description: "Shorthand for scope='global'." },
|
|
279
279
|
},
|
|
280
280
|
output: {
|
|
281
281
|
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
4
4
|
"description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"lint": "oxlint src test",
|
|
57
57
|
"clean": "rm -rf lib",
|
|
58
58
|
"check:docs": "tsx scripts/run-gates.ts docs",
|
|
59
|
+
"check:handoff": "tsx scripts/verify-handoff-structure.ts --self-test && tsx scripts/verify-handoff-structure.ts",
|
|
59
60
|
"check:governance": "tsx scripts/verify-governance.ts"
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|