@possumtech/rummy 0.2.8 → 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.
Files changed (114) hide show
  1. package/.env.example +13 -2
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +422 -188
  4. package/SPEC.md +440 -106
  5. package/migrations/001_initial_schema.sql +5 -3
  6. package/package.json +17 -5
  7. package/service.js +5 -3
  8. package/src/agent/AgentLoop.js +252 -55
  9. package/src/agent/ContextAssembler.js +20 -4
  10. package/src/agent/KnownStore.js +82 -25
  11. package/src/agent/ProjectAgent.js +4 -1
  12. package/src/agent/ResponseHealer.js +86 -32
  13. package/src/agent/TurnExecutor.js +542 -207
  14. package/src/agent/XmlParser.js +77 -41
  15. package/src/agent/known_store.sql +68 -4
  16. package/src/agent/schemes.sql +3 -0
  17. package/src/agent/tokens.js +7 -21
  18. package/src/agent/turns.sql +15 -1
  19. package/src/hooks/HookRegistry.js +7 -0
  20. package/src/hooks/Hooks.js +15 -0
  21. package/src/hooks/PluginContext.js +14 -1
  22. package/src/hooks/RummyContext.js +16 -4
  23. package/src/hooks/ToolRegistry.js +77 -19
  24. package/src/llm/LlmProvider.js +27 -8
  25. package/src/llm/OpenAiClient.js +20 -0
  26. package/src/llm/OpenRouterClient.js +24 -2
  27. package/src/llm/XaiClient.js +47 -2
  28. package/src/plugins/ask_user/README.md +4 -4
  29. package/src/plugins/ask_user/ask_user.js +5 -5
  30. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  31. package/src/plugins/budget/README.md +31 -0
  32. package/src/plugins/budget/budget.js +55 -0
  33. package/src/plugins/cp/README.md +5 -4
  34. package/src/plugins/cp/cp.js +10 -6
  35. package/src/plugins/cp/cpDoc.js +29 -0
  36. package/src/plugins/engine/engine.sql +1 -8
  37. package/src/plugins/engine/turn_context.sql +4 -9
  38. package/src/plugins/env/README.md +3 -4
  39. package/src/plugins/env/env.js +5 -5
  40. package/src/plugins/env/envDoc.js +29 -0
  41. package/src/plugins/file/README.md +9 -12
  42. package/src/plugins/file/file.js +34 -35
  43. package/src/plugins/get/README.md +2 -2
  44. package/src/plugins/get/get.js +77 -6
  45. package/src/plugins/get/getDoc.js +51 -0
  46. package/src/plugins/hedberg/hedberg.js +2 -1
  47. package/src/plugins/hedberg/matcher.js +10 -29
  48. package/src/plugins/hedberg/normalize.js +28 -0
  49. package/src/plugins/hedberg/patterns.js +25 -27
  50. package/src/plugins/hedberg/sed.js +17 -10
  51. package/src/plugins/index.js +66 -14
  52. package/src/plugins/instructions/README.md +6 -2
  53. package/src/plugins/instructions/instructions.js +20 -4
  54. package/src/plugins/instructions/preamble.md +19 -5
  55. package/src/plugins/known/README.md +10 -7
  56. package/src/plugins/known/known.js +23 -17
  57. package/src/plugins/known/knownDoc.js +34 -0
  58. package/src/plugins/mv/README.md +5 -4
  59. package/src/plugins/mv/mv.js +27 -6
  60. package/src/plugins/mv/mvDoc.js +45 -0
  61. package/src/plugins/performed/README.md +15 -0
  62. package/src/plugins/performed/performed.js +45 -0
  63. package/src/plugins/persona/persona.js +78 -0
  64. package/src/plugins/previous/README.md +3 -2
  65. package/src/plugins/previous/previous.js +33 -24
  66. package/src/plugins/progress/README.md +1 -2
  67. package/src/plugins/progress/progress.js +33 -21
  68. package/src/plugins/prompt/README.md +5 -5
  69. package/src/plugins/prompt/prompt.js +15 -17
  70. package/src/plugins/rm/README.md +4 -4
  71. package/src/plugins/rm/rm.js +32 -20
  72. package/src/plugins/rm/rmDoc.js +30 -0
  73. package/src/plugins/rpc/README.md +15 -28
  74. package/src/plugins/rpc/rpc.js +42 -77
  75. package/src/plugins/set/README.md +13 -12
  76. package/src/plugins/set/set.js +107 -16
  77. package/src/plugins/set/setDoc.js +49 -0
  78. package/src/plugins/sh/README.md +4 -4
  79. package/src/plugins/sh/sh.js +5 -5
  80. package/src/plugins/sh/shDoc.js +29 -0
  81. package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
  82. package/src/plugins/summarize/README.md +6 -5
  83. package/src/plugins/summarize/summarize.js +7 -6
  84. package/src/plugins/summarize/summarizeDoc.js +33 -0
  85. package/src/plugins/telemetry/telemetry.js +16 -9
  86. package/src/plugins/think/README.md +20 -0
  87. package/src/plugins/think/think.js +5 -0
  88. package/src/plugins/unknown/README.md +6 -5
  89. package/src/plugins/unknown/unknown.js +12 -9
  90. package/src/plugins/unknown/unknownDoc.js +31 -0
  91. package/src/plugins/update/README.md +3 -8
  92. package/src/plugins/update/update.js +7 -6
  93. package/src/plugins/update/updateDoc.js +33 -0
  94. package/src/server/ClientConnection.js +59 -45
  95. package/src/server/RpcRegistry.js +52 -4
  96. package/src/sql/v_model_context.sql +10 -25
  97. package/src/plugins/ask_user/docs.md +0 -2
  98. package/src/plugins/cp/docs.md +0 -2
  99. package/src/plugins/current/README.md +0 -14
  100. package/src/plugins/current/current.js +0 -47
  101. package/src/plugins/env/docs.md +0 -4
  102. package/src/plugins/get/docs.md +0 -10
  103. package/src/plugins/known/docs.md +0 -3
  104. package/src/plugins/mv/docs.md +0 -2
  105. package/src/plugins/rm/docs.md +0 -6
  106. package/src/plugins/set/docs.md +0 -6
  107. package/src/plugins/sh/docs.md +0 -2
  108. package/src/plugins/skills/README.md +0 -25
  109. package/src/plugins/store/README.md +0 -20
  110. package/src/plugins/store/docs.md +0 -6
  111. package/src/plugins/store/store.js +0 -63
  112. package/src/plugins/summarize/docs.md +0 -4
  113. package/src/plugins/unknown/docs.md +0 -5
  114. package/src/plugins/update/docs.md +0 -4
