@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.
Files changed (55) hide show
  1. package/.env.example +1 -0
  2. package/FIDELITY_CONTRACT.md +172 -0
  3. package/migrations/001_initial_schema.sql +3 -3
  4. package/package.json +1 -1
  5. package/src/agent/AgentLoop.js +1 -2
  6. package/src/agent/ContextAssembler.js +2 -0
  7. package/src/agent/KnownStore.js +1 -2
  8. package/src/agent/ResponseHealer.js +54 -1
  9. package/src/agent/TurnExecutor.js +51 -6
  10. package/src/agent/XmlParser.js +150 -41
  11. package/src/agent/known_store.sql +18 -11
  12. package/src/hooks/PluginContext.js +8 -2
  13. package/src/hooks/RummyContext.js +6 -3
  14. package/src/hooks/ToolRegistry.js +23 -27
  15. package/src/plugins/ask_user/ask_user.js +2 -2
  16. package/src/plugins/ask_user/ask_userDoc.js +4 -2
  17. package/src/plugins/budget/README.md +6 -4
  18. package/src/plugins/budget/budget.js +29 -9
  19. package/src/plugins/cp/cp.js +5 -5
  20. package/src/plugins/cp/cpDoc.js +0 -8
  21. package/src/plugins/engine/engine.sql +1 -1
  22. package/src/plugins/env/env.js +4 -4
  23. package/src/plugins/env/envDoc.js +2 -2
  24. package/src/plugins/file/file.js +2 -7
  25. package/src/plugins/get/get.js +31 -10
  26. package/src/plugins/get/getDoc.js +26 -37
  27. package/src/plugins/helpers.js +2 -2
  28. package/src/plugins/instructions/instructions.js +6 -5
  29. package/src/plugins/instructions/preamble.md +41 -33
  30. package/src/plugins/known/known.js +17 -16
  31. package/src/plugins/known/knownDoc.js +1 -13
  32. package/src/plugins/mv/mv.js +6 -6
  33. package/src/plugins/mv/mvDoc.js +2 -13
  34. package/src/plugins/previous/previous.js +10 -14
  35. package/src/plugins/progress/progress.js +22 -5
  36. package/src/plugins/prompt/prompt.js +14 -11
  37. package/src/plugins/rm/rm.js +4 -4
  38. package/src/plugins/rm/rmDoc.js +4 -8
  39. package/src/plugins/rpc/rpc.js +1 -1
  40. package/src/plugins/set/set.js +10 -12
  41. package/src/plugins/set/setDoc.js +4 -4
  42. package/src/plugins/sh/sh.js +4 -4
  43. package/src/plugins/sh/shDoc.js +2 -2
  44. package/src/plugins/skill/skill.js +2 -1
  45. package/src/plugins/summarize/summarize.js +2 -2
  46. package/src/plugins/summarize/summarizeDoc.js +9 -10
  47. package/src/plugins/telemetry/telemetry.js +36 -11
  48. package/src/plugins/think/think.js +2 -1
  49. package/src/plugins/think/thinkDoc.js +3 -5
  50. package/src/plugins/unknown/unknown.js +21 -14
  51. package/src/plugins/unknown/unknownDoc.js +2 -6
  52. package/src/plugins/update/update.js +2 -2
  53. package/src/plugins/update/updateDoc.js +9 -6
  54. package/src/sql/functions/slugify.js +13 -1
  55. package/src/sql/v_model_context.sql +3 -3
@@ -16,13 +16,8 @@ export default class File {
16
16
  this.#core = core;
17
17
  // "file" scheme covers bare paths (scheme IS NULL in DB)
18
18
  core.registerScheme({ category: "data" });
19
- core.registerScheme({ name: "http", category: "data" });
20
- core.registerScheme({ name: "https", category: "data" });
21
- core.on("full", this.full.bind(this));
22
- core.on("summary", this.summary.bind(this));
23
- // Default identity views for http/https — rummy.web overrides these
24
- core.hooks.tools.onView("http", (entry) => entry.body);
25
- core.hooks.tools.onView("https", (entry) => entry.body);
19
+ core.on("promoted", this.full.bind(this));
20
+ core.on("demoted", this.summary.bind(this));
26
21
  }
