@possumtech/rummy 0.4.0 → 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 +1 -0
- package/FIDELITY_CONTRACT.md +172 -0
- package/migrations/001_initial_schema.sql +3 -3
- package/package.json +1 -1
- package/src/agent/AgentLoop.js +1 -2
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/KnownStore.js +1 -2
- package/src/agent/ResponseHealer.js +54 -1
- package/src/agent/TurnExecutor.js +51 -6
- package/src/agent/XmlParser.js +150 -41
- package/src/agent/known_store.sql +18 -11
- package/src/hooks/PluginContext.js +8 -2
- package/src/hooks/RummyContext.js +6 -3
- package/src/hooks/ToolRegistry.js +23 -27
- package/src/plugins/ask_user/ask_user.js +2 -2
- package/src/plugins/ask_user/ask_userDoc.js +4 -2
- package/src/plugins/budget/README.md +6 -4
- package/src/plugins/budget/budget.js +29 -9
- package/src/plugins/cp/cp.js +5 -5
- package/src/plugins/cp/cpDoc.js +0 -8
- package/src/plugins/engine/engine.sql +1 -1
- package/src/plugins/env/env.js +4 -4
- package/src/plugins/env/envDoc.js +2 -2
- package/src/plugins/file/file.js +2 -7
- package/src/plugins/get/get.js +31 -10
- package/src/plugins/get/getDoc.js +26 -37
- package/src/plugins/helpers.js +2 -2
- package/src/plugins/instructions/instructions.js +6 -5
- package/src/plugins/instructions/preamble.md +41 -33
- package/src/plugins/known/known.js +17 -16
- package/src/plugins/known/knownDoc.js +1 -13
- package/src/plugins/mv/mv.js +6 -6
- package/src/plugins/mv/mvDoc.js +2 -13
- package/src/plugins/previous/previous.js +10 -14
- package/src/plugins/progress/progress.js +22 -5
- package/src/plugins/prompt/prompt.js +14 -11
- package/src/plugins/rm/rm.js +4 -4
- package/src/plugins/rm/rmDoc.js +4 -8
- package/src/plugins/rpc/rpc.js +1 -1
- package/src/plugins/set/set.js +10 -12
- package/src/plugins/set/setDoc.js +4 -4
- package/src/plugins/sh/sh.js +4 -4
- package/src/plugins/sh/shDoc.js +2 -2
- package/src/plugins/skill/skill.js +2 -1
- package/src/plugins/summarize/summarize.js +2 -2
- package/src/plugins/summarize/summarizeDoc.js +9 -10
- package/src/plugins/telemetry/telemetry.js +36 -11
- package/src/plugins/think/think.js +2 -1
- package/src/plugins/think/thinkDoc.js +3 -5
- package/src/plugins/unknown/unknown.js +21 -14
- package/src/plugins/unknown/unknownDoc.js +2 -6
- package/src/plugins/update/update.js +2 -2
- package/src/plugins/update/updateDoc.js +9 -6
- package/src/sql/functions/slugify.js +13 -1
- package/src/sql/v_model_context.sql +3 -3
package/src/plugins/rpc/rpc.js
CHANGED
|
@@ -111,7 +111,7 @@ export default class Rpc {
|
|
|
111
111
|
});
|
|
112
112
|
return { status: "ok" };
|
|
113
113
|
},
|
|
114
|
-
description: "Promote entry
|
|
114
|
+
description: "Promote entry fidelity.",
|
|
115
115
|
params: {
|
|
116
116
|
path: "string — file path or glob pattern",
|
|
117
117
|
run: "string — run alias",
|
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;
|
|
@@ -27,8 +27,7 @@ export default class Set {
|
|
|
27
27
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
28
28
|
const attrs = entry.attributes;
|
|
29
29
|
const fidelityAttr = VALID_FIDELITY[attrs.fidelity] ? attrs.fidelity : null;
|
|
30
|
-
const rawSummary =
|
|
31
|
-
typeof attrs.summary === "string" ? attrs.summary : null;
|
|
30
|
+
const rawSummary = typeof attrs.summary === "string" ? attrs.summary : null;
|
|
32
31
|
const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
|
|
33
32
|
|
|
34
33
|
// Pure fidelity/metadata change — no body content
|
|
@@ -46,7 +45,7 @@ export default class Set {
|
|
|
46
45
|
entry.resultPath,
|
|
47
46
|
`${target} not found`,
|
|
48
47
|
404,
|
|
49
|
-
{ fidelity: "
|
|
48
|
+
{ fidelity: "archived", loopId },
|
|
50
49
|
);
|
|
51
50
|
return;
|
|
52
51
|
}
|
|
@@ -58,15 +57,14 @@ export default class Set {
|
|
|
58
57
|
});
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
|
-
const label =
|
|
62
|
-
fidelityAttr === "archive" ? "archived" : `set to ${fidelityAttr}`;
|
|
60
|
+
const label = `set to ${fidelityAttr}`;
|
|
63
61
|
await store.upsert(
|
|
64
62
|
runId,
|
|
65
63
|
turn,
|
|
66
64
|
entry.resultPath,
|
|
67
65
|
`${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
68
66
|
200,
|
|
69
|
-
{ fidelity: "
|
|
67
|
+
{ fidelity: "archived", loopId },
|
|
70
68
|
);
|
|
71
69
|
return;
|
|
72
70
|
}
|
|
@@ -137,7 +135,7 @@ export default class Set {
|
|
|
137
135
|
} else {
|
|
138
136
|
// Direct scheme write
|
|
139
137
|
await store.upsert(runId, turn, target, entry.body, 200, {
|
|
140
|
-
fidelity: fidelityAttr || "
|
|
138
|
+
fidelity: fidelityAttr || "promoted",
|
|
141
139
|
attributes: summaryText ? { summary: summaryText } : null,
|
|
142
140
|
loopId,
|
|
143
141
|
});
|
|
@@ -169,8 +167,8 @@ export default class Set {
|
|
|
169
167
|
return `# set ${file}${tokens}\n${attrs.merge}`;
|
|
170
168
|
}
|
|
171
169
|
|
|
172
|
-
summary(
|
|
173
|
-
return
|
|
170
|
+
summary() {
|
|
171
|
+
return "";
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
async #processEdit(rummy, entry, attrs) {
|
|
@@ -6,7 +6,7 @@ const LINES = [
|
|
|
6
6
|
'## <set path="[path/to/file]">[content or edit]</set> - Create, edit, or update a file or entry',
|
|
7
7
|
],
|
|
8
8
|
[
|
|
9
|
-
'Example: <set path="known://project/milestones" fidelity="
|
|
9
|
+
'Example: <set path="known://project/milestones" fidelity="demoted" summary="milestone,deadline,2026"/>',
|
|
10
10
|
"Fidelity control first — most unique capability of set.",
|
|
11
11
|
],
|
|
12
12
|
[
|
|
@@ -20,7 +20,7 @@ new text
|
|
|
20
20
|
"SEARCH/REPLACE block — primary edit pattern for existing files.",
|
|
21
21
|
],
|
|
22
22
|
[
|
|
23
|
-
|
|
23
|
+
`Example: <set path="src/config.js">s/port = 3000/port = 8080/g;s/We're almost done/We're done./g;</set>`,
|
|
24
24
|
"Sed syntax: chained s/old/new/ patterns with semicolons.",
|
|
25
25
|
],
|
|
26
26
|
[
|
|
@@ -28,8 +28,8 @@ new text
|
|
|
28
28
|
"Create: body contents are entire file.",
|
|
29
29
|
],
|
|
30
30
|
[
|
|
31
|
-
"* YOU MUST NOT use <sh
|
|
32
|
-
"
|
|
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
33
|
],
|
|
34
34
|
];
|
|
35
35
|
|
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
|
@@ -12,11 +12,11 @@ const LINES = [
|
|
|
12
12
|
"Test execution. Another common side-effect action.",
|
|
13
13
|
],
|
|
14
14
|
[
|
|
15
|
-
"* YOU MUST NOT use <sh
|
|
15
|
+
"* YOU MUST NOT use <sh></sh> to read, create, or edit files — use <get></get> and <set></set>",
|
|
16
16
|
"Forces file operations through the entry system.",
|
|
17
17
|
],
|
|
18
18
|
[
|
|
19
|
-
"* YOU MUST use <env
|
|
19
|
+
"* YOU MUST use <env></env> for commands without side effects",
|
|
20
20
|
"Reinforces the env/sh split. Read = env, mutate = sh.",
|
|
21
21
|
],
|
|
22
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
|
|
|
@@ -8,8 +8,8 @@ export default class Summarize {
|
|
|
8
8
|
core.ensureTool();
|
|
9
9
|
core.registerScheme({ category: "logging" });
|
|
10
10
|
core.on("handler", this.handler.bind(this));
|
|
11
|
-
core.on("
|
|
12
|
-
core.on("
|
|
11
|
+
core.on("promoted", this.full.bind(this));
|
|
12
|
+
core.on("demoted", this.summary.bind(this));
|
|
13
13
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
14
14
|
docsMap.summarize = docs;
|
|
15
15
|
return docsMap;
|
|
@@ -2,25 +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
|
-
["## <summarize>[answer or summary]</summarize> - Signal completion"],
|
|
6
5
|
[
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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.",
|
|
9
8
|
],
|
|
10
9
|
[
|
|
11
|
-
"Example: <summarize>
|
|
12
|
-
"
|
|
10
|
+
"Example: <summarize>The port is 8080</summarize>",
|
|
11
|
+
"Direct answer. Summarize delivers answers.",
|
|
13
12
|
],
|
|
14
13
|
[
|
|
15
|
-
"*
|
|
16
|
-
"
|
|
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).",
|
|
17
16
|
],
|
|
18
17
|
[
|
|
19
|
-
"* YOU MUST NOT
|
|
20
|
-
"
|
|
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.",
|
|
21
20
|
],
|
|
22
21
|
[
|
|
23
|
-
"* YOU MUST keep <summarize> to <= 80 characters",
|
|
22
|
+
"* YOU MUST keep <summarize></summarize> to <= 80 characters",
|
|
24
23
|
"Length cap.",
|
|
25
24
|
],
|
|
26
25
|
];
|
|
@@ -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,7 +1,8 @@
|
|
|
1
1
|
import docs from "./thinkDoc.js";
|
|
2
2
|
|
|
3
3
|
const THINK_ENABLED = process.env.RUMMY_THINK;
|
|
4
|
-
if (THINK_ENABLED === undefined)
|
|
4
|
+
if (THINK_ENABLED === undefined)
|
|
5
|
+
throw new Error("RUMMY_THINK must be set (1 or 0)");
|
|
5
6
|
|
|
6
7
|
export default class Think {
|
|
7
8
|
constructor(core) {
|
|
@@ -2,15 +2,13 @@
|
|
|
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
|
+
["## <think>[reasoning]</think> - Think before acting"],
|
|
5
6
|
[
|
|
6
|
-
"
|
|
7
|
-
],
|
|
8
|
-
[
|
|
9
|
-
"* Use <think> before any other tools to plan your approach",
|
|
7
|
+
"* Use <think></think> before any other tools to plan your approach",
|
|
10
8
|
"Positioning: think first, then act. Prevents degenerate tool-call storms.",
|
|
11
9
|
],
|
|
12
10
|
[
|
|
13
|
-
"* Reasoning inside <think> is private — it does not appear in your context",
|
|
11
|
+
"* Reasoning inside <think></think> is private — it does not appear in your context",
|
|
14
12
|
"Frees the model to reason without consuming context budget.",
|
|
15
13
|
],
|
|
16
14
|
];
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import docs from "./unknownDoc.js";
|
|
2
|
-
|
|
3
1
|
export default class Unknown {
|
|
4
2
|
#core;
|
|
5
3
|
|
|
@@ -10,13 +8,13 @@ export default class Unknown {
|
|
|
10
8
|
category: "unknown",
|
|
11
9
|
});
|
|
12
10
|
core.on("handler", this.handler.bind(this));
|
|
13
|
-
core.on("
|
|
14
|
-
core.on("
|
|
11
|
+
core.on("promoted", this.full.bind(this));
|
|
12
|
+
core.on("demoted", this.summary.bind(this));
|
|
15
13
|
core.filter("assembly.system", this.assembleUnknowns.bind(this), 300);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
// <unknown> is internal — written via <set path="unknown://...">. Hidden
|
|
15
|
+
// from all model-facing tool lists. Handler still dispatches if the
|
|
16
|
+
// model emits <unknown> directly out of habit.
|
|
17
|
+
core.markHidden();
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
async handler(entry, rummy) {
|
|
@@ -29,17 +27,23 @@ export default class Unknown {
|
|
|
29
27
|
return;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
// Generate slug path and upsert
|
|
33
|
-
|
|
30
|
+
// Generate slug path and upsert. Summary (if provided) becomes the
|
|
31
|
+
// path so the model can round-trip it via <get>; body is the fallback.
|
|
32
|
+
const unknownPath = await store.slugPath(
|
|
33
|
+
runId,
|
|
34
|
+
"unknown",
|
|
35
|
+
entry.body,
|
|
36
|
+
entry.attributes?.summary,
|
|
37
|
+
);
|
|
34
38
|
await store.upsert(runId, turn, unknownPath, entry.body, 200, { loopId });
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
full(entry) {
|
|
38
|
-
return
|
|
42
|
+
return entry.body;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
summary(
|
|
42
|
-
return
|
|
45
|
+
summary() {
|
|
46
|
+
return "";
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
async assembleUnknowns(content, ctx) {
|
|
@@ -49,7 +53,10 @@ export default class Unknown {
|
|
|
49
53
|
const lines = entries.map((u) => {
|
|
50
54
|
const fidelity = u.fidelity ? ` fidelity="${u.fidelity}"` : "";
|
|
51
55
|
const tokens = u.tokens ? ` tokens="${u.tokens}"` : "";
|
|
52
|
-
|
|
56
|
+
if (u.body) {
|
|
57
|
+
return `<unknown path="${u.path}" turn="${u.source_turn || u.turn}"${fidelity}${tokens}>${u.body}</unknown>`;
|
|
58
|
+
}
|
|
59
|
+
return `<unknown path="${u.path}" turn="${u.source_turn || u.turn}"${fidelity}${tokens}/>`;
|
|
53
60
|
});
|
|
54
61
|
return `${content}\n\n<unknowns>\n${lines.join("\n")}\n</unknowns>`;
|
|
55
62
|
}
|
|
@@ -3,22 +3,18 @@
|
|
|
3
3
|
// Changing ANY line requires reading ALL rationales first.
|
|
4
4
|
const LINES = [
|
|
5
5
|
[
|
|
6
|
-
"## <unknown>[specific thing I need to learn]</unknown> -
|
|
6
|
+
"## <unknown>[specific thing I need to learn]</unknown> - Register gaps for research",
|
|
7
7
|
],
|
|
8
8
|
[
|
|
9
9
|
'Example: <unknown path="unknown://answer">contents of answer.txt</unknown>',
|
|
10
10
|
"Path form: explicit unknown path for structured tracking.",
|
|
11
11
|
],
|
|
12
|
-
[
|
|
13
|
-
"Example: <unknown>which database adapter is configured</unknown>",
|
|
14
|
-
"Body form: question as body, path auto-generated.",
|
|
15
|
-
],
|
|
16
12
|
[
|
|
17
13
|
"* Investigate with Tool Commands",
|
|
18
14
|
"Unknowns drive action — get, env, search, ask_user.",
|
|
19
15
|
],
|
|
20
16
|
[
|
|
21
|
-
'* When resolved or irrelevant, remove with <set path="unknown://..." fidelity="
|
|
17
|
+
'* When resolved or irrelevant, remove with <set path="unknown://..." fidelity="archived"/>',
|
|
22
18
|
"Archive instead of delete — preserves the question for context history.",
|
|
23
19
|
],
|
|
24
20
|
];
|
|
@@ -8,8 +8,8 @@ export default class Update {
|
|
|
8
8
|
core.ensureTool();
|
|
9
9
|
core.registerScheme({ category: "logging" });
|
|
10
10
|
core.on("handler", this.handler.bind(this));
|
|
11
|
-
core.on("
|
|
12
|
-
core.on("
|
|
11
|
+
core.on("promoted", this.full.bind(this));
|
|
12
|
+
core.on("demoted", this.summary.bind(this));
|
|
13
13
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
14
14
|
docsMap.update = docs;
|
|
15
15
|
return docsMap;
|
|
@@ -2,7 +2,10 @@
|
|
|
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
|
-
[
|
|
5
|
+
[
|
|
6
|
+
"## <update>[brief status]</update> - Heartbeat for ongoing work (one per turn, at the end)",
|
|
7
|
+
"Header defines position and frequency. Without this, model uses update as inline narration between tools — multiple updates per turn.",
|
|
8
|
+
],
|
|
6
9
|
[
|
|
7
10
|
"Example: <update>Reading config files</update>",
|
|
8
11
|
"Progress checkpoint. Status signal, not a log entry.",
|
|
@@ -12,15 +15,15 @@ const LINES = [
|
|
|
12
15
|
"Multi-step progress. Ongoing work.",
|
|
13
16
|
],
|
|
14
17
|
[
|
|
15
|
-
"*
|
|
16
|
-
"
|
|
18
|
+
"* Urgent: ONE <update></update> per turn, AT THE END. Not inline narration between tools.",
|
|
19
|
+
"Single-update-per-turn is the missing rule. Model was emitting 3-6 updates per turn as progress commentary.",
|
|
17
20
|
],
|
|
18
21
|
[
|
|
19
|
-
"*
|
|
20
|
-
"
|
|
22
|
+
"* If you'd repeat the same <update></update> as last turn, the work is either stuck or done. Take a different action or <summarize></summarize>.",
|
|
23
|
+
"Points at the zombie-loop failure mode directly. Gives the model a trigger (same-text-as-prior-update) and two remedies.",
|
|
21
24
|
],
|
|
22
25
|
[
|
|
23
|
-
"* YOU MUST keep <update> to <= 80 characters",
|
|
26
|
+
"* YOU MUST keep <update></update> to <= 80 characters",
|
|
24
27
|
"Length cap.",
|
|
25
28
|
],
|
|
26
29
|
];
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
export const deterministic = true;
|
|
2
2
|
|
|
3
|
+
// Build URI paths the model can round-trip:
|
|
4
|
+
// "history,mongol,khan" → "history/mongol/khan" (commas become path separators)
|
|
5
|
+
// "contents of Document 1" → "contents_of_Document_1" (spaces become underscores)
|
|
6
|
+
// Slice on decoded text, then split-encode-join per segment so / survives as
|
|
7
|
+
// a separator while anything URL-unsafe inside a segment gets escaped.
|
|
3
8
|
export default function slugify(text) {
|
|
4
9
|
if (!text) return "";
|
|
5
|
-
return
|
|
10
|
+
return text
|
|
11
|
+
.slice(0, 80)
|
|
12
|
+
.replace(/,/g, "/")
|
|
13
|
+
.replace(/ /g, "_")
|
|
14
|
+
.split("/")
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.map(encodeURIComponent)
|
|
17
|
+
.join("/");
|
|
6
18
|
}
|
|
@@ -17,7 +17,7 @@ visible AS (
|
|
|
17
17
|
, COALESCE(s.category, 'logging') AS category
|
|
18
18
|
, CASE
|
|
19
19
|
-- Archived entries not in context
|
|
20
|
-
WHEN ke.fidelity = '
|
|
20
|
+
WHEN ke.fidelity = 'archived' THEN NULL
|
|
21
21
|
-- 202 Accepted (proposed) hidden until resolved
|
|
22
22
|
WHEN ke.status = 202 THEN NULL
|
|
23
23
|
-- Audit schemes (model_visible = 0) hidden
|
|
@@ -43,7 +43,7 @@ projected AS (
|
|
|
43
43
|
, category
|
|
44
44
|
, tokens
|
|
45
45
|
, CASE
|
|
46
|
-
WHEN visible_fidelity IN ('
|
|
46
|
+
WHEN visible_fidelity IN ('promoted', 'demoted') THEN body
|
|
47
47
|
ELSE ''
|
|
48
48
|
END AS body
|
|
49
49
|
FROM visible
|
|
@@ -72,7 +72,7 @@ SELECT
|
|
|
72
72
|
END
|
|
73
73
|
, CASE scheme WHEN 'skill' THEN 0 ELSE 1 END
|
|
74
74
|
, CASE fidelity
|
|
75
|
-
WHEN '
|
|
75
|
+
WHEN 'demoted' THEN 0
|
|
76
76
|
ELSE 1
|
|
77
77
|
END
|
|
78
78
|
, turn
|