@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
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
export default class
|
|
1
|
+
export default class Performed {
|
|
2
2
|
#core;
|
|
3
3
|
|
|
4
4
|
constructor(core) {
|
|
5
5
|
this.#core = core;
|
|
6
|
-
core.filter("assembly.user", this.
|
|
6
|
+
core.filter("assembly.user", this.assemblePerformed.bind(this), 100);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
async
|
|
9
|
+
async assemblePerformed(content, ctx) {
|
|
10
10
|
const entries = ctx.rows.filter(
|
|
11
|
-
(r) =>
|
|
11
|
+
(r) =>
|
|
12
|
+
r.category === "logging" &&
|
|
13
|
+
r.source_turn >= ctx.loopStartTurn &&
|
|
14
|
+
r.scheme !== "unknown",
|
|
12
15
|
);
|
|
13
16
|
if (entries.length === 0) return content;
|
|
14
17
|
|
|
15
|
-
const lines =
|
|
16
|
-
|
|
17
|
-
);
|
|
18
|
-
return `${content}<current>\n${lines.join("\n")}\n</current>\n`;
|
|
18
|
+
const lines = entries.map((e) => renderToolTag(e));
|
|
19
|
+
return `${content}<performed>\n${lines.join("\n")}\n</performed>\n`;
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
function renderToolTag(entry) {
|
|
23
24
|
const attrs =
|
|
24
25
|
typeof entry.attributes === "string"
|
|
25
26
|
? JSON.parse(entry.attributes)
|
|
@@ -28,23 +29,17 @@ async function renderToolTag(entry, core) {
|
|
|
28
29
|
const target = attrs?.path || attrs?.file || attrs?.command || "";
|
|
29
30
|
const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
|
|
30
31
|
const status = entry.status ? ` status="${entry.status}"` : "";
|
|
32
|
+
const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
|
|
33
|
+
const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
|
|
31
34
|
const summary =
|
|
32
35
|
typeof attrs?.summary === "string"
|
|
33
36
|
? ` summary="${attrs.summary.slice(0, 80)}"`
|
|
34
37
|
: "";
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
body = await core.hooks.tools.view(entry.scheme, {
|
|
39
|
-
...entry,
|
|
40
|
-
attributes: attrs,
|
|
41
|
-
});
|
|
42
|
-
} catch {
|
|
43
|
-
body = entry.body;
|
|
44
|
-
}
|
|
39
|
+
const body = entry.body || null;
|
|
45
40
|
|
|
46
41
|
if (body) {
|
|
47
|
-
return `<${entry.scheme} path="${target}"${turn}${status}${summary}>${body}</${entry.scheme}>`;
|
|
42
|
+
return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}>${body}</${entry.scheme}>`;
|
|
48
43
|
}
|
|
49
|
-
return `<${entry.scheme} path="${target}"${turn}${status}${summary}/>`;
|
|
44
|
+
return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}/>`;
|
|
50
45
|
}
|
|
@@ -12,4 +12,5 @@ history from prior ask/act invocations on this run.
|
|
|
12
12
|
|
|
13
13
|
Filters turn_context rows where `category` is `logging` or `prompt`
|
|
14
14
|
and `source_turn < loopStartTurn`. Renders each entry chronologically
|
|
15
|
-
with turn
|
|
15
|
+
with turn, status, summary, fidelity, and tokens. The model can target
|
|
16
|
+
these entries by path with `<set>` or `<rm>` to free context space.
|
|
@@ -9,11 +9,20 @@ export default class Previous {
|
|
|
9
9
|
async assemblePrevious(content, ctx) {
|
|
10
10
|
if (ctx.loopStartTurn <= 1) return content;
|
|
11
11
|
|
|
12
|
-
const entries = ctx.rows
|
|
13
|
-
(
|
|
14
|
-
(r
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const entries = ctx.rows
|
|
13
|
+
.filter(
|
|
14
|
+
(r) =>
|
|
15
|
+
(r.category === "logging" || r.category === "prompt") &&
|
|
16
|
+
r.source_turn < ctx.loopStartTurn,
|
|
17
|
+
)
|
|
18
|
+
.toSorted((a, b) => {
|
|
19
|
+
if (a.source_turn !== b.source_turn)
|
|
20
|
+
return a.source_turn - b.source_turn;
|
|
21
|
+
// Within the same turn: prompt first (cause before effect)
|
|
22
|
+
if (a.category === "prompt" && b.category !== "prompt") return -1;
|
|
23
|
+
if (b.category === "prompt" && a.category !== "prompt") return 1;
|
|
24
|
+
return 0;
|
|
25
|
+
});
|
|
17
26
|
if (entries.length === 0) return content;
|
|
18
27
|
|
|
19
28
|
const lines = await Promise.all(
|
|
@@ -23,7 +32,7 @@ export default class Previous {
|
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
async function renderToolTag(entry,
|
|
35
|
+
async function renderToolTag(entry, _core) {
|
|
27
36
|
const attrs =
|
|
28
37
|
typeof entry.attributes === "string"
|
|
29
38
|
? JSON.parse(entry.attributes)
|
|
@@ -32,23 +41,20 @@ async function renderToolTag(entry, core) {
|
|
|
32
41
|
const target = attrs?.path || attrs?.file || attrs?.command || "";
|
|
33
42
|
const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
|
|
34
43
|
const status = entry.status ? ` status="${entry.status}"` : "";
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
body
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return `<${entry.scheme} path="${target}"${turn}${status}${summary}>${body}</${entry.scheme}>`;
|
|
52
|
-
}
|
|
53
|
-
return `<${entry.scheme} path="${target}"${turn}${status}${summary}/>`;
|
|
44
|
+
const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
|
|
45
|
+
const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
|
|
46
|
+
|
|
47
|
+
// Previous entries render at summary. Prompts get 512 chars for orientation.
|
|
48
|
+
const limit = entry.scheme === "prompt" ? 512 : 80;
|
|
49
|
+
const rawSummary =
|
|
50
|
+
(typeof attrs?.summary === "string" ? attrs.summary : null) ||
|
|
51
|
+
entry.body?.slice(0, limit) ||
|
|
52
|
+
"";
|
|
53
|
+
// Strip internal dedup namespace prefixes (e.g. "get://turn_3/src/app.js" → "src/app.js")
|
|
54
|
+
const summaryText = rawSummary.replace(/\b\w+:\/\/turn_\d+\//g, "");
|
|
55
|
+
const summaryAttr = summaryText
|
|
56
|
+
? ` summary="${summaryText.replace(/"/g, "'").slice(0, limit)}"`
|
|
57
|
+
: "";
|
|
58
|
+
|
|
59
|
+
return `<${entry.scheme} path="${target}"${turn}${status}${summaryAttr}${fidelity}${tokens}/>`;
|
|
54
60
|
}
|
|
@@ -9,9 +9,8 @@ current work log to the active prompt.
|
|
|
9
9
|
|
|
10
10
|
## Behavior
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Emits `<progress turn="N">` carrying token budget and fidelity stats.
|
|
13
13
|
On continuation turns with current entries: "The above actions were
|
|
14
14
|
performed in response to the following prompt:"
|
|
15
|
-
If a `progress://` entry exists, uses its body directly.
|
|
16
15
|
|
|
17
16
|
Progress text is the tuning knob for model orientation between turns.
|
|
@@ -7,38 +7,34 @@ export default class Progress {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
async assembleProgress(content, ctx) {
|
|
10
|
-
|
|
11
|
-
// Falls back to row token sum (less accurate — missing system prompt overhead).
|
|
12
|
-
const rowTokens = ctx.rows.reduce((sum, r) => sum + (r.tokens || 0), 0);
|
|
13
|
-
const usedTokens = ctx.lastContextTokens || rowTokens;
|
|
14
|
-
const contextSize = ctx.contextSize || 0;
|
|
10
|
+
const { lastContextTokens: usedTokens, contextSize } = ctx;
|
|
15
11
|
const pct = contextSize ? Math.round((usedTokens / contextSize) * 100) : 0;
|
|
16
12
|
|
|
17
|
-
// Fidelity distribution across
|
|
18
|
-
const
|
|
13
|
+
// Fidelity distribution across all manageable entries (data + logging)
|
|
14
|
+
const dataEntries = ctx.rows.filter((r) => r.category === "data");
|
|
15
|
+
const loggingEntries = ctx.rows.filter((r) => r.category === "logging");
|
|
16
|
+
const entries = [...dataEntries, ...loggingEntries];
|
|
19
17
|
const fullEntries = entries.filter((r) => r.fidelity === "full");
|
|
20
18
|
const summaryEntries = entries.filter((r) => r.fidelity === "summary");
|
|
21
19
|
const indexEntries = entries.filter((r) => r.fidelity === "index");
|
|
22
|
-
const fullTokens = fullEntries.reduce((s, r) => s +
|
|
23
|
-
const summaryTokens = summaryEntries.reduce(
|
|
24
|
-
|
|
25
|
-
0,
|
|
26
|
-
);
|
|
27
|
-
const indexTokens = indexEntries.reduce((s, r) => s + (r.tokens || 0), 0);
|
|
20
|
+
const fullTokens = fullEntries.reduce((s, r) => s + r.tokens, 0);
|
|
21
|
+
const summaryTokens = summaryEntries.reduce((s, r) => s + r.tokens, 0);
|
|
22
|
+
const indexTokens = indexEntries.reduce((s, r) => s + r.tokens, 0);
|
|
28
23
|
|
|
29
24
|
const unknownCount = ctx.rows.filter(
|
|
30
25
|
(r) => r.category === "unknown",
|
|
31
26
|
).length;
|
|
32
27
|
|
|
33
|
-
const
|
|
34
|
-
(r) => r.
|
|
28
|
+
const hasPerformed = loggingEntries.some(
|
|
29
|
+
(r) => r.source_turn >= ctx.loopStartTurn,
|
|
35
30
|
);
|
|
36
31
|
|
|
37
32
|
const parts = [];
|
|
38
33
|
|
|
39
|
-
const knownCount =
|
|
34
|
+
const knownCount = dataEntries.length;
|
|
35
|
+
const loggingCount = loggingEntries.length;
|
|
40
36
|
const tokenLine = contextSize
|
|
41
|
-
? `${usedTokens} of ${contextSize} tokens (${pct}%) · ${knownCount} known${knownCount !== 1 ? "s" : ""} · ${unknownCount} unknown${unknownCount !== 1 ? "s" : ""}`
|
|
37
|
+
? `${usedTokens} of ${contextSize} tokens (${pct}%) · ${knownCount} known${knownCount !== 1 ? "s" : ""} · ${loggingCount} logging · ${unknownCount} unknown${unknownCount !== 1 ? "s" : ""}`
|
|
42
38
|
: "";
|
|
43
39
|
if (tokenLine) parts.push(tokenLine);
|
|
44
40
|
|
|
@@ -55,22 +51,12 @@ export default class Progress {
|
|
|
55
51
|
if (fidelityParts.length > 0)
|
|
56
52
|
parts.push(`Entries: ${fidelityParts.join(" · ")}`);
|
|
57
53
|
|
|
58
|
-
if (
|
|
59
|
-
parts.push(
|
|
60
|
-
'Context above 75%. YOU MUST free space: <set fidelity="summary" summary="topic,detail,keyword"/>, <set fidelity="archive"/>, or <rm/>. Target the largest entries.',
|
|
61
|
-
);
|
|
62
|
-
} else if (pct > 50) {
|
|
63
|
-
parts.push(
|
|
64
|
-
'Context above 50%. You may free space: <set fidelity="summary" summary="topic,detail,keyword"/>, <set fidelity="archive"/>, or <rm/>.',
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (hasCurrent) {
|
|
54
|
+
if (hasPerformed) {
|
|
69
55
|
parts.push(
|
|
70
56
|
"The above actions were performed in response to the following prompt:",
|
|
71
57
|
);
|
|
72
58
|
}
|
|
73
59
|
|
|
74
|
-
return `${content}<progress>${parts.join("\n")}</progress>\n`;
|
|
60
|
+
return `${content}<progress turn="${ctx.turn}">${parts.join("\n")}</progress>\n`;
|
|
75
61
|
}
|
|
76
62
|
}
|
|
@@ -4,7 +4,6 @@ export default class Prompt {
|
|
|
4
4
|
constructor(core) {
|
|
5
5
|
this.#core = core;
|
|
6
6
|
core.hooks.tools.onView("prompt", (entry) => entry.body);
|
|
7
|
-
core.hooks.tools.onView("progress", (entry) => entry.body);
|
|
8
7
|
core.on("turn.started", this.onTurnStarted.bind(this));
|
|
9
8
|
core.filter("assembly.user", this.assemblePrompt.bind(this), 300);
|
|
10
9
|
}
|
|
@@ -17,10 +16,6 @@ export default class Prompt {
|
|
|
17
16
|
attributes: { mode },
|
|
18
17
|
loopId,
|
|
19
18
|
});
|
|
20
|
-
} else {
|
|
21
|
-
await store.upsert(runId, turn, `progress://${turn}`, prompt || "", 200, {
|
|
22
|
-
loopId,
|
|
23
|
-
});
|
|
24
19
|
}
|
|
25
20
|
}
|
|
26
21
|
|
|
@@ -41,8 +36,6 @@ export default class Prompt {
|
|
|
41
36
|
const tools = toolNames.join(",");
|
|
42
37
|
let warn = "";
|
|
43
38
|
if (mode === "ask") warn = ' warn="File editing disallowed."';
|
|
44
|
-
if (mode === "panic")
|
|
45
|
-
warn = ' warn="Context overflow. Free space to continue."';
|
|
46
39
|
|
|
47
40
|
return `${content}<prompt mode="${mode}" tools="${tools}"${warn}>${body}</prompt>`;
|
|
48
41
|
}
|
package/src/plugins/rm/rm.js
CHANGED
|
@@ -41,25 +41,37 @@ export default class Rm {
|
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
44
|
+
const fileMatches = matches.filter((m) => m.scheme === null);
|
|
45
|
+
const schemeMatches = matches.filter((m) => m.scheme !== null);
|
|
46
|
+
|
|
47
|
+
// Scheme entries: remove all, write one aggregate result entry
|
|
48
|
+
for (const match of schemeMatches) await store.remove(runId, match.path);
|
|
49
|
+
if (schemeMatches.length > 0) {
|
|
50
|
+
const paths = schemeMatches.map((m) => m.path).join("\n");
|
|
51
|
+
await store.upsert(runId, turn, entry.resultPath, paths, 200, {
|
|
52
|
+
attributes: { path: target },
|
|
53
|
+
loopId,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// File entries: individual 202 proposals (require user resolution)
|
|
58
|
+
if (fileMatches.length > 0 && schemeMatches.length > 0)
|
|
59
|
+
await store.remove(runId, entry.resultPath);
|
|
60
|
+
for (const match of fileMatches) {
|
|
61
|
+
const resultPath =
|
|
62
|
+
schemeMatches.length === 0 && fileMatches.length === 1
|
|
63
|
+
? entry.resultPath
|
|
64
|
+
: await store.dedup(runId, "rm", match.path, turn);
|
|
65
|
+
await store.upsert(runId, turn, resultPath, match.path, 202, {
|
|
66
|
+
attributes: { path: match.path },
|
|
67
|
+
loopId,
|
|
68
|
+
});
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
full(entry) {
|
|
62
|
-
|
|
73
|
+
const header = `# rm ${entry.attributes.path || entry.path}`;
|
|
74
|
+
return entry.body ? `${header}\n${entry.body}` : header;
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
summary(entry) {
|
package/src/plugins/rm/rmDoc.js
CHANGED
|
@@ -8,8 +8,8 @@ const LINES = [
|
|
|
8
8
|
// --- Examples: file, known (with slug path), preview safety
|
|
9
9
|
['Example: <rm path="src/config.js"/>', "File removal. Simplest form."],
|
|
10
10
|
[
|
|
11
|
-
'Example: <rm path="known://
|
|
12
|
-
"Shows
|
|
11
|
+
'Example: <rm path="known://config/deprecated_service"/>',
|
|
12
|
+
"Shows topic-hierarchy path convention. Paths are category/key, not sentence slugs.",
|
|
13
13
|
],
|
|
14
14
|
[
|
|
15
15
|
'Example: <rm path="known://temp_*" preview/>',
|
|
@@ -22,7 +22,7 @@ const LINES = [
|
|
|
22
22
|
"Nudges toward archive over rm. Archive keeps the key; rm deletes permanently.",
|
|
23
23
|
],
|
|
24
24
|
[
|
|
25
|
-
"* Paths accept
|
|
25
|
+
"* Paths accept patterns — use `preview` to check matches first",
|
|
26
26
|
"Reinforces preview safety pattern. Prevents accidental bulk deletion.",
|
|
27
27
|
],
|
|
28
28
|
];
|
package/src/plugins/set/set.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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";
|
|
@@ -66,16 +67,33 @@ export default class Set {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
if (matches.length === 0) {
|
|
71
|
+
await store.upsert(
|
|
72
|
+
runId,
|
|
73
|
+
turn,
|
|
74
|
+
entry.resultPath,
|
|
75
|
+
`${target} not found`,
|
|
76
|
+
404,
|
|
77
|
+
{
|
|
78
|
+
fidelity: "archive",
|
|
79
|
+
loopId,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
69
84
|
const label =
|
|
70
85
|
fidelityAttr === "archive" ? "archived" : `set to ${fidelityAttr}`;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
await store.upsert(
|
|
87
|
+
runId,
|
|
88
|
+
turn,
|
|
89
|
+
entry.resultPath,
|
|
90
|
+
`${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
91
|
+
200,
|
|
92
|
+
{
|
|
93
|
+
fidelity: "archive",
|
|
94
|
+
loopId,
|
|
95
|
+
},
|
|
96
|
+
);
|
|
79
97
|
return;
|
|
80
98
|
}
|
|
81
99
|
|
|
@@ -198,7 +216,7 @@ export default class Set {
|
|
|
198
216
|
? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
|
|
199
217
|
: null;
|
|
200
218
|
const beforeTokens = match.tokens_full || 0;
|
|
201
|
-
const afterTokens = patch ? (patch
|
|
219
|
+
const afterTokens = patch ? countTokens(patch) : beforeTokens;
|
|
202
220
|
|
|
203
221
|
await store.upsert(runId, turn, resultPath, match.body, status, {
|
|
204
222
|
attributes: {
|
|
@@ -265,7 +283,7 @@ export default class Set {
|
|
|
265
283
|
: null;
|
|
266
284
|
const merge = mergeBlocks.length > 0 ? mergeBlocks.join("\n") : null;
|
|
267
285
|
const beforeTokens = fileEntry[0].tokens_full || 0;
|
|
268
|
-
const afterTokens = current ? (current
|
|
286
|
+
const afterTokens = current ? countTokens(current) : beforeTokens;
|
|
269
287
|
|
|
270
288
|
await store.upsert(runId, turn, entry.path, original, state, {
|
|
271
289
|
attributes: {
|
|
@@ -287,10 +305,7 @@ export default class Set {
|
|
|
287
305
|
return { search: attrs.search, replace: attrs.replace ?? "" };
|
|
288
306
|
}
|
|
289
307
|
if (attrs.blocks?.length > 0) {
|
|
290
|
-
return {
|
|
291
|
-
search: attrs.blocks[0].search,
|
|
292
|
-
replace: attrs.blocks[0].replace,
|
|
293
|
-
};
|
|
308
|
+
return { blocks: attrs.blocks };
|
|
294
309
|
}
|
|
295
310
|
return null;
|
|
296
311
|
}
|
|
@@ -312,11 +327,32 @@ export default class Set {
|
|
|
312
327
|
};
|
|
313
328
|
}
|
|
314
329
|
if (body && attrs.blocks?.length > 0) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
330
|
+
if (attrs.blocks.length === 1) {
|
|
331
|
+
const block = attrs.blocks[0];
|
|
332
|
+
return Hedberg.replace(body, block.search, block.replace, {
|
|
333
|
+
sed: block.sed,
|
|
334
|
+
flags: block.flags,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// Multi-block: apply sequentially, no per-hunk merge notation
|
|
338
|
+
let current = body;
|
|
339
|
+
let lastWarning = null;
|
|
340
|
+
for (const block of attrs.blocks) {
|
|
341
|
+
const result = Hedberg.replace(current, block.search, block.replace, {
|
|
342
|
+
sed: block.sed,
|
|
343
|
+
flags: block.flags,
|
|
344
|
+
});
|
|
345
|
+
if (result.error) return result;
|
|
346
|
+
if (result.warning) lastWarning = result.warning;
|
|
347
|
+
if (result.patch) current = result.patch;
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
patch: current !== body ? current : null,
|
|
351
|
+
searchText: null,
|
|
352
|
+
replaceText: null,
|
|
353
|
+
warning: lastWarning,
|
|
354
|
+
error: null,
|
|
355
|
+
};
|
|
320
356
|
}
|
|
321
357
|
return {
|
|
322
358
|
patch: null,
|
|
@@ -19,8 +19,8 @@ const LINES = [
|
|
|
19
19
|
"SEARCH/REPLACE block: literal match and replace. Use when sed escaping is complex.",
|
|
20
20
|
],
|
|
21
21
|
[
|
|
22
|
-
'Example: <set path="known://
|
|
23
|
-
"Fidelity
|
|
22
|
+
'Example: <set path="known://project/milestones" fidelity="summary" summary="milestone,deadline,2026"/> ... <set path="prompt://3" fidelity="index"/>',
|
|
23
|
+
"Fidelity control: compress a known entry to keywords, demote a previous prompt to index-only. Both free context while keeping paths visible.",
|
|
24
24
|
],
|
|
25
25
|
|
|
26
26
|
// --- Constraints
|
|
@@ -28,6 +28,10 @@ const LINES = [
|
|
|
28
28
|
'* `fidelity="..."`: `archive`, `summary`, `index`, `full`',
|
|
29
29
|
"Fidelity control. Archive removes from context but preserves for retrieval.",
|
|
30
30
|
],
|
|
31
|
+
[
|
|
32
|
+
'* `fidelity="summary"` HIDES the body — does NOT require reading or compressing content. Write any short keyword label you already know.',
|
|
33
|
+
"M-10 fix: model was reading files before compressing to summary, believing it needed semantic content. It does not. The body is preserved on disk; only context visibility changes.",
|
|
34
|
+
],
|
|
31
35
|
[
|
|
32
36
|
'* `summary="..."` (<= 80 chars) persists across fidelity changes',
|
|
33
37
|
"Model-authored descriptions survive demotion. No janitorial pass needed.",
|
|
@@ -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
|
}
|
|
@@ -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.
|
|
@@ -25,10 +25,11 @@ export default class Unknown {
|
|
|
25
25
|
const entries = ctx.rows.filter((r) => r.category === "unknown");
|
|
26
26
|
if (entries.length === 0) return content;
|
|
27
27
|
|
|
28
|
-
const lines = entries.map(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const lines = entries.map((u) => {
|
|
29
|
+
const fidelity = u.fidelity ? ` fidelity="${u.fidelity}"` : "";
|
|
30
|
+
const tokens = u.tokens ? ` tokens="${u.tokens}"` : "";
|
|
31
|
+
return `<unknown path="${u.path}" turn="${u.source_turn || u.turn}"${fidelity}${tokens}>${u.body}</unknown>`;
|
|
32
|
+
});
|
|
32
33
|
return `${content}\n\n<unknowns>\n${lines.join("\n")}\n</unknowns>`;
|
|
33
34
|
}
|
|
34
35
|
}
|