@possumtech/rummy 2.1.0 → 2.2.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 (140) hide show
  1. package/.env.example +40 -15
  2. package/.xai.key +1 -0
  3. package/PLUGINS.md +169 -53
  4. package/README.md +38 -32
  5. package/SPEC.md +366 -179
  6. package/bin/digest.js +1097 -0
  7. package/biome/no-fallbacks.grit +2 -2
  8. package/gemini.key +1 -0
  9. package/lang/en.json +10 -1
  10. package/migrations/001_initial_schema.sql +9 -2
  11. package/package.json +19 -8
  12. package/service.js +1 -0
  13. package/src/agent/AgentLoop.js +76 -26
  14. package/src/agent/ContextAssembler.js +2 -0
  15. package/src/agent/Entries.js +238 -60
  16. package/src/agent/ProjectAgent.js +44 -0
  17. package/src/agent/TurnExecutor.js +99 -30
  18. package/src/agent/XmlParser.js +206 -111
  19. package/src/agent/errors.js +35 -0
  20. package/src/agent/known_queries.sql +1 -1
  21. package/src/agent/known_store.sql +3 -42
  22. package/src/agent/materializeContext.js +30 -1
  23. package/src/agent/runs.sql +8 -18
  24. package/src/agent/tokens.js +0 -1
  25. package/src/agent/turns.sql +1 -0
  26. package/src/hooks/Hooks.js +26 -0
  27. package/src/hooks/RummyContext.js +12 -1
  28. package/src/lib/hedberg/README.md +60 -0
  29. package/src/lib/hedberg/hedberg.js +60 -0
  30. package/src/lib/hedberg/marker.js +158 -0
  31. package/src/{plugins → lib}/hedberg/matcher.js +1 -2
  32. package/src/llm/LlmProvider.js +41 -3
  33. package/src/llm/openaiStream.js +17 -0
  34. package/src/plugins/ask_user/ask_user.js +12 -2
  35. package/src/plugins/ask_user/ask_userDoc.md +1 -5
  36. package/src/plugins/budget/README.md +29 -24
  37. package/src/plugins/budget/budget.js +166 -110
  38. package/src/plugins/cli/README.md +3 -4
  39. package/src/plugins/cli/cli.js +31 -5
  40. package/src/plugins/cloudflare/cloudflare.js +136 -0
  41. package/src/plugins/cp/cp.js +41 -4
  42. package/src/plugins/cp/cpDoc.md +5 -6
  43. package/src/plugins/engine/engine.sql +1 -1
  44. package/src/plugins/env/README.md +5 -4
  45. package/src/plugins/env/env.js +7 -4
  46. package/src/plugins/env/envDoc.md +7 -8
  47. package/src/plugins/error/error.js +56 -15
  48. package/src/plugins/file/README.md +12 -3
  49. package/src/plugins/file/file.js +2 -2
  50. package/src/plugins/get/get.js +59 -36
  51. package/src/plugins/get/getDoc.md +10 -34
  52. package/src/plugins/google/google.js +115 -0
  53. package/src/plugins/hedberg/hedberg.js +13 -56
  54. package/src/plugins/helpers.js +66 -12
  55. package/src/plugins/index.js +1 -2
  56. package/src/plugins/instructions/README.md +44 -47
  57. package/src/plugins/instructions/instructions-system.md +44 -0
  58. package/src/plugins/instructions/instructions-user.md +53 -0
  59. package/src/plugins/instructions/instructions.js +58 -189
  60. package/src/plugins/known/README.md +6 -7
  61. package/src/plugins/known/known.js +24 -30
  62. package/src/plugins/log/log.js +41 -32
  63. package/src/plugins/mv/mv.js +40 -1
  64. package/src/plugins/mv/mvDoc.md +1 -8
  65. package/src/plugins/ollama/ollama.js +4 -3
  66. package/src/plugins/openai/openai.js +4 -3
  67. package/src/plugins/openrouter/openrouter.js +14 -4
  68. package/src/plugins/persona/README.md +11 -13
  69. package/src/plugins/persona/default.md +29 -0
  70. package/src/plugins/persona/persona.js +10 -66
  71. package/src/plugins/policy/policy.js +23 -22
  72. package/src/plugins/prompt/README.md +37 -27
  73. package/src/plugins/prompt/prompt.js +13 -19
  74. package/src/plugins/rm/rm.js +18 -0
  75. package/src/plugins/rm/rmDoc.md +5 -6
  76. package/src/plugins/rpc/rpc.js +3 -3
  77. package/src/plugins/set/set.js +205 -323
  78. package/src/plugins/set/setDoc.md +47 -17
  79. package/src/plugins/sh/README.md +6 -5
  80. package/src/plugins/sh/sh.js +8 -5
  81. package/src/plugins/sh/shDoc.md +7 -8
  82. package/src/plugins/skill/README.md +37 -14
  83. package/src/plugins/skill/skill.js +200 -101
  84. package/src/plugins/skill/skillDoc.js +3 -0
  85. package/src/plugins/skill/skillDoc.md +9 -0
  86. package/src/plugins/stream/README.md +7 -6
  87. package/src/plugins/stream/finalize.js +100 -0
  88. package/src/plugins/stream/stream.js +13 -45
  89. package/src/plugins/telemetry/telemetry.js +27 -4
  90. package/src/plugins/think/think.js +2 -3
  91. package/src/plugins/think/thinkDoc.md +2 -4
  92. package/src/plugins/unknown/README.md +1 -1
  93. package/src/plugins/unknown/unknown.js +17 -19
  94. package/src/plugins/update/update.js +4 -51
  95. package/src/plugins/update/updateDoc.md +21 -6
  96. package/src/plugins/xai/xai.js +68 -102
  97. package/src/plugins/yolo/yolo.js +102 -75
  98. package/src/sql/functions/hedmatch.js +1 -1
  99. package/src/sql/functions/hedreplace.js +1 -1
  100. package/src/sql/functions/hedsearch.js +1 -1
  101. package/src/sql/functions/slugify.js +16 -2
  102. package/BENCH_ENVIRONMENT.md +0 -230
  103. package/CLIENT_INTERFACE.md +0 -396
  104. package/last_run.txt +0 -5617
  105. package/scriptify/ask_run.js +0 -77
  106. package/scriptify/cache_probe.js +0 -66
  107. package/scriptify/cache_probe_grok.js +0 -74
  108. package/src/agent/budget.js +0 -33
  109. package/src/agent/config.js +0 -38
  110. package/src/plugins/hedberg/README.md +0 -71
  111. package/src/plugins/hedberg/docs.md +0 -0
  112. package/src/plugins/hedberg/edits.js +0 -55
  113. package/src/plugins/hedberg/normalize.js +0 -17
  114. package/src/plugins/hedberg/sed.js +0 -49
  115. package/src/plugins/instructions/instructions.md +0 -34
  116. package/src/plugins/instructions/instructions_104.md +0 -8
  117. package/src/plugins/instructions/instructions_105.md +0 -39
  118. package/src/plugins/instructions/instructions_106.md +0 -22
  119. package/src/plugins/instructions/instructions_107.md +0 -17
  120. package/src/plugins/instructions/instructions_108.md +0 -0
  121. package/src/plugins/known/knownDoc.js +0 -3
  122. package/src/plugins/known/knownDoc.md +0 -8
  123. package/src/plugins/unknown/unknownDoc.js +0 -3
  124. package/src/plugins/unknown/unknownDoc.md +0 -11
  125. package/turns/cli_1777462658211/turn_001.txt +0 -772
  126. package/turns/cli_1777462658211/turn_002.txt +0 -606
  127. package/turns/cli_1777462658211/turn_003.txt +0 -667
  128. package/turns/cli_1777462658211/turn_004.txt +0 -297
  129. package/turns/cli_1777462658211/turn_005.txt +0 -301
  130. package/turns/cli_1777462658211/turn_006.txt +0 -262
  131. package/turns/cli_1777465095132/turn_001.txt +0 -715
  132. package/turns/cli_1777465095132/turn_002.txt +0 -236
  133. package/turns/cli_1777465095132/turn_003.txt +0 -287
  134. package/turns/cli_1777465095132/turn_004.txt +0 -694
  135. package/turns/cli_1777465095132/turn_005.txt +0 -422
  136. package/turns/cli_1777465095132/turn_006.txt +0 -365
  137. package/turns/cli_1777465095132/turn_007.txt +0 -885
  138. package/turns/cli_1777465095132/turn_008.txt +0 -1277
  139. package/turns/cli_1777465095132/turn_009.txt +0 -736
  140. /package/src/{plugins → lib}/hedberg/patterns.js +0 -0