27
22
 
28
23
  full(entry) {
@@ -9,8 +9,8 @@ export default class Get {
9
9
  this.#core = core;
10
10
  core.registerScheme();
11
11
  core.on("handler", this.handler.bind(this));
12
- core.on("full", this.full.bind(this));
13
- core.on("summary", this.summary.bind(this));
12
+ core.on("promoted", this.full.bind(this));
13
+ core.on("demoted", this.summary.bind(this));
14
14
  core.filter("instructions.toolDocs", async (docsMap) => {
15
15
  docsMap.get = docs;
16
16
  return docsMap;
@@ -29,6 +29,7 @@ export default class Get {
29
29
  }
30
30
  const normalized = KnownStore.normalizePath(target);
31
31
  const bodyFilter = entry.attributes.body || null;
32
+ const preview = entry.attributes.preview !== undefined;
32
33
  const isPattern = bodyFilter || normalized.includes("*");
33
34
 
34
35
  const line =
@@ -46,6 +47,25 @@ export default class Get {
46
47
  bodyFilter,
47
48
  );
48
49
 
50
+ // Preview — list matches with their full-body token costs. No promotion,
51
+ // no fidelity change, no Token Budget spent. Model uses this to plan
52
+ // which entries to actually promote. getDoc promises this behavior; the
53
+ // prior implementation silently promoted anyway, burning the Token Budget
54
+ // on entries the model thought it was only inspecting.
55
+ if (preview) {
56
+ await storePatternResult(
57
+ store,
58
+ runId,
59
+ turn,
60
+ "get",
61
+ target,
62
+ bodyFilter,
63
+ matches,
64
+ { preview: true, loopId, attributes: { path: target } },
65
+ );
66
+ return;
67
+ }
68
+
49
69
  // Partial read — no fidelity promotion, returns a line slice as the log item.
50
70
  if (line !== null || limit !== null) {
51
71
  if (isPattern) {
@@ -55,7 +75,7 @@ export default class Get {
55
75
  entry.resultPath,
56
76
  "line/limit requires a single path, not a glob or body filter",
57
77
  400,
58
- { loopId },
78
+ { loopId, attributes: { path: target } },
59
79
  );
60
80
  return;
61
81
  }
@@ -66,7 +86,7 @@ export default class Get {
66
86
  entry.resultPath,
67
87
  `${target} not found`,
68
88
  200,
69
- { loopId },
89
+ { loopId, attributes: { path: target } },
70
90
  );
71
91
  return;
72
92
  }
@@ -84,15 +104,15 @@ export default class Get {
84
104
  entry.resultPath,
85
105
  `${header}\n${slice}`,
86
106
  200,
87
- { loopId },
107
+ { loopId, attributes: { path: target } },
88
108
  );
89
109
  return;
90
110
  }
91
111
 
92
112
  const VALID_FIDELITY = {
93
- summary: 1,
94
- full: 1,
95
- archive: 1,
113
+ demoted: 1,
114
+ promoted: 1,
115
+ archived: 1,
96
116
  };
97
117
  const fidelityAttr = VALID_FIDELITY[entry.attributes.fidelity]
98
118
  ? entry.attributes.fidelity
@@ -113,17 +133,18 @@ export default class Get {
113
133
  target,
114
134
  bodyFilter,
115
135
  matches,
116
- { loopId },
136
+ { loopId, attributes: { path: target } },
117
137
  );
118
138
  } else {
119
139
  const total = matches.reduce((s, m) => s + m.tokens, 0);
120
140
  const paths = matches.map((m) => m.path).join(", ");
121
141
  const body =
122
142
  matches.length > 0
123
- ? `${paths} promoted to full (${total} tokens)`
143
+ ? `${paths} promoted (${total} tokens)`
124
144
  : `${target} not found`;
125
145
  await store.upsert(runId, turn, entry.resultPath, body, 200, {
126
146
  loopId,
147
+ attributes: { path: target },
127
148
  });
128
149
  }
129
150
  }
@@ -2,43 +2,32 @@
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
- ["## <get>[path/to/file]</get> - Load a file or entry into context"],
6
- [
7
- "Example: <get>src/app.js</get>",
8
- "Simplest form. Body = path.",
9
- ],
10
- [
11
- 'Example: <get path="known://*">auth</get>',
12
- "Keyword recall: glob in path, search term in body.",
13
- ],
14
- [
15
- 'Example: <get path="src/**/*.js" preview>authentication</get>',
16
- "Full pattern: recursive glob + preview + content filter.",
17
- ],
18
- [
19
- 'Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>',
20
- "Partial read. Returns lines 644–723 without promoting.",
21
- ],
22
- [
23
- "* Paths accept patterns: `src/**/*.js`, `known://api_*`",
24
- "Reinforces picomatch patterns work everywhere.",
25
- ],
26
- [
27
- "* `preview` lists matches without loading into context",
28
- "Budget-awareness. Preview avoids promotion.",
29
- ],
30
- [
31
- "* Body text filters results by content match",
32
- "Body = filter, not just path.",
33
- ],
34
- [
35
- "* `line` and `limit` read a slice without promoting — patterns not allowed",
36
- "Partial read is safe: context budget unaffected.",
37
- ],
38
- [
39
- '* Use <set path="src/file.txt" fidelity="summary"/> when the content is irrelevant to save tokens.',
40
- "Cross-tool lifecycle: get promotes, set demotes.",
41
- ],
5
+ ["## <get>[path/to/file]</get> - Promote an entry"],
6
+ ["Example: <get>src/app.js</get>", "Simplest form. Body = path."],
7
+ [
8
+ 'Example: <get path="known://*">auth</get>',
9
+ "Keyword recall: glob in path, search term in body.",
10
+ ],
11
+ [
12
+ 'Example: <get path="src/**/*.js">authentication</get>',
13
+ "Full pattern: recursive glob + content filter.",
14
+ ],
15
+ [
16
+ 'Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>',
17
+ "Partial read. Returns lines 644–723 without promoting.",
18
+ ],
19
+ [
20
+ "* Paths accept patterns: `src/**/*.js`, `known://api_*`",
21
+ "Reinforces picomatch patterns work everywhere.",
22
+ ],
23
+ [
24
+ "* Body text filters results by content match",
25
+ "Body = filter, not just path.",
26
+ ],
27
+ [
28
+ "* `line` and `limit` read a slice without promoting the entry, which costs as many tokens as the slice contains.",
29
+ "Partial read is safe: context budget unaffected.",
30
+ ],
42
31
  ];