@@ -1,18 +1,21 @@
1
1
  # known
2
2
 
3
- Writes arbitrary key/value entries into the store at full fidelity.
3
+ Writes knowledge entries into the store at full fidelity.
4
4
 
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `known`
8
- - **Modes**: ask, act
9
- - **Category**: act
10
- - **Handler**: Upserts the entry body at the target path with `full` state.
8
+ - **Category**: `data`
9
+ - **Handler**: Upserts the entry body at the target path with status 200.
10
+ - **Filter**: `assembly.system` at priority 100 renders `<knowns>` section.
11
11
 
12
12
  ## Projection
13
13
 
14
- Shows `known {path}` followed by the entry body.
14
+ Shows `# known {path}` followed by the entry body.
15
15
 
16
- ## Behavior
16
+ ## Assembly
17
17
 
18
- The target path defaults to `entry.resultPath` but can be overridden via `attrs.path`. Used by the model to persist structured notes and context.
18
+ Filters turn_context rows where `category === "data"`. Renders all
19
+ data entries (files, knowledge, skills, URLs) into the `<knowns>` section
20
+ of the system message. Third-party plugins that register with
21
+ `category: "data"` automatically appear here.
@@ -1,24 +1,24 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./knownDoc.js";
2
2
 
3
3
  export default class Known {
4
4
  #core;
5
5
 
6
6
  constructor(core) {
7
7
  this.#core = core;
8
- core.registerScheme({ category: "knowledge" });
8
+ core.registerScheme({ category: "data" });
9
9
  core.on("handler", this.handler.bind(this));
10
10
  core.on("full", this.full.bind(this));
11
11
  core.filter("assembly.system", this.assembleKnown.bind(this), 100);
12
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
13
- core.filter("instructions.toolDocs", async (content) =>
14
- content ? `${content}\n\n${docs}` : docs,
15
- );
12
+ core.filter("instructions.toolDocs", async (docsMap) => {
13
+ docsMap.known = docs;
14
+ return docsMap;
15
+ });
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) {
@@ -26,13 +26,7 @@ export default class Known {
26
26
  }
27
27
 
28
28
  async assembleKnown(content, ctx) {
29
- const entries = ctx.rows.filter(
30
- (r) =>
31
- r.category === "file" ||
32
- r.category === "file_index" ||
33
- r.category === "known" ||
34
- r.category === "known_index",
35
- );
29
+ const entries = ctx.rows.filter((r) => r.category === "data");
36
30
  if (entries.length === 0) return content;
37
31
 
38
32
  // Rows arrive pre-sorted by SQL: skill → index → summary → full, then by recency
@@ -43,13 +37,25 @@ export default class Known {
43
37
  }
44
38
 
45
39
  function renderKnownTag(entry, demotedSet) {
40
+ const tag = entry.scheme || "file";
41
+ const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
46
42
  const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
47
43
  const status = entry.status ? ` status="${entry.status}"` : "";
44
+ const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
48
45
  const flag = demotedSet?.has(entry.path) ? " demoted" : "";
49
46
 
47
+ const attrs =
48
+ typeof entry.attributes === "string"
49
+ ? JSON.parse(entry.attributes)
50
+ : entry.attributes;
51
+ const summary =
52
+ typeof attrs?.summary === "string"
53
+ ? ` summary="${attrs.summary.slice(0, 80)}"`
54
+ : "";
55
+
50
56
  if (entry.body) {
51
- return `<known path="${entry.path}"${status}${tokens}${flag}>${entry.body}</known>`;
57
+ return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}>${entry.body}</${tag}>`;
52
58
  }
53
59
 
54
- return `<known path="${entry.path}"${status}${tokens}${flag}/>`;
60
+ return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}/>`;
55
61
  }
@@ -0,0 +1,34 @@
1
+ // Tool doc for <known/>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ // --- Syntax: path = slash-separated topic hierarchy, body = the information to save
6
+ [
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
+ ],
9
+ // --- Examples: category-level entries — multiple related facts per entry, not one per item
10
+ [
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
+ ],
14
+ [
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
+ ],
18
+ // --- Constraints: summary and grouping first (model forms generation pattern from header + examples)
19
+ [
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
+ ],
23
+ [
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
+ ],
27
+ // --- Lifecycle
28
+ [
29
+ '* Recall with <get path="known://config/*">replica</get>',
30
+ "Cross-tool lifecycle: glob by category, filter by keyword. Matches the slashed path convention.",
31
+ ],
32
+ ];
33
+
34
+ export default LINES.map(([text]) => text).join("\n");
@@ -5,9 +5,8 @@ Moves (renames) an entry from one path to another within the K/V store.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `mv`
8
- - **Modes**: ask, act
9
- - **Category**: act
10
- - **Handler**: Reads source body, writes to destination, removes source. K/V destinations resolve immediately (`pass`); file destinations produce a `proposed` entry.
8
+ - **Category**: `logging`
9
+ - **Handler**: Reads source body, writes to destination, removes source. Scheme destinations resolve immediately (status 200); file destinations produce status 202 (proposed).
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,6 @@ Shows `mv {from} {to}`.
15
14
 
16
15
  ## Behavior
17
16
 
18
- Warns if the destination already exists and will be overwritten. Uses `KnownStore.scheme()` to determine K/V vs file paths. Source entry is removed on successful K/V moves.
17
+ Warns if the destination already exists and will be overwritten. Uses
18
+ `KnownStore.scheme()` to determine scheme vs file paths. Source entry
19
+ is removed on successful scheme moves.
@@ -1,5 +1,5 @@
1
- import { readFileSync } from "node:fs";
2
1
  import KnownStore from "../../agent/KnownStore.js";
2
+ import docs from "./mvDoc.js";
3
3
 
4
4
  export default class Mv {
5
5
  #core;
@@ -10,15 +10,36 @@ export default class Mv {
10
10
  core.on("handler", this.handler.bind(this));
11
11
  core.on("full", this.full.bind(this));
12
12
  core.on("summary", this.summary.bind(this));
13
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
14
- core.filter("instructions.toolDocs", async (content) =>
15
- content ? `${content}\n\n${docs}` : docs,
16
- );
13
+ core.filter("instructions.toolDocs", async (docsMap) => {
14
+ docsMap.mv = docs;
15
+ return docsMap;
16
+ });
17
17
  }
18
18
 
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, archive: 1 };
23
+ const fidelity = VALID[entry.attributes.fidelity]
24
+ ? entry.attributes.fidelity
25
+ : undefined;
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
+ }
22
43
 
23
44
  const source = await store.getBody(runId, path);
24
45
  if (source === null) return;
@@ -37,7 +58,7 @@ export default class Mv {
37
58
  loopId,
38
59
  });