@@ -1,144 +1,86 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { readFileSync } from "node:fs";
2
2
  import Protocol from "./protocol.js";
3
3
 
4
+ // Base system-prompt content (header + Core XML Command Grammar). The
5
+ // `[%TOOLS%]` placeholder is substituted with the active-toolset tag list
6
+ // at render time. Tool-specific docs no longer live here; they're a
7
+ // separate `assembly.system` participant at priority 100.
4
8
  const baseInstructions = readFileSync(
5
- new URL("./instructions.md", import.meta.url),
9
+ new URL("./instructions-system.md", import.meta.url),
6
10
  "utf8",
7
11
  );
8
12
 
9
- // 1XY phase routing; see plugin README.
10
- const PHASES = [4, 5, 6, 7, 8, 9];
11
- const phaseInstructions = Object.fromEntries(
12
- PHASES.flatMap((p) => {
13
- const url = new URL(`./instructions_10${p}.md`, import.meta.url);
14
- return existsSync(url) ? [[p, readFileSync(url, "utf8").trim()]] : [];
15
- }),
16
- );
17
- const TURN_FROM_PATH = /^log:\/\/turn_(\d+)\/update\//;
18
-
19
- function phaseForStatus(status) {
20
- if (status == null) return 4;
21
- if (status === 200) return 7;
22
- const last = status % 10;
23
- return PHASES.includes(last) ? last : 4;
24
- }
13
+ // Tight, non-modal reminder rendered LATE in the user message
14
+ // (`assembly.user` priority 165, between unknowns at 150 and budget at
15
+ // 175) so the rules sit adjacent to the action site — recency keeps the
16
+ // per-turn discipline warm.
17
+ const userInstructions = readFileSync(
18
+ new URL("./instructions-user.md", import.meta.url),
19
+ "utf8",
20
+ ).trim();
25
21
 
