@possumtech/rummy 0.3.1 → 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 +11 -0
- package/README.md +5 -1
- package/SPEC.md +31 -17
- package/migrations/001_initial_schema.sql +2 -3
- package/package.json +1 -1
- package/src/agent/AgentLoop.js +50 -151
- package/src/agent/KnownStore.js +15 -7
- package/src/agent/TurnExecutor.js +75 -318
- package/src/agent/XmlParser.js +25 -4
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +11 -61
- package/src/agent/runs.sql +2 -2
- package/src/hooks/Hooks.js +1 -0
- package/src/hooks/ToolRegistry.js +6 -5
- package/src/plugins/ask_user/ask_userDoc.js +3 -8
- package/src/plugins/budget/README.md +26 -18
- package/src/plugins/budget/budget.js +60 -3
- package/src/plugins/budget/recovery.js +47 -0
- package/src/plugins/cp/cpDoc.js +4 -9
- package/src/plugins/env/envDoc.js +3 -8
- package/src/plugins/get/get.js +2 -4
- package/src/plugins/get/getDoc.js +11 -18
- package/src/plugins/helpers.js +2 -2
- package/src/plugins/instructions/instructions.js +3 -2
- package/src/plugins/instructions/preamble.md +27 -16
- package/src/plugins/known/known.js +63 -8
- package/src/plugins/known/knownDoc.js +10 -14
- package/src/plugins/mv/mvDoc.js +6 -21
- package/src/plugins/policy/policy.js +47 -0
- package/src/plugins/progress/progress.js +9 -45
- package/src/plugins/prompt/prompt.js +10 -1
- package/src/plugins/rm/rmDoc.js +5 -10
- package/src/plugins/rpc/rpc.js +3 -1
- package/src/plugins/set/set.js +82 -85
- package/src/plugins/set/setDoc.js +28 -41
- 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/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +18 -0
- package/src/plugins/unknown/unknown.js +21 -0
- 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 +11 -1
- package/src/sql/v_model_context.sql +4 -4
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 = { archive: 1, summary: 1,
|
|
7
|
+
const VALID_FIDELITY = { archive: 1, summary: 1, full: 1 };
|
|
8
8
|
|
|
9
9
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
|
|
10
10
|
export default class Set {
|
|
@@ -26,47 +26,19 @@ 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 =
|
|
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) {
|
|
33
36
|
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
37
|
const matches = await store.getEntriesByPattern(
|
|
38
38
|
runId,
|
|
39
39
|
target,
|
|
40
40
|
attrs.body,
|
|
41
41
|
);
|
|
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
42
|
if (matches.length === 0) {
|
|
71
43
|
await store.upsert(
|
|
72
44
|
runId,
|
|
@@ -74,13 +46,18 @@ export default class Set {
|
|
|
74
46
|
entry.resultPath,
|
|
75
47
|
`${target} not found`,
|
|
76
48
|
404,
|
|
77
|
-
{
|
|
78
|
-
fidelity: "archive",
|
|
79
|
-
loopId,
|
|
80
|
-
},
|
|
49
|
+
{ fidelity: "archive", loopId },
|
|
81
50
|
);
|
|
82
51
|
return;
|
|
83
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,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
84
61
|
const label =
|
|
85
62
|
fidelityAttr === "archive" ? "archived" : `set to ${fidelityAttr}`;
|
|
86
63
|
await store.upsert(
|
|
@@ -89,20 +66,16 @@ export default class Set {
|
|
|
89
66
|
entry.resultPath,
|
|
90
67
|
`${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
91
68
|
200,
|
|
92
|
-
{
|
|
93
|
-
fidelity: "archive",
|
|
94
|
-
loopId,
|
|
95
|
-
},
|
|
69
|
+
{ fidelity: "archive", loopId },
|
|
96
70
|
);
|
|
97
71
|
return;
|
|
98
72
|
}
|
|
99
73
|
|
|
74
|
+
// Edit: sed patterns or SEARCH/REPLACE blocks
|
|
100
75
|
if (attrs.blocks || attrs.search != null) {
|
|
101
76
|
await this.#processEdit(rummy, entry, attrs);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (attrs.preview && attrs.path) {
|
|
77
|
+
} else if (attrs.preview && attrs.path) {
|
|
78
|
+
// Preview
|
|
106
79
|
const matches = await store.getEntriesByPattern(
|
|
107
80
|
runId,
|
|
108
81
|
attrs.path,
|
|
@@ -119,43 +92,68 @@ export default class Set {
|
|
|
119
92
|
{ preview: true, loopId },
|
|
120
93
|
);
|
|
121
94
|
return;
|
|
122
|
-
}
|
|
95
|
+
} else {
|
|
96
|
+
// Write content
|
|
97
|
+
const target = attrs.path;
|
|
98
|
+
if (!target) return;
|
|
123
99
|
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
}
|
|
126
146
|
|
|
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 });
|
|
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
|
+
}
|
|
159
157
|
}
|
|
160
158
|
}
|
|
161
159
|
|
|
@@ -215,7 +213,7 @@ export default class Set {
|
|
|
215
213
|
searchText != null
|
|
216
214
|
? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
|
|
217
215
|
: null;
|
|
218
|
-
const beforeTokens = match.
|
|
216
|
+
const beforeTokens = match.tokens || 0;
|
|
219
217
|
const afterTokens = patch ? countTokens(patch) : beforeTokens;
|
|
220
218
|
|
|
221
219
|
await store.upsert(runId, turn, resultPath, match.body, status, {
|
|
@@ -282,7 +280,7 @@ export default class Set {
|
|
|
282
280
|
? generatePatch(filePath, original, current)
|
|
283
281
|
: null;
|
|
284
282
|
const merge = mergeBlocks.length > 0 ? mergeBlocks.join("\n") : null;
|
|
285
|
-
const beforeTokens = fileEntry[0].
|
|
283
|
+
const beforeTokens = fileEntry[0].tokens || 0;
|
|
286
284
|
const afterTokens = current ? countTokens(current) : beforeTokens;
|
|
287
285
|
|
|
288
286
|
await store.upsert(runId, turn, entry.path, original, state, {
|
|
@@ -334,7 +332,6 @@ export default class Set {
|
|
|
334
332
|
flags: block.flags,
|
|
335
333
|
});
|
|
336
334
|
}
|
|
337
|
-
// Multi-block: apply sequentially, no per-hunk merge notation
|
|
338
335
|
let current = body;
|
|
339
336
|
let lastWarning = null;
|
|
340
337
|
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="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
|
-
'* `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/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
|
+
],
|
|
47
34
|
];
|
|
48
35
|
|
|
49
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,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");
|
|
@@ -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,10 +19,29 @@ 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;
|
|
@@ -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
|
|
|
@@ -7,6 +7,7 @@ export default class Update {
|
|
|
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 Update {
|
|
|
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, "update", entry.body);
|
|
22
|
+
await store.upsert(runId, turn, statusPath, entry.body, 200, { loopId });
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
full(entry) {
|
|
19
26
|
return `# update\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
|
["## <update>[brief status]</update> - Signal continuation"],
|
|
7
|
-
|
|
8
|
-
// --- Examples: research progress and multi-step work
|
|
9
6
|
[
|
|
10
7
|
"Example: <update>Reading config files</update>",
|
|
11
|
-
"Progress checkpoint.
|
|
8
|
+
"Progress checkpoint. Status signal, not a log entry.",
|
|
12
9
|
],
|
|
13
10
|
[
|
|
14
11
|
"Example: <update>Found 3 issues, fixing first</update>",
|
|
15
|
-
"Multi-step progress.
|
|
12
|
+
"Multi-step progress. Ongoing work.",
|
|
16
13
|
],
|
|
17
|
-
|
|
18
|
-
// --- Constraints: RFC-style MUST/MUST NOT
|
|
19
14
|
[
|
|
20
|
-
"* YOU MUST use <update> if still working — describes the current state",
|
|
21
|
-
"Continuation signal. Triggers the next turn
|
|
15
|
+
"* YOU MUST use <update></update> if still working — describes the current state",
|
|
16
|
+
"Continuation signal. Triggers the next turn.",
|
|
22
17
|
],
|
|
23
18
|
[
|
|
24
19
|
"* YOU MUST NOT use <update> if done — use <summarize/> instead",
|
|
25
|
-
"Mutual exclusion with summarize.
|
|
20
|
+
"Mutual exclusion with summarize.",
|
|
26
21
|
],
|
|
27
22
|
[
|
|
28
23
|
"* YOU MUST keep <update> to <= 80 characters",
|
|
29
|
-
"Length cap.
|
|
24
|
+
"Length cap.",
|
|
30
25
|
],
|
|
31
26
|
];
|
|
32
27
|
|
|
@@ -36,6 +36,15 @@ export default class ClientConnection {
|
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
#onProposal = (payload) => {
|
|
40
|
+
if (payload.projectId === this.#context.projectId) {
|
|
41
|
+
this.#sendNotification("run/proposal", {
|
|
42
|
+
run: payload.run,
|
|
43
|
+
proposed: payload.proposed,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
39
48
|
#onRender = (payload) => {
|
|
40
49
|
if (payload.projectId === this.#context.projectId) {
|
|
41
50
|
this.#sendNotification("ui/render", {
|
|
@@ -63,7 +72,6 @@ export default class ClientConnection {
|
|
|
63
72
|
summary: payload.summary,
|
|
64
73
|
history: payload.history,
|
|
65
74
|
unknowns: payload.unknowns,
|
|
66
|
-
proposed: payload.proposed,
|
|
67
75
|
telemetry: payload.telemetry,
|
|
68
76
|
});
|
|
69
77
|
}
|
|
@@ -71,6 +79,7 @@ export default class ClientConnection {
|
|
|
71
79
|
|
|
72
80
|
#setupNotifications() {
|
|
73
81
|
this.#hooks.run.progress.on(this.#onProgress);
|
|
82
|
+
this.#hooks.turn.proposal.on(this.#onProposal);
|
|
74
83
|
this.#hooks.ui.render.on(this.#onRender);
|
|
75
84
|
this.#hooks.ui.notify.on(this.#onNotify);
|
|
76
85
|
this.#hooks.run.state.on(this.#onState);
|
|
@@ -78,6 +87,7 @@ export default class ClientConnection {
|
|
|
78
87
|
|
|
79
88
|
#teardown() {
|
|
80
89
|
this.#hooks.run.progress.off(this.#onProgress);
|
|
90
|
+
this.#hooks.turn.proposal.off(this.#onProposal);
|
|
81
91
|
this.#hooks.ui.render.off(this.#onRender);
|
|
82
92
|
this.#hooks.ui.notify.off(this.#onNotify);
|
|
83
93
|
this.#hooks.run.state.off(this.#onState);
|
|
@@ -13,7 +13,7 @@ visible AS (
|
|
|
13
13
|
, ke.turn
|
|
14
14
|
, ke.updated_at
|
|
15
15
|
, ke.attributes
|
|
16
|
-
, ke.tokens
|
|
16
|
+
, ke.tokens
|
|
17
17
|
, COALESCE(s.category, 'logging') AS category
|
|
18
18
|
, CASE
|
|
19
19
|
-- Archived entries not in context
|
|
@@ -41,6 +41,7 @@ projected AS (
|
|
|
41
41
|
, attributes
|
|
42
42
|
-- Category comes from schemes table — plugins declare it via registerScheme().
|
|
43
43
|
, category
|
|
44
|
+
, tokens
|
|
44
45
|
, CASE
|
|
45
46
|
WHEN visible_fidelity IN ('full', 'summary') THEN body
|
|
46
47
|
ELSE ''
|
|
@@ -71,9 +72,8 @@ SELECT
|
|
|
71
72
|
END
|
|
72
73
|
, CASE scheme WHEN 'skill' THEN 0 ELSE 1 END
|
|
73
74
|
, CASE fidelity
|
|
74
|
-
WHEN '
|
|
75
|
-
|
|
76
|
-
ELSE 2
|
|
75
|
+
WHEN 'summary' THEN 0
|
|
76
|
+
ELSE 1
|
|
77
77
|
END
|
|
78
78
|
, turn
|
|
79
79
|
, updated_at
|