43
32
 
44
33
  export default LINES.map(([text]) => text).join("\n");
@@ -10,7 +10,7 @@ export async function storePatternResult(
10
10
  path,
11
11
  bodyFilter,
12
12
  matches,
13
- { preview = false, loopId = null } = {},
13
+ { preview = false, loopId = null, attributes = null } = {},
14
14
  ) {
15
15
  const slug = await store.slugPath(runId, scheme, path);
16
16
  const filter = bodyFilter ? ` body="${bodyFilter}"` : "";
@@ -18,5 +18,5 @@ export async function storePatternResult(
18
18
  const listing = matches.map((m) => `${m.path} (${m.tokens})`).join("\n");
19
19
  const prefix = preview ? "PREVIEW " : "";
20
20
  const body = `${prefix}${scheme} path="${path}"${filter}: ${matches.length} matched (${total} tokens)\n${listing}`;
21
- await store.upsert(runId, turn, slug, body, 200, { loopId });
21
+ await store.upsert(runId, turn, slug, body, 200, { loopId, attributes });
22
22
  }
@@ -10,7 +10,7 @@ export default class Instructions {
10
10
 
11
11
  constructor(core) {
12
12
  this.#core = core;
13
- core.on("full", this.full.bind(this));
13
+ core.on("promoted", this.full.bind(this));
14
14
  core.on("turn.started", this.onTurnStarted.bind(this));
15
15
  }
16
16
 
@@ -33,14 +33,15 @@ export default class Instructions {
33
33
  const activeTools = attrs.toolSet
34
34
  ? new Set(attrs.toolSet)
35
35
  : new Set(this.#core.hooks.tools.names);
36
- const sorted = this.#core.hooks.tools.names.filter((n) =>
37
- activeTools.has(n),
38
- );
39
- const tools = sorted.join(", ");
40
36
  const toolDocs = await this.#core.hooks.instructions.toolDocs.filter(
41
37
  {},
42
38
  { toolSet: activeTools },
43
39
  );
40
+ // Hidden tools are excluded at the registry level (see ToolRegistry).
41
+ const sorted = this.#core.hooks.tools.advertisedNames.filter((n) =>
42
+ activeTools.has(n),
43
+ );
44
+ const tools = sorted.join(", ");
44
45
  const docsText = sorted
45
46
  .filter((key) => toolDocs[key])
46
47
  .map((key) => toolDocs[key])
@@ -1,37 +1,45 @@
1
- You are a folksonomic knowledgebase assistant. YOU MUST discern what you don't know into unknowns, then extract and organize your findings into navigable and searchable knowns, then YOU MAY answer questions and/or perform actions.
2
-
3
- # Tool Commands
4
-
5
- Tools: [%TOOLS%]
6
-
7
- # Tool Rules
8
-
9
- ## Response Rules
10
- Required: YOU MUST respond with Tool Commands in the XML format. YOU MAY use up to 12 tools in your response.
11
- Required: YOU MUST register all unknowns with <unknown>[specific thing I need to learn]</unknown>.
12
- Required: YOU MUST register all new facts, decisions, and plans with <known path="topic/subtopic" summary="keyword,keyword,keyword">[specific facts, decisions, or plans]</known>.
13
-
14
- ## Folksonomic Memory Management
15
- * Write paths with navigable hierarchies and summaries with searchable tags.
16
- * When new facts, decisions, and plans appear, set them as <known/> entries with navigable hierarchies and summaries with searchable tags to improve your folksonomic knowledgebase.
17
- * When new questions emerge, use pattern matching operations to optimize the fidelity and relevance of your knowledgebase.
18
- * The turn attribute can be helpful for discerning what's fresh or stale, prefer more recent information if conflicts exist.
19
- * YOU MUST promote all relevant entries and demote all irrelevant entries before acting or answering. Use body pattern search (Example: <get path="known://*">John Doe</get>) to recall archived entries when needed.
20
- * Logging entries in <previous/> can also be demoted to optimize context.
21
-
22
- ## Fidelity Management
23
- * full: Entire contents are shown (consumes token budget)
24
- * summary: Only path and summary are shown. (<= 80 chars, saves token budget)
25
- * archive: Archived in an unlimited archive. Entries can be recalled with path recall or pattern search. (use caution)
26
-
27
- ## Token Budget Management
28
- * Entries contain a "fidelity" and a "token" attribute to enable token budget management and context optimization.
29
- * Set relevant entries to "full" and irrelevant entries to "summary" to optimize context.
30
- * The less irrelevant information in your context, the better.
31
-
32
- ## Response Termination
33
- Required: YOU MUST conclude every turn with EITHER <update></update> if still working OR <summarize></summarize> if done. Never both.
1
+ You are a folksonomic knowledgebase assistant. Define what's unknown, gather knowns to resolve the unknown, act, then answer.
2
+
3
+ Required: YOU MUST only respond with Tool Commands in the XML format (max 12/turn): [%TOOLS%]
4
+
5
+ Required: YOU MUST register your unresolved questions as unknown:// entries, then resolve them.
6
+ Example: <set path="unknown://{topic_or_question}" summary="keyword,keyword,keyword">specific question I need to research</set>
7
+
8
+ Required: YOU MUST gather relevant facts, decisions, and information to store in known:// entries.
9
+ Required: YOU MUST include navigable paths and specific, searchable summary tags to enable pattern search and promotion.
10
+ Example: <set path="known://topic/subtopic1" summary="keyword,keyword,keyword">{known facts, decisions, or plans}</set>
11
+
12
+ Required: YOU MUST add the paths of related entries to your entry, and edit existing related entries to add linkbacks.
13
+ Example: <set path="known://topic/subtopic2" summary="keyword,keyword,keyword">{facts} Related: known://topic/subtopic1</set>
14
+
15
+ Required: YOU MUST promote relevant entries to verify their contents. Paths and summaries are approximate and unreliable.
16
+ Example: <get path="facts.txt"/>
17
+ Required: YOU MUST demote entries after organizing and categorizing relevant information into known entries.
18
+ Example: <set path="prompt://42" fidelity="demoted"/>
19
+
20
+ Required: YOU MUST calculate and estimate the token totals (tokens="N") of entries before promoting and not exceed 50% of Token Budget.
21
+ Warning: Promotions and new entries cost tokens. Demotions recover tokens. Exceeding your budget will result in a 413 Token Budget Error.
22
+ Tip: Entries with higher turn numbers are more recent and relevant.
23
+
24
+ Required: YOU MUST create and maintain a checklist to guide and track your progress. Only check items when they're completed.
25
+ Required: YOU MUST adapt and expand this checklist for the specific context, entries, and prompt requirements.
26
+ Example:
27
+ <set path="known://rummy_plan" summary="plan,strategy,steps,roadmap">
28
+ - [ ] identify and record unknown facts, unresolved decisions, and unclear plans
29
+ - [ ] identify, organize, and categorize known facts, decisions, and plans before acting on prompt
30
+ - [ ] identify relevant entries to verify, analyze, review, and record contents (don't assume from path or summary!)
31
+ - [ ] after promoting an entry, organize and categorize findings into known entries
32
+ - [ ] after the entry's information has been stored in known entries, demote it to optimize context relevance and token budget
33
+ - [ ] iteratively analyze and explore until the unknowns that can be resolved are resolved
34
+ - [ ] { specific action required by prompt }
35
+ - [ ] ...
36
+ - [ ] summarize when complete with summarize tag
37
+ </set>
38
+ Example: <set path="known://rummy_plan">s/- [ ] specific action required by prompt/- [x] specific action required by prompt/g</set>
34
39
 
35
40
  # Tool Usage
36
41
 
42
+ Warning: YOU MUST NOT use shell commands for file operations. Files are entries that require Tool Command operations.
43
+ Example: <set path="newFile.txt" summary="keyword,keyword,keyword">{new file contents}</set>
44
+
37
45
  [%TOOLDOCS%]
@@ -1,5 +1,4 @@
1
1
  import { countTokens } from "../../agent/tokens.js";
2
- import docs from "./knownDoc.js";
3
2
 
4
3
  const MAX_ENTRY_TOKENS = Number(process.env.RUMMY_MAX_ENTRY_TOKENS) || 512;
5
4
 
@@ -10,13 +9,13 @@ export default class Known {
10
9
  this.#core = core;
11
10
  core.registerScheme({ category: "data" });
12
11
  core.on("handler", this.handler.bind(this));
13
- core.on("full", this.full.bind(this));
14
- core.on("summary", this.summary.bind(this));
12
+ core.on("promoted", this.full.bind(this));
13
+ core.on("demoted", this.summary.bind(this));
15
14
  core.filter("assembly.system", this.assembleKnown.bind(this), 100);
16
- core.filter("instructions.toolDocs", async (docsMap) => {
17
- docsMap.known = docs;
18
- return docsMap;
19
- });
15
+ // <known> is internal — written via <set path="known://...">. Hidden
16
+ // from all model-facing tool lists. Handler still dispatches if the
17
+ // model emits <known> directly out of habit.
18
+ core.markHidden();
20
19
  }
21
20
 
22
21
  async handler(entry, rummy) {
@@ -73,11 +72,11 @@ export default class Known {
73
72
  }
74
73
 
75
74
  full(entry) {
76
- return `# known ${entry.path}\n${entry.body}`;
75
+ return entry.body;
77
76
  }
78
77
 
79
- summary(entry) {
80
- return this.full(entry);
78
+ summary() {
79
+ return "";
81
80
  }
82
81
 
83
82
  async assembleKnown(content, ctx) {
@@ -103,14 +102,16 @@ function renderKnownTag(entry, demotedSet) {
103
102
  typeof entry.attributes === "string"
104
103
  ? JSON.parse(entry.attributes)
105
104
  : entry.attributes;
106
- const summary =
105
+ // Always render summary attribute on knowns — empty value hints the model
106
+ // it forgot to add searchable keywords.
107
+ const summaryText =
107
108
  typeof attrs?.summary === "string"
108
- ? ` summary="${attrs.summary.replace(/"/g, "'").slice(0, 80)}"`
109
+ ? attrs.summary.replace(/"/g, "'").slice(0, 80)
109
110
  : "";
111
+ const summary = ` summary="${summaryText}"`;
110
112
 
111
- if (entry.fidelity === "archive") return "";
112
- if (entry.fidelity === "summary") {
113
- return `<${tag} path="${entry.path}"${turn}${status}${summary}${fidelity}${tokens}${flag}/>`;
113
+ if (entry.body) {
114
+ return `<${tag} path="${entry.path}"${turn}${status}${summary}${fidelity}${tokens}${flag}>${entry.body}</${tag}>`;
114
115
  }
115
- return `<${tag} path="${entry.path}"${turn}${status}${summary}${fidelity}${tokens}${flag}>${entry.body}</${tag}>`;
116
+ return `<${tag} path="${entry.path}"${turn}${status}${summary}${fidelity}${tokens}${flag}/>`;
116
117
  }
@@ -5,25 +5,13 @@ const LINES = [
5
5
  [
6
6
  '## <known path="known://topic/subtopic" summary="keyword,keyword,keyword">[specific facts, decisions, or plans]</known> - Sort and save what you learn for later recall',
7
7
  ],
8
- [
9
- 'Example: <known summary="hedberg,comedian,death,2005">Mitch Hedberg died on March 30, 2005</known>',
10
- "Summary-first pattern: comma-separated keywords, path auto-generated.",
11
- ],
12
8
  [
13
9
  'Example: <known path="known://people/rumsfeld" summary="defense,secretary,born,1932">Donald Rumsfeld was born in 1932 and served as Secretary of Defense</known>',
14
10
  "Explicit path form: slashed path=category/key, summary=keywords.",
15
11
  ],
16
12
  [
17
13
  '* Recall with <get path="known://people/*">keyword</get>',
18
- "Cross-tool lifecycle: glob by category, filter by keyword.",
19
- ],
20
- [
21
- "* YOU SHOULD write `summary` keywords, you can search for them later",
22
- "Motivates summary writing through self-interest.",
23
- ],
24
- [
25
- "* YOU MUST sort and save all new facts, decisions, and plans in their own <known> entries",
26
- "Critical behavioral constraint. 'new' prevents re-saving known facts.",
14
+ "Cross-tool lifecycle: pattern by category, filter by keyword.",
27
15
  ],
28
16
  ];
29
17
 
@@ -8,8 +8,8 @@ export default class Mv {
8
8
  this.#core = core;
9
9
  core.registerScheme();
10
10
  core.on("handler", this.handler.bind(this));
11
- core.on("full", this.full.bind(this));
12
- core.on("summary", this.summary.bind(this));
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.mv = docs;
15
15
  return docsMap;
@@ -29,14 +29,14 @@ export default class Mv {
29
29
  const matches = await store.getEntriesByPattern(runId, path);
30
30
  for (const match of matches)
31
31
  await store.setFidelity(runId, match.path, fidelity);
32
- const label = fidelity === "archive" ? "archived" : `set to ${fidelity}`;
32
+ const label = `set to ${fidelity}`;
33
33
  await store.upsert(
34
34
  runId,
35
35
  turn,
36
36
  entry.resultPath,
37
37
  `${matches.map((m) => m.path).join(", ")} ${label}`,
38
38
  200,
39
- { fidelity: "archive", loopId },
39
+ { fidelity: "archived", loopId },
40
40
  );
41
41
  return;
42
42
  }
@@ -71,7 +71,7 @@ export default class Mv {
71
71
  return `# mv ${entry.attributes.from || ""} ${entry.attributes.to || ""}`;
72
72
  }
73
73
 
74
- summary(entry) {
75
- return this.full(entry);
74
+ summary() {
75
+ return "";
76
76
  }
77
77
  }
@@ -9,22 +9,11 @@ const LINES = [
9
9
  'Example: <mv path="known://active_task">known://completed_task</mv>',
10
10
  "Entry rename. Most common mv use case.",
11
11
  ],
12
+ ['Example: <mv path="src/old_name.js">src/new_name.js</mv>', "File rename."],
12
13
  [
13
- 'Example: <mv path="src/old_name.js">src/new_name.js</mv>',
14
- "File rename.",
15
- ],
16
- [
17
- 'Example: <mv path="known://project/*" fidelity="summary"/>',
14
+ 'Example: <mv path="known://project/*" fidelity="demoted"/>',
18
15
  "Batch fidelity change via pattern. No destination = fidelity in place.",
19
16
  ],
20
- [
21
- "* Source path accepts patterns for batch moves",
22
- "Pattern support consistent with get/cp/rm.",
23
- ],
24
- [
25
- "* Use `preview` to check matches before pattern-based bulk moves",
26
- "Safety pattern consistent with rm/cp.",
27
- ],
28
17
  ];
29
18
 
30
19
  export default LINES.map(([text]) => text).join("\n");
@@ -43,18 +43,14 @@ async function renderToolTag(entry, _core) {
43
43
  const status = entry.status ? ` status="${entry.status}"` : "";
44
44
  const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
45
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}/>`;
46
+ const summary =
47
+ typeof attrs?.summary === "string"
48
+ ? ` summary="${attrs.summary.replace(/"/g, "'")}"`
49
+ : "";
50
+
51
+ // Trust the projected body. Plugin decided per-fidelity what to show.
52
+ if (entry.body) {
53
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}>${entry.body}</${entry.scheme}>`;
54
+ }
55
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}/>`;
60
56
  }
@@ -1,3 +1,6 @@
1
+ const CEILING_RATIO = Number(process.env.RUMMY_BUDGET_CEILING);
2
+ if (!CEILING_RATIO) throw new Error("RUMMY_BUDGET_CEILING must be set");
3
+
1
4
  export default class Progress {
2
5
  #core;
3
6
 
@@ -7,17 +10,31 @@ export default class Progress {
7
10
  }
8
11
 
9
12
  async assembleProgress(content, ctx) {
10
- const { lastContextTokens: usedTokens, contextSize } = ctx;
11
- const pct = contextSize ? Math.round((usedTokens / contextSize) * 100) : 0;
12
-
13
+ const { rows, contextSize, baselineTokens } = ctx;
13
14
  const lines = [];
15
+
14
16
  if (contextSize) {
17
+ const ceiling = Math.floor(contextSize * CEILING_RATIO);
18
+ const tokenBudget = Math.max(0, ceiling - (baselineTokens || 0));
19
+ // Used = sum of promoted controllable entries' tokens. Same units as
20
+ // per-entry tokens="N" so the model can predict the effect of a
21
+ // promote/demote: change is exactly the entry's tokens attribute.
22
+ const used = rows.reduce((sum, r) => {
23
+ if (
24
+ (r.category === "data" || r.category === "logging") &&
25
+ r.fidelity === "promoted"
26
+ ) {
27
+ return sum + (r.tokens || 0);
28
+ }
29
+ return sum;
30
+ }, 0);
31
+ const remaining = Math.max(0, tokenBudget - used);
15
32
  lines.push(
16
- `Using ${usedTokens} tokens (${pct}%) of ${contextSize} token budget. Use <get/> or set entry fidelity to "full" to spend tokens. Set entry fidelity to "summary" to save tokens.`,
33
+ `Token Budget: ${tokenBudget}. Using ${used}. ${remaining} remaining. Promote relevant entries with <get/> to spend. Demote irrelevant entries with <set fidelity="demoted"/> to save.`,
17
34
  );
18
35
  }
19
36
  lines.push(
20
- 'Conclude with a brief <update></update> to continue or a brief <summarize></summarize> if done.',
37
+ "Conclude with a brief <update></update> to continue or a brief <summarize></summarize> if done.",
21
38
  );
22
39
  const body = lines.join("\n");
23
40
 
@@ -3,16 +3,18 @@ export default class Prompt {
3
3
 
4
4
  constructor(core) {
5
5
  this.#core = core;
6
- core.hooks.tools.onView("prompt", (entry) => {
7
- if (entry.fidelity === "summary") {
6
+ core.hooks.tools.onView("prompt", (entry) => entry.body, "promoted");
7
+ core.hooks.tools.onView(
8
+ "prompt",
9
+ (entry) => {
8
10
  const limit = 500;
9
11
  const text = entry.body?.slice(0, limit) || "";
10
12
  return text.length < (entry.body?.length || 0)
11
- ? `${text}\n[truncated — promote to full to see the complete prompt]`
13
+ ? `${text}\n[truncated — promote to see the complete prompt]`
12
14
  : text;
13
- }
14
- return entry.body;
15
- });
15
+ },
16
+ "demoted",
17
+ );
16
18
  core.on("turn.started", this.onTurnStarted.bind(this));
17
19
  core.filter("assembly.user", this.assemblePrompt.bind(this), 300);
18
20
  }
@@ -39,13 +41,14 @@ export default class Prompt {
39
41
  : promptEntry?.attributes;
40
42
  const mode = attrs?.mode || ctx.type;
41
43
  const body = promptEntry?.body || "";
42
- const toolNames = ctx.toolSet
43
- ? [...ctx.toolSet]
44
- : [...this.#core.hooks.tools.resolveForLoop(mode)];
45
- const tools = toolNames.join(",");
44
+ // No tools="..." attribute. The OpenAI-shaped
45
+ // `<prompt mode tools="x,y,z">` rendering was priming gemma's
46
+ // native-tool-call training prior — A/B test confirmed removing
47
+ // the attribute dropped native-format emissions from ~50% to 0%.
48
+ // Tools list lives in the system prompt as "XML Command Tools:".
46
49
  let warn = "";
47
50
  if (mode === "ask") warn = ' warn="File editing disallowed."';
48
51
 
49
- return `${content}<prompt mode="${mode}" tools="${tools}"${warn}>${body}</prompt>`;
52
+ return `${content}<prompt mode="${mode}"${warn}>${body}</prompt>`;
50
53
  }
51
54
  }
@@ -8,8 +8,8 @@ export default class Rm {
8
8
  this.#core = core;
9
9
  core.registerScheme();
10
10
  core.on("handler", this.handler.bind(this));
11
- core.on("full", this.full.bind(this));
12
- core.on("summary", this.summary.bind(this));
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.rm = docs;
15
15
  return docsMap;
@@ -74,7 +74,7 @@ export default class Rm {
74
74
  return entry.body ? `${header}\n${entry.body}` : header;
75
75
  }
76
76
 
77
- summary(entry) {
78
- return this.full(entry);
77
+ summary() {
78
+ return "";
79
79
  }
80
80
  }
@@ -4,21 +4,17 @@
4
4
  const LINES = [
5
5
  ['## <rm path="[path]"/> - Remove a file or entry'],
6
6
  ['Example: <rm path="src/config.js"/>', "File removal. Simplest form."],
7
- [
8
- 'Example: <rm path="known://config/deprecated_service"/>',
9
- "Shows topic-hierarchy path convention.",
10
- ],
11
7
  [
12
8
  'Example: <rm path="known://temp_*" preview/>',
13
9
  "Preview before deleting. Safety pattern for bulk operations.",
14
10
  ],
15
11
  [
16
- '* Permanent. Prefer <set fidelity="archive"/> to preserve for later retrieval',
17
- "Nudges toward archive over rm.",
12
+ '* Permanent. Prefer <set path="..." fidelity="archived"/> to preserve for later retrieval',
13
+ "Nudges toward archive over rm. Path attr included so the model sees a complete invocation shape, not a fragment.",
18
14
  ],
19
15
  [
20
- "* Use `preview` to check matches before pattern-based bulk deletion",
21
- "Reinforces preview safety pattern.",
16
+ "* `preview` shows what paths would be affected without performing the operation.",
17
+ "Canonical preview teaching lives here — rm is the most intuitive 'check before committing' case. Model generalizes to cp/mv/get by analogy. Advanced uses (e.g. archive rediscovery via <get preview>) belong in persona/skill docs, not here.",
22
18
  ],
23
19
  ];
24
20