@modusensus/dsh-mneme 0.8.4 → 0.8.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.
Files changed (69) hide show
  1. package/README.md +16 -7
  2. package/bin/dsh-mneme-mcp.mjs +11 -3
  3. package/lib/api-standalone.js +11 -0
  4. package/lib/api.js +21 -0
  5. package/lib/client.js +213 -31
  6. package/lib/config.js +82 -0
  7. package/lib/document.js +221 -0
  8. package/lib/dream/sleep.js +6 -4
  9. package/lib/dream.js +1347 -1319
  10. package/lib/guide.js +44 -0
  11. package/lib/heat.js +2 -0
  12. package/lib/inject.js +91 -15
  13. package/lib/lang.js +6 -0
  14. package/lib/organize.js +450 -0
  15. package/lib/recall-stats.js +23 -1
  16. package/lib/runtime/layout.js +30 -0
  17. package/lib/runtime/loader.js +4 -1
  18. package/lib/runtime/verify.js +19 -6
  19. package/lib/search/adaptive.js +38 -0
  20. package/lib/service.js +165 -12
  21. package/lib/settings.js +31 -2
  22. package/lib/store.js +109 -8
  23. package/lib/summarize.js +253 -8
  24. package/lib/tools.js +136 -8
  25. package/package.json +6 -6
  26. package/runtime-manifest.json +104 -174
  27. package/scripts/build-runtime-manifest.mjs +8 -1
  28. package/scripts/sync-test-badge.mjs +18 -12
  29. package/src/api-standalone.js +11 -0
  30. package/src/api.js +21 -0
  31. package/src/config.js +82 -0
  32. package/src/document.js +221 -0
  33. package/src/dream/sleep.js +6 -4
  34. package/src/dream.js +1347 -1319
  35. package/src/guide.js +44 -0
  36. package/src/heat.js +2 -0
  37. package/src/inject.js +91 -15
  38. package/src/lang.js +6 -0
  39. package/src/organize.js +450 -0
  40. package/src/recall-stats.js +23 -1
  41. package/src/runtime/layout.js +30 -0
  42. package/src/runtime/loader.js +4 -1
  43. package/src/runtime/verify.js +19 -6
  44. package/src/search/adaptive.js +38 -0
  45. package/src/service.js +165 -12
  46. package/src/settings.js +31 -2
  47. package/src/store.js +109 -8
  48. package/src/summarize.js +253 -8
  49. package/src/tools.js +136 -8
  50. package/test/adaptive-inject-budget.test.js +85 -0
  51. package/test/api.test.js +65 -3
  52. package/test/client.test.js +63 -2
  53. package/test/document.test.js +523 -0
  54. package/test/dream.test.js +51 -0
  55. package/test/inject-content-cap.test.js +5 -3
  56. package/test/inject-pools.test.js +170 -0
  57. package/test/inject-recall.test.js +105 -0
  58. package/test/inject.test.js +35 -1
  59. package/test/llm-audit.test.js +153 -0
  60. package/test/organize.test.js +418 -0
  61. package/test/recall-stats.test.js +7 -1
  62. package/test/runtime-loader.test.js +18 -0
  63. package/test/runtime-manifest.test.js +2 -1
  64. package/test/runtime-verify.test.js +65 -0
  65. package/test/sleep.test.js +37 -0
  66. package/test/store.test.js +13 -0
  67. package/test/summarize.test.js +407 -3
  68. package/test/tools-dto-schema-matrix.test.js +325 -0
  69. package/test/tools.test.js +36 -3