39
60
  } else {
40
- await store.upsert(runId, turn, to, source, 200, { loopId });
61
+ await store.upsert(runId, turn, to, source, 200, { fidelity, loopId });
41
62
  await store.remove(runId, path);
42
63
  await store.upsert(runId, turn, entry.resultPath, body, 200, {
43
64
  attributes: { from: path, to, isMove: true, warning },
@@ -0,0 +1,45 @@
1
+ // Tool doc for <mv>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ // --- Syntax: path attr = source, body = destination
6
+ [
7
+ '## <mv path="[source]">[destination]</mv> - Move or rename a file or entry',
8
+ ],
9
+
10
+ // --- Examples: entry rename and file move
11
+ [
12
+ 'Example: <mv path="known://active_task">known://completed_task</mv>',
13
+ "Entry rename. Most common mv use case. Shows known:// path convention.",
14
+ ],
15
+ [
16
+ 'Example: <mv path="src/old_name.js">src/new_name.js</mv>',
17
+ "File rename. Shows that mv works on files too, not just known entries.",
18
+ ],
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
+
34
+ // --- Constraints
35
+ [
36
+ "* Source path accepts patterns for batch moves",
37
+ "Pattern support consistent with get/cp/rm.",
38
+ ],
39
+ [
40
+ "* In ask mode, destination MUST be a scheme path (not a file)",
41
+ "Mode constraint. Prevents file mutations in ask mode via mv.",
42
+ ],
43
+ ];
44
+
45
+ export default LINES.map(([text]) => text).join("\n");
@@ -0,0 +1,15 @@
1
+ # performed
2
+
3
+ Renders the `<performed>` section of the user message — the active loop's
4
+ tool results and lifecycle signals.
5
+
6
+ ## Registration
7
+
8
+ - **Filter**: `assembly.user` at priority 100
9
+
10
+ ## Behavior
11
+
12
+ Filters turn_context rows where `category === "logging"` and
13
+ `source_turn >= loopStartTurn`. Renders each entry chronologically
14
+ with turn, status, summary, fidelity, and tokens. Empty on the first
15
+ turn of a loop.
@@ -0,0 +1,45 @@
1
+ export default class Performed {
2
+ #core;
3
+
4
+ constructor(core) {
5
+ this.#core = core;
6
+ core.filter("assembly.user", this.assemblePerformed.bind(this), 100);
7
+ }
8
+
9
+ async assemblePerformed(content, ctx) {
10
+ const entries = ctx.rows.filter(
11
+ (r) =>
12
+ r.category === "logging" &&
13
+ r.source_turn >= ctx.loopStartTurn &&
14
+ r.scheme !== "unknown",
15
+ );
16
+ if (entries.length === 0) return content;
17
+
18
+ const lines = entries.map((e) => renderToolTag(e));
19
+ return `${content}<performed>\n${lines.join("\n")}\n</performed>\n`;
20
+ }
21
+ }
22
+
23
+ function renderToolTag(entry) {
24
+ const attrs =
25
+ typeof entry.attributes === "string"
26
+ ? JSON.parse(entry.attributes)
27
+ : entry.attributes;
28
+
29
+ const target = attrs?.path || attrs?.file || attrs?.command || "";
30
+ const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
31
+ const status = entry.status ? ` status="${entry.status}"` : "";
32
+ const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
33
+ const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
34
+ const summary =
35
+ typeof attrs?.summary === "string"
36
+ ? ` summary="${attrs.summary.slice(0, 80)}"`
37
+ : "";
38
+
39
+ const body = entry.body || null;
40
+
41
+ if (body) {
42
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}>${body}</${entry.scheme}>`;
43
+ }
44
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}/>`;
45
+ }
@@ -0,0 +1,78 @@
1
+ import fs from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export default class Persona {
5
+ #core;
6
+
7
+ constructor(core) {
8
+ this.#core = core;
9
+ const r = core.hooks.rpc.registry;
10
+
11
+ r.register("persona/set", {
12
+ handler: async (params, ctx) => {
13
+ if (!params.run) throw new Error("run is required");
14
+
15
+ const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
16
+ if (!runRow) throw new Error(`Run not found: ${params.run}`);
17
+
18
+ let text = params.text;
19
+ if (params.name && !text) {
20
+ text = await loadFile(params.name);
21
+ }
22
+
23
+ await ctx.db.update_run_config.run({
24
+ id: runRow.id,
25
+ temperature: null,
26
+ persona: text || null,
27
+ context_limit: null,
28
+ model: null,
29
+ });
30
+
31
+ return { status: "ok" };
32
+ },
33
+ description:
34
+ "Set persona on a run. Pass name or text. Pass neither to clear.",
35
+ params: {
36
+ run: "string — run alias",
37
+ name: "string? — persona filename (without .md)",
38
+ text: "string? — raw persona text (overrides name)",
39
+ },
40
+ requiresInit: true,
41
+ });
42
+
43
+ r.register("listPersonas", {
44
+ handler: async () => {
45
+ const dir = configDir();
46
+ if (!dir) return [];
47
+ try {
48
+ const files = await fs.readdir(dir);
49
+ return files
50
+ .filter((f) => f.endsWith(".md"))
51
+ .map((f) => ({ name: f.replace(".md", ""), path: join(dir, f) }));
52
+ } catch {
53
+ return [];
54
+ }
55
+ },
56
+ description: "List available persona files. Returns [{ name, path }].",
57
+ requiresInit: true,
58
+ });
59
+ }
60
+ }
61
+
62
+ function configDir() {
63
+ const home = process.env.RUMMY_HOME;
64
+ if (home) return join(home, "personas");
65
+ return null;
66
+ }
67
+
68
+ async function loadFile(name) {
69
+ const dir = configDir();
70
+ if (!dir) throw new Error("RUMMY_HOME not configured");
71
+ const path = join(dir, `${name}.md`);
72
+ try {
73
+ return await fs.readFile(path, "utf8");
74
+ } catch (err) {
75
+ if (err.code === "ENOENT") throw new Error(`Not found: ${path}`);
76
+ throw err;
77
+ }
78
+ }
@@ -10,6 +10,7 @@ history from prior ask/act invocations on this run.
10
10
 
11
11
  ## Behavior
12
12
 
13
- Filters turn_context rows where `category` is `result` or `structural`
13
+ Filters turn_context rows where `category` is `logging` or `prompt`
14
14
  and `source_turn < loopStartTurn`. Renders each entry chronologically
15
- with status symbols (✓/✗/·).
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,13 +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.filter(
13
- (r) =>
14
- (r.category === "result" ||
15
- r.category === "structural" ||
16
- r.category === "prompt") &&
17
- r.source_turn < ctx.loopStartTurn,
18
- );
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
+ });
19
26
  if (entries.length === 0) return content;
