@possumtech/rummy 0.2.8 → 0.3.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.
Files changed (108) hide show
  1. package/.env.example +11 -1
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +422 -188
  4. package/SPEC.md +284 -93
  5. package/migrations/001_initial_schema.sql +6 -4
  6. package/package.json +13 -5
  7. package/src/agent/AgentLoop.js +166 -15
  8. package/src/agent/ContextAssembler.js +18 -4
  9. package/src/agent/KnownStore.js +127 -13
  10. package/src/agent/ProjectAgent.js +4 -1
  11. package/src/agent/ResponseHealer.js +21 -1
  12. package/src/agent/TurnExecutor.js +365 -175
  13. package/src/agent/XmlParser.js +72 -39
  14. package/src/agent/known_store.sql +20 -4
  15. package/src/agent/schemes.sql +3 -0
  16. package/src/agent/tokens.js +6 -21
  17. package/src/agent/turns.sql +10 -1
  18. package/src/hooks/Hooks.js +18 -0
  19. package/src/hooks/PluginContext.js +14 -1
  20. package/src/hooks/RummyContext.js +16 -4
  21. package/src/hooks/ToolRegistry.js +83 -19
  22. package/src/llm/LlmProvider.js +27 -8
  23. package/src/llm/OpenAiClient.js +20 -0
  24. package/src/llm/OpenRouterClient.js +24 -2
  25. package/src/llm/XaiClient.js +47 -2
  26. package/src/plugins/ask_user/README.md +4 -4
  27. package/src/plugins/ask_user/ask_user.js +5 -5
  28. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  29. package/src/plugins/budget/BudgetGuard.js +74 -0
  30. package/src/plugins/budget/README.md +43 -0
  31. package/src/plugins/budget/budget.js +79 -0
  32. package/src/plugins/cp/README.md +5 -4
  33. package/src/plugins/cp/cp.js +10 -6
  34. package/src/plugins/cp/cpDoc.js +29 -0
  35. package/src/plugins/current/README.md +4 -4
  36. package/src/plugins/current/current.js +9 -6
  37. package/src/plugins/engine/engine.sql +1 -8
  38. package/src/plugins/engine/turn_context.sql +4 -9
  39. package/src/plugins/env/README.md +3 -4
  40. package/src/plugins/env/env.js +5 -5
  41. package/src/plugins/env/envDoc.js +29 -0
  42. package/src/plugins/file/README.md +9 -12
  43. package/src/plugins/file/file.js +34 -35
  44. package/src/plugins/get/README.md +2 -2
  45. package/src/plugins/get/get.js +6 -5
  46. package/src/plugins/get/getDoc.js +41 -0
  47. package/src/plugins/hedberg/hedberg.js +2 -1
  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 +9 -5
  55. package/src/plugins/known/README.md +10 -7
  56. package/src/plugins/known/known.js +29 -17
  57. package/src/plugins/known/knownDoc.js +33 -0
  58. package/src/plugins/mv/README.md +5 -4
  59. package/src/plugins/mv/mv.js +10 -6
  60. package/src/plugins/mv/mvDoc.js +31 -0
  61. package/src/plugins/persona/persona.js +78 -0
  62. package/src/plugins/previous/README.md +2 -2
  63. package/src/plugins/previous/previous.js +9 -6
  64. package/src/plugins/progress/progress.js +41 -15
  65. package/src/plugins/prompt/README.md +5 -5
  66. package/src/plugins/prompt/prompt.js +18 -13
  67. package/src/plugins/rm/README.md +4 -4
  68. package/src/plugins/rm/rm.js +5 -5
  69. package/src/plugins/rm/rmDoc.js +30 -0
  70. package/src/plugins/rpc/README.md +15 -28
  71. package/src/plugins/rpc/rpc.js +42 -77
  72. package/src/plugins/set/README.md +13 -12
  73. package/src/plugins/set/set.js +60 -5
  74. package/src/plugins/set/setDoc.js +45 -0
  75. package/src/plugins/sh/README.md +4 -4
  76. package/src/plugins/sh/sh.js +5 -5
  77. package/src/plugins/sh/shDoc.js +29 -0
  78. package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
  79. package/src/plugins/summarize/README.md +6 -5
  80. package/src/plugins/summarize/summarize.js +7 -6
  81. package/src/plugins/summarize/summarizeDoc.js +33 -0
  82. package/src/plugins/telemetry/telemetry.js +3 -1
  83. package/src/plugins/think/README.md +20 -0
  84. package/src/plugins/think/think.js +5 -0
  85. package/src/plugins/unknown/README.md +5 -5
  86. package/src/plugins/unknown/unknown.js +9 -7
  87. package/src/plugins/unknown/unknownDoc.js +31 -0
  88. package/src/plugins/update/README.md +3 -8
  89. package/src/plugins/update/update.js +7 -6
  90. package/src/plugins/update/updateDoc.js +33 -0
  91. package/src/server/RpcRegistry.js +52 -4
  92. package/src/sql/v_model_context.sql +16 -21
  93. package/src/plugins/ask_user/docs.md +0 -2
  94. package/src/plugins/cp/docs.md +0 -2
  95. package/src/plugins/env/docs.md +0 -4
  96. package/src/plugins/get/docs.md +0 -10
  97. package/src/plugins/known/docs.md +0 -3
  98. package/src/plugins/mv/docs.md +0 -2
  99. package/src/plugins/rm/docs.md +0 -6
  100. package/src/plugins/set/docs.md +0 -6
  101. package/src/plugins/sh/docs.md +0 -2
  102. package/src/plugins/skills/README.md +0 -25
  103. package/src/plugins/store/README.md +0 -20
  104. package/src/plugins/store/docs.md +0 -6
  105. package/src/plugins/store/store.js +0 -63
  106. package/src/plugins/summarize/docs.md +0 -4
  107. package/src/plugins/unknown/docs.md +0 -5
  108. package/src/plugins/update/docs.md +0 -4
