@possumtech/rummy 0.3.0 → 0.4.0

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 (65) hide show
  1. package/.env.example +13 -1
  2. package/PLUGINS.md +1 -1
  3. package/README.md +5 -1
  4. package/SPEC.md +211 -54
  5. package/migrations/001_initial_schema.sql +3 -4
  6. package/package.json +7 -3
  7. package/service.js +5 -3
  8. package/src/agent/AgentLoop.js +183 -238
  9. package/src/agent/ContextAssembler.js +2 -0
  10. package/src/agent/KnownStore.js +36 -85
  11. package/src/agent/ResponseHealer.js +65 -31
  12. package/src/agent/TurnExecutor.js +284 -382
  13. package/src/agent/XmlParser.js +28 -4
  14. package/src/agent/known_queries.sql +1 -1
  15. package/src/agent/known_store.sql +32 -34
  16. package/src/agent/runs.sql +2 -2
  17. package/src/agent/tokens.js +1 -0
  18. package/src/agent/turns.sql +5 -0
  19. package/src/hooks/HookRegistry.js +7 -0
  20. package/src/hooks/Hooks.js +2 -4
  21. package/src/hooks/ToolRegistry.js +8 -13
  22. package/src/plugins/ask_user/ask_userDoc.js +3 -8
  23. package/src/plugins/budget/README.md +26 -30
  24. package/src/plugins/budget/budget.js +69 -36
  25. package/src/plugins/budget/recovery.js +47 -0
  26. package/src/plugins/cp/cp.js +1 -1
  27. package/src/plugins/cp/cpDoc.js +5 -10
  28. package/src/plugins/env/envDoc.js +3 -8
  29. package/src/plugins/get/get.js +70 -2
  30. package/src/plugins/get/getDoc.js +19 -16
  31. package/src/plugins/hedberg/matcher.js +10 -29
  32. package/src/plugins/helpers.js +2 -2
  33. package/src/plugins/instructions/instructions.js +3 -2
  34. package/src/plugins/instructions/preamble.md +33 -12
  35. package/src/plugins/known/known.js +66 -17
  36. package/src/plugins/known/knownDoc.js +7 -10
  37. package/src/plugins/mv/mv.js +18 -1
  38. package/src/plugins/mv/mvDoc.js +9 -10
  39. package/src/plugins/{current → performed}/README.md +4 -3
  40. package/src/plugins/{current/current.js → performed/performed.js} +15 -20
  41. package/src/plugins/policy/policy.js +47 -0
  42. package/src/plugins/previous/README.md +2 -1
  43. package/src/plugins/previous/previous.js +31 -25
  44. package/src/plugins/progress/README.md +1 -2
  45. package/src/plugins/progress/progress.js +10 -60
  46. package/src/plugins/prompt/prompt.js +10 -8
  47. package/src/plugins/rm/rm.js +27 -15
  48. package/src/plugins/rm/rmDoc.js +6 -11
  49. package/src/plugins/rpc/rpc.js +3 -1
  50. package/src/plugins/set/set.js +125 -92
  51. package/src/plugins/set/setDoc.js +28 -37
  52. package/src/plugins/sh/shDoc.js +2 -7
  53. package/src/plugins/summarize/summarize.js +7 -0
  54. package/src/plugins/summarize/summarizeDoc.js +6 -11
  55. package/src/plugins/telemetry/telemetry.js +14 -9
  56. package/src/plugins/think/think.js +12 -0
  57. package/src/plugins/think/thinkDoc.js +18 -0
  58. package/src/plugins/unknown/README.md +2 -1
  59. package/src/plugins/unknown/unknown.js +26 -4
  60. package/src/plugins/unknown/unknownDoc.js +9 -14
  61. package/src/plugins/update/update.js +7 -0
  62. package/src/plugins/update/updateDoc.js +6 -11
  63. package/src/server/ClientConnection.js +69 -45
  64. package/src/sql/v_model_context.sql +7 -17
  65. package/src/plugins/budget/BudgetGuard.js +0 -74
@@ -1,9 +1,10 @@
1
1
  import KnownStore from "../../agent/KnownStore.js";
2
+ import { countTokens } from "../../agent/tokens.js";
2
3
  import Hedberg, { generatePatch } from "../hedberg/hedberg.js";
3
4
  import { storePatternResult } from "../helpers.js";
4
5
  import docs from "./setDoc.js";
5
6
 