20
27
 
21
28
  const lines = await Promise.all(
@@ -25,27 +32,29 @@ export default class Previous {
25
32
  }
26
33
  }
27
34
 
28
- async function renderToolTag(entry, core) {
35
+ async function renderToolTag(entry, _core) {
29
36
  const attrs =
30
37
  typeof entry.attributes === "string"
31
38
  ? JSON.parse(entry.attributes)
32
39
  : entry.attributes;
33
40
 
34
- const path = `${entry.scheme}://${attrs?.path || attrs?.file || attrs?.command || ""}`;
41
+ const target = attrs?.path || attrs?.file || attrs?.command || "";
42
+ const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
35
43
  const status = entry.status ? ` status="${entry.status}"` : "";
36
-
37
- let body;
38
- try {
39
- body = await core.hooks.tools.view(entry.scheme, {
40
- ...entry,
41
- attributes: attrs,
42
- });
43
- } catch {
44
- body = entry.body;
45
- }
46
-
47
- if (body) {
48
- return `<tool path="${path}"${status}>${body}</tool>`;
49
- }
50
- return `<tool path="${path}"${status}/>`;
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}/>`;
51
60
  }
@@ -9,9 +9,8 @@ current work log to the active prompt.
9
9
 
10
10
  ## Behavior
11
11
 
12
- On first turn: "Begin."
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,44 +7,56 @@ export default class Progress {
7
7
  }
8
8
 
9
9
  async assembleProgress(content, ctx) {
10
- const usedTokens = ctx.rows.reduce((sum, r) => sum + (r.tokens || 0), 0);
11
- const contextSize = ctx.contextSize || 0;
10
+ const { lastContextTokens: usedTokens, contextSize } = ctx;
12
11
  const pct = contextSize ? Math.round((usedTokens / contextSize) * 100) : 0;
13
12
 
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];
17
+ const fullEntries = entries.filter((r) => r.fidelity === "full");
18
+ const summaryEntries = entries.filter((r) => r.fidelity === "summary");
19
+ const indexEntries = entries.filter((r) => r.fidelity === "index");
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);
23
+
14
24
  const unknownCount = ctx.rows.filter(
15
25
  (r) => r.category === "unknown",
16
26
  ).length;
17
27
 
18
- const hasCurrent = ctx.rows.some(
19
- (r) =>
20
- (r.category === "result" || r.category === "structural") &&
21
- r.source_turn >= ctx.loopStartTurn,
28
+ const hasPerformed = loggingEntries.some(
29
+ (r) => r.source_turn >= ctx.loopStartTurn,
22
30
  );
23
31
 
24
32
  const parts = [];
25
33
 
26
- const tokenInfo = contextSize
27
- ? `${usedTokens} of ${contextSize} tokens (${pct}%)`
34
+ const knownCount = dataEntries.length;
35
+ const loggingCount = loggingEntries.length;
36
+ const tokenLine = contextSize
37
+ ? `${usedTokens} of ${contextSize} tokens (${pct}%) · ${knownCount} known${knownCount !== 1 ? "s" : ""} · ${loggingCount} logging · ${unknownCount} unknown${unknownCount !== 1 ? "s" : ""}`
28
38
  : "";
29
- const unknownInfo =
30
- unknownCount > 0
31
- ? `${unknownCount} unknown${unknownCount > 1 ? "s" : ""} remaining`
32
- : "0 unknowns";
33
- const status = [tokenInfo, unknownInfo].filter(Boolean).join(" · ");
34
- if (status) parts.push(status);
35
-
36
- if (ctx.demoted?.length > 0) {
37
- parts.push(
38
- `⚠ ${ctx.demoted.length} entries demoted to summary to fit context budget. Use <get/> to restore.`,
39
+ if (tokenLine) parts.push(tokenLine);
40
+
41
+ // Fidelity distribution
42
+ const fidelityParts = [];
43
+ if (fullEntries.length > 0)
44
+ fidelityParts.push(`${fullEntries.length} full (${fullTokens} tok)`);
45
+ if (summaryEntries.length > 0)
46
+ fidelityParts.push(
47
+ `${summaryEntries.length} summary (${summaryTokens} tok)`,
39
48
  );
40
- }
49
+ if (indexEntries.length > 0)
50
+ fidelityParts.push(`${indexEntries.length} index (${indexTokens} tok)`);
51
+ if (fidelityParts.length > 0)
52
+ parts.push(`Entries: ${fidelityParts.join(" · ")}`);
41
53
 
42
- if (hasCurrent) {
54
+ if (hasPerformed) {
43
55
  parts.push(
44
56
  "The above actions were performed in response to the following prompt:",
45
57
  );
46
58
  }
47
59
 
48
- return `${content}<progress>${parts.join("\n")}</progress>\n`;
60
+ return `${content}<progress turn="${ctx.turn}">${parts.join("\n")}</progress>\n`;
49
61
  }
50
62
  }
@@ -1,6 +1,6 @@
1
1
  # prompt
2
2
 
3
- Renders the `<ask>` or `<act>` tag at the end of the user message.
3
+ Renders the `<prompt mode="ask|act">` tag at the end of the user message.
4
4
  Always present on every turn — the model always sees its task.
5
5
 
6
6
  ## Registration
@@ -9,8 +9,8 @@ Always present on every turn — the model always sees its task.
9
9
 
10
10
  ## Behavior
11
11
 
12
- Finds the latest `ask://` or `act://` entry in the turn_context rows.
13
- Renders with `tools` attribute (available tool list) and optional `warn`
14
- attribute in ask mode ("File and system modification prohibited on this
15
- turn."). Falls back to the mode passed by the core if no prompt entry
12
+ Finds the latest `prompt://` entry in the turn_context rows. The mode
13
+ (`ask` or `act`) is stored in `attributes.mode`. Renders with `tools`
14
+ attribute (available tool list) and optional `warn` attribute in ask
15
+ mode. Falls back to the mode passed by the core if no prompt entry
16
16
  exists.
@@ -3,6 +3,7 @@ export default class Prompt {
3
3
 
4
4
  constructor(core) {
5
5
  this.#core = core;
6
+ core.hooks.tools.onView("prompt", (entry) => entry.body);
6
7
  core.on("turn.started", this.onTurnStarted.bind(this));
7
8
  core.filter("assembly.user", this.assemblePrompt.bind(this), 300);
8
9
  }
@@ -11,34 +12,31 @@ export default class Prompt {
11
12
  const { entries: store, sequence: turn, runId, loopId } = rummy;
12
13
 
13
14
  if (!isContinuation && prompt) {
14
- await store.upsert(runId, turn, `prompt://${turn}`, "", 200, {
15
+ await store.upsert(runId, turn, `prompt://${turn}`, prompt, 200, {
15
16
  attributes: { mode },
16
17
  loopId,
17
18
  });
18
- await store.upsert(runId, turn, `${mode}://${turn}`, prompt, 200, {
19
- loopId,
20
- });
21
- } else {
22
- await store.upsert(runId, turn, `progress://${turn}`, prompt || "", 200, {
23
- loopId,
24
- });
25
19
  }
26
20
  }
27
21
 
28
22
  async assemblePrompt(content, ctx) {
29
23
  const promptEntry = ctx.rows.findLast(
30
- (r) =>
31
- r.category === "prompt" && (r.scheme === "ask" || r.scheme === "act"),
24
+ (r) => r.category === "prompt" && r.scheme === "prompt",
32
25
  );
33
26
 
34
- const mode = promptEntry?.scheme || ctx.type;
27
+ const attrs =
28
+ typeof promptEntry?.attributes === "string"
29
+ ? JSON.parse(promptEntry.attributes)
30
+ : promptEntry?.attributes;
31
+ const mode = attrs?.mode || ctx.type;
35
32
  const body = promptEntry?.body || "";
36
- const tools = this.#core.hooks.tools.namesForMode(mode).join(", ");
37
- const warn =
38
- mode === "ask"
39
- ? ' warn="File and system modification prohibited on this turn."'
40
- : "";
33
+ const toolNames = ctx.toolSet
34
+ ? [...ctx.toolSet]
35
+ : [...this.#core.hooks.tools.resolveForLoop(mode)];
36
+ const tools = toolNames.join(",");
37
+ let warn = "";
38
+ if (mode === "ask") warn = ' warn="File editing disallowed."';
41
39
 
42
- return `${content}<${mode} tools="${tools}"${warn}>${body}</${mode}>`;
40
+ return `${content}<prompt mode="${mode}" tools="${tools}"${warn}>${body}</prompt>`;
43
41
  }
44
42
  }