@@ -1,18 +1,18 @@
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) {
@@ -26,30 +26,42 @@ 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
39
33
  const demotedSet = new Set(ctx.demoted || []);
40
- const lines = entries.map((e) => renderKnownTag(e, demotedSet));
34
+ const panic = ctx.type === "panic";
35
+ const lines = entries.map((e) => renderKnownTag(e, demotedSet, panic));
41
36
  return `${content}\n\n<knowns>\n${lines.join("\n")}\n</knowns>`;
42
37
  }
43
38
  }
44
39
 
45
- function renderKnownTag(entry, demotedSet) {
40
+ function renderKnownTag(entry, demotedSet, panic = false) {
41
+ const tag = entry.scheme || "file";
42
+ const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
46
43
  const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
47
44
  const status = entry.status ? ` status="${entry.status}"` : "";
45
+ const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
48
46
  const flag = demotedSet?.has(entry.path) ? " demoted" : "";
49
47
 
48
+ // Panic mode: index-only view so context fits in LLM window
49
+ if (panic) {
50
+ return `<${tag} path="${entry.path}"${turn}${fidelity}${tokens}/>`;
51
+ }
52
+
53
+ const attrs =
54
+ typeof entry.attributes === "string"
55
+ ? JSON.parse(entry.attributes)
56
+ : entry.attributes;
57
+ const summary =
58
+ typeof attrs?.summary === "string"
59
+ ? ` summary="${attrs.summary.slice(0, 80)}"`
60
+ : "";
61
+
50
62
  if (entry.body) {
51
- return `<known path="${entry.path}"${status}${tokens}${flag}>${entry.body}</known>`;
63
+ return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}>${entry.body}</${tag}>`;
52
64
  }
53
65
 
54
- return `<known path="${entry.path}"${status}${tokens}${flag}/>`;
66
+ return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}/>`;
55
67
  }
@@ -0,0 +1,33 @@
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: body = the information to save
6
+ [
7
+ "## <known>[specific information, ideas, or plans]</known> - Sort and save what you learn for later recall",
8
+ ],
9
+ // --- Examples: summary-with-keywords first (teaches the right pattern)
10
+ [
11
+ 'Example: <known summary="hedberg,comedian,death,2005">Mitch Hedberg died on March 30, 2005</known>',
12
+ "Primary pattern: comma-separated keywords in summary. Path auto-generated from summary as known://hedberg/comedian/death/2005. Keywords become searchable path segments.",
13
+ ],
14
+ [
15
+ 'Example: <known path="known://people/rumsfeld" summary="defense,secretary,born,1932">Donald Rumsfeld was born in 1932 and served as Secretary of Defense</known>',
16
+ "Explicit path form: slashed path=category/key, summary=keywords. For when the model wants direct control over taxonomy.",
17
+ ],
18
+ // --- Lifecycle
19
+ [
20
+ '* Recall with <get path="known://people/*">keyword</get>',
21
+ "Cross-tool lifecycle: glob by category, filter by keyword. Matches the slashed path convention.",
22
+ ],
23
+ [
24
+ "* `summary` keywords survive compression — write keywords you'll search for later",
25
+ "Teaches WHY summaries matter. Keywords become the path AND the compressed view.",
26
+ ],
27
+ [
28
+ "* YOU MUST sort and save all new information, ideas, and plans in their own <known> entries",
29
+ "Critical behavioral constraint. 'new' prevents re-saving known facts.",
30
+ ],
31
+ ];
32
+
33
+ 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,19 @@ 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 };
23
+ const fidelity = VALID[entry.attributes.fidelity]
24
+ ? entry.attributes.fidelity
25
+ : undefined;
22
26
 
