@possumtech/rummy 0.3.0 → 0.3.1
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 +2 -1
- package/PLUGINS.md +1 -1
- package/SPEC.md +181 -38
- package/migrations/001_initial_schema.sql +1 -1
- package/package.json +7 -3
- package/service.js +5 -3
- package/src/agent/AgentLoop.js +182 -136
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/KnownStore.js +28 -85
- package/src/agent/ResponseHealer.js +65 -31
- package/src/agent/TurnExecutor.js +326 -181
- package/src/agent/XmlParser.js +5 -2
- package/src/agent/known_store.sql +48 -0
- 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 +1 -4
- package/src/hooks/ToolRegistry.js +2 -8
- package/src/plugins/budget/README.md +2 -14
- package/src/plugins/budget/budget.js +15 -39
- package/src/plugins/cp/cp.js +1 -1
- package/src/plugins/cp/cpDoc.js +1 -1
- package/src/plugins/get/get.js +71 -1
- package/src/plugins/get/getDoc.js +14 -4
- package/src/plugins/hedberg/matcher.js +10 -29
- package/src/plugins/instructions/preamble.md +16 -6
- package/src/plugins/known/known.js +4 -10
- package/src/plugins/known/knownDoc.js +15 -14
- package/src/plugins/mv/mv.js +18 -1
- package/src/plugins/mv/mvDoc.js +15 -1
- package/src/plugins/{current → performed}/README.md +4 -3
- package/src/plugins/{current/current.js → performed/performed.js} +15 -20
- 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 +15 -29
- package/src/plugins/prompt/prompt.js +0 -7
- package/src/plugins/rm/rm.js +27 -15
- package/src/plugins/rm/rmDoc.js +3 -3
- package/src/plugins/set/set.js +55 -19
- package/src/plugins/set/setDoc.js +6 -2
- package/src/plugins/telemetry/telemetry.js +14 -9
- package/src/plugins/unknown/README.md +2 -1
- package/src/plugins/unknown/unknown.js +5 -4
- package/src/server/ClientConnection.js +59 -45
- package/src/sql/v_model_context.sql +3 -13
- package/src/plugins/budget/BudgetGuard.js +0 -74
|
@@ -189,3 +189,51 @@ WHERE
|
|
|
189
189
|
run_id = :run_id
|
|
190
190
|
AND hedmatch(:path, path)
|
|
191
191
|
AND (:body IS NULL OR hedsearch(:body, body));
|
|
192
|
+
|
|
193
|
+
-- PREP: restore_summarized_prompts
|
|
194
|
+
-- Restore prompt entries demoted to summary by a recovery phase that was
|
|
195
|
+
-- interrupted (e.g. server crash). Safe to call unconditionally at loop
|
|
196
|
+
-- start: if the full prompt would overflow, Prompt Demotion handles it.
|
|
197
|
+
UPDATE known_entries
|
|
198
|
+
SET
|
|
199
|
+
fidelity = 'full'
|
|
200
|
+
, tokens = tokens_full
|
|
201
|
+
, updated_at = CURRENT_TIMESTAMP
|
|
202
|
+
WHERE run_id = :run_id AND scheme = 'prompt' AND fidelity = 'summary';
|
|
203
|
+
|
|
204
|
+
-- PREP: demote_previous_loop_logging
|
|
205
|
+
-- Demote full logging entries from all other loops to summary.
|
|
206
|
+
-- Fires at loop start so <previous> entries are already compact.
|
|
207
|
+
UPDATE known_entries
|
|
208
|
+
SET
|
|
209
|
+
fidelity = 'summary'
|
|
210
|
+
, tokens = COALESCE(
|
|
211
|
+
countTokens(json_extract(attributes, '$.summary'))
|
|
212
|
+
, countTokens(substr(body, 1, 80))
|
|
213
|
+
)
|
|
214
|
+
, updated_at = CURRENT_TIMESTAMP
|
|
215
|
+
WHERE
|
|
216
|
+
run_id = :run_id
|
|
217
|
+
AND (loop_id IS NULL OR loop_id != :loop_id)
|
|
218
|
+
AND fidelity = 'full'
|
|
219
|
+
AND scheme IN (SELECT name FROM schemes WHERE category = 'logging');
|
|
220
|
+
|
|
221
|
+
-- PREP: demote_turn_data_entries
|
|
222
|
+
-- Demote full data entries from a turn to summary with 413 status.
|
|
223
|
+
-- Fires when end-of-turn materialization exceeds the context ceiling.
|
|
224
|
+
UPDATE known_entries
|
|
225
|
+
SET
|
|
226
|
+
fidelity = 'summary'
|
|
227
|
+
, status = 413
|
|
228
|
+
, tokens = COALESCE(
|
|
229
|
+
countTokens(json_extract(attributes, '$.summary'))
|
|
230
|
+
, countTokens(substr(body, 1, 80))
|
|
231
|
+
)
|
|
232
|
+
, updated_at = CURRENT_TIMESTAMP
|
|
233
|
+
WHERE
|
|
234
|
+
run_id = :run_id
|
|
235
|
+
AND turn = :turn
|
|
236
|
+
AND fidelity = 'full'
|
|
237
|
+
AND status < 400
|
|
238
|
+
AND scheme IN (SELECT name FROM schemes WHERE category = 'data')
|
|
239
|
+
RETURNING path;
|
package/src/agent/tokens.js
CHANGED
package/src/agent/turns.sql
CHANGED
|
@@ -27,6 +27,11 @@ SELECT
|
|
|
27
27
|
FROM turns
|
|
28
28
|
WHERE run_id = :run_id;
|
|
29
29
|
|
|
30
|
+
-- PREP: get_turn_context_tokens
|
|
31
|
+
SELECT context_tokens
|
|
32
|
+
FROM turns
|
|
33
|
+
WHERE run_id = :run_id AND sequence = :sequence;
|
|
34
|
+
|
|
30
35
|
-- PREP: get_last_context_tokens
|
|
31
36
|
SELECT context_tokens
|
|
32
37
|
FROM turns
|
|
@@ -63,6 +63,13 @@ export default class HookRegistry {
|
|
|
63
63
|
this.#events.get(tag).sort((a, b) => a.priority - b.priority);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
removeEvent(tag, callback) {
|
|
67
|
+
const hooks = this.#events.get(tag);
|
|
68
|
+
if (!hooks) return;
|
|
69
|
+
const idx = hooks.findIndex((h) => h.callback === callback);
|
|
70
|
+
if (idx !== -1) hooks.splice(idx, 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
async emitEvent(tag, ...args) {
|
|
67
74
|
const hooks = this.#events.get(tag) || [];
|
|
68
75
|
for (const h of hooks) {
|
package/src/hooks/Hooks.js
CHANGED
|
@@ -11,6 +11,7 @@ export default function createHooks(debug = false) {
|
|
|
11
11
|
|
|
12
12
|
const createEvent = (tag) => ({
|
|
13
13
|
on: (callback, priority) => registry.addEvent(tag, callback, priority),
|
|
14
|
+
off: (callback) => registry.removeEvent(tag, callback),
|
|
14
15
|
emit: (...args) => registry.emitEvent(tag, ...args),
|
|
15
16
|
});
|
|
16
17
|
|
|
@@ -73,10 +74,6 @@ export default function createHooks(debug = false) {
|
|
|
73
74
|
started: createEvent("act.started"),
|
|
74
75
|
completed: createEvent("act.completed"),
|
|
75
76
|
},
|
|
76
|
-
panic: {
|
|
77
|
-
started: createEvent("panic.started"),
|
|
78
|
-
completed: createEvent("panic.completed"),
|
|
79
|
-
},
|
|
80
77
|
llm: {
|
|
81
78
|
request: {
|
|
82
79
|
started: createEvent("llm.request.started"),
|
|
@@ -118,19 +118,13 @@ export default class ToolRegistry {
|
|
|
118
118
|
*/
|
|
119
119
|
resolveForLoop(
|
|
120
120
|
mode,
|
|
121
|
-
{ noInteraction = false, noWeb = false,
|
|
121
|
+
{ noInteraction = false, noWeb = false, noProposals = false } = {},
|
|
122
122
|
) {
|
|
123
123
|
const excluded = new Set();
|
|
124
124
|
if (mode === "ask") excluded.add("sh");
|
|
125
|
-
if (mode === "panic") {
|
|
126
|
-
excluded.add("sh");
|
|
127
|
-
excluded.add("env");
|
|
128
|
-
excluded.add("search");
|
|
129
|
-
excluded.add("ask_user");
|
|
130
|
-
}
|
|
131
125
|
if (noInteraction) excluded.add("ask_user");
|
|
132
126
|
if (noWeb) excluded.add("search");
|
|
133
|
-
if (
|
|
127
|
+
if (noProposals) {
|
|
134
128
|
excluded.add("ask_user");
|
|
135
129
|
excluded.add("env");
|
|
136
130
|
excluded.add("sh");
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# budget
|
|
2
2
|
|
|
3
|
-
Context ceiling enforcement
|
|
3
|
+
Context ceiling enforcement.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
7
|
-
- **budget.js** — Plugin. Pre-LLM enforce, BudgetGuard activation
|
|
8
|
-
panic prompt generation.
|
|
7
|
+
- **budget.js** — Plugin. Pre-LLM enforce, BudgetGuard activation.
|
|
9
8
|
- **BudgetGuard.js** — Write-layer gate. Installed on KnownStore during
|
|
10
9
|
dispatch. Checks token delta on every upsert, promote, and body update.
|
|
11
10
|
|
|
@@ -14,7 +13,6 @@ Context ceiling enforcement and panic mode recovery.
|
|
|
14
13
|
- **Hook**: `hooks.budget.enforce` — pre-LLM ceiling check.
|
|
15
14
|
- **Hook**: `hooks.budget.activate(store, contextSize, assembledTokens)` — install guard.
|
|
16
15
|
- **Hook**: `hooks.budget.deactivate(store)` — remove guard.
|
|
17
|
-
- **Hook**: `hooks.budget.panicPrompt({ shortfall, assembledTokens, contextSize })` — generate panic prompt.
|
|
18
16
|
|
|
19
17
|
## Budget Contract
|
|
20
18
|
|
|
@@ -31,13 +29,3 @@ Exemptions: `status >= 400` (error entries), `model_visible = 0` (audit),
|
|
|
31
29
|
|
|
32
30
|
On first violation: `BudgetExceeded` thrown, guard trips, all subsequent
|
|
33
31
|
writes fail. TurnExecutor catches per-tool, writes 413 result entry.
|
|
34
|
-
|
|
35
|
-
## Panic Mode
|
|
36
|
-
|
|
37
|
-
When a new prompt exceeds the ceiling, AgentLoop enqueues a panic loop.
|
|
38
|
-
The model receives the exact shortfall and must free space using core
|
|
39
|
-
tools (get, set, known, unknown, rm, mv, cp, summarize, update).
|
|
40
|
-
Excluded: sh, env, search, ask_user.
|
|
41
|
-
|
|
42
|
-
Strike system: 3 consecutive turns without context reduction = hard 413.
|
|
43
|
-
Any reduction resets the counter. One panic attempt per drain cycle.
|
|
@@ -1,56 +1,42 @@
|
|
|
1
1
|
import { countTokens } from "../../agent/tokens.js";
|
|
2
|
-
import BudgetGuard, { BudgetExceeded } from "./BudgetGuard.js";
|
|
3
2
|
|
|
4
3
|
function measureMessages(messages) {
|
|
5
4
|
return messages.reduce((sum, m) => sum + countTokens(m.content), 0);
|
|
6
5
|
}
|
|
7
6
|
|
|
8
|
-
export { BudgetExceeded };
|
|
9
|
-
|
|
10
7
|
export default class Budget {
|
|
11
8
|
#core;
|
|
12
9
|
|
|
13
10
|
constructor(core) {
|
|
14
11
|
this.#core = core;
|
|
12
|
+
core.registerScheme({
|
|
13
|
+
name: "budget",
|
|
14
|
+
modelVisible: 1,
|
|
15
|
+
category: "logging",
|
|
16
|
+
});
|
|
17
|
+
core.hooks.tools.onView("budget", (entry) => entry.body);
|
|
15
18
|
core.hooks.budget = {
|
|
16
19
|
enforce: this.enforce.bind(this),
|
|
17
|
-
activate: this.activate.bind(this),
|
|
18
|
-
deactivate: this.deactivate.bind(this),
|
|
19
|
-
panicPrompt: Budget.panicPrompt,
|
|
20
|
-
BudgetExceeded,
|
|
21
20
|
};
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
const target = Math.floor(contextSize * 0.75);
|
|
26
|
-
const mustFree = assembledTokens - target;
|
|
27
|
-
return [
|
|
28
|
-
`CONTEXT OVERFLOW: ${assembledTokens} tokens, ceiling ${contextSize}.`,
|
|
29
|
-
`YOU MUST free ${mustFree} tokens to get below ${target} (75%).`,
|
|
30
|
-
"YOU MUST NOT load or create new content. Only reduce.",
|
|
31
|
-
"",
|
|
32
|
-
"<knowns> above shows each entry with its token count.",
|
|
33
|
-
"Target the largest entries first.",
|
|
34
|
-
'<rm path="..."/> to delete entries you no longer need.',
|
|
35
|
-
'<set path="..." fidelity="summary" summary="keywords"/> to compress.',
|
|
36
|
-
'<set path="..." fidelity="archive"/> to archive out of context.',
|
|
37
|
-
"<summarize/> when done. <update/> if still working.",
|
|
38
|
-
].join("\n");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async enforce({ contextSize, messages, rows }) {
|
|
23
|
+
async enforce({ contextSize, messages, rows, lastPromptTokens = 0 }) {
|
|
42
24
|
if (!contextSize) {
|
|
43
25
|
return { messages, rows, demoted: [], assembledTokens: 0, status: 200 };
|
|
44
26
|
}
|
|
45
27
|
|
|
46
|
-
|
|
28
|
+
// Prefer actual prompt_tokens from the last API response — the estimate
|
|
29
|
+
// from measureMessages can be wildly off for structured/XML-heavy content.
|
|
30
|
+
const assembledTokens =
|
|
31
|
+
lastPromptTokens > 0 ? lastPromptTokens : measureMessages(messages);
|
|
47
32
|
|
|
48
33
|
console.warn(
|
|
49
|
-
`[RUMMY] Budget enforce: ${assembledTokens} tokens, ceiling ${contextSize}, ${rows.length} rows`,
|
|
34
|
+
`[RUMMY] Budget enforce: ${assembledTokens} tokens (${lastPromptTokens > 0 ? "actual" : "estimated"}), ceiling ${contextSize}, ${rows.length} rows`,
|
|
50
35
|
);
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
const ceiling = Math.floor(contextSize * 0.9);
|
|
38
|
+
if (assembledTokens > ceiling) {
|
|
39
|
+
const overflow = assembledTokens - ceiling;
|
|
54
40
|
console.warn(
|
|
55
41
|
`[RUMMY] Budget 413: ${assembledTokens} tokens > ${contextSize} ceiling (${overflow} over)`,
|
|
56
42
|
);
|
|
@@ -66,14 +52,4 @@ export default class Budget {
|
|
|
66
52
|
|
|
67
53
|
return { messages, rows, demoted: [], assembledTokens, status: 200 };
|
|
68
54
|
}
|
|
69
|
-
|
|
70
|
-
activate(store, contextSize, assembledTokens) {
|
|
71
|
-
const guard = new BudgetGuard(contextSize, assembledTokens);
|
|
72
|
-
store.budgetGuard = guard;
|
|
73
|
-
return guard;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
deactivate(store) {
|
|
77
|
-
store.budgetGuard = null;
|
|
78
|
-
}
|
|
79
55
|
}
|
package/src/plugins/cp/cp.js
CHANGED
|
@@ -19,7 +19,7 @@ export default class Cp {
|
|
|
19
19
|
async handler(entry, rummy) {
|
|
20
20
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
21
21
|
const { path, to } = entry.attributes;
|
|
22
|
-
const VALID = { stored: 1, summary: 1, index: 1, full: 1 };
|
|
22
|
+
const VALID = { stored: 1, summary: 1, index: 1, full: 1, archive: 1 };
|
|
23
23
|
const fidelity = VALID[entry.attributes.fidelity]
|
|
24
24
|
? entry.attributes.fidelity
|
|
25
25
|
: undefined;
|
package/src/plugins/cp/cpDoc.js
CHANGED
package/src/plugins/get/get.js
CHANGED
|
@@ -30,13 +30,81 @@ export default class Get {
|
|
|
30
30
|
const normalized = KnownStore.normalizePath(target);
|
|
31
31
|
const bodyFilter = entry.attributes.body || null;
|
|
32
32
|
const isPattern = bodyFilter || normalized.includes("*");
|
|
33
|
+
|
|
34
|
+
const line =
|
|
35
|
+
entry.attributes.line != null
|
|
36
|
+
? Math.max(1, parseInt(entry.attributes.line, 10))
|
|
37
|
+
: null;
|
|
38
|
+
const limit =
|
|
39
|
+
entry.attributes.limit != null
|
|
40
|
+
? Math.max(1, parseInt(entry.attributes.limit, 10))
|
|
41
|
+
: null;
|
|
42
|
+
|
|
33
43
|
const matches = await store.getEntriesByPattern(
|
|
34
44
|
runId,
|
|
35
45
|
normalized,
|
|
36
46
|
bodyFilter,
|
|
37
47
|
);
|
|
38
48
|
|
|
49
|
+
// Partial read — no fidelity promotion, returns a line slice as the log item.
|
|
50
|
+
if (line !== null || limit !== null) {
|
|
51
|
+
if (isPattern) {
|
|
52
|
+
await store.upsert(
|
|
53
|
+
runId,
|
|
54
|
+
turn,
|
|
55
|
+
entry.resultPath,
|
|
56
|
+
"line/limit requires a single path, not a glob or body filter",
|
|
57
|
+
400,
|
|
58
|
+
{ loopId },
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (matches.length === 0) {
|
|
63
|
+
await store.upsert(
|
|
64
|
+
runId,
|
|
65
|
+
turn,
|
|
66
|
+
entry.resultPath,
|
|
67
|
+
`${target} not found`,
|
|
68
|
+
200,
|
|
69
|
+
{ loopId },
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const allLines = matches[0].body.split("\n");
|
|
74
|
+
const total = allLines.length;
|
|
75
|
+
const startLine = line ?? 1;
|
|
76
|
+
const startIdx = startLine - 1;
|
|
77
|
+
const endIdx = limit !== null ? Math.min(startIdx + limit, total) : total;
|
|
78
|
+
const slice = allLines.slice(startIdx, endIdx).join("\n");
|
|
79
|
+
const endLine = endIdx;
|
|
80
|
+
const header = `[lines ${startLine}–${endLine} / ${total} total]`;
|
|
81
|
+
await store.upsert(
|
|
82
|
+
runId,
|
|
83
|
+
turn,
|
|
84
|
+
entry.resultPath,
|
|
85
|
+
`${header}\n${slice}`,
|
|
86
|
+
200,
|
|
87
|
+
{ loopId },
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const VALID_FIDELITY = {
|
|
93
|
+
stored: 1,
|
|
94
|
+
summary: 1,
|
|
95
|
+
index: 1,
|
|
96
|
+
full: 1,
|
|
97
|
+
archive: 1,
|
|
98
|
+
};
|
|
99
|
+
const fidelityAttr = VALID_FIDELITY[entry.attributes.fidelity]
|
|
100
|
+
? entry.attributes.fidelity
|
|
101
|
+
: null;
|
|
102
|
+
|
|
39
103
|
await store.promoteByPattern(runId, normalized, bodyFilter, turn);
|
|
104
|
+
if (fidelityAttr) {
|
|
105
|
+
for (const match of matches)
|
|
106
|
+
await store.setFidelity(runId, match.path, fidelityAttr);
|
|
107
|
+
}
|
|
40
108
|
|
|
41
109
|
if (isPattern) {
|
|
42
110
|
await storePatternResult(
|
|
@@ -53,7 +121,9 @@ export default class Get {
|
|
|
53
121
|
const total = matches.reduce((s, m) => s + m.tokens_full, 0);
|
|
54
122
|
const paths = matches.map((m) => m.path).join(", ");
|
|
55
123
|
const body =
|
|
56
|
-
matches.length > 0
|
|
124
|
+
matches.length > 0
|
|
125
|
+
? `${paths} loaded into <knowns> (${total} tokens)`
|
|
126
|
+
: `${target} not found`;
|
|
57
127
|
await store.upsert(runId, turn, entry.resultPath, body, 200, {
|
|
58
128
|
loopId,
|
|
59
129
|
});
|
|
@@ -15,13 +15,19 @@ const LINES = [
|
|
|
15
15
|
"Keyword recall: glob in path, search term in body. Cross-scheme hedberg pattern.",
|
|
16
16
|
],
|
|
17
17
|
[
|
|
18
|
-
'Example: <get path="src/**/*.js" preview>
|
|
19
|
-
"Full pattern: recursive glob + preview + content filter. Shows all 3 features at once.",
|
|
18
|
+
'Example: <get path="src/**/*.js" preview>authentication</get>',
|
|
19
|
+
"Full pattern: recursive glob + preview + content filter. Shows all 3 features at once. Body is a filter keyword, never file content.",
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
// --- Partial read: line/limit — show before constraints so model sees it as a first-class pattern
|
|
23
|
+
[
|
|
24
|
+
'Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>',
|
|
25
|
+
"Partial read. Returns lines 644–723 as the log item without promoting the entry to full. Use summary fidelity to find line numbers, then target the symbol directly.",
|
|
20
26
|
],
|
|
21
27
|
|
|
22
28
|
// --- Constraints: RFC-style. Each prevents a specific failure mode.
|
|
23
29
|
[
|
|
24
|
-
"* Paths accept
|
|
30
|
+
"* Paths accept patterns: `src/**/*.js`, `known://api_*`",
|
|
25
31
|
"Reinforces picomatch patterns work everywhere, not just in examples.",
|
|
26
32
|
],
|
|
27
33
|
[
|
|
@@ -33,7 +39,11 @@ const LINES = [
|
|
|
33
39
|
"Generalizes examples 2-3. Body = filter, not just path.",
|
|
34
40
|
],
|
|
35
41
|
[
|
|
36
|
-
|
|
42
|
+
"* `line` and `limit` read a slice without promoting — patterns not allowed",
|
|
43
|
+
"The no-promotion constraint is what makes partial read safe: context budget is unaffected.",
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
'* Use <set path="..." fidelity="archive"/> to remove loaded content from context',
|
|
37
47
|
"Lifecycle: get→set. Load, read, archive. Prevents context hoarding.",
|
|
38
48
|
],
|
|
39
49
|
];
|
|
@@ -1,34 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
1
|
+
import { createTwoFilesPatch } from "diff";
|
|
5
2
|
|
|
6
3
|
export function generatePatch(filePath, oldContent, newContent) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
`diff -u --label "${filePath}\told" --label "${filePath}\tnew" "${oldPath}" "${newPath}"`,
|
|
17
|
-
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] },
|
|
18
|
-
);
|
|
19
|
-
return result;
|
|
20
|
-
} catch (err) {
|
|
21
|
-
// diff exits 1 when files differ — that's the success case
|
|
22
|
-
if (err.stdout) return err.stdout;
|
|
23
|
-
return "";
|
|
24
|
-
} finally {
|
|
25
|
-
try {
|
|
26
|
-
unlinkSync(oldPath);
|
|
27
|
-
} catch {}
|
|
28
|
-
try {
|
|
29
|
-
unlinkSync(newPath);
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
4
|
+
return createTwoFilesPatch(
|
|
5
|
+
`${filePath}\told`,
|
|
6
|
+
`${filePath}\tnew`,
|
|
7
|
+
oldContent,
|
|
8
|
+
newContent,
|
|
9
|
+
"",
|
|
10
|
+
"",
|
|
11
|
+
{ context: 3 },
|
|
12
|
+
);
|
|
32
13
|
}
|
|
33
14
|
|
|
34
15
|
export default class HeuristicMatcher {
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
You are
|
|
1
|
+
You are a folksonomic memory agent. YOU MUST organize all information into searchable taxonomies with navigable path hierarchies and searchable summary tags, then YOU MAY answer questions and/or take action.
|
|
2
2
|
|
|
3
3
|
# Response Rules
|
|
4
4
|
|
|
5
5
|
Required: YOU MUST respond with Tool Commands in the XML format. YOU MAY use multiple tools in your response.
|
|
6
|
+
|
|
6
7
|
Optional: YOU MAY think in an optional <think></think> tag before using any other Tool Commands.
|
|
7
|
-
|
|
8
|
-
Required: YOU MUST register all
|
|
9
|
-
|
|
10
|
-
Required:
|
|
8
|
+
|
|
9
|
+
Required: YOU MUST register all unknowns with <unknown>[specific thing I need to learn]</unknown>.
|
|
10
|
+
|
|
11
|
+
Required: YOU MUST register all new facts, decisions, and plans with <known path="topic/subtopic" summary="keyword,keyword,keyword">[specific facts, decisions, or plans]</known>.
|
|
12
|
+
Required: Every <known> MUST include summary="keyword,keyword" tags.
|
|
13
|
+
Info: Paths are addresses for tools. Summary tags tell you what's inside.
|
|
14
|
+
Info: Path and summary information is approximate. YOU MUST use <get/> to verify before acting on summarized content.
|
|
11
15
|
Info: When information conflicts, later turns are more likely to be relevant and correct than earlier turns.
|
|
12
|
-
Info: Your context is limited but your
|
|
16
|
+
Info: Your context is limited but your archive is not. Organize and categorize your facts, decisions, plans, and history to optimize your context.
|
|
17
|
+
|
|
18
|
+
Required: YOU MUST promote all relevant "summary" entries to "full".
|
|
19
|
+
Required: YOU MUST demote all irrelevant "full" entries to "summary".
|
|
20
|
+
|
|
21
|
+
Required: YOU MUST conclude every turn with EITHER <update></update> if still working OR <summarize></summarize> if done. Never both.
|
|
22
|
+
Required: YOU MUST use one and only one <update></update> or <summarize></summarize> tag, and only at the end.
|
|
13
23
|
|
|
14
24
|
# Tool Commands
|
|
15
25
|
|
|
@@ -16,9 +16,9 @@ export default class Known {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async handler(entry, rummy) {
|
|
19
|
-
const { entries: store, sequence: turn, runId } = rummy;
|
|
19
|
+
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
20
20
|
const target = entry.attributes.path || entry.resultPath;
|
|
21
|
-
await store.upsert(runId, turn, target, entry.body, 200);
|
|
21
|
+
await store.upsert(runId, turn, target, entry.body, 200, { loopId });
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
full(entry) {
|
|
@@ -31,13 +31,12 @@ export default class Known {
|
|
|
31
31
|
|
|
32
32
|
// Rows arrive pre-sorted by SQL: skill → index → summary → full, then by recency
|
|
33
33
|
const demotedSet = new Set(ctx.demoted || []);
|
|
34
|
-
const
|
|
35
|
-
const lines = entries.map((e) => renderKnownTag(e, demotedSet, panic));
|
|
34
|
+
const lines = entries.map((e) => renderKnownTag(e, demotedSet));
|
|
36
35
|
return `${content}\n\n<knowns>\n${lines.join("\n")}\n</knowns>`;
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
function renderKnownTag(entry, demotedSet
|
|
39
|
+
function renderKnownTag(entry, demotedSet) {
|
|
41
40
|
const tag = entry.scheme || "file";
|
|
42
41
|
const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
|
|
43
42
|
const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
|
|
@@ -45,11 +44,6 @@ function renderKnownTag(entry, demotedSet, panic = false) {
|
|
|
45
44
|
const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
|
|
46
45
|
const flag = demotedSet?.has(entry.path) ? " demoted" : "";
|
|
47
46
|
|
|
48
|
-
// Panic mode: index-only view so context fits in LLM window
|
|
49
|
-
if (panic) {
|
|
50
|
-
return `<${tag} path="${entry.path}"${turn}${fidelity}${tokens}/>`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
47
|
const attrs =
|
|
54
48
|
typeof entry.attributes === "string"
|
|
55
49
|
? JSON.parse(entry.attributes)
|
|
@@ -2,31 +2,32 @@
|
|
|
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 = the information to save
|
|
5
|
+
// --- Syntax: path = slash-separated topic hierarchy, body = the information to save
|
|
6
6
|
[
|
|
7
|
-
|
|
7
|
+
'## <known path="known://topic/subtopic" summary="keyword,keyword,keyword">[specific facts, decisions, or plans]</known> - Sort and save what you learn for later recall',
|
|
8
8
|
],
|
|
9
|
-
// --- Examples:
|
|
9
|
+
// --- Examples: category-level entries — multiple related facts per entry, not one per item
|
|
10
10
|
[
|
|
11
|
-
'Example: <known summary="
|
|
12
|
-
"
|
|
11
|
+
'Example: <known path="known://config/database" summary="database,host,port,pool,replica">Host: db.internal. Port: 5432. Pool: 10 connections. Replica: db-replica.internal:5432.</known>',
|
|
12
|
+
"Category entry: all database config facts in one entry. Path is an address (topic/subtopic), body collects every related fact, summary is comma-separated search keywords — not a description.",
|
|
13
13
|
],
|
|
14
14
|
[
|
|
15
|
-
'Example: <known path="known://
|
|
16
|
-
"
|
|
15
|
+
'Example: <known path="known://project/milestones" summary="milestone,deadline,alpha,launch,2026">Alpha: 2026-03-01. Beta cutoff: 2026-04-15. GA launch: 2026-06-01.</known>',
|
|
16
|
+
"Timeline entry: all milestone dates under one path. Multiple facts per entry reduces fragmentation. Recall by glob or keyword.",
|
|
17
17
|
],
|
|
18
|
-
// ---
|
|
18
|
+
// --- Constraints: summary and grouping first (model forms generation pattern from header + examples)
|
|
19
19
|
[
|
|
20
|
-
|
|
21
|
-
"
|
|
20
|
+
"* `summary` REQUIRED — at summary fidelity the body is hidden; these keywords are your only description",
|
|
21
|
+
"Self-interest framing: without summary, the model has a path but no idea what's inside.",
|
|
22
22
|
],
|
|
23
23
|
[
|
|
24
|
-
"*
|
|
25
|
-
"
|
|
24
|
+
"* Group related facts by topic — one entry per topic category, not one per input chunk",
|
|
25
|
+
"Critical behavioral constraint. Topic grouping enables semantic recall; chunk-based filing creates positional, irretrievable entries.",
|
|
26
26
|
],
|
|
27
|
+
// --- Lifecycle
|
|
27
28
|
[
|
|
28
|
-
|
|
29
|
-
"
|
|
29
|
+
'* Recall with <get path="known://config/*">replica</get>',
|
|
30
|
+
"Cross-tool lifecycle: glob by category, filter by keyword. Matches the slashed path convention.",
|
|
30
31
|
],
|
|
31
32
|
];
|
|
32
33
|
|
package/src/plugins/mv/mv.js
CHANGED
|
@@ -19,11 +19,28 @@ export default class Mv {
|
|
|
19
19
|
async handler(entry, rummy) {
|
|
20
20
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
21
21
|
const { path, to } = entry.attributes;
|
|
22
|
-
const VALID = { stored: 1, summary: 1, index: 1, full: 1 };
|
|
22
|
+
const VALID = { stored: 1, summary: 1, index: 1, full: 1, archive: 1 };
|
|
23
23
|
const fidelity = VALID[entry.attributes.fidelity]
|
|
24
24
|
? entry.attributes.fidelity
|
|
25
25
|
: undefined;
|
|
26
26
|
|
|
27
|
+
// Fidelity-in-place: no destination, change visibility of matched entries
|
|
28
|
+
if (fidelity && !to) {
|
|
29
|
+
const matches = await store.getEntriesByPattern(runId, path);
|
|
30
|
+
for (const match of matches)
|
|
31
|
+
await store.setFidelity(runId, match.path, fidelity);
|
|
32
|
+
const label = fidelity === "archive" ? "archived" : `set to ${fidelity}`;
|
|
33
|
+
await store.upsert(
|
|
34
|
+
runId,
|
|
35
|
+
turn,
|
|
36
|
+
entry.resultPath,
|
|
37
|
+
`${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
38
|
+
200,
|
|
39
|
+
{ fidelity: "archive", loopId },
|
|
40
|
+
);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
const source = await store.getBody(runId, path);
|
|
28
45
|
if (source === null) return;
|
|
29
46
|
|
package/src/plugins/mv/mvDoc.js
CHANGED
|
@@ -17,9 +17,23 @@ const LINES = [
|
|
|
17
17
|
"File rename. Shows that mv works on files too, not just known entries.",
|
|
18
18
|
],
|
|
19
19
|
|
|
20
|
+
// --- Archive lifecycle
|
|
21
|
+
[
|
|
22
|
+
"* You may move entries or pattern-matching batches of entries to and from the archive to manage your context budget.",
|
|
23
|
+
"Teaches archival as a reversible budget operation, not permanent deletion.",
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
'Example: <mv path="known://project/*" fidelity="index"/> ... <mv path="known://project/active_sprint" fidelity="full"/>',
|
|
27
|
+
"Index a whole category to free context while keeping paths visible, restore one entry when needed. No destination = fidelity change in place.",
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
"* YOU SHOULD demote irrelevant entries to `index` or `archive` — clean context improves reasoning.",
|
|
31
|
+
"Core curation principle: clean context is a quality signal, not just a budget concern. Teach the model to curate eagerly.",
|
|
32
|
+
],
|
|
33
|
+
|
|
20
34
|
// --- Constraints
|
|
21
35
|
[
|
|
22
|
-
"* Source path accepts
|
|
36
|
+
"* Source path accepts patterns for batch moves",
|
|
23
37
|
"Pattern support consistent with get/cp/rm.",
|
|
24
38
|
],
|
|
25
39
|
[
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# performed
|
|
2
2
|
|
|
3
|
-
Renders the `<
|
|
3
|
+
Renders the `<performed>` section of the user message — the active loop's
|
|
4
4
|
tool results and lifecycle signals.
|
|
5
5
|
|
|
6
6
|
## Registration
|
|
@@ -11,4 +11,5 @@ tool results and lifecycle signals.
|
|
|
11
11
|
|
|
12
12
|
Filters turn_context rows where `category === "logging"` and
|
|
13
13
|
`source_turn >= loopStartTurn`. Renders each entry chronologically
|
|
14
|
-
with turn
|
|
14
|
+
with turn, status, summary, fidelity, and tokens. Empty on the first
|
|
15
|
+
turn of a loop.
|