6
- const VALID_FIDELITY = { archive: 1, summary: 1, index: 1, full: 1 };
7
+ const VALID_FIDELITY = { archive: 1, summary: 1, full: 1 };
7
8
 
8
9
  // biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
9
10
  export default class Set {
@@ -25,66 +26,56 @@ export default class Set {
25
26
  async handler(entry, rummy) {
26
27
  const { entries: store, sequence: turn, runId, loopId } = rummy;
27
28
  const attrs = entry.attributes;
28
-
29
- // Fidelity control: <set path="..." fidelity="archive"/>
30
29
  const fidelityAttr = VALID_FIDELITY[attrs.fidelity] ? attrs.fidelity : null;
31
- if (fidelityAttr && attrs.path) {
30
+ const rawSummary =
31
+ typeof attrs.summary === "string" ? attrs.summary : null;
32
+ const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
33
+
34
+ // Pure fidelity/metadata change — no body content
35
+ if (!entry.body && fidelityAttr && attrs.path) {
32
36
  const target = attrs.path;
33
- const rawSummary =
34
- typeof attrs.summary === "string" ? attrs.summary : null;
35
- const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
36
37
  const matches = await store.getEntriesByPattern(
37
38
  runId,
38
39
  target,
39
40
  attrs.body,
40
41
  );
41
- if (entry.body) {
42
- // Write content directly at specified fidelity
43
- const entryAttrs = summaryText ? { summary: summaryText } : null;
44
- for (const match of matches) {
45
- await store.upsert(runId, turn, match.path, entry.body, 200, {
46
- fidelity: fidelityAttr,
47
- attributes: entryAttrs,
48
- loopId,
49
- });
50
- }
51
- if (matches.length === 0) {
52
- await store.upsert(runId, turn, target, entry.body, 200, {
53
- fidelity: fidelityAttr,
54
- attributes: entryAttrs,
55
- loopId,
42
+ if (matches.length === 0) {
43
+ await store.upsert(
44
+ runId,
45
+ turn,
46
+ entry.resultPath,
47
+ `${target} not found`,
48
+ 404,
49
+ { fidelity: "archive", loopId },
50
+ );
51
+ return;
52
+ }
53
+ for (const match of matches) {
54
+ await store.setFidelity(runId, match.path, fidelityAttr);
55
+ if (summaryText) {
56
+ await store.setAttributes(runId, match.path, {
57
+ summary: summaryText,
56
58
  });
57
59
  }
58
- } else {
59
- // No body — change fidelity, attach summary if provided
60
- for (const match of matches) {
61
- await store.setFidelity(runId, match.path, fidelityAttr);
62
- if (summaryText) {
63
- await store.setAttributes(runId, match.path, {
64
- summary: summaryText,
65
- });
66
- }
67
- }
68
60
  }
69
61
  const label =
70
62
  fidelityAttr === "archive" ? "archived" : `set to ${fidelityAttr}`;
71
- const body =
72
- matches.length > 0
73
- ? `${matches.map((m) => m.path).join(", ")} ${label}`
74
- : `${target} not found`;
75
- await store.upsert(runId, turn, entry.resultPath, body, 200, {
76
- fidelity: "archive",
77
- loopId,
78
- });
63
+ await store.upsert(
64
+ runId,
65
+ turn,
66
+ entry.resultPath,
67
+ `${matches.map((m) => m.path).join(", ")} ${label}`,
68
+ 200,
69
+ { fidelity: "archive", loopId },
70
+ );
79
71
  return;
80
72
  }
81
73
 
74
+ // Edit: sed patterns or SEARCH/REPLACE blocks
82
75
  if (attrs.blocks || attrs.search != null) {
83
76
  await this.#processEdit(rummy, entry, attrs);
84
- return;
85
- }
86
-
87
- if (attrs.preview && attrs.path) {
77
+ } else if (attrs.preview && attrs.path) {
78
+ // Preview
88
79
  const matches = await store.getEntriesByPattern(
89
80
  runId,
90
81
  attrs.path,
@@ -101,43 +92,68 @@ export default class Set {
101
92
  { preview: true, loopId },
102
93
  );
103
94
  return;
104
- }
95
+ } else {
96
+ // Write content
97
+ const target = attrs.path;
98
+ if (!target) return;
105
99
 
106
- const target = attrs.path;
107
- if (!target) return;
100
+ const scheme = KnownStore.scheme(target);
101
+ if (scheme === null) {
102
+ // File write — diff against existing content
103
+ const existing = await store.getBody(runId, target);
104
+ const oldContent = existing ?? "";
105
+ const newContent = entry.body || "";
106
+ const udiff = generatePatch(target, oldContent, newContent);
107
+ const merge = oldContent
108
+ ? `<<<<<<< SEARCH\n${oldContent}\n=======\n${newContent}\n>>>>>>> REPLACE`
109
+ : `<<<<<<< SEARCH\n=======\n${newContent}\n>>>>>>> REPLACE`;
110
+ await store.upsert(runId, turn, entry.resultPath, oldContent, 202, {
111
+ attributes: { file: target, patch: udiff, merge },
112
+ loopId,
113
+ });
114
+ } else if (attrs.filter || target.includes("*")) {
115
+ // Pattern update
116
+ const matches = await store.getEntriesByPattern(
117
+ runId,
118
+ target,
119
+ attrs.filter,
120
+ );
121
+ await store.updateBodyByPattern(
122
+ runId,
123
+ target,
124
+ attrs.filter || null,
125
+ entry.body,
126
+ );
127
+ await storePatternResult(
128
+ store,
129
+ runId,
130
+ turn,
131
+ "set",
132
+ target,
133
+ attrs.filter,
134
+ matches,
135
+ { loopId },
136
+ );
137
+ } else {
138
+ // Direct scheme write
139
+ await store.upsert(runId, turn, target, entry.body, 200, {
140
+ fidelity: fidelityAttr || "full",
141
+ attributes: summaryText ? { summary: summaryText } : null,
142
+ loopId,
143
+ });
144
+ }
145
+ }
108
146
 
109
- const scheme = KnownStore.scheme(target);
110
- if (scheme === null) {
111
- const udiff = generatePatch(target, "", entry.body || "");
112
- const merge = `<<<<<<< SEARCH\n=======\n${entry.body || ""}\n>>>>>>> REPLACE`;
113
- await store.upsert(runId, turn, entry.resultPath, "", 202, {
114
- attributes: { file: target, patch: udiff, merge },
115
- loopId,
116
- });
117
- } else if (attrs.filter || target.includes("*")) {
118
- const matches = await store.getEntriesByPattern(
119
- runId,
120
- target,
121
- attrs.filter,
122
- );
123
- await store.updateBodyByPattern(
124
- runId,
125
- target,
126
- attrs.filter || null,
127
- entry.body,
128
- );
129
- await storePatternResult(
130
- store,
131
- runId,
132
- turn,
133
- "set",
134
- target,
135
- attrs.filter,
136
- matches,
137
- { loopId },
138
- );
139
- } else {
140
- await store.upsert(runId, turn, target, entry.body, 200, { loopId });
147
+ // Apply fidelity after all write operations
148
+ if (fidelityAttr && attrs.path) {
149
+ const target = attrs.path;
150
+ const scheme = KnownStore.scheme(target);
151
+ if (scheme !== null) {
152
+ await store.setFidelity(runId, target, fidelityAttr);
153
+ }
154
+ if (summaryText) {
155
+ await store.setAttributes(runId, target, { summary: summaryText });
156
+ }
141
157
  }
142
158
  }
143
159
 
@@ -197,8 +213,8 @@ export default class Set {
197
213
  searchText != null
198
214
  ? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
199
215
  : null;
200
- const beforeTokens = match.tokens_full || 0;
201
- const afterTokens = patch ? (patch.length / 4) | 0 : beforeTokens;
216
+ const beforeTokens = match.tokens || 0;
217
+ const afterTokens = patch ? countTokens(patch) : beforeTokens;
202
218
 
203
219
  await store.upsert(runId, turn, resultPath, match.body, status, {
204
220
  attributes: {
@@ -264,8 +280,8 @@ export default class Set {
264
280
  ? generatePatch(filePath, original, current)
265
281
  : null;
266
282
  const merge = mergeBlocks.length > 0 ? mergeBlocks.join("\n") : null;
267
- const beforeTokens = fileEntry[0].tokens_full || 0;
268
- const afterTokens = current ? (current.length / 4) | 0 : beforeTokens;
283
+ const beforeTokens = fileEntry[0].tokens || 0;
284
+ const afterTokens = current ? countTokens(current) : beforeTokens;
269
285
 
270
286
  await store.upsert(runId, turn, entry.path, original, state, {
271
287
  attributes: {
@@ -287,10 +303,7 @@ export default class Set {
287
303
  return { search: attrs.search, replace: attrs.replace ?? "" };
288
304
  }
289
305
  if (attrs.blocks?.length > 0) {
290
- return {
291
- search: attrs.blocks[0].search,
292
- replace: attrs.blocks[0].replace,
293
- };
306
+ return { blocks: attrs.blocks };
294
307
  }
295
308
  return null;
296
309
  }
@@ -312,11 +325,31 @@ export default class Set {
312
325
  };
313
326
  }
314
327
  if (body && attrs.blocks?.length > 0) {
315
- const block = attrs.blocks[0];
316
- return Hedberg.replace(body, block.search, block.replace, {
317
- sed: block.sed,
318
- flags: block.flags,
319
- });
328
+ if (attrs.blocks.length === 1) {
329
+ const block = attrs.blocks[0];
330
+ return Hedberg.replace(body, block.search, block.replace, {
331
+ sed: block.sed,
332
+ flags: block.flags,
333
+ });
334
+ }
335
+ let current = body;
336
+ let lastWarning = null;
337
+ for (const block of attrs.blocks) {
338
+ const result = Hedberg.replace(current, block.search, block.replace, {
339
+ sed: block.sed,
340
+ flags: block.flags,
341
+ });
342
+ if (result.error) return result;
343
+ if (result.warning) lastWarning = result.warning;
344
+ if (result.patch) current = result.patch;
345
+ }
346
+ return {
347
+ patch: current !== body ? current : null,
348
+ searchText: null,
349
+ replaceText: null,
350
+ warning: lastWarning,
351
+ error: null,
352
+ };
320
353
  }
321
354
  return {
322
355
  patch: null,
@@ -2,44 +2,35 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax: path attr + body = edit content
6
- ['## <set path="[path/to/file]">[edit]</set> - Edit a file or entry'],
7
-
8
- // --- Examples: sed, SEARCH/REPLACE, fidelity control
9
- [
10
- 'Example: <set path="src/config.js">s/port = 3000/port = 8080/g</set>',
11
- "Sed syntax: most common edit pattern. Shows s/old/new/ with g flag.",
12
- ],
13
- [
14
- `Example: <set path="src/app.js"><<<<<<< SEARCH
15
- // TODO: add error handling
5
+ [
6
+ '## <set path="[path/to/file]">[content or edit]</set> - Create, edit, or update a file or entry',
7
+ ],
8
+ [
9
+ 'Example: <set path="known://project/milestones" fidelity="summary" summary="milestone,deadline,2026"/>',
10
+ "Fidelity control first most unique capability of set.",
11
+ ],
12
+ [
13
+ `Example: <set path="src/app.js">
14
+ <<<<<<< SEARCH
15
+ old text
16
16
  =======
17
- // error handler configured
18
- >>>>>>> REPLACE</set>`,
19
- "SEARCH/REPLACE block: literal match and replace. Use when sed escaping is complex.",
20
- ],
21
- [
22
- 'Example: <set path="known://plan" stored summary="Migration plan for Q2"/>',
23
- "Fidelity + summary: archive an entry while preserving a description. Lifecycle endpoint.",
24
- ],
25
-
26
- // --- Constraints
27
- [
28
- '* `fidelity="..."`: `archive`, `summary`, `index`, `full`',
29
- "Fidelity control. Archive removes from context but preserves for retrieval.",
30
- ],
31
- [
32
- '* `summary="..."` (<= 80 chars) persists across fidelity changes',
33
- "Model-authored descriptions survive demotion. No janitorial pass needed.",
34
- ],
35
- [
36
- "* YOU MUST NOT use <sh/> or <env/> to read, create, or edit files",
37
- "Forces file operations through set/get. Prevents untracked mutations.",
38
- ],
39
- [
40
- "* Editing: s/old/new/ sed patterns and literal SEARCH/REPLACE blocks",
41
- "Both syntaxes supported. Hedberg normalizes either form.",
42
- ],
17
+ new text
18
+ >>>>>>> REPLACE
19
+ </set>`,
20
+ "SEARCH/REPLACE block — primary edit pattern for existing files.",
21
+ ],
22
+ [
23
+ 'Example: <set path="src/config.js">s/port = 3000/port = 8080/g;s/host = 127.0.0.1/host = localhost/g;</set>',
24
+ "Sed syntax: chained s/old/new/ patterns with semicolons.",
25
+ ],
26
+ [
27
+ 'Example: <set path="example.md">Full file content here</set>',
28
+ "Create: body contents are entire file.",
29
+ ],
30
+ [
31
+ "* YOU MUST NOT use <sh/> or <env/> to list, create, read, or edit files. Use the Tool Commands.",
32
+ "Forces file operations through set/get.",
33
+ ],
43
34
  ];
44
35
 
45
36
  export default LINES.map(([text]) => text).join("\n");
@@ -2,23 +2,18 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax
6
5
  ["## <sh>[command]</sh> - Run a shell command with side effects"],
7
-
8
- // --- Examples: install and test — real mutations
9
6
  [
10
7
  "Example: <sh>npm install express</sh>",
11
- "Package install. Shows a real side-effect command.",
8
+ "Package install. Real side-effect command.",
12
9
  ],
13
10
  [
14
11
  "Example: <sh>npm test</sh>",
15
12
  "Test execution. Another common side-effect action.",
16
13
  ],
17
-
18
- // --- Constraints
19
14
  [
20
15
  "* YOU MUST NOT use <sh/> to read, create, or edit files — use <get/> and <set/>",
21
- "Forces file operations through the entry system. Prevents untracked mutations.",
16
+ "Forces file operations through the entry system.",
22
17
  ],
23
18
  [
24
19
  "* YOU MUST use <env/> for commands without side effects",
@@ -7,6 +7,7 @@ export default class Summarize {
7
7
  this.#core = core;
8
8
  core.ensureTool();
9
9
  core.registerScheme({ category: "logging" });
10
+ core.on("handler", this.handler.bind(this));
10
11
  core.on("full", this.full.bind(this));
11
12
  core.on("summary", this.summary.bind(this));
12
13
  core.filter("instructions.toolDocs", async (docsMap) => {
@@ -15,6 +16,12 @@ export default class Summarize {
15
16
  });
16
17
  }
17
18
 
19
+ async handler(entry, rummy) {
20
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
21
+ const statusPath = await store.slugPath(runId, "summarize", entry.body);
22
+ await store.upsert(runId, turn, statusPath, entry.body, 200, { loopId });
23
+ }
24
+
18
25
  full(entry) {
19
26
  return `# summarize\n${entry.body}`;
20
27
  }
@@ -2,31 +2,26 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax
6
5
  ["## <summarize>[answer or summary]</summarize> - Signal completion"],
7
-
8
- // --- Examples: answer and task completion
9
6
  [
10
7
  "Example: <summarize>The port is 8080</summarize>",
11
- "Direct answer. Shows summarize as the vehicle for delivering answers.",
8
+ "Direct answer. Summarize delivers answers.",
12
9
  ],
13
10
  [
14
11
  "Example: <summarize>Installed express, updated config</summarize>",
15
- "Task summary. Shows summarize for action completion.",
12
+ "Task summary. Action completion.",
16
13
  ],
17
-
18
- // --- Constraints: RFC-style MUST/MUST NOT
19
14
  [
20
- "* YOU MUST use <summarize> when done — describes the final state",
21
- "Completion signal. Without this, the loop continues indefinitely.",
15
+ "* YOU MUST use <summarize></summarize> when done — describes the final state",
16
+ "Completion signal.",
22
17
  ],
23
18
  [
24
19
  "* YOU MUST NOT use <summarize> if still working — use <update/> instead",
25
- "Mutual exclusion with update. Prevents premature completion.",
20
+ "Mutual exclusion with update.",
26
21
  ],
27
22
  [
28
23
  "* YOU MUST keep <summarize> to <= 80 characters",
29
- "Length cap. Matches the summary attribute constraint. Prevents verbose output.",
24
+ "Length cap.",
30
25
  ],
31
26
  ];
32
27
 
@@ -1,4 +1,4 @@
1
- import { writeFileSync } from "node:fs";
1
+ import { writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  export default class Telemetry {
@@ -85,17 +85,20 @@ export default class Telemetry {
85
85
  // assistant://N — the model's raw response
86
86
  await store.upsert(runId, turn, `assistant://${turn}`, content, 200, {
87
87
  loopId,
88
+ fidelity: "archive",
88
89
  });
89
90
 
90
91
  // system://N, user://N — assembled messages as audit
91
92
  if (systemMsg) {
92
93
  await store.upsert(runId, turn, `system://${turn}`, systemMsg, 200, {
93
94
  loopId,
95
+ fidelity: "archive",
94
96
  });
95
97
  }
96
98
  if (userMsg) {
97
99
  await store.upsert(runId, turn, `user://${turn}`, userMsg, 200, {
98
100
  loopId,
101
+ fidelity: "archive",
99
102
  });
100
103
  }
101
104
 
@@ -112,7 +115,7 @@ export default class Telemetry {
112
115
  model: result.model || null,
113
116
  }),
114
117
  200,
115
- { loopId },
118
+ { loopId, fidelity: "archive" },
116
119
  );
117
120
 
118
121
  // reasoning://N
@@ -123,7 +126,7 @@ export default class Telemetry {
123
126
  `reasoning://${turn}`,
124
127
  responseMessage.reasoning_content,
125
128
  200,
126
- { loopId },
129
+ { loopId, fidelity: "archive" },
127
130
  );
128
131
  }
129
132
 
@@ -131,6 +134,7 @@ export default class Telemetry {
131
134
  if (unparsed) {
132
135
  await store.upsert(runId, turn, `content://${turn}`, unparsed, 200, {
133
136
  loopId,
137
+ fidelity: "archive",
134
138
  });
135
139
  }
136
140
 
@@ -147,9 +151,12 @@ export default class Telemetry {
147
151
  usage.completion_tokens_details?.reasoning_tokens ||
148
152
  usage.output_tokens_details?.reasoning_tokens ||
149
153
  0;
154
+ // Use LLM's actual prompt_tokens as the ground-truth context size when available.
155
+ // This back-fills context_tokens so get_last_context_tokens reflects reality for the next turn.
156
+ const actualContextTokens = usage.prompt_tokens || assembledTokens || 0;
150
157
  await rummy.db.update_turn_stats.run({
151
158
  id: rummy.turnId,
152
- context_tokens: assembledTokens ?? 0,
159
+ context_tokens: actualContextTokens,
153
160
  reasoning_content: responseMessage?.reasoning_content || null,
154
161
  prompt_tokens: usage.prompt_tokens ?? 0,
155
162
  cached_tokens: cachedTokens ?? 0,
@@ -189,10 +196,8 @@ export default class Telemetry {
189
196
 
190
197
  #flush() {
191
198
  if (!this.#lastRunPath || this.#turnLog.length === 0) return;
192
- try {
193
- writeFileSync(this.#lastRunPath, `${this.#turnLog.join("\n")}\n`);
194
- } catch {
195
- // RUMMY_HOME may not exist yet
196
- }
199
+ writeFile(this.#lastRunPath, `${this.#turnLog.join("\n")}\n`).catch(
200
+ () => {},
201
+ );
197
202
  }
198
203
  }
@@ -1,5 +1,17 @@
1
+ import docs from "./thinkDoc.js";
2
+
3
+ const THINK_ENABLED = process.env.RUMMY_THINK;
4
+ if (THINK_ENABLED === undefined) throw new Error("RUMMY_THINK must be set (1 or 0)");
5
+
1
6
  export default class Think {
2
7
  constructor(core) {
3
8
  core.registerScheme({ modelVisible: 0, category: "logging" });
9
+ if (THINK_ENABLED === "1") {
10
+ core.ensureTool();
11
+ core.filter("instructions.toolDocs", async (docsMap) => {
12
+ docsMap.think = docs;
13
+ return docsMap;
14
+ });
15
+ }
4
16
  }
5
17
  }
@@ -0,0 +1,18 @@
1
+ // Tool doc for <think/>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ [
6
+ "## <think>[reasoning]</think> - Think before acting",
7
+ ],
8
+ [
9
+ "* Use <think> before any other tools to plan your approach",
10
+ "Positioning: think first, then act. Prevents degenerate tool-call storms.",
11
+ ],
12
+ [
13
+ "* Reasoning inside <think> is private — it does not appear in your context",
14
+ "Frees the model to reason without consuming context budget.",
15
+ ],
16
+ ];
17
+
18
+ export default LINES.map(([text]) => text).join("\n");
@@ -20,4 +20,5 @@ The Rumsfeld mechanism. The model registers what it doesn't know before acting.
20
20
  Unknowns are sticky — they persist across turns until the model explicitly
21
21
  removes them with `<rm>`. The model investigates unknowns using `<get>`,
22
22
  `<env>`, or `<ask_user>`, then removes resolved ones. Server deduplicates
23
- on insert. Turn numbers shown on each unknown for temporal reasoning.
23
+ on insert. Each unknown renders with turn, fidelity, and tokens for
24
+ temporal reasoning and context management.
@@ -9,7 +9,9 @@ export default class Unknown {
9
9
  core.registerScheme({
10
10
  category: "unknown",
11
11
  });
12
+ core.on("handler", this.handler.bind(this));
12
13
  core.on("full", this.full.bind(this));
14
+ core.on("summary", this.summary.bind(this));
13
15
  core.filter("assembly.system", this.assembleUnknowns.bind(this), 300);
14
16
  core.filter("instructions.toolDocs", async (docsMap) => {
15
17
  docsMap.unknown = docs;
@@ -17,18 +19,38 @@ export default class Unknown {
17
19
  });
18
20
  }
19
21
 
22
+ async handler(entry, rummy) {
23
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
24
+
25
+ // Deduplicate — if this exact body already exists, skip
26
+ const existingValues = await store.getUnknownValues(runId);
27
+ if (existingValues.has(entry.body)) {
28
+ console.warn(`[RUMMY] Unknown deduped: "${entry.body.slice(0, 60)}"`);
29
+ return;
30
+ }
31
+
32
+ // Generate slug path and upsert
33
+ const unknownPath = await store.slugPath(runId, "unknown", entry.body);
34
+ await store.upsert(runId, turn, unknownPath, entry.body, 200, { loopId });
35
+ }
36
+
20
37
  full(entry) {
21
38
  return `# unknown\n${entry.body}`;
22
39
  }
23
40
 
41
+ summary(entry) {
42
+ return this.full(entry);
43
+ }
44
+
24
45
  async assembleUnknowns(content, ctx) {
25
46
  const entries = ctx.rows.filter((r) => r.category === "unknown");
26
47
  if (entries.length === 0) return content;
27
48
 
28
- const lines = entries.map(
29
- (u) =>
30
- `<unknown path="${u.path}" turn="${u.source_turn || u.turn}">${u.body}</unknown>`,
31
- );
49
+ const lines = entries.map((u) => {
50
+ const fidelity = u.fidelity ? ` fidelity="${u.fidelity}"` : "";
51
+ const tokens = u.tokens ? ` tokens="${u.tokens}"` : "";
52
+ return `<unknown path="${u.path}" turn="${u.source_turn || u.turn}"${fidelity}${tokens}>${u.body}</unknown>`;
53
+ });
32
54
  return `${content}\n\n<unknowns>\n${lines.join("\n")}\n</unknowns>`;
33
55
  }
34
56
  }
@@ -2,29 +2,24 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax: body = what you need to learn
6
5
  [
7
- `## <unknown>[specific thing I need to learn]</unknown> - Track open questions`,
6
+ "## <unknown>[specific thing I need to learn]</unknown> - Track open questions",
8
7
  ],
9
-
10
- // --- Examples: concrete unknowns, not abstract
11
8
  [
12
- `Example: <unknown path="unknown://answer">contents of answer.txt</unknown>`,
13
- `Specific and actionable. Shows that unknowns are concrete investigation targets.`,
9
+ 'Example: <unknown path="unknown://answer">contents of answer.txt</unknown>',
10
+ "Path form: explicit unknown path for structured tracking.",
14
11
  ],
15
12
  [
16
- `Example: <unknown>which database adapter is configured</unknown>`,
17
- `Domain question. Shows unknowns for configuration/architecture questions.`,
13
+ "Example: <unknown>which database adapter is configured</unknown>",
14
+ "Body form: question as body, path auto-generated.",
18
15
  ],
19
-
20
- // --- Lifecycle: register → investigate → resolve
21
16
  [
22
- `* Investigate with Tool Commands`,
23
- `Cross-tool lifecycle: unknowns drive get/env/ask_user actions.`,
17
+ "* Investigate with Tool Commands",
18
+ "Unknowns drive action get, env, search, ask_user.",
24
19
  ],
25
20
  [
26
- `* When resolved or irrelevant, remove with <rm path="unknown://..."/>`,
27
- `Cross-tool lifecycle: rm cleans resolved unknowns from context.`,
21
+ '* When resolved or irrelevant, remove with <set path="unknown://..." fidelity="archive"/>',
22
+ "Archive instead of delete preserves the question for context history.",
28
23
  ],
29
24
  ];
30
25