@@ -0,0 +1,325 @@
1
+ // issue #195:全工具矩阵的「DTO 键集 ⊆ output schema」系统性断言——防 #184 类漂移。
2
+ //
3
+ // 背景:#184 是 memory_get 的内联 output schema 漏声明了 v0.8.1 新增的 scope 来源
4
+ // 三键,而 schema 是 `additionalProperties: false`——toApiList 条件展开的键一旦没被
5
+ // 声明,宿主 in-process 校验直接拒收("invalid output"),表现为「个别 id 必失败、
6
+ // 多数正常」。单点回归(tools-memory-get-schema.test.js)只护住了 memory_get 一个
7
+ // 工具:换一个工具、换一个键,同类事故可以原样重演。
8
+ //
9
+ // 这里把护栏提到矩阵层,四层断言各管一段:
10
+ // A 实跑:9 个工具每个可安全触达的分支,拿**真实 execute 的返回值**过生产同款校验器
11
+ // (validateJsonSchemaValue)——不写手抄的期望值,让运行时自己说话;
12
+ // B 产地:DTO 的唯一产地 service.toApiList 在全形态数据下的输出 ⊆ MEMORY_ITEM_SCHEMA,
13
+ // 并反向要求声明里的每个键都被至少一种形态真实产出(声明了却永不出现的键=死声明,
14
+ // 会在下一次 schema 增键时立刻暴露);
15
+ // C 结构:每个工具的 schema 必须闭合(additionalProperties:false)、required ⊆ properties、
16
+ // 每项都带 type——否则 A/B 两层会因为校验器形同虚设而静默失效;
17
+ // D 负例锁:故意注入一个未声明键,断言校验器必须报错。没有这一层,上面三层可以一起
18
+ // 绿着却毫无保护力(校验器被换掉、schema 被放开都会静默通过)。
19
+ import test from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
25
+ import { createStore } from "../src/store.js";
26
+ import { createService } from "../src/service.js";
27
+ import { createTools, MEMORY_ITEM_SCHEMA } from "../src/tools.js";
28
+
29
+ // 矩阵清单:新增工具必须同时补进本文件的分支表,否则下面第一条断言会先失败提醒
30
+ // ——这是「系统性」的含义:漏一个工具不会静默通过。
31
+ const TOOL_NAMES = [
32
+ "memory_save",
33
+ "memory_search",
34
+ "memory_list",
35
+ "memory_get",
36
+ "memory_update",
37
+ "memory_delete",
38
+ "memory_forget",
39
+ "memory_archive",
40
+ "memory_register_document",
41
+ "memory_runtime"
42
+ ];
43
+
44
+ function setup(config = {}) {
45
+ const store = createStore(":memory:");
46
+ // config 同时喂 service 与 tools:#230 起 registerDocument 的闸在 service
47
+ // 侧读同一个 config,只喂 tools 会让注册工具在矩阵里永远抛 disabled。
48
+ const service = createService({ store, mirror: null, config });
49
+ const registered = [];
50
+ const ctx = {
51
+ tools: {
52
+ register(def) {
53
+ registered.push(def);
54
+ return () => {};
55
+ }
56
+ }
57
+ };
58
+ createTools(ctx, service, config, undefined);
59
+ const byName = Object.fromEntries(registered.map((def) => [def.name, def]));
60
+ return { store, service, byName };
61
+ }
62
+
63
+ // 驱动一个工具并立刻用它自己声明的 schema 校验真实返回值。
64
+ // 返回 { value }:调用方需要时再对值做语义断言。
65
+ async function drive(byName, name, args = [], exec) {
66
+ const tool = byName[name];
67
+ assert.ok(tool, `工具 ${name} 未注册`);
68
+ const value = await tool.execute(args, exec);
69
+ assertMatches(byName, name, value, `args=${JSON.stringify(args)}`);
70
+ return value;
71
+ }
72
+
73
+ // 真实返回值必须被声明 schema 完全覆盖;失败时把违规明细一起抛出,定位更快。
74
+ function assertMatches(byName, name, value, label) {
75
+ const errors = validateJsonSchemaValue(byName[name].output.schema, value);
76
+ assert.deepEqual(
77
+ errors,
78
+ [],
79
+ `${name} 的真实返回未被 output schema 覆盖(${label}):${JSON.stringify(errors)}\n返回键:${JSON.stringify(Object.keys(value ?? {}))}`
80
+ );
81
+ }
82
+
83
+ // 全形态种子:toApiList 的 7 个条件键只在对应列非 undefined 时展开,所以每种形态
84
+ // 都要真实落一行,否则 B 层会(正确地)报「声明了却从未产出」。
85
+ function seedShapes(store) {
86
+ const rows = [];
87
+ const push = (over) => {
88
+ const memory = {
89
+ type: "project",
90
+ title: `shape-${rows.length}`,
91
+ content: `形态 ${rows.length} 的正文`,
92
+ source: "test",
93
+ ...over
94
+ };
95
+ // #230:document 走存储层唯一铸造口(通用 save 拒绝 document)。
96
+ rows.push(memory.type === "document" ? store.saveDocument(memory) : store.save(memory));
97
+ };
98
+ push({}); // 极简形态:只有基础键
99
+ push({ sensitivity: "personal" });
100
+ push({ occurred_at: "2026-09-15T00:00:00Z" });
101
+ push({ agent_scope: "coder", workspace_scope: "proj-alpha" });
102
+ push({
103
+ // 全量标注:#184 的原始触发形态
104
+ agent_scope: "coder",
105
+ agent_scope_source: "auto",
106
+ workspace_scope: "proj-alpha",
107
+ workspace_scope_source: "explicit",
108
+ scope_decided_at: "2026-09-15T00:00:00Z",
109
+ sensitivity: "personal",
110
+ occurred_at: "2026-09-14T12:00:00Z",
111
+ tags: ["shape", "annotated"],
112
+ importance: 5
113
+ });
114
+ // #230:document 指针行——doc_path 是 toApiList 的条件键,必须有一行真的产出它。
115
+ push({ type: "document", title: "shape-doc", content: "指针行摘要", doc_path: "X:\\seed\\doc.md", evidence: [] });
116
+ return rows;
117
+ }
118
+
119
+ test("工具矩阵与 TOOL_NAMES 清单一致(新增工具必须补进矩阵)", () => {
120
+ const { byName } = setup();
121
+ assert.deepEqual(Object.keys(byName).sort(), [...TOOL_NAMES].sort());
122
+ });
123
+
124
+ test("A 实跑:10 个工具每个可安全触达的分支,真实返回 ⊆ 声明的 output schema", async () => {
125
+ const { store, service, byName } = setup({ documentMemoryEnabled: true });
126
+ const shapes = seedShapes(store);
127
+ const annotated = shapes[shapes.length - 1];
128
+ const plain = shapes[0];
129
+ const driven = new Set();
130
+
131
+ const note = (name) => driven.add(name);
132
+
133
+ // --- memory_save:created 与 merged 两条分支 ---
134
+ await drive(byName, "memory_save", { type: "project", title: "save-new", content: "首次写入" });
135
+ note("memory_save");
136
+ await drive(byName, "memory_save", { type: "project", title: "save-new", content: "同键再写 → merged" });
137
+ // 显式 scope + 敏感度 + 事件时间:把「条件展开的键」真的写进库(#184 的真实入口)
138
+ const scopedSave = await drive(byName, "memory_save", {
139
+ type: "preference",
140
+ title: "save-scoped",
141
+ content: "带显式 scope 的写入",
142
+ agent_scope: "global",
143
+ workspace_scope: "proj-beta",
144
+ sensitivity: "personal",
145
+ occurred_at: "2026-09-16T00:00:00Z"
146
+ });
147
+ assert.ok(scopedSave.id, "memory_save 必须回传 id");
148
+
149
+ // --- memory_search:命中 / 空结果 / 显式 keyword 档 ---
150
+ const hit = await drive(byName, "memory_search", { query: "形态" });
151
+ assert.ok(hit.items.length > 0, "关键词检索应当命中种子行");
152
+ await drive(byName, "memory_search", { query: "绝不存在的检索词-195" });
153
+ await drive(byName, "memory_search", { query: "形态", mode: "keyword", limit: 2 });
154
+ // occurred 时间窗分支(tools.js 的 occurredWindow 接线)
155
+ await drive(byName, "memory_search", { query: "形态", occurred_from: "2026-01-01", occurred_to: "2026-12-31" });
156
+ note("memory_search");
157
+
158
+ // --- memory_list:默认 / 类型过滤 / 含归档 / 分页 / occurred 窗口 ---
159
+ await drive(byName, "memory_list", {});
160
+ await drive(byName, "memory_list", { type: "project", limit: 1, offset: 1 });
161
+ await drive(byName, "memory_list", { include_archived: true });
162
+ await drive(byName, "memory_list", { occurred_from: "2026-01-01", occurred_to: "2026-12-31" });
163
+ note("memory_list");
164
+
165
+ // --- memory_get:极简行 / 全量标注行(#184 的原始现场) ---
166
+ await drive(byName, "memory_get", { id: plain.id });
167
+ await drive(byName, "memory_get", { id: annotated.id });
168
+ note("memory_get");
169
+
170
+ // --- memory_update:字段修正 + 显式 scope 纠正(另一条会展开条件键的写路径) ---
171
+ await drive(byName, "memory_update", { id: plain.id, title: "shape-0 renamed", importance: 4 });
172
+ await drive(byName, "memory_update", {
173
+ id: plain.id,
174
+ content: "改过的正文",
175
+ agent_scope: "global",
176
+ workspace_scope: "proj-gamma",
177
+ reason: "issue #195 矩阵覆盖"
178
+ });
179
+ note("memory_update");
180
+
181
+ // --- memory_delete:删得掉 / 删不掉(不存在 → false,不抛错) ---
182
+ const doomed = store.save({ type: "history", title: "doomed", content: "待删", source: "test" });
183
+ const deleted = await drive(byName, "memory_delete", { id: doomed.id });
184
+ assert.equal(deleted.deleted, true);
185
+ const notDeleted = await drive(byName, "memory_delete", { id: "no-such-id-195" });
186
+ assert.equal(notDeleted.deleted, false);
187
+ note("memory_delete");
188
+
189
+ // --- memory_forget / memory_archive:两个方向的分支 ---
190
+ const forgetTarget = store.save({ type: "history", title: "forget-me", content: "待遗忘", source: "test" });
191
+ assert.equal((await drive(byName, "memory_forget", { id: forgetTarget.id })).memory.forgotten, true);
192
+ assert.equal((await drive(byName, "memory_forget", { id: forgetTarget.id, forgotten: false })).memory.forgotten, false);
193
+ note("memory_forget");
194
+
195
+ const archiveTarget = store.save({ type: "history", title: "archive-me", content: "待归档", source: "test" });
196
+ assert.equal((await drive(byName, "memory_archive", { id: archiveTarget.id })).memory.archived, true);
197
+ assert.equal((await drive(byName, "memory_archive", { id: archiveTarget.id, archived: false })).memory.archived, false);
198
+ note("memory_archive");
199
+
200
+ // --- memory_runtime:只驱动只读与「无载荷早退」两条分支 ---
201
+ // provision 会联网下载数十至上百 MB(且可能 adopt 本机载荷、硬链接数以百 MB 计),
202
+ // verify 命中载荷时会真实加载模型——两者都不属于单元测试该做的事。status 与其
203
+ // 「无载荷」早退分支已覆盖该 schema 的 summary/status/cost/payloadId/packages/reason
204
+ // 六个键;provision 侧的 strategy/files/bytes 与 verify 侧的 dimension 由 C 层
205
+ // 结构断言兜底(声明合规),分支行为本身留给 e2e/人工验证。
206
+ const emptyRuntimeDir = mkdtempSync(join(tmpdir(), "mneme-195-runtime-"));
207
+ try {
208
+ const rt = setup({ runtimeDir: emptyRuntimeDir });
209
+ await drive(rt.byName, "memory_runtime", { action: "status" });
210
+ await drive(rt.byName, "memory_runtime", { action: "verify" });
211
+ assert.ok(service, "service 保留引用,避免未使用变量告警");
212
+ note("memory_runtime");
213
+ } finally {
214
+ rmSync(emptyRuntimeDir, { recursive: true, force: true });
215
+ }
216
+
217
+ // --- memory_register_document(#230):created 分支 + evidence 求交(含丢弃)。
218
+ // superseded 分支由 document.test.js 覆盖;flag 关闭的拒绝路径同样在那边。
219
+ const docDir = mkdtempSync(join(tmpdir(), "mneme-230-doc-"));
220
+ const docFile = join(docDir, "report.md");
221
+ try {
222
+ writeFileSync(docFile, "# report\n\nbody", "utf8");
223
+ const reg = await drive(byName, "memory_register_document", {
224
+ path: docFile,
225
+ title: "matrix-doc",
226
+ summary: "指针行摘要",
227
+ tags: ["matrix"],
228
+ evidence: [plain.id, "fabricated-230"]
229
+ });
230
+ assert.equal(reg.action, "created");
231
+ assert.equal(reg.evidence_kept, 1);
232
+ assert.equal(reg.evidence_dropped, 1);
233
+ assert.equal(reg.degraded, true);
234
+ note("memory_register_document");
235
+ } finally {
236
+ rmSync(docDir, { recursive: true, force: true });
237
+ }
238
+
239
+ assert.deepEqual([...driven].sort(), [...TOOL_NAMES].sort(), "每个工具都必须至少被驱动一次(矩阵不允许空缺)");
240
+ });
241
+
242
+ test("A' 未命中 id 的三个工具按契约抛错(而不是返回半截对象)", async () => {
243
+ const { byName } = setup();
244
+ await assert.rejects(() => byName.memory_get.execute({ id: "missing-195" }), /memory not found/);
245
+ await assert.rejects(() => byName.memory_forget.execute({ id: "missing-195" }), /memory not found/);
246
+ await assert.rejects(() => byName.memory_archive.execute({ id: "missing-195" }), /memory not found/);
247
+ });
248
+
249
+ test("B DTO 产地:toApiList 全形态输出 ⊆ MEMORY_ITEM_SCHEMA,且声明键全部被真实产出", () => {
250
+ const { store, service } = setup();
251
+ seedShapes(store);
252
+ const rows = service.toApiList(service.list({ limit: 100 }));
253
+ assert.ok(rows.length >= 5, "种子行应当全部可读回");
254
+
255
+ const observed = new Set();
256
+ for (const dto of rows) {
257
+ const errors = validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, dto);
258
+ assert.deepEqual(errors, [], `toApiList 产出的 DTO 未被 MEMORY_ITEM_SCHEMA 覆盖:${JSON.stringify(errors)}`);
259
+ for (const key of Object.keys(dto)) observed.add(key);
260
+ }
261
+
262
+ // 反向:声明的每个键都必须有形态真的产出它。这条让 schema 与种子互相咬合——
263
+ // 以后给 toApiList 增键却忘了补种子,会在这里失败并提醒补形态。
264
+ const declared = Object.keys(MEMORY_ITEM_SCHEMA.properties);
265
+ const neverProduced = declared.filter((key) => !observed.has(key));
266
+ assert.deepEqual(neverProduced, [], `MEMORY_ITEM_SCHEMA 声明了但矩阵从未产出的键(补种子形态或删死声明):${neverProduced.join(", ")}`);
267
+ });
268
+
269
+ test("B' search / list / get 三个消费方共用同一份 item schema(#184 的根因是有人另抄了一份)", () => {
270
+ const { byName } = setup();
271
+ const itemSchemaOf = (name) => {
272
+ const props = byName[name].output.schema.properties;
273
+ return name === "memory_get" ? props.memory : props.items.items;
274
+ };
275
+ const search = itemSchemaOf("memory_search");
276
+ const list = itemSchemaOf("memory_list");
277
+ const get = itemSchemaOf("memory_get");
278
+ const keys = (s) => Object.keys(s.properties).sort();
279
+
280
+ assert.deepEqual(keys(search), keys(MEMORY_ITEM_SCHEMA));
281
+ assert.deepEqual(keys(list), keys(search));
282
+ assert.deepEqual(keys(get), keys(search));
283
+ for (const [name, schema] of [["search", search], ["list", list], ["get", get]]) {
284
+ assert.equal(schema.additionalProperties, false, `${name} 的 item schema 必须闭合`);
285
+ }
286
+ });
287
+
288
+ test("C 结构:全部 10 个工具的 output schema 闭合、required ⊆ properties、每项带 type", () => {
289
+ const { byName } = setup();
290
+ for (const name of TOOL_NAMES) {
291
+ const schema = byName[name].output.schema;
292
+ assert.ok(schema, `${name} 必须声明 output.schema`);
293
+ assert.equal(schema.additionalProperties, false, `${name} 的 schema 必须闭合(否则未声明键不会被拒收)`);
294
+ assert.equal(schema.type, "object", `${name} 的顶层 schema 必须是 object`);
295
+ for (const key of schema.required ?? []) {
296
+ assert.ok(schema.properties?.[key], `${name}.required 里的 ${key} 不在 properties 中`);
297
+ }
298
+ for (const [key, prop] of Object.entries(schema.properties ?? {})) {
299
+ assert.ok(prop.type, `${name}.${key} 缺 type——没有 type 的声明校验器无法真正约束它`);
300
+ }
301
+ }
302
+ });
303
+
304
+ test("D 负例锁:注入未声明键时校验器必须报错(否则 A/B/C 三层都是空转)", () => {
305
+ const { store, service, byName } = setup();
306
+ const row = store.save({ type: "project", title: "negative-control", content: "负例", source: "test" });
307
+ const dto = service.toApiList([store.getById(row.id)])[0];
308
+
309
+ // 正例:干净 DTO 必须通过
310
+ assert.deepEqual(validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, dto), []);
311
+ assert.deepEqual(validateJsonSchemaValue(byName.memory_get.output.schema, { memory: dto }), []);
312
+
313
+ // 负例:多一个键就必须被拒——这正是 #184 的形状
314
+ const poisoned = { ...dto, agent_scope_source_new: "auto" };
315
+ assert.notDeepEqual(
316
+ validateJsonSchemaValue(MEMORY_ITEM_SCHEMA, poisoned),
317
+ [],
318
+ "未声明键必须被拒收:#184 类漂移就是靠这条被抓出来的"
319
+ );
320
+ assert.notDeepEqual(
321
+ validateJsonSchemaValue(byName.memory_get.output.schema, { memory: poisoned }),
322
+ [],
323
+ "memory_get 的 schema 同样必须拒收未声明键"
324
+ );
325
+ });
@@ -47,15 +47,48 @@ function walkSchema(node, path, problems) {
47
47
  }