23
27
  const source = await store.getBody(runId, path);
24
28
  if (source === null) return;
@@ -37,7 +41,7 @@ export default class Mv {
37
41
  loopId,
38
42
  });
39
43
  } else {
40
- await store.upsert(runId, turn, to, source, 200, { loopId });
44
+ await store.upsert(runId, turn, to, source, 200, { fidelity, loopId });
41
45
  await store.remove(runId, path);
42
46
  await store.upsert(runId, turn, entry.resultPath, body, 200, {
43
47
  attributes: { from: path, to, isMove: true, warning },
@@ -0,0 +1,31 @@
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
+ // --- Constraints
21
+ [
22
+ "* Source path accepts globs for batch moves",
23
+ "Pattern support consistent with get/cp/rm.",
24
+ ],
25
+ [
26
+ "* In ask mode, destination MUST be a scheme path (not a file)",
27
+ "Mode constraint. Prevents file mutations in ask mode via mv.",
28
+ ],
29
+ ];
30
+
31
+ export default LINES.map(([text]) => text).join("\n");
@@ -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,6 @@ 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 number and status.
@@ -11,9 +11,7 @@ export default class Previous {
11
11
 
12
12
  const entries = ctx.rows.filter(
13
13
  (r) =>
14
- (r.category === "result" ||
15
- r.category === "structural" ||
16
- r.category === "prompt") &&
14
+ (r.category === "logging" || r.category === "prompt") &&
17
15
  r.source_turn < ctx.loopStartTurn,
18
16
  );
19
17
  if (entries.length === 0) return content;
@@ -31,8 +29,13 @@ async function renderToolTag(entry, core) {
31
29
  ? JSON.parse(entry.attributes)
32
30
  : entry.attributes;
33
31
 
34
- const path = `${entry.scheme}://${attrs?.path || attrs?.file || attrs?.command || ""}`;
32
+ const target = attrs?.path || attrs?.file || attrs?.command || "";
33
+ const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
35
34
  const status = entry.status ? ` status="${entry.status}"` : "";
35
+ const summary =
36
+ typeof attrs?.summary === "string"
37
+ ? ` summary="${attrs.summary.slice(0, 80)}"`
38
+ : "";
36
39
 
37
40
  let body;
38
41
  try {
@@ -45,7 +48,7 @@ async function renderToolTag(entry, core) {
45
48
  }
46
49
 
47
50
  if (body) {
48
- return `<tool path="${path}"${status}>${body}</tool>`;
51
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}>${body}</${entry.scheme}>`;
49
52
  }
50
- return `<tool path="${path}"${status}/>`;
53
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}/>`;
51
54
  }
@@ -7,35 +7,61 @@ 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);
10
+ // Use last turn's real assembled token count when available.
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;
11
14
  const contextSize = ctx.contextSize || 0;
12
15
  const pct = contextSize ? Math.round((usedTokens / contextSize) * 100) : 0;
13
16
 
17
+ // Fidelity distribution across known/file entries
18
+ const entries = ctx.rows.filter((r) => r.category === "data");
19
+ const fullEntries = entries.filter((r) => r.fidelity === "full");
20
+ const summaryEntries = entries.filter((r) => r.fidelity === "summary");
21
+ const indexEntries = entries.filter((r) => r.fidelity === "index");
22
+ const fullTokens = fullEntries.reduce((s, r) => s + (r.tokens || 0), 0);
23
+ const summaryTokens = summaryEntries.reduce(
24
+ (s, r) => s + (r.tokens || 0),
25
+ 0,
26
+ );
27
+ const indexTokens = indexEntries.reduce((s, r) => s + (r.tokens || 0), 0);
28
+
14
29
  const unknownCount = ctx.rows.filter(
15
30
  (r) => r.category === "unknown",
16
31
  ).length;
17
32
 
18
33
  const hasCurrent = ctx.rows.some(
19
- (r) =>
20
- (r.category === "result" || r.category === "structural") &&
21
- r.source_turn >= ctx.loopStartTurn,
34
+ (r) => r.category === "logging" && r.source_turn >= ctx.loopStartTurn,
22
35
  );
23
36
 
24
37
  const parts = [];
25
38
 
26
- const tokenInfo = contextSize
27
- ? `${usedTokens} of ${contextSize} tokens (${pct}%)`
39
+ const knownCount = entries.length;
40
+ const tokenLine = contextSize
41
+ ? `${usedTokens} of ${contextSize} tokens (${pct}%) · ${knownCount} known${knownCount !== 1 ? "s" : ""} · ${unknownCount} unknown${unknownCount !== 1 ? "s" : ""}`
28
42
  : "";
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) {
43
+ if (tokenLine) parts.push(tokenLine);
44
+
45
+ // Fidelity distribution
46
+ const fidelityParts = [];
47
+ if (fullEntries.length > 0)
48
+ fidelityParts.push(`${fullEntries.length} full (${fullTokens} tok)`);
49
+ if (summaryEntries.length > 0)
50
+ fidelityParts.push(
51
+ `${summaryEntries.length} summary (${summaryTokens} tok)`,
52
+ );
53
+ if (indexEntries.length > 0)
54
+ fidelityParts.push(`${indexEntries.length} index (${indexTokens} tok)`);
55
+ if (fidelityParts.length > 0)
56
+ parts.push(`Entries: ${fidelityParts.join(" · ")}`);
57
+
58
+ if (pct > 75) {
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) {
37
63
  parts.push(
38
- `⚠ ${ctx.demoted.length} entries demoted to summary to fit context budget. Use <get/> to restore.`,
64
+ 'Context above 50%. You may free space: <set fidelity="summary" summary="topic,detail,keyword"/>, <set fidelity="archive"/>, or <rm/>.',
39
65
  );
40
66
  }
41
67
 
@@ -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,8 @@ export default class Prompt {
3
3
 
4
4
  constructor(core) {
5
5
  this.#core = core;
6
+ core.hooks.tools.onView("prompt", (entry) => entry.body);
7
+ core.hooks.tools.onView("progress", (entry) => entry.body);
6
8
  core.on("turn.started", this.onTurnStarted.bind(this));
7
9
  core.filter("assembly.user", this.assemblePrompt.bind(this), 300);
8
10
  }
@@ -11,13 +13,10 @@ export default class Prompt {
11
13
  const { entries: store, sequence: turn, runId, loopId } = rummy;
12
14
 
13
15
  if (!isContinuation && prompt) {
14
- await store.upsert(runId, turn, `prompt://${turn}`, "", 200, {
16
+ await store.upsert(runId, turn, `prompt://${turn}`, prompt, 200, {
15
17
  attributes: { mode },
16
18
  loopId,
17
19
  });
18
- await store.upsert(runId, turn, `${mode}://${turn}`, prompt, 200, {
19
- loopId,
20
- });
21
20
  } else {
22
21
  await store.upsert(runId, turn, `progress://${turn}`, prompt || "", 200, {
23
22
  loopId,
@@ -27,18 +26,24 @@ export default class Prompt {
27
26
 
28
27
  async assemblePrompt(content, ctx) {
29
28
  const promptEntry = ctx.rows.findLast(
30
- (r) =>
31
- r.category === "prompt" && (r.scheme === "ask" || r.scheme === "act"),
29
+ (r) => r.category === "prompt" && r.scheme === "prompt",
32
30
  );
33
31
 
34
- const mode = promptEntry?.scheme || ctx.type;
32
+ const attrs =
33
+ typeof promptEntry?.attributes === "string"
34
+ ? JSON.parse(promptEntry.attributes)
35
+ : promptEntry?.attributes;
36
+ const mode = attrs?.mode || ctx.type;
35
37
  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
- : "";
38
+ const toolNames = ctx.toolSet
39
+ ? [...ctx.toolSet]
40
+ : [...this.#core.hooks.tools.resolveForLoop(mode)];
41
+ const tools = toolNames.join(",");
42
+ let warn = "";
43
+ if (mode === "ask") warn = ' warn="File editing disallowed."';
44
+ if (mode === "panic")
45
+ warn = ' warn="Context overflow. Free space to continue."';
41
46
 
42
- return `${content}<${mode} tools="${tools}"${warn}>${body}</${mode}>`;
47
+ return `${content}<prompt mode="${mode}" tools="${tools}"${warn}>${body}</prompt>`;
43
48
  }
44
49
  }
@@ -5,9 +5,8 @@ Removes entries by path or glob pattern.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `rm`
8
- - **Modes**: ask, act
9
- - **Category**: act
10
- - **Handler**: Matches entries by pattern. K/V entries are removed immediately (`pass`); file entries produce `proposed` state for client approval.
8
+ - **Category**: `logging`
9
+ - **Handler**: Matches entries by pattern. Scheme entries are removed immediately (status 200); file entries produce status 202 (proposed) for client approval.
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,5 @@ Shows `rm {path}`.
15
14
 
16
15
  ## Behavior
17
16
 
18
- Supports glob patterns and body filters via `getEntriesByPattern`. Each matched entry is processed independently.
17
+ Supports glob patterns and body filters via `getEntriesByPattern`. Each
18
+ matched entry is processed independently.
@@ -1,5 +1,5 @@
1
- import { readFileSync } from "node:fs";
2
1
  import KnownStore from "../../agent/KnownStore.js";
2
+ import docs from "./rmDoc.js";
3
3
 
4
4
  export default class Rm {
5
5
  #core;
@@ -10,10 +10,10 @@ export default class Rm {
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.rm = docs;
15
+ return docsMap;
16
+ });
17
17
  }
18
18
 
19
19
  async handler(entry, rummy) {
@@ -0,0 +1,30 @@
1
+ // Tool doc for <rm>. 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, self-closing
6
+ ['## <rm path="[path]"/> - Remove a file or entry'],
7
+
8
+ // --- Examples: file, known (with slug path), preview safety
9
+ ['Example: <rm path="src/config.js"/>', "File removal. Simplest form."],
10
+ [
11
+ 'Example: <rm path="known://donald-rumsfeld-was-born-in-1932"/>',
12
+ "Shows the slugified path convention. Model sees these paths in <knowns> section.",
13
+ ],
14
+ [
15
+ 'Example: <rm path="known://temp_*" preview/>',
16
+ "Preview before deleting. Glob pattern. Safety pattern for bulk operations.",
17
+ ],
18
+
19
+ // --- Constraints
20
+ [
21
+ '* Permanent. Prefer <set fidelity="archive"/> to preserve for later retrieval',
22
+ "Nudges toward archive over rm. Archive keeps the key; rm deletes permanently.",
23
+ ],
24
+ [
25
+ "* Paths accept globs — use `preview` to check matches first",
26
+ "Reinforces preview safety pattern. Prevents accidental bulk deletion.",
27
+ ],
28
+ ];
29
+
30
+ export default LINES.map(([text]) => text).join("\n");
@@ -1,45 +1,32 @@
1
1
  # rpc
2
2
 
3
- Registers all core RPC methods and dispatches client operations through the tool handler chain.
3
+ Registers core RPC methods and provides automatic tool dispatch for
4
+ all registered tools.
4
5
 
5
6
  ## Registration
6
7
 
7
- - **No tool handler** — this plugin registers RPC methods on `hooks.rpc.registry`, not tool handlers.
8
+ - **No tool handler** — registers RPC methods on `hooks.rpc.registry`.
9
+ - **Tool fallback** — any registered tool is automatically callable via
10
+ RPC without explicit registration. Third-party plugins get RPC for free.
8
11
 
9
12
  ## RPC Methods
10
13
 
11
14
  ### Protocol
12
- - `ping` — liveness check.
13
- - `discover` — returns method/notification catalog.
14
- - `init` — initialize project (sets projectId, projectRoot, configPath).
15
+ - `ping`, `discover`, `init`
15
16
 
16
17
  ### Models
17
- - `getModels`, `addModel`, `removeModel` — CRUD for model aliases.
18
+ - `getModels`, `addModel`, `removeModel`
18
19
 
19
- ### Entry Operations
20
- - `read` — promote entry to full state, or create persistent file constraint.
21
- - `store` — demote entry to stored state, or manage file constraints (ignore/clear).
22
- - `write` create/update entry. K/V paths write directly; file paths dispatch through `set` handler.
23
- - `delete` — remove entry via `rm` handler dispatch.
20
+ ### Entry Operations (all dispatch through tool handler chain)
21
+ - `get` — promote entry; with `persist` flag, also sets file constraint.
22
+ - `store` — demote entry or manage file constraints (not a model tool).
23
+ - All other registered tools auto-dispatched via tool fallback.
24
24
  - `getEntries` — query entries by glob pattern.
25
25
 
26
26
  ### Runs
27
- - `startRun` — pre-create a run with model/config.
28
- - `ask` non-mutating model query.
29
- - `act` — mutating model directive.
30
- - `run/resolve` — resolve a proposed entry (accept/reject).
31
- - `run/abort` — abort an in-flight run.
32
- - `run/rename` — rename a run alias.
33
- - `run/inject` — inject a message into an idle or active run.
34
- - `run/config` — update run parameters (temperature, persona, context_limit, model).
35
- - `getRuns`, `getRun` — query run list and full run detail.
27
+ - `startRun`, `ask`, `act`
28
+ - `run/resolve`, `run/abort`, `run/rename`, `run/inject`, `run/config`
29
+ - `getRuns`, `getRun`
36
30
 
37
31
  ### Notifications
38
- - `run/state` turn state update with history, unknowns, proposed, telemetry.
39
- - `run/progress` — turn status (thinking/processing).
40
- - `ui/render` — streaming output.
41
- - `ui/notify` — toast notification.
42
-
43
- ## Behavior
44
-
45
- Client operations (read, write, delete, store) build a `RummyContext` for the target run and dispatch through the same handler chain as model operations via `dispatchTool`.
32
+ - `run/state`, `run/progress`, `ui/render`, `ui/notify`