26
- // Latest non-rejected update status from materialized rows.
27
- function latestUpdateStatusFromRows(rows) {
28
- let bestTurn = -1;
29
- let bestStatus = null;
30
- for (const r of rows) {
31
- const m = TURN_FROM_PATH.exec(r.path);
32
- if (!m) continue;
33
- const turn = Number(m[1]);
34
- const attrs =
35
- typeof r.attributes === "string"
36
- ? JSON.parse(r.attributes)
37
- : r.attributes;
38
- const status = attrs?.status;
39
- if (status == null) continue;
40
- if (attrs?.rejected) continue;
41
- if (turn > bestTurn || (turn === bestTurn && status > bestStatus)) {
42
- bestTurn = turn;
43
- bestStatus = status;
44
- }
45
- }
46
- return bestStatus;
47
- }
22
+ const TURN_FROM_PATH = /^log:\/\/turn_(\d+)\/update\//;
48
23
 
49
24
  export default class Instructions {
50
25
  #core;
51
26
 
52
27
  constructor(core) {
53
28
  this.#core = core;
54
- core.on("visible", this.full.bind(this));
55
- core.on("turn.started", this.onTurnStarted.bind(this));
56
- core.hooks.instructions.resolveSystemPrompt =
57
- this.resolveSystemPrompt.bind(this);
58
- core.hooks.instructions.validateNavigation =
59
- this.validateNavigation.bind(this);
60
29
  core.hooks.instructions.findLatestSummary =
61
30
  this.findLatestSummary.bind(this);
62
- core.filter("assembly.user", this.assembleInstructions.bind(this), 200);
31
+ // System message: priority chain. Base header + grammar at 50,
32
+ // joined per-tool docs at 100, persona at 150 (in persona.js).
33
+ core.filter("assembly.system", this.assembleSystemBase.bind(this), 50);
34
+ core.filter("assembly.system", this.assembleSystemToolDocs.bind(this), 100);
35
+ // User message: per-turn reminder block at the front of the user
36
+ // packet — sets discipline before the prompt.
37
+ core.filter("assembly.user", this.assembleInstructions.bind(this), 30);
63
38
  new Protocol(core);
64
39
  }
65
40
 
66
- // Project instructions://system through the visible view; called once per turn.
67
- async resolveSystemPrompt(rummy) {
68
- const { entries: store, runId, hooks } = rummy;
69
- const entries = await store.getEntriesByPattern(
70
- runId,
71
- "instructions://system",
72
- null,
73
- { includeAuditSchemes: true },
74
- );
75
- // The entry is always written by onTurnStarted before this runs.
76
- const entry = entries[0];
77
- const attributes = await store.getAttributes(
78
- runId,
79
- "instructions://system",
80
- );
81
- return hooks.tools.view("instructions", {
82
- path: "instructions://system",
83
- scheme: "instructions",
84
- body: entry.body,
85
- attributes,
86
- visibility: "visible",
87
- category: "system",
88
- });
41
+ #activeToolSet(ctx) {
42
+ return ctx.toolSet
43
+ ? new Set(ctx.toolSet)
44
+ : new Set(this.#core.hooks.tools.names);
89
45
  }
90
46
 
91
- // Reject illegal stage navigation; see plugin README.
92
- async validateNavigation(status, rummy) {
93
- const currentPhase = await this.#getCurrentPhase(rummy);
94
- const nextPhase = phaseForStatus(status);
95
- if (nextPhase > currentPhase + 1) {
96
- return { ok: false, reason: "Illegal navigation attempt" };
97
- }
98
- if (status === 200 && currentPhase !== 7) {
99
- return { ok: false, reason: "Illegal navigation attempt" };
100
- }
101
- if (nextPhase === 7) {
102
- const visible = await this.#countVisiblePriorPrompts(rummy);
103
- if (visible > 0) {
104
- return {
105
- ok: false,
106
- reason: `Illegal navigation attempt: ${visible} visible prior prompts`,
107
- };
108
- }
109
- }
110
- return { ok: true };
47
+ // assembly.system @ 50 header + Core XML Command Grammar with the
48
+ // advertised tool tag list substituted into the `[%TOOLS%]` slot.
49
+ assembleSystemBase(content, ctx) {
50
+ const activeTools = this.#activeToolSet(ctx);
51
+ const advertised = this.#core.hooks.tools.advertisedNames.filter((n) =>
52
+ activeTools.has(n),
53
+ );
54
+ const tools = advertised.map((n) => `<${n}/>`).join(", ");
55
+ return `${content}${baseInstructions.replace("[%TOOLS%]", tools)}`;
111
56
  }
112
57
 
113
- async #getCurrentPhase(rummy) {
114
- // `**` not `*`: update slugs may contain URL-encoded `/`.
115
- const updates = await rummy.entries.getEntriesByPattern(
116
- rummy.runId,
117
- "log://*/update/**",
118
- null,
58
+ // assembly.system @ 100 — per-tool docs joined in tool-registration
59
+ // order. Each tool plugin contributes its block via the
60
+ // `instructions.toolDocs` sub-filter (registry-style: filter
61
+ // participants mutate a docsMap keyed by tool name).
62
+ async assembleSystemToolDocs(content, ctx) {
63
+ const activeTools = this.#activeToolSet(ctx);
64
+ const toolDocs = await this.#core.hooks.instructions.toolDocs.filter(
65
+ {},
66
+ { toolSet: activeTools },
119
67
  );
120
- let bestTurn = -1;
121
- let bestStatus = null;
122
- for (const e of updates) {
123
- const m = TURN_FROM_PATH.exec(e.path);
124
- if (!m) continue;
125
- const turn = Number(m[1]);
126
- if (turn >= rummy.sequence) continue;
127
- const attrs =
128
- typeof e.attributes === "string"
129
- ? JSON.parse(e.attributes)
130
- : e.attributes;
131
- if (attrs?.rejected) continue;
132
- if (attrs?.status == null) continue;
133
- if (turn > bestTurn) {
134
- bestTurn = turn;
135
- bestStatus = attrs.status;
136
- }
137
- }
138
- return phaseForStatus(bestStatus);
68
+ const docsText = this.#core.hooks.tools.names
69
+ .filter((n) => activeTools.has(n))
70
+ .filter((key) => toolDocs[key])
71
+ .map((key) => toolDocs[key])
72
+ .join("\n\n");
73
+ if (!docsText) return content;
74
+ return `${content}\n\n${docsText}`;
75
+ }
76
+
77
+ // assembly.user @ 165 — per-turn reminder, same body every turn.
78
+ assembleInstructions(content, _ctx) {
79
+ return `${content}<instructions>\n${userInstructions}\n</instructions>\n`;
139
80
  }
140
81
 
141
- // Latest phase-7 success (status=200); state-machine knowledge lives here, not AgentLoop.
82
+ // Latest terminal update (status=200) used by cli.js to print the
83
+ // run's final answer. State-machine knowledge lives here, not AgentLoop.
142
84
  findLatestSummary(logEntries) {
143
85
  return logEntries
144
86
  .filter((e) => {
@@ -151,77 +93,4 @@ export default class Instructions {
151
93
  })
152
94
  .at(-1);
153
95
  }
154
-
155
- async #countVisiblePriorPrompts(rummy) {
156
- const prompts = await rummy.entries.getEntriesByPattern(
157
- rummy.runId,
158
- "prompt://*",
159
- null,
160
- );
161
- const visible = prompts.filter((p) => p.visibility === "visible");
162
- if (visible.length === 0) return 0;
163
- // Exclude the latest prompt; only PRIOR prompts trigger demote-before-Deployment.
164
- let maxNum = -1;
165
- for (const p of visible) {
166
- const m = /^prompt:\/\/(\d+)$/.exec(p.path);
167
- if (m && Number(m[1]) > maxNum) maxNum = Number(m[1]);
168
- }
169
- return visible.filter((p) => {
170
- const m = /^prompt:\/\/(\d+)$/.exec(p.path);
171
- return !m || Number(m[1]) !== maxNum;
172
- }).length;
173
- }
174
-
175
- async onTurnStarted({ rummy }) {
176
- const { entries: store, sequence: turn, runId } = rummy;
177
- const runRow = await store.getRun(runId);
178
- const toolSet = rummy.toolSet
179
- ? [...rummy.toolSet]
180
- : this.#core.hooks.tools.names;
181
- // instructions://system stays cache-stable; phase selection at assembly.user.
182
- await store.set({
183
- runId,
184
- turn,
185
- path: "instructions://system",
186
- body: "",
187
- state: "resolved",
188
- writer: "system",
189
- attributes: {
190
- persona: runRow.persona,
191
- toolSet,
192
- },
193
- });
194
- }
195
-
196
- async full(entry) {
197
- const attrs = entry.attributes;
198
- const activeTools = attrs.toolSet
199
- ? new Set(attrs.toolSet)
200
- : new Set(this.#core.hooks.tools.names);
201
- const toolDocs = await this.#core.hooks.instructions.toolDocs.filter(
202
- {},
203
- { toolSet: activeTools },
204
- );
205
- const sorted = this.#core.hooks.tools.advertisedNames.filter((n) =>
206
- activeTools.has(n),
207
- );
208
- const tools = sorted.map((n) => `<${n}/>`).join(", ");
209
- const docsText = sorted
210
- .filter((key) => toolDocs[key])
211
- .map((key) => toolDocs[key])
212
- .join("\n\n");
213
- let prompt = baseInstructions
214
- .replace("[%TOOLS%]", tools)
215
- .replace("[%TOOLDOCS%]", docsText);
216
- if (attrs.persona) prompt += `\n\n## Persona\n\n${attrs.persona}`;
217
- return prompt;
218
- }
219
-
220
- // Render <instructions> for current phase; absent phase file → no block.
221
- assembleInstructions(content, ctx) {
222
- const status = latestUpdateStatusFromRows(ctx.rows);
223
- const step = phaseInstructions[phaseForStatus(status)];
224
- if (!step) return content;
225
- return `${content}<instructions>\n${step}\n</instructions>\n`;
226
- }
227
96
  }