48
48
  }
49
49
 
50
- test("registers nine tools with correct names", () => {
50
+ test("registers ten tools with correct names", () => {
51
51
  const { registered } = setup();
52
52
  const names = registered.map((t) => t.name).sort();
53
- assert.deepEqual(names, ["memory_archive", "memory_delete", "memory_forget", "memory_get", "memory_list", "memory_runtime", "memory_save", "memory_search", "memory_update"]);
53
+ // #230 新增 memory_register_document(agent 产长文档的指针行铸造口)。
54
+ assert.deepEqual(names, ["memory_archive", "memory_delete", "memory_forget", "memory_get", "memory_list", "memory_register_document", "memory_runtime", "memory_save", "memory_search", "memory_update"]);
55
+ });
56
+
57
+ // v0.8.5 tool-exposure gate: disabling a tool hides it from the model entirely
58
+ // (it is not registered), which is the reliable lever against over-calling on
59
+ // slow/lightweight models — the injected memory block already covers recall.
60
+ test("disableMemorySearch hides only memory_search", () => {
61
+ const { registered } = setup({}, { disableMemorySearch: true });
62
+ const names = registered.map((t) => t.name).sort();
63
+ assert.deepEqual(names, ["memory_archive", "memory_delete", "memory_forget", "memory_get", "memory_list", "memory_register_document", "memory_runtime", "memory_save", "memory_update"]);
64
+ });
65
+
66
+ test("disableMemoryArchive hides only memory_archive", () => {
67
+ const { registered } = setup({}, { disableMemoryArchive: true });
68
+ const names = registered.map((t) => t.name).sort();
69
+ assert.deepEqual(names, ["memory_delete", "memory_forget", "memory_get", "memory_list", "memory_register_document", "memory_runtime", "memory_save", "memory_search", "memory_update"]);
70
+ });
71
+
72
+ test("both disable flags hide both tools", () => {
73
+ const { registered } = setup({}, { disableMemorySearch: true, disableMemoryArchive: true });
74
+ const names = registered.map((t) => t.name).sort();
75
+ assert.deepEqual(names, ["memory_delete", "memory_forget", "memory_get", "memory_list", "memory_register_document", "memory_runtime", "memory_save", "memory_update"]);
76
+ });
77
+
78
+ // Description discipline: with all tools exposed, the descriptions must steer a
79
+ // weak model away from calling them every turn.
80
+ test("memory_search/archive descriptions carry tool-use discipline", () => {
81
+ const { registered } = setup();
82
+ const search = registered.find((t) => t.name === "memory_search");
83
+ const archive = registered.find((t) => t.name === "memory_archive");
84
+ assert.match(search.description, /already injected into your context every turn/i);
85
+ assert.match(search.description, /only call this when/i);
86
+ assert.match(archive.description, /do not archive proactively mid-session/i);
54
87
  });
55
88
 
56
89
  test("compiled schemas pass the enforced DSH subset (defineTool projection)", () => {
57
90
  const { registered } = setup();
58
- assert.equal(registered.length, 9);
91
+ assert.equal(registered.length, 10);
59
92
  for (const tool of registered) {
60
93
  assertSupportedJsonSchema(tool.parameters);
61
94
  assertSupportedJsonSchema(tool.output.schema);