@possumtech/rummy 0.3.1 → 0.5.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 +12 -0
- package/FIDELITY_CONTRACT.md +172 -0
- package/README.md +5 -1
- package/SPEC.md +31 -17
- package/migrations/001_initial_schema.sql +3 -4
- package/package.json +1 -1
- package/src/agent/AgentLoop.js +51 -153
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/KnownStore.js +16 -9
- package/src/agent/ResponseHealer.js +54 -1
- package/src/agent/TurnExecutor.js +125 -323
- package/src/agent/XmlParser.js +172 -42
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +29 -72
- package/src/agent/runs.sql +2 -2
- package/src/hooks/Hooks.js +1 -0
- package/src/hooks/PluginContext.js +8 -2
- package/src/hooks/RummyContext.js +6 -3
- package/src/hooks/ToolRegistry.js +29 -32
- package/src/plugins/ask_user/ask_user.js +2 -2
- package/src/plugins/ask_user/ask_userDoc.js +7 -10
- package/src/plugins/budget/README.md +28 -18
- package/src/plugins/budget/budget.js +80 -3
- package/src/plugins/budget/recovery.js +47 -0
- package/src/plugins/cp/cp.js +5 -5
- package/src/plugins/cp/cpDoc.js +1 -14
- package/src/plugins/engine/engine.sql +1 -1
- package/src/plugins/env/env.js +4 -4
- package/src/plugins/env/envDoc.js +4 -9
- package/src/plugins/file/file.js +2 -7
- package/src/plugins/get/get.js +32 -13
- package/src/plugins/get/getDoc.js +26 -44
- package/src/plugins/helpers.js +4 -4
- package/src/plugins/instructions/instructions.js +9 -7
- package/src/plugins/instructions/preamble.md +45 -26
- package/src/plugins/known/known.js +71 -15
- package/src/plugins/known/knownDoc.js +4 -20
- package/src/plugins/mv/mv.js +6 -6
- package/src/plugins/mv/mvDoc.js +4 -30
- package/src/plugins/policy/policy.js +47 -0
- package/src/plugins/previous/previous.js +10 -14
- package/src/plugins/progress/progress.js +29 -48
- package/src/plugins/prompt/prompt.js +18 -6
- package/src/plugins/rm/rm.js +4 -4
- package/src/plugins/rm/rmDoc.js +5 -14
- package/src/plugins/rpc/rpc.js +4 -2
- package/src/plugins/set/set.js +86 -91
- package/src/plugins/set/setDoc.js +28 -41
- package/src/plugins/sh/sh.js +4 -4
- package/src/plugins/sh/shDoc.js +4 -9
- package/src/plugins/skill/skill.js +2 -1
- package/src/plugins/summarize/summarize.js +9 -2
- package/src/plugins/summarize/summarizeDoc.js +10 -16
- package/src/plugins/telemetry/telemetry.js +36 -11
- package/src/plugins/think/think.js +13 -0
- package/src/plugins/think/thinkDoc.js +16 -0
- package/src/plugins/unknown/unknown.js +37 -9
- package/src/plugins/unknown/unknownDoc.js +7 -16
- package/src/plugins/update/update.js +9 -2
- package/src/plugins/update/updateDoc.js +12 -14
- package/src/server/ClientConnection.js +11 -1
- package/src/sql/functions/slugify.js +13 -1
- package/src/sql/v_model_context.sql +6 -6
package/src/plugins/set/set.js
CHANGED
|
@@ -4,7 +4,7 @@ import Hedberg, { generatePatch } from "../hedberg/hedberg.js";
|
|
|
4
4
|
import { storePatternResult } from "../helpers.js";
|
|
5
5
|
import docs from "./setDoc.js";
|
|
6
6
|
|
|
7
|
-
const VALID_FIDELITY = {
|
|
7
|
+
const VALID_FIDELITY = { archived: 1, demoted: 1, promoted: 1 };
|
|
8
8
|
|
|
9
9
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
|
|
10
10
|
export default class Set {
|
|
@@ -14,8 +14,8 @@ export default class Set {
|
|
|
14
14
|
this.#core = core;
|
|
15
15
|
core.registerScheme();
|
|
16
16
|
core.on("handler", this.handler.bind(this));
|
|
17
|
-
core.on("
|
|
18
|
-
core.on("
|
|
17
|
+
core.on("promoted", this.full.bind(this));
|
|
18
|
+
core.on("demoted", this.summary.bind(this));
|
|
19
19
|
core.on("turn.proposing", this.#materializeRevisions.bind(this));
|
|
20
20
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
21
21
|
docsMap.set = docs;
|
|
@@ -26,47 +26,18 @@ export default class Set {
|
|
|
26
26
|
async handler(entry, rummy) {
|
|
27
27
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
28
28
|
const attrs = entry.attributes;
|
|
29
|
-
|
|
30
|
-
// Fidelity control: <set path="..." fidelity="archive"/>
|
|
31
29
|
const fidelityAttr = VALID_FIDELITY[attrs.fidelity] ? attrs.fidelity : null;
|
|
32
|
-
|
|
30
|
+
const rawSummary = typeof attrs.summary === "string" ? attrs.summary : null;
|
|
31
|
+
const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
|
|
32
|
+
|
|
33
|
+
// Pure fidelity/metadata change — no body content
|
|
34
|
+
if (!entry.body && fidelityAttr && attrs.path) {
|
|
33
35
|
const target = attrs.path;
|
|
34
|
-
const rawSummary =
|
|
35
|
-
typeof attrs.summary === "string" ? attrs.summary : null;
|
|
36
|
-
const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
|
|
37
36
|
const matches = await store.getEntriesByPattern(
|
|
38
37
|
runId,
|
|
39
38
|
target,
|
|
40
39
|
attrs.body,
|
|
41
40
|
);
|
|
42
|
-
if (entry.body) {
|
|
43
|
-
// Write content directly at specified fidelity
|
|
44
|
-
const entryAttrs = summaryText ? { summary: summaryText } : null;
|
|
45
|
-
for (const match of matches) {
|
|
46
|
-
await store.upsert(runId, turn, match.path, entry.body, 200, {
|
|
47
|
-
fidelity: fidelityAttr,
|
|
48
|
-
attributes: entryAttrs,
|
|
49
|
-
loopId,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
if (matches.length === 0) {
|
|
53
|
-
await store.upsert(runId, turn, target, entry.body, 200, {
|
|
54
|
-
fidelity: fidelityAttr,
|
|
55
|
-
attributes: entryAttrs,
|
|
56
|
-
loopId,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
// No body — change fidelity, attach summary if provided
|
|
61
|
-
for (const match of matches) {
|
|
62
|
-
await store.setFidelity(runId, match.path, fidelityAttr);
|
|
63
|
-
if (summaryText) {
|
|
64
|
-
await store.setAttributes(runId, match.path, {
|
|
65
|
-
summary: summaryText,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
41
|
if (matches.length === 0) {
|
|
71
42
|
await store.upsert(
|
|
72
43
|
runId,
|
|
@@ -74,35 +45,35 @@ export default class Set {
|
|
|
74
45
|
entry.resultPath,
|
|
75
46
|
`${target} not found`,
|
|
76
47
|
404,
|
|
77
|
-
{
|
|
78
|
-
fidelity: "archive",
|
|
79
|
-
loopId,
|
|
80
|
-
},
|
|
48
|
+
{ fidelity: "archived", loopId },
|
|
81
49
|
);
|
|
82
50
|
return;
|
|
83
51
|
}
|
|
84
|
-
const
|
|
85
|
-
|
|
52
|
+
for (const match of matches) {
|
|
53
|
+
await store.setFidelity(runId, match.path, fidelityAttr);
|
|
54
|
+
if (summaryText) {
|
|
55
|
+
await store.setAttributes(runId, match.path, {
|
|
56
|
+
summary: summaryText,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const label = `set to ${fidelityAttr}`;
|
|
86
61
|
await store.upsert(
|
|
87
62
|
runId,
|
|
88
63
|
turn,
|
|
89
64
|
entry.resultPath,
|
|
90
65
|
`${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
91
66
|
200,
|
|
92
|
-
{
|
|
93
|
-
fidelity: "archive",
|
|
94
|
-
loopId,
|
|
95
|
-
},
|
|
67
|
+
{ fidelity: "archived", loopId },
|
|
96
68
|
);
|
|
97
69
|
return;
|
|
98
70
|
}
|
|
99
71
|
|
|
72
|
+
// Edit: sed patterns or SEARCH/REPLACE blocks
|
|
100
73
|
if (attrs.blocks || attrs.search != null) {
|
|
101
74
|
await this.#processEdit(rummy, entry, attrs);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (attrs.preview && attrs.path) {
|
|
75
|
+
} else if (attrs.preview && attrs.path) {
|
|
76
|
+
// Preview
|
|
106
77
|
const matches = await store.getEntriesByPattern(
|
|
107
78
|
runId,
|
|
108
79
|
attrs.path,
|
|
@@ -119,43 +90,68 @@ export default class Set {
|
|
|
119
90
|
{ preview: true, loopId },
|
|
120
91
|
);
|
|
121
92
|
return;
|
|
122
|
-
}
|
|
93
|
+
} else {
|
|
94
|
+
// Write content
|
|
95
|
+
const target = attrs.path;
|
|
96
|
+
if (!target) return;
|
|
123
97
|
|
|
124
|
-
|
|
125
|
-
|
|
98
|
+
const scheme = KnownStore.scheme(target);
|
|
99
|
+
if (scheme === null) {
|
|
100
|
+
// File write — diff against existing content
|
|
101
|
+
const existing = await store.getBody(runId, target);
|
|
102
|
+
const oldContent = existing ?? "";
|
|
103
|
+
const newContent = entry.body || "";
|
|
104
|
+
const udiff = generatePatch(target, oldContent, newContent);
|
|
105
|
+
const merge = oldContent
|
|
106
|
+
? `<<<<<<< SEARCH\n${oldContent}\n=======\n${newContent}\n>>>>>>> REPLACE`
|
|
107
|
+
: `<<<<<<< SEARCH\n=======\n${newContent}\n>>>>>>> REPLACE`;
|
|
108
|
+
await store.upsert(runId, turn, entry.resultPath, oldContent, 202, {
|
|
109
|
+
attributes: { file: target, patch: udiff, merge },
|
|
110
|
+
loopId,
|
|
111
|
+
});
|
|
112
|
+
} else if (attrs.filter || target.includes("*")) {
|
|
113
|
+
// Pattern update
|
|
114
|
+
const matches = await store.getEntriesByPattern(
|
|
115
|
+
runId,
|
|
116
|
+
target,
|
|
117
|
+
attrs.filter,
|
|
118
|
+
);
|
|
119
|
+
await store.updateBodyByPattern(
|
|
120
|
+
runId,
|
|
121
|
+
target,
|
|
122
|
+
attrs.filter || null,
|
|
123
|
+
entry.body,
|
|
124
|
+
);
|
|
125
|
+
await storePatternResult(
|
|
126
|
+
store,
|
|
127
|
+
runId,
|
|
128
|
+
turn,
|
|
129
|
+
"set",
|
|
130
|
+
target,
|
|
131
|
+
attrs.filter,
|
|
132
|
+
matches,
|
|
133
|
+
{ loopId },
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
// Direct scheme write
|
|
137
|
+
await store.upsert(runId, turn, target, entry.body, 200, {
|
|
138
|
+
fidelity: fidelityAttr || "promoted",
|
|
139
|
+
attributes: summaryText ? { summary: summaryText } : null,
|
|
140
|
+
loopId,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
126
144
|
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
runId,
|
|
138
|
-
target,
|
|
139
|
-
attrs.filter,
|
|
140
|
-
);
|
|
141
|
-
await store.updateBodyByPattern(
|
|
142
|
-
runId,
|
|
143
|
-
target,
|
|
144
|
-
attrs.filter || null,
|
|
145
|
-
entry.body,
|
|
146
|
-
);
|
|
147
|
-
await storePatternResult(
|
|
148
|
-
store,
|
|
149
|
-
runId,
|
|
150
|
-
turn,
|
|
151
|
-
"set",
|
|
152
|
-
target,
|
|
153
|
-
attrs.filter,
|
|
154
|
-
matches,
|
|
155
|
-
{ loopId },
|
|
156
|
-
);
|
|
157
|
-
} else {
|
|
158
|
-
await store.upsert(runId, turn, target, entry.body, 200, { loopId });
|
|
145
|
+
// Apply fidelity after all write operations
|
|
146
|
+
if (fidelityAttr && attrs.path) {
|
|
147
|
+
const target = attrs.path;
|
|
148
|
+
const scheme = KnownStore.scheme(target);
|
|
149
|
+
if (scheme !== null) {
|
|
150
|
+
await store.setFidelity(runId, target, fidelityAttr);
|
|
151
|
+
}
|
|
152
|
+
if (summaryText) {
|
|
153
|
+
await store.setAttributes(runId, target, { summary: summaryText });
|
|
154
|
+
}
|
|
159
155
|
}
|
|
160
156
|
}
|
|
161
157
|
|
|
@@ -171,8 +167,8 @@ export default class Set {
|
|
|
171
167
|
return `# set ${file}${tokens}\n${attrs.merge}`;
|
|
172
168
|
}
|
|
173
169
|
|
|
174
|
-
summary(
|
|
175
|
-
return
|
|
170
|
+
summary() {
|
|
171
|
+
return "";
|
|
176
172
|
}
|
|
177
173
|
|
|
178
174
|
async #processEdit(rummy, entry, attrs) {
|
|
@@ -215,7 +211,7 @@ export default class Set {
|
|
|
215
211
|
searchText != null
|
|
216
212
|
? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
|
|
217
213
|
: null;
|
|
218
|
-
const beforeTokens = match.
|
|
214
|
+
const beforeTokens = match.tokens || 0;
|
|
219
215
|
const afterTokens = patch ? countTokens(patch) : beforeTokens;
|
|
220
216
|
|
|
221
217
|
await store.upsert(runId, turn, resultPath, match.body, status, {
|
|
@@ -282,7 +278,7 @@ export default class Set {
|
|
|
282
278
|
? generatePatch(filePath, original, current)
|
|
283
279
|
: null;
|
|
284
280
|
const merge = mergeBlocks.length > 0 ? mergeBlocks.join("\n") : null;
|
|
285
|
-
const beforeTokens = fileEntry[0].
|
|
281
|
+
const beforeTokens = fileEntry[0].tokens || 0;
|
|
286
282
|
const afterTokens = current ? countTokens(current) : beforeTokens;
|
|
287
283
|
|
|
288
284
|
await store.upsert(runId, turn, entry.path, original, state, {
|
|
@@ -334,7 +330,6 @@ export default class Set {
|
|
|
334
330
|
flags: block.flags,
|
|
335
331
|
});
|
|
336
332
|
}
|
|
337
|
-
// Multi-block: apply sequentially, no per-hunk merge notation
|
|
338
333
|
let current = body;
|
|
339
334
|
let lastWarning = null;
|
|
340
335
|
for (const block of attrs.blocks) {
|
|
@@ -2,48 +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="demoted" 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
|
-
'* `summary="..."` (<= 80 chars) persists across fidelity changes',
|
|
37
|
-
"Model-authored descriptions survive demotion. No janitorial pass needed.",
|
|
38
|
-
],
|
|
39
|
-
[
|
|
40
|
-
"* YOU MUST NOT use <sh/> or <env/> to read, create, or edit files",
|
|
41
|
-
"Forces file operations through set/get. Prevents untracked mutations.",
|
|
42
|
-
],
|
|
43
|
-
[
|
|
44
|
-
"* Editing: s/old/new/ sed patterns and literal SEARCH/REPLACE blocks",
|
|
45
|
-
"Both syntaxes supported. Hedberg normalizes either form.",
|
|
46
|
-
],
|
|
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/We're almost done/We're done./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></sh> or <env></env> to list, create, read, or edit files — use <get></get> and <set></set>",
|
|
32
|
+
"Reinforces at the decision point — model reading setDoc for file ops sees the prohibition here, not just buried in shDoc/envDoc which it may not be reading.",
|
|
33
|
+
],
|
|
47
34
|
];
|
|
48
35
|
|
|
49
36
|
export default LINES.map(([text]) => text).join("\n");
|
package/src/plugins/sh/sh.js
CHANGED
|
@@ -7,8 +7,8 @@ export default class Sh {
|
|
|
7
7
|
this.#core = core;
|
|
8
8
|
core.registerScheme();
|
|
9
9
|
core.on("handler", this.handler.bind(this));
|
|
10
|
-
core.on("
|
|
11
|
-
core.on("
|
|
10
|
+
core.on("promoted", this.full.bind(this));
|
|
11
|
+
core.on("demoted", this.summary.bind(this));
|
|
12
12
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
13
13
|
docsMap.sh = docs;
|
|
14
14
|
return docsMap;
|
|
@@ -27,7 +27,7 @@ export default class Sh {
|
|
|
27
27
|
return `# sh ${entry.attributes.command || ""}\n${entry.body}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
summary(
|
|
31
|
-
return
|
|
30
|
+
summary() {
|
|
31
|
+
return "";
|
|
32
32
|
}
|
|
33
33
|
}
|
package/src/plugins/sh/shDoc.js
CHANGED
|
@@ -2,26 +2,21 @@
|
|
|
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
|
-
"* YOU MUST NOT use <sh
|
|
21
|
-
"Forces file operations through the entry system.
|
|
15
|
+
"* YOU MUST NOT use <sh></sh> to read, create, or edit files — use <get></get> and <set></set>",
|
|
16
|
+
"Forces file operations through the entry system.",
|
|
22
17
|
],
|
|
23
18
|
[
|
|
24
|
-
"* YOU MUST use <env
|
|
19
|
+
"* YOU MUST use <env></env> for commands without side effects",
|
|
25
20
|
"Reinforces the env/sh split. Read = env, mutate = sh.",
|
|
26
21
|
],
|
|
27
22
|
];
|
|
@@ -10,7 +10,8 @@ export default class Skill {
|
|
|
10
10
|
name: "skill",
|
|
11
11
|
category: "data",
|
|
12
12
|
});
|
|
13
|
-
core.hooks.tools.onView("skill", (entry) => entry.body);
|
|
13
|
+
core.hooks.tools.onView("skill", (entry) => entry.body, "promoted");
|
|
14
|
+
core.hooks.tools.onView("skill", () => "", "demoted");
|
|
14
15
|
|
|
15
16
|
const r = core.hooks.rpc.registry;
|
|
16
17
|
|
|
@@ -7,14 +7,21 @@ export default class Summarize {
|
|
|
7
7
|
this.#core = core;
|
|
8
8
|
core.ensureTool();
|
|
9
9
|
core.registerScheme({ category: "logging" });
|
|
10
|
-
core.on("
|
|
11
|
-
core.on("
|
|
10
|
+
core.on("handler", this.handler.bind(this));
|
|
11
|
+
core.on("promoted", this.full.bind(this));
|
|
12
|
+
core.on("demoted", this.summary.bind(this));
|
|
12
13
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
13
14
|
docsMap.summarize = docs;
|
|
14
15
|
return docsMap;
|
|
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,25 @@
|
|
|
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
|
-
["## <summarize>[answer or summary]</summarize> - Signal completion"],
|
|
7
|
-
|
|
8
|
-
// --- Examples: answer and task completion
|
|
9
5
|
[
|
|
10
|
-
"
|
|
11
|
-
"
|
|
6
|
+
"## <summarize>[answer or final summary]</summarize> - Terminate the run with the final answer",
|
|
7
|
+
"Header teaches consequence (run ends), not just label. Model now knows emitting this stops everything.",
|
|
12
8
|
],
|
|
13
9
|
[
|
|
14
|
-
"Example: <summarize>
|
|
15
|
-
"
|
|
10
|
+
"Example: <summarize>The port is 8080</summarize>",
|
|
11
|
+
"Direct answer. Summarize delivers answers.",
|
|
16
12
|
],
|
|
17
|
-
|
|
18
|
-
// --- Constraints: RFC-style MUST/MUST NOT
|
|
19
13
|
[
|
|
20
|
-
"*
|
|
21
|
-
"
|
|
14
|
+
"* Urgent: <summarize></summarize> ENDS THE RUN. After this, no more turns happen.",
|
|
15
|
+
"Direct statement of terminal behavior — the model treating summarize as a generic 'done message' was causing zombie-update loops (model unsure if truly finished, defaulted to update).",
|
|
22
16
|
],
|
|
23
17
|
[
|
|
24
|
-
"* YOU MUST NOT
|
|
25
|
-
"
|
|
18
|
+
"* Urgent: YOU MUST NOT include <summarize></summarize> with other tools. Termination is a deliberate, isolated act — not a side effect of a turn doing other things.",
|
|
19
|
+
"Prior 'they might fail' rationale was argued around (when set on known:// succeeds, model rationalized bundling). Reframing as architectural ('termination is deliberate') removes the argument surface.",
|
|
26
20
|
],
|
|
27
21
|
[
|
|
28
|
-
"* YOU MUST keep <summarize> to <= 80 characters",
|
|
29
|
-
"Length cap.
|
|
22
|
+
"* YOU MUST keep <summarize></summarize> to <= 80 characters",
|
|
23
|
+
"Length cap.",
|
|
30
24
|
],
|
|
31
25
|
];
|
|
32
26
|
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
export default class Telemetry {
|
|
5
5
|
#core;
|
|
6
6
|
#starts = new Map();
|
|
7
7
|
#lastRunPath = null;
|
|
8
|
+
#turnsDir = null;
|
|
8
9
|
#turnLog = [];
|
|
10
|
+
#currentRunAlias = null;
|
|
11
|
+
#currentTurn = null;
|
|
9
12
|
|
|
10
13
|
constructor(core) {
|
|
11
14
|
this.#core = core;
|
|
12
15
|
|
|
13
16
|
const home = process.env.RUMMY_HOME;
|
|
14
|
-
if (home)
|
|
17
|
+
if (home) {
|
|
18
|
+
this.#lastRunPath = join(home, "last_run.txt");
|
|
19
|
+
this.#turnsDir = join(home, "turns");
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
core.on("rpc.started", this.#onRpcStarted.bind(this));
|
|
17
23
|
core.on("rpc.completed", this.#onRpcCompleted.bind(this));
|
|
@@ -85,20 +91,20 @@ export default class Telemetry {
|
|
|
85
91
|
// assistant://N — the model's raw response
|
|
86
92
|
await store.upsert(runId, turn, `assistant://${turn}`, content, 200, {
|
|
87
93
|
loopId,
|
|
88
|
-
fidelity: "
|
|
94
|
+
fidelity: "archived",
|
|
89
95
|
});
|
|
90
96
|
|
|
91
97
|
// system://N, user://N — assembled messages as audit
|
|
92
98
|
if (systemMsg) {
|
|
93
99
|
await store.upsert(runId, turn, `system://${turn}`, systemMsg, 200, {
|
|
94
100
|
loopId,
|
|
95
|
-
fidelity: "
|
|
101
|
+
fidelity: "archived",
|
|
96
102
|
});
|
|
97
103
|
}
|
|
98
104
|
if (userMsg) {
|
|
99
105
|
await store.upsert(runId, turn, `user://${turn}`, userMsg, 200, {
|
|
100
106
|
loopId,
|
|
101
|
-
fidelity: "
|
|
107
|
+
fidelity: "archived",
|
|
102
108
|
});
|
|
103
109
|
}
|
|
104
110
|
|
|
@@ -115,7 +121,7 @@ export default class Telemetry {
|
|
|
115
121
|
model: result.model || null,
|
|
116
122
|
}),
|
|
117
123
|
200,
|
|
118
|
-
{ loopId, fidelity: "
|
|
124
|
+
{ loopId, fidelity: "archived" },
|
|
119
125
|
);
|
|
120
126
|
|
|
121
127
|
// reasoning://N
|
|
@@ -126,15 +132,18 @@ export default class Telemetry {
|
|
|
126
132
|
`reasoning://${turn}`,
|
|
127
133
|
responseMessage.reasoning_content,
|
|
128
134
|
200,
|
|
129
|
-
{ loopId, fidelity: "
|
|
135
|
+
{ loopId, fidelity: "archived" },
|
|
130
136
|
);
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
// content://N — unparsed text
|
|
139
|
+
// content://N — unparsed text. 400 Bad Request because anything in
|
|
140
|
+
// unparsed is text the parser couldn't dispatch (malformed XML, native
|
|
141
|
+
// tool call attempts, reasoning bleed). Visible to the model so it
|
|
142
|
+
// sees the rejection on its next turn and can correct.
|
|
134
143
|
if (unparsed) {
|
|
135
|
-
await store.upsert(runId, turn, `content://${turn}`, unparsed,
|
|
144
|
+
await store.upsert(runId, turn, `content://${turn}`, unparsed, 400, {
|
|
136
145
|
loopId,
|
|
137
|
-
fidelity: "
|
|
146
|
+
fidelity: "promoted",
|
|
138
147
|
});
|
|
139
148
|
}
|
|
140
149
|
|
|
@@ -168,8 +177,10 @@ export default class Telemetry {
|
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
async #logMessages(messages, context) {
|
|
180
|
+
this.#currentRunAlias = context.runAlias || `run_${context.runId}`;
|
|
181
|
+
this.#currentTurn = context.turn ?? null;
|
|
171
182
|
this.#turnLog.push(
|
|
172
|
-
`\n${"=".repeat(60)}\nTURN — model=${context.model} run=${
|
|
183
|
+
`\n${"=".repeat(60)}\nTURN ${this.#currentTurn ?? "?"} — model=${context.model} run=${this.#currentRunAlias}\n${"=".repeat(60)}`,
|
|
173
184
|
);
|
|
174
185
|
for (const msg of messages) {
|
|
175
186
|
const label = msg.role.toUpperCase();
|
|
@@ -191,6 +202,7 @@ export default class Telemetry {
|
|
|
191
202
|
const usage = response.usage || {};
|
|
192
203
|
this.#turnLog.push(`\n--- USAGE ---\n${JSON.stringify(usage)}`);
|
|
193
204
|
this.#flush();
|
|
205
|
+
this.#writeTurnFile();
|
|
194
206
|
return response;
|
|
195
207
|
}
|
|
196
208
|
|
|
@@ -200,4 +212,17 @@ export default class Telemetry {
|
|
|
200
212
|
() => {},
|
|
201
213
|
);
|
|
202
214
|
}
|
|
215
|
+
|
|
216
|
+
async #writeTurnFile() {
|
|
217
|
+
if (!this.#turnsDir || !this.#currentRunAlias || this.#currentTurn == null)
|
|
218
|
+
return;
|
|
219
|
+
const runDir = join(this.#turnsDir, this.#currentRunAlias);
|
|
220
|
+
try {
|
|
221
|
+
await mkdir(runDir, { recursive: true });
|
|
222
|
+
const fileName = `turn_${String(this.#currentTurn).padStart(3, "0")}.txt`;
|
|
223
|
+
await writeFile(join(runDir, fileName), `${this.#turnLog.join("\n")}\n`);
|
|
224
|
+
} catch {
|
|
225
|
+
// best effort — diagnostic feature, don't fail the turn
|
|
226
|
+
}
|
|
227
|
+
}
|
|
203
228
|
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import docs from "./thinkDoc.js";
|
|
2
|
+
|
|
3
|
+
const THINK_ENABLED = process.env.RUMMY_THINK;
|
|
4
|
+
if (THINK_ENABLED === undefined)
|
|
5
|
+
throw new Error("RUMMY_THINK must be set (1 or 0)");
|
|
6
|
+
|
|
1
7
|
export default class Think {
|
|
2
8
|
constructor(core) {
|
|
3
9
|
core.registerScheme({ modelVisible: 0, category: "logging" });
|
|
10
|
+
if (THINK_ENABLED === "1") {
|
|
11
|
+
core.ensureTool();
|
|
12
|
+
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
13
|
+
docsMap.think = docs;
|
|
14
|
+
return docsMap;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
4
17
|
}
|
|
5
18
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
["## <think>[reasoning]</think> - Think before acting"],
|
|
6
|
+
[
|
|
7
|
+
"* Use <think></think> before any other tools to plan your approach",
|
|
8
|
+
"Positioning: think first, then act. Prevents degenerate tool-call storms.",
|
|
9
|
+
],
|
|
10
|
+
[
|
|
11
|
+
"* Reasoning inside <think></think> is private — it does not appear in your context",
|
|
12
|
+
"Frees the model to reason without consuming context budget.",
|
|
13
|
+
],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export default LINES.map(([text]) => text).join("\n");
|