@@ -1,7 +1,7 @@
1
1
  # known {#known_plugin}
2
2
 
3
3
  Writes knowledge entries into the store at full visibility, and renders
4
- the project's data surface as the bifurcated `<summarized>` /
4
+ the project's data surface as the bifurcated `<summary>` /
5
5
  `<visible>` blocks at the top of the user message.
6
6
 
7
7
  ## Registration
@@ -10,7 +10,7 @@ the project's data surface as the bifurcated `<summarized>` /
10
10
  - **Category**: `data`
11
11
  - **Handler**: Upserts the entry body at the target path with status 200.
12
12
  - **Filters**:
13
- - `assembly.user` priority 50 — renders `<summarized>`.
13
+ - `assembly.user` priority 50 — renders `<summary>`.
14
14
  - `assembly.user` priority 75 — renders `<visible>`.
15
15
 
16
16
  ## Projection
@@ -22,19 +22,18 @@ Shows `# known {path}` followed by the entry body.
22
22
  Filters `ctx.rows` where `category === "data"`. Two separate blocks
23
23
  emit at the top of the user message in this order:
24
24
 
25
- - `<summarized>` — each data entry whose visibility is `visible` or
25
+ - `<summary>` — each data entry whose visibility is `visible` or
26
26
  `summarized`, rendered under its scheme tag with the plugin's
