@possumtech/rummy 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/FIDELITY_CONTRACT.md +172 -0
- package/README.md +5 -1
- package/SPEC.md +31 -17
- package/migrations/001_initial_schema.sql +3 -4
- package/package.json +1 -1
- package/src/agent/AgentLoop.js +51 -153
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/KnownStore.js +16 -9
- package/src/agent/ResponseHealer.js +54 -1
- package/src/agent/TurnExecutor.js +125 -323
- package/src/agent/XmlParser.js +172 -42
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +29 -72
- package/src/agent/runs.sql +2 -2
- package/src/hooks/Hooks.js +1 -0
- package/src/hooks/PluginContext.js +8 -2
- package/src/hooks/RummyContext.js +6 -3
- package/src/hooks/ToolRegistry.js +29 -32
- package/src/plugins/ask_user/ask_user.js +2 -2
- package/src/plugins/ask_user/ask_userDoc.js +7 -10
- package/src/plugins/budget/README.md +28 -18
- package/src/plugins/budget/budget.js +80 -3
- package/src/plugins/budget/recovery.js +47 -0
- package/src/plugins/cp/cp.js +5 -5
- package/src/plugins/cp/cpDoc.js +1 -14
- package/src/plugins/engine/engine.sql +1 -1
- package/src/plugins/env/env.js +4 -4
- package/src/plugins/env/envDoc.js +4 -9
- package/src/plugins/file/file.js +2 -7
- package/src/plugins/get/get.js +32 -13
- package/src/plugins/get/getDoc.js +26 -44
- package/src/plugins/helpers.js +4 -4
- package/src/plugins/instructions/instructions.js +9 -7
- package/src/plugins/instructions/preamble.md +45 -26
- package/src/plugins/known/known.js +71 -15
- package/src/plugins/known/knownDoc.js +4 -20
- package/src/plugins/mv/mv.js +6 -6
- package/src/plugins/mv/mvDoc.js +4 -30
- package/src/plugins/policy/policy.js +47 -0
- package/src/plugins/previous/previous.js +10 -14
- package/src/plugins/progress/progress.js +29 -48
- package/src/plugins/prompt/prompt.js +18 -6
- package/src/plugins/rm/rm.js +4 -4
- package/src/plugins/rm/rmDoc.js +5 -14
- package/src/plugins/rpc/rpc.js +4 -2
- package/src/plugins/set/set.js +86 -91
- package/src/plugins/set/setDoc.js +28 -41
- package/src/plugins/sh/sh.js +4 -4
- package/src/plugins/sh/shDoc.js +4 -9
- package/src/plugins/skill/skill.js +2 -1
- package/src/plugins/summarize/summarize.js +9 -2
- package/src/plugins/summarize/summarizeDoc.js +10 -16
- package/src/plugins/telemetry/telemetry.js +36 -11
- package/src/plugins/think/think.js +13 -0
- package/src/plugins/think/thinkDoc.js +16 -0
- package/src/plugins/unknown/unknown.js +37 -9
- package/src/plugins/unknown/unknownDoc.js +7 -16
- package/src/plugins/update/update.js +9 -2
- package/src/plugins/update/updateDoc.js +12 -14
- package/src/server/ClientConnection.js +11 -1
- package/src/sql/functions/slugify.js +13 -1
- package/src/sql/v_model_context.sql +6 -6
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import docs from "./unknownDoc.js";
|
|
2
|
-
|
|
3
1
|
export default class Unknown {
|
|
4
2
|
#core;
|
|
5
3
|
|
|
@@ -9,16 +7,43 @@ export default class Unknown {
|
|
|
9
7
|
core.registerScheme({
|
|
10
8
|
category: "unknown",
|
|
11
9
|
});
|
|
12
|
-
core.on("
|
|
10
|
+
core.on("handler", this.handler.bind(this));
|
|
11
|
+
core.on("promoted", this.full.bind(this));
|
|
12
|
+
core.on("demoted", this.summary.bind(this));
|
|
13
13
|
core.filter("assembly.system", this.assembleUnknowns.bind(this), 300);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async handler(entry, rummy) {
|
|
21
|
+
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
22
|
+
|
|
23
|
+
// Deduplicate — if this exact body already exists, skip
|
|
24
|
+
const existingValues = await store.getUnknownValues(runId);
|
|
25
|
+
if (existingValues.has(entry.body)) {
|
|
26
|
+
console.warn(`[RUMMY] Unknown deduped: "${entry.body.slice(0, 60)}"`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
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
|
+
);
|
|
38
|
+
await store.upsert(runId, turn, unknownPath, entry.body, 200, { loopId });
|
|
18
39
|
}
|
|
19
40
|
|
|
20
41
|
full(entry) {
|
|
21
|
-
return
|
|
42
|
+
return entry.body;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
summary() {
|
|
46
|
+
return "";
|
|
22
47
|
}
|
|
23
48
|
|
|
24
49
|
async assembleUnknowns(content, ctx) {
|
|
@@ -28,7 +53,10 @@ export default class Unknown {
|
|
|
28
53
|
const lines = entries.map((u) => {
|
|
29
54
|
const fidelity = u.fidelity ? ` fidelity="${u.fidelity}"` : "";
|
|
30
55
|
const tokens = u.tokens ? ` tokens="${u.tokens}"` : "";
|
|
31
|
-
|
|
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}/>`;
|
|
32
60
|
});
|
|
33
61
|
return `${content}\n\n<unknowns>\n${lines.join("\n")}\n</unknowns>`;
|
|
34
62
|
}
|
|
@@ -2,29 +2,20 @@
|
|
|
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
|
-
|
|
8
|
-
],
|
|
9
|
-
|
|
10
|
-
// --- Examples: concrete unknowns, not abstract
|
|
11
|
-
[
|
|
12
|
-
`Example: <unknown path="unknown://answer">contents of answer.txt</unknown>`,
|
|
13
|
-
`Specific and actionable. Shows that unknowns are concrete investigation targets.`,
|
|
6
|
+
"## <unknown>[specific thing I need to learn]</unknown> - Register gaps for research",
|
|
14
7
|
],
|
|
15
8
|
[
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
'Example: <unknown path="unknown://answer">contents of answer.txt</unknown>',
|
|
10
|
+
"Path form: explicit unknown path for structured tracking.",
|
|
18
11
|
],
|
|
19
|
-
|
|
20
|
-
// --- Lifecycle: register → investigate → resolve
|
|
21
12
|
[
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
"* Investigate with Tool Commands",
|
|
14
|
+
"Unknowns drive action — get, env, search, ask_user.",
|
|
24
15
|
],
|
|
25
16
|
[
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
'* When resolved or irrelevant, remove with <set path="unknown://..." fidelity="archived"/>',
|
|
18
|
+
"Archive instead of delete — preserves the question for context history.",
|
|
28
19
|
],
|
|
29
20
|
];
|
|
30
21
|
|
|
@@ -7,14 +7,21 @@ export default class Update {
|
|
|
7
7
|
this.#core = core;
|
|
8
8
|
core.ensureTool();
|
|
9
9
|
core.registerScheme({ category: "logging" });
|
|
10
|
-
core.on("
|
|
11
|
-
core.on("
|
|
10
|
+
core.on("handler", this.handler.bind(this));
|
|
11
|
+
core.on("promoted", this.full.bind(this));
|
|
12
|
+
core.on("demoted", this.summary.bind(this));
|
|
12
13
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
13
14
|
docsMap.update = docs;
|
|
14
15
|
return docsMap;
|
|
15
16
|
});
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
async handler(entry, rummy) {
|
|
20
|
+
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
21
|
+
const statusPath = await store.slugPath(runId, "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,29 @@
|
|
|
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
|
-
|
|
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
|
+
],
|
|
9
9
|
[
|
|
10
10
|
"Example: <update>Reading config files</update>",
|
|
11
|
-
"Progress checkpoint.
|
|
11
|
+
"Progress checkpoint. Status signal, not a log entry.",
|
|
12
12
|
],
|
|
13
13
|
[
|
|
14
14
|
"Example: <update>Found 3 issues, fixing first</update>",
|
|
15
|
-
"Multi-step progress.
|
|
15
|
+
"Multi-step progress. Ongoing work.",
|
|
16
16
|
],
|
|
17
|
-
|
|
18
|
-
// --- Constraints: RFC-style MUST/MUST NOT
|
|
19
17
|
[
|
|
20
|
-
"*
|
|
21
|
-
"
|
|
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.",
|
|
22
20
|
],
|
|
23
21
|
[
|
|
24
|
-
"*
|
|
25
|
-
"
|
|
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.",
|
|
26
24
|
],
|
|
27
25
|
[
|
|
28
|
-
"* YOU MUST keep <update> to <= 80 characters",
|
|
29
|
-
"Length cap.
|
|
26
|
+
"* YOU MUST keep <update></update> to <= 80 characters",
|
|
27
|
+
"Length cap.",
|
|
30
28
|
],
|
|
31
29
|
];
|
|
32
30
|
|
|
@@ -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);
|
|
@@ -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
|
}
|
|
@@ -13,11 +13,11 @@ 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
|
|
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
|
|
@@ -41,8 +41,9 @@ 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
|
-
WHEN visible_fidelity IN ('
|
|
46
|
+
WHEN visible_fidelity IN ('promoted', 'demoted') THEN body
|
|
46
47
|
ELSE ''
|
|
47
48
|
END AS body
|
|
48
49
|
FROM visible
|
|
@@ -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 'demoted' THEN 0
|
|
76
|
+
ELSE 1
|
|
77
77
|
END
|
|
78
78
|
, turn
|
|
79
79
|
, updated_at
|