@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.
- package/.env.example +13 -1
- package/PLUGINS.md +1 -1
- package/README.md +5 -1
- package/SPEC.md +211 -54
- package/migrations/001_initial_schema.sql +3 -4
- package/package.json +7 -3
- package/service.js +5 -3
- package/src/agent/AgentLoop.js +183 -238
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/KnownStore.js +36 -85
- package/src/agent/ResponseHealer.js +65 -31
- package/src/agent/TurnExecutor.js +284 -382
- package/src/agent/XmlParser.js +28 -4
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +32 -34
- package/src/agent/runs.sql +2 -2
- package/src/agent/tokens.js +1 -0
- package/src/agent/turns.sql +5 -0
- package/src/hooks/HookRegistry.js +7 -0
- package/src/hooks/Hooks.js +2 -4
- package/src/hooks/ToolRegistry.js +8 -13
- package/src/plugins/ask_user/ask_userDoc.js +3 -8
- package/src/plugins/budget/README.md +26 -30
- package/src/plugins/budget/budget.js +69 -36
- package/src/plugins/budget/recovery.js +47 -0
- package/src/plugins/cp/cp.js +1 -1
- package/src/plugins/cp/cpDoc.js +5 -10
- package/src/plugins/env/envDoc.js +3 -8
- package/src/plugins/get/get.js +70 -2
- package/src/plugins/get/getDoc.js +19 -16
- package/src/plugins/hedberg/matcher.js +10 -29
- package/src/plugins/helpers.js +2 -2
- package/src/plugins/instructions/instructions.js +3 -2
- package/src/plugins/instructions/preamble.md +33 -12
- package/src/plugins/known/known.js +66 -17
- package/src/plugins/known/knownDoc.js +7 -10
- package/src/plugins/mv/mv.js +18 -1
- package/src/plugins/mv/mvDoc.js +9 -10
- package/src/plugins/{current → performed}/README.md +4 -3
- package/src/plugins/{current/current.js → performed/performed.js} +15 -20
- package/src/plugins/policy/policy.js +47 -0
- package/src/plugins/previous/README.md +2 -1
- package/src/plugins/previous/previous.js +31 -25
- package/src/plugins/progress/README.md +1 -2
- package/src/plugins/progress/progress.js +10 -60
- package/src/plugins/prompt/prompt.js +10 -8
- package/src/plugins/rm/rm.js +27 -15
- package/src/plugins/rm/rmDoc.js +6 -11
- package/src/plugins/rpc/rpc.js +3 -1
- package/src/plugins/set/set.js +125 -92
- package/src/plugins/set/setDoc.js +28 -37
- package/src/plugins/sh/shDoc.js +2 -7
- package/src/plugins/summarize/summarize.js +7 -0
- package/src/plugins/summarize/summarizeDoc.js +6 -11
- package/src/plugins/telemetry/telemetry.js +14 -9
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +18 -0
- package/src/plugins/unknown/README.md +2 -1
- package/src/plugins/unknown/unknown.js +26 -4
- package/src/plugins/unknown/unknownDoc.js +9 -14
- package/src/plugins/update/update.js +7 -0
- package/src/plugins/update/updateDoc.js +6 -11
- package/src/server/ClientConnection.js +69 -45
- package/src/sql/v_model_context.sql +7 -17
- package/src/plugins/budget/BudgetGuard.js +0 -74
package/src/plugins/set/set.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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 (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
if (
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
201
|
-
const afterTokens = patch ? (patch
|
|
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].
|
|
268
|
-
const afterTokens = current ? (current
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
>>>>>>> REPLACE
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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");
|
package/src/plugins/sh/shDoc.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
8
|
+
"Direct answer. Summarize delivers answers.",
|
|
12
9
|
],
|
|
13
10
|
[
|
|
14
11
|
"Example: <summarize>Installed express, updated config</summarize>",
|
|
15
|
-
"Task summary.
|
|
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.
|
|
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.
|
|
20
|
+
"Mutual exclusion with update.",
|
|
26
21
|
],
|
|
27
22
|
[
|
|
28
23
|
"* YOU MUST keep <summarize> to <= 80 characters",
|
|
29
|
-
"Length cap.
|
|
24
|
+
"Length cap.",
|
|
30
25
|
],
|
|
31
26
|
];
|
|
32
27
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
6
|
+
"## <unknown>[specific thing I need to learn]</unknown> - Track open questions",
|
|
8
7
|
],
|
|
9
|
-
|
|
10
|
-
// --- Examples: concrete unknowns, not abstract
|
|
11
8
|
[
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
17
|
+
"* Investigate with Tool Commands",
|
|
18
|
+
"Unknowns drive action — get, env, search, ask_user.",
|
|
24
19
|
],
|
|
25
20
|
[
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|