27
27
  summary projection as body (truncated knowns, code symbols,
28
28
  page abstracts — whatever the plugin's `summary()` hook produces).
29
- Plus the named carve-out: archived prompts pass through
30
- (visibility="archived") so the model can `<get>` the active prompt
31
- back after demotion.
29
+ Archived entries including prompts are filtered out uniformly
30
+ (no carve-out).
32
31
  - `<visible>` — each data entry whose visibility is `visible`,
33
32
  rendered with the plugin's visible projection (full body) as the
34
33
  tag body. A visible entry appears in *both* blocks: summary
35
34
  projection up top, full body below.
36
35
 
37
- This split lets `<summarized>` stay cache-stable across promote/demote
36
+ This split lets `<summary>` stay cache-stable across promote/demote
38
37
  operations — only `<visible>` mutates when the model promotes a
39
38
  summary or demotes a visible entry. Third-party plugins that register
40
39
  with `category: "data"` automatically appear in both blocks under
@@ -1,5 +1,6 @@
1
1
  import { stateToStatus } from "../../agent/httpStatus.js";
2
2
  import { countTokens } from "../../agent/tokens.js";
3
+ import { renderEntry, SUMMARY_MAX_CHARS } from "../helpers.js";
3
4
 
4
5
  const MAX_ENTRY_TOKENS = Number(process.env.RUMMY_MAX_ENTRY_TOKENS);
5
6
 
@@ -8,13 +9,15 @@ export default class Known {
8
9
 
9
10
  constructor(core) {
10
11
  this.#core = core;
12
+ core.ensureTool();
11
13
  core.registerScheme({ category: "data" });
12
14
  core.on("handler", this.handler.bind(this));
13
15
  core.on("visible", this.full.bind(this));
14
16
  core.on("summarized", this.summary.bind(this));
15
- core.filter("assembly.user", this.assembleSummarized.bind(this), 50);
16
- core.filter("assembly.user", this.assembleVisible.bind(this), 75);
17
- // Hidden tool: written via <set path="known://...">; handler tolerates direct <known>.
17
+ core.filter("assembly.system", this.assembleSummarized.bind(this), 200);
18
+ core.filter("assembly.system", this.assembleVisible.bind(this), 250);
19
+ // Hidden from the advertised tool list — written via <set path="known://...">.
20
+ // The known:// scheme lifecycle is taught in instructions-user.md.
18
21
  core.markHidden();
19
22
  }
20
23
 
@@ -45,7 +48,7 @@ export default class Known {
45
48
  runId,
46
49
  "known",
47
50
  entry.body,
48
- entry.attributes?.summary,
51
+ entry.attributes?.tags,
49
52
  );
50
53
  }
51
54
 
@@ -80,11 +83,13 @@ export default class Known {
80
83
  return entry.body;
81
84
  }
82
85
 
83
- // Summarized: first 500 chars; matches <prompt> summarized.
86
+ // Summarized: first SUMMARY_MAX_CHARS of the body. The model already
87
+ // knows summarized data is approximate (taught in instructions), so
88
+ // we don't owe it a "[truncated]" marker that would push the body
89
+ // past the contract floor.
84
90
  summary(entry) {
85
91
  if (!entry.body) return "";
86
- if (entry.body.length <= 500) return entry.body;
87
- return `${entry.body.slice(0, 500)}\n[truncated — promote to see the full body]`;
92
+ return entry.body.slice(0, SUMMARY_MAX_CHARS);
88
93
  }
89
94
 
90
95
  // Identity-keyed summary lines: every data entry the run is tracking
@@ -99,7 +104,7 @@ export default class Known {
99
104
  const lines = entries.map((e) =>
100
105
  renderContextTag(e, e.sBody != null ? e.sBody : e.body),
101
106
  );
102
- return `${content}<summarized>\n${lines.join("\n")}\n</summarized>\n`;
107
+ return `${content}<summary>\n${lines.join("\n")}\n</summary>\n`;
103
108
  }
104
109
 
105
110
  async assembleVisible(content, ctx) {
@@ -115,10 +120,6 @@ export default class Known {
115
120
  }
116
121
 
117
122
  function renderContextTag(entry, projectedBody) {
118
- const tag = entry.scheme ? entry.scheme : "file";
119
- const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
120
- const tokens = entry.aTokens != null ? ` tokens="${entry.aTokens}"` : "";
121
- const lines = entry.vLines != null ? ` lines="${entry.vLines}"` : "";
122
123
  const attrs =
123
124
  typeof entry.attributes === "string"
124
125
  ? JSON.parse(entry.attributes)
@@ -129,23 +130,16 @@ function renderContextTag(entry, projectedBody) {
129
130
  : entry.state
130
131
  ? stateToStatus(entry.state, entry.outcome)
131
132
  : null;
132
- const status =
133
- statusValue != null && statusValue !== 200
134
- ? ` status="${statusValue}"`
135
- : "";
136
- const stateAttr =
137
- entry.state && entry.state !== "resolved" ? ` state="${entry.state}"` : "";
138
- const outcomeAttr = entry.outcome ? ` outcome="${entry.outcome}"` : "";
139
- const visibility =
140
- entry.visibility === "archived" ? ` visibility="archived"` : "";
141
- const summaryText =
142
- typeof attrs?.summary === "string"
143
- ? attrs.summary.replace(/"/g, "'").slice(0, 80)
144
- : "";
145
- const summary = ` summary="${summaryText}"`;
146
- const attrStr = `${turn}${status}${stateAttr}${outcomeAttr}${summary}${visibility}${tokens}${lines}`;
147
- if (projectedBody) {
148
- return `<${tag} path="${entry.path}"${attrStr}>${projectedBody}</${tag}>`;
133
+ const meta = {};
134
+ if (entry.source_turn) meta.turn = entry.source_turn;
135
+ if (statusValue != null && statusValue !== 200) meta.status = statusValue;
136
+ if (entry.state && entry.state !== "resolved") meta.state = entry.state;
137
+ if (entry.outcome) meta.outcome = entry.outcome;
138
+ if (typeof attrs?.tags === "string") {
139
+ meta.tags = attrs.tags.slice(0, 80);
149
140
  }
150
- return `<${tag} path="${entry.path}"${attrStr}/>`;
141
+ if (entry.visibility === "archived") meta.visibility = "archived";
142
+ if (entry.aTokens != null) meta.tokens = entry.aTokens;
143
+ if (entry.vLines != null) meta.lines = entry.vLines;
144
+ return renderEntry(entry.path, meta, projectedBody);
151
145
  }
@@ -1,4 +1,5 @@
1
1
  import { stateToStatus } from "../../agent/httpStatus.js";
2
+ import { renderEntry } from "../helpers.js";
2
3
 
3
4
  // sh/env span multiple channels; channels render their own tokens in <visible>.
4
5
  const STREAM_NO_TOKENS = new Set(["sh", "env"]);
@@ -8,7 +9,7 @@ export default class Log {
8
9
 
9
10
  constructor(core) {
10
11
  this.#core = core;
11
- core.filter("assembly.user", this.assembleLog.bind(this), 100);
12
+ core.filter("assembly.system", this.assembleLog.bind(this), 300);
12
13
  }
13
14
 
14
15
  async assembleLog(content, ctx) {
@@ -16,8 +17,13 @@ export default class Log {
16
17
  const latestPrompt = ctx.rows.findLast(
17
18
  (r) => r.category === "prompt" && r.scheme === "prompt",
18
19
  );
20
+ // All time-indexed activity belongs here: log entries (actions,
21
+ // errors, updates) AND streaming data channels from env/sh which
22
+ // are also time-indexed. Visibility controls the body projection
23
+ // (vBody for visible, sBody for summarized) — not which section
24
+ // the entry lives in.
19
25
  const entries = ctx.rows.filter((r) => {
20
- if (r.category === "logging" && r.scheme === "log") return true;
26
+ if (r.category === "logging") return true;
21
27
  if (r.category === "prompt" && r.scheme === "prompt") {
22
28
  return r !== latestPrompt;
23
29
  }
@@ -31,13 +37,30 @@ export default class Log {
31
37
  }
32
38
  }
33
39
 
34
- // Action segment of log://turn_N/action/slug → XML tag.
40
+ // Action label for the entry's <log> rendering. log://turn_N/<action>/<slug>
41
+ // uses the path's action segment; env://turn_N/* and sh://turn_N/* are
42
+ // streaming channels, so the scheme itself is the action.
35
43
  function actionFromPath(path) {
36
44
  if (path?.startsWith("prompt://")) return "prompt";
45
+ if (path?.startsWith("env://")) return "env";
46
+ if (path?.startsWith("sh://")) return "sh";
37
47
  const match = path?.match(/^log:\/\/turn_\d+\/([^/]+)\//);
38
48
  return match ? match[1] : "log";
39
49
  }
40
50
 
51
+ // Visibility controls projection within <log>: summarized entries render
52
+ // the compact sBody; visible entries render the full vBody (or fall back
53
+ // to the raw body when no projection exists).
54
+ function projectedBody(entry) {
55
+ if (entry.visibility === "summarized" && entry.sBody != null) {
56
+ return entry.sBody;
57
+ }
58
+ if (entry.visibility === "visible" && entry.vBody != null) {
59
+ return entry.vBody;
60
+ }
61
+ return entry.body;
62
+ }
63
+
41
64
  function renderLogTag(entry, rowsByPath) {
42
65
  const attrs =
43
66
  typeof entry.attributes === "string"
@@ -45,20 +68,12 @@ function renderLogTag(entry, rowsByPath) {
45
68
  : entry.attributes;
46
69
 
47
70
  const action = actionFromPath(entry.path);
48
-
49
71
  const statusValue =
50
72
  attrs?.status != null
51
73
  ? attrs.status
52
74
  : entry.state
53
75
  ? stateToStatus(entry.state, entry.outcome)
54
76
  : null;
55
- // Suppress status on prompts; uniform 200 carries no signal.
56
- const status =
57
- statusValue != null && action !== "prompt"
58
- ? ` status="${statusValue}"`
59
- : "";
60
- const outcomeAttr = entry.outcome ? ` outcome="${entry.outcome}"` : "";
61
- // tokens = aTokens of the thing this tag represents (target via attrs.path, else self).
62
77
  const isSlice = attrs?.lineStart != null;
63
78
  const targetEntry = attrs?.path ? rowsByPath.get(attrs.path) : null;
64
79
  let tokenSource = null;
@@ -76,27 +91,21 @@ function renderLogTag(entry, rowsByPath) {
76
91
  tokenSource = entry.aTokens;
77
92
  lineSource = entry.vLines;
78
93
  }
79
- const tokens = tokenSource != null ? ` tokens="${tokenSource}"` : "";
80
- const summary =
81
- typeof attrs?.summary === "string"
82
- ? ` summary="${attrs.summary.slice(0, 80)}"`
83
- : "";
84
- const query =
85
- typeof attrs?.query === "string" ? ` query="${attrs.query}"` : "";
86
- const command =
87
- typeof attrs?.command === "string" ? ` command="${attrs.command}"` : "";
88
- const target = attrs?.path ? ` target="${attrs.path}"` : "";
89
- // Slice reads emit lines="a-b/total"; others emit simple lines="N".
90
- const lines = isSlice
91
- ? ` lines="${attrs.lineStart}-${attrs.lineEnd}/${attrs.totalLines}"`
92
- : lineSource != null
93
- ? ` lines="${lineSource}"`
94
- : "";
95
-
96
- const attrStr = `${target}${status}${outcomeAttr}${query}${command}${summary}${lines}${tokens}`;
97
94
 
98
- if (entry.body) {
99
- return `<${action} path="${entry.path}"${attrStr}>${entry.body}</${action}>`;
95
+ const meta = { action };
96
+ if (attrs?.path) meta.target = attrs.path;
97
+ // Suppress status on prompts; uniform 200 carries no signal.
98
+ if (statusValue != null && action !== "prompt") meta.status = statusValue;
99
+ if (entry.outcome) meta.outcome = entry.outcome;
100
+ if (typeof attrs?.query === "string") meta.query = attrs.query;
101
+ if (typeof attrs?.command === "string") meta.command = attrs.command;
102
+ if (typeof attrs?.tags === "string") meta.tags = attrs.tags.slice(0, 80);
103
+ if (isSlice) {
104
+ meta.lines = `${attrs.lineStart}-${attrs.lineEnd}/${attrs.totalLines}`;
105
+ } else if (lineSource != null) {
106
+ meta.lines = lineSource;
100
107
  }
101
- return `<${action} path="${entry.path}"${attrStr}/>`;
108
+ if (tokenSource != null) meta.tokens = tokenSource;
109
+
110
+ return renderEntry(entry.path, meta, projectedBody(entry));
102
111
  }
@@ -1,4 +1,5 @@
1
1
  import Entries from "../../agent/Entries.js";
2
+ import { storePatternResult } from "../helpers.js";
2
3
  import docs from "./mvDoc.js";
3
4
 
4
5
  const LOG_ACTION_RE = /^log:\/\/turn_\d+\/(\w+)\//;
@@ -34,6 +35,17 @@ export default class Mv {
34
35
  ? entry.attributes.visibility
35
36
  : undefined;
36
37
 
38
+ // Manifest: list what would be affected without performing the mv.
39
+ if (entry.attributes.manifest !== undefined) {
40
+ const matches = await store.getEntriesByPattern(runId, path);
41
+ await storePatternResult(store, runId, turn, "mv", path, null, matches, {
42
+ manifest: true,
43
+ loopId,
44
+ attributes: { path, to },
45
+ });
46
+ return;
47
+ }
48
+
37
49
  // Visibility-in-place: no destination, change visibility of matched entries
38
50
  if (visibility && !to) {
39
51
  const matches = await store.getEntriesByPattern(runId, path);
@@ -58,6 +70,18 @@ export default class Mv {
58
70
 
59
71
  const source = await store.getBody(runId, path);
60
72
  if (source === null) return;
73
+ // Tags propagate: explicit `tags=` on the mv wins; otherwise the
74
+ // destination inherits the source entry's tags. Same shape as
75
+ // visibility — explicit attr overrides, default inherits.
76
+ let destTags = null;
77
+ if (typeof entry.attributes.tags === "string") {
78
+ destTags = entry.attributes.tags;
79
+ } else {
80
+ const sourceAttrs = await store.getAttributes(runId, path);
81
+ if (sourceAttrs && typeof sourceAttrs.tags === "string") {
82
+ destTags = sourceAttrs.tags;
83
+ }
84
+ }
61
85
 
62
86
  const destScheme = Entries.scheme(to);
63
87
  const existing = await store.getBody(runId, to);
@@ -68,13 +92,27 @@ export default class Mv {
68
92
 
69
93
  const body = `${path} ${to}`;
70
94
  if (destScheme === null) {
95
+ // Bare-file destination: hand the shared materializer (set.js
96
+ // #materializeFile, gated on attrs.path + attrs.patched) the
97
+ // authoritative new body so it writes the source content to
98
+ // disk on accept. Without this the source rm fired but the
99
+ // destination was never created. Same shape as cp's bare-file
100
+ // branch.
71
101
  await store.set({
72
102
  runId,
73
103
  turn,
74
104
  path: entry.resultPath,
75
105
  body,
76
106
  state: "proposed",
77
- attributes: { from: path, to, isMove: true, warning },
107
+ attributes: {
108
+ from: path,
109
+ to,
110
+ isMove: true,
111
+ warning,
112
+ path: to,
113
+ patched: source,
114
+ visibility,
115
+ },
78
116
  loopId,
79
117
  });
80
118
  } else {
@@ -85,6 +123,7 @@ export default class Mv {
85
123
  body: source,
86
124
  state: "resolved",
87
125
  visibility,
126
+ attributes: destTags ? { tags: destTags } : null,
88
127
  loopId,
89
128
  });
90
129
  await store.rm({ runId: runId, path: path });
@@ -1,10 +1,3 @@
1
1
  ## <mv path="[source]">[destination]</mv> - Move or rename a file or entry
2
-
3
- Example: <mv path="known://active_task">known://completed_task</mv>
4
- <!-- Entry rename. Most common mv use case. -->
5
-
6
2
  Example: <mv path="src/old_name.js">src/new_name.js</mv>
7
- <!-- File rename. -->
8
-
9
- Example: <mv path="known://project/*" visibility="summarized"/>
10
- <!-- Batch visibility change via pattern. No destination = visibility in place. -->
3
+ Example: <mv path="known://drafts/final_report.md">final_report.md</mv>
@@ -1,9 +1,9 @@
1
- import config from "../../agent/config.js";
2
1
  import msg from "../../agent/messages.js";
3
2
  import { chatCompletionStream } from "../../llm/openaiStream.js";
4
3
  import { retryWithBackoff } from "../../llm/retry.js";
5
4
 
6
- const { FETCH_TIMEOUT } = config;
5
+ const FETCH_TIMEOUT = Number(process.env.RUMMY_FETCH_TIMEOUT);
6
+ const THINK = process.env.RUMMY_THINK === "1";
7
7
 
8
8
  const PROVIDER = "ollama";
9
9
 
@@ -28,7 +28,8 @@ export default class Ollama {
28
28
  }
29
29
 
30
30
  async #completion(messages, model, options = {}) {
31
- const body = { model, messages, think: true };
31
+ const body = { model, messages, think: THINK };
32
+ if (options.maxTokens !== undefined) body.max_tokens = options.maxTokens;
32
33
  if (options.temperature !== undefined)
33
34
  body.temperature = options.temperature;
34
35