@possumtech/rummy 2.0.0 → 2.1.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 (117) hide show
  1. package/.env.example +31 -5
  2. package/BENCH_ENVIRONMENT.md +230 -0
  3. package/CLIENT_INTERFACE.md +396 -0
  4. package/PLUGINS.md +93 -1
  5. package/SPEC.md +389 -28
  6. package/bin/postinstall.js +2 -2
  7. package/bin/rummy.js +2 -2
  8. package/last_run.txt +5617 -0
  9. package/migrations/001_initial_schema.sql +2 -1
  10. package/package.json +13 -9
  11. package/scriptify/ask_run.js +77 -0
  12. package/scriptify/cache_probe.js +66 -0
  13. package/scriptify/cache_probe_grok.js +74 -0
  14. package/service.js +22 -11
  15. package/src/agent/AgentLoop.js +62 -157
  16. package/src/agent/ContextAssembler.js +2 -9
  17. package/src/agent/Entries.js +54 -98
  18. package/src/agent/ProjectAgent.js +4 -11
  19. package/src/agent/TurnExecutor.js +48 -83
  20. package/src/agent/XmlParser.js +247 -273
  21. package/src/agent/budget.js +5 -28
  22. package/src/agent/config.js +38 -0
  23. package/src/agent/errors.js +7 -13
  24. package/src/agent/httpStatus.js +1 -19
  25. package/src/agent/known_queries.sql +1 -1
  26. package/src/agent/known_store.sql +12 -2
  27. package/src/agent/materializeContext.js +15 -18
  28. package/src/agent/pathEncode.js +5 -0
  29. package/src/agent/rummyHome.js +9 -0
  30. package/src/agent/runs.sql +37 -0
  31. package/src/agent/tokens.js +7 -7
  32. package/src/hooks/HookRegistry.js +1 -16
  33. package/src/hooks/Hooks.js +8 -33
  34. package/src/hooks/PluginContext.js +3 -21
  35. package/src/hooks/RpcRegistry.js +1 -4
  36. package/src/hooks/RummyContext.js +6 -16
  37. package/src/hooks/ToolRegistry.js +5 -15
  38. package/src/llm/LlmProvider.js +41 -33
  39. package/src/llm/errors.js +41 -4
  40. package/src/llm/openaiStream.js +125 -0
  41. package/src/llm/retry.js +109 -0
  42. package/src/plugins/budget/budget.js +55 -76
  43. package/src/plugins/cli/README.md +87 -0
  44. package/src/plugins/cli/bin.js +61 -0
  45. package/src/plugins/cli/cli.js +120 -0
  46. package/src/plugins/env/README.md +2 -1
  47. package/src/plugins/env/env.js +4 -6
  48. package/src/plugins/env/envDoc.md +2 -2
  49. package/src/plugins/error/error.js +23 -23
  50. package/src/plugins/file/file.js +2 -22
  51. package/src/plugins/get/get.js +12 -34
  52. package/src/plugins/get/getDoc.md +8 -6
  53. package/src/plugins/hedberg/edits.js +1 -11
  54. package/src/plugins/hedberg/hedberg.js +3 -26
  55. package/src/plugins/hedberg/normalize.js +1 -5
  56. package/src/plugins/hedberg/patterns.js +4 -15
  57. package/src/plugins/hedberg/sed.js +1 -7
  58. package/src/plugins/helpers.js +28 -20
  59. package/src/plugins/index.js +25 -41
  60. package/src/plugins/instructions/README.md +18 -0
  61. package/src/plugins/instructions/instructions.js +97 -38
  62. package/src/plugins/instructions/instructions.md +24 -15
  63. package/src/plugins/instructions/instructions_104.md +5 -4
  64. package/src/plugins/instructions/instructions_105.md +29 -36
  65. package/src/plugins/instructions/instructions_106.md +22 -0
  66. package/src/plugins/instructions/instructions_107.md +17 -0
  67. package/src/plugins/instructions/instructions_108.md +0 -8
  68. package/src/plugins/known/README.md +26 -6
  69. package/src/plugins/known/known.js +37 -34
  70. package/src/plugins/log/README.md +2 -2
  71. package/src/plugins/log/log.js +27 -34
  72. package/src/plugins/ollama/ollama.js +50 -66
  73. package/src/plugins/openai/openai.js +26 -44
  74. package/src/plugins/openrouter/openrouter.js +28 -52
  75. package/src/plugins/policy/README.md +8 -2
  76. package/src/plugins/policy/policy.js +8 -21
  77. package/src/plugins/prompt/README.md +22 -0
  78. package/src/plugins/prompt/prompt.js +14 -16
  79. package/src/plugins/rm/rm.js +5 -2
  80. package/src/plugins/rm/rmDoc.md +4 -4
  81. package/src/plugins/rpc/README.md +2 -1
  82. package/src/plugins/rpc/rpc.js +62 -48
  83. package/src/plugins/set/README.md +5 -1
  84. package/src/plugins/set/set.js +23 -33
  85. package/src/plugins/set/setDoc.md +1 -1
  86. package/src/plugins/sh/README.md +2 -1
  87. package/src/plugins/sh/sh.js +5 -11
  88. package/src/plugins/sh/shDoc.md +2 -2
  89. package/src/plugins/stream/README.md +6 -5
  90. package/src/plugins/stream/stream.js +6 -35
  91. package/src/plugins/telemetry/telemetry.js +26 -19
  92. package/src/plugins/think/think.js +4 -7
  93. package/src/plugins/unknown/unknown.js +8 -13
  94. package/src/plugins/update/update.js +42 -25
  95. package/src/plugins/update/updateDoc.md +3 -3
  96. package/src/plugins/xai/xai.js +30 -20
  97. package/src/plugins/yolo/yolo.js +159 -0
  98. package/src/server/ClientConnection.js +17 -47
  99. package/src/server/SocketServer.js +14 -14
  100. package/src/server/protocol.js +1 -10
  101. package/src/sql/functions/slugify.js +5 -7
  102. package/src/sql/v_model_context.sql +4 -11
  103. package/turns/cli_1777462658211/turn_001.txt +772 -0
  104. package/turns/cli_1777462658211/turn_002.txt +606 -0
  105. package/turns/cli_1777462658211/turn_003.txt +667 -0
  106. package/turns/cli_1777462658211/turn_004.txt +297 -0
  107. package/turns/cli_1777462658211/turn_005.txt +301 -0
  108. package/turns/cli_1777462658211/turn_006.txt +262 -0
  109. package/turns/cli_1777465095132/turn_001.txt +715 -0
  110. package/turns/cli_1777465095132/turn_002.txt +236 -0
  111. package/turns/cli_1777465095132/turn_003.txt +287 -0
  112. package/turns/cli_1777465095132/turn_004.txt +694 -0
  113. package/turns/cli_1777465095132/turn_005.txt +422 -0
  114. package/turns/cli_1777465095132/turn_006.txt +365 -0
  115. package/turns/cli_1777465095132/turn_007.txt +885 -0
  116. package/turns/cli_1777465095132/turn_008.txt +1277 -0
  117. package/turns/cli_1777465095132/turn_009.txt +736 -0
@@ -1,46 +1,39 @@
1
- # Discovery Stage
1
+ # Distillation Stage: YOU MUST select an unknown:// entry, then discover its source entries and distill them into known:// entries
2
2
 
3
- YOU MUST ONLY perform discovery actions (Discover -> Distill -> Demote) during the Discovery Stage.
4
- YOU MUST discover and get source entries with information relevant to the unknown:// entries.
5
- YOU MUST create topical, taxonomized, and tagged known:// entries to resolve unknown:// entries.
6
- YOU MUST include at least 1 link to a relevant unknown and at least 1 link to a relevant source entry in every known:// entry.
7
- YOU MUST demote source entries to "summarized" after extracting and decomposing their relevant information into known:// entries.
8
- YOU MUST demote the unknown:// entries to "summarized" after they are referenced or resolved by known:// entries.
9
- YOU MUST demote all irrelevant source entries and log events to maximize FCRM.
10
- Tip: Source entry "summarized" information is not reliable. Only place "visible" source entry information in known:// entries.
11
- Tip: A "relevant" source entry that has been successfully distilled into known:// entries is no longer relevant.
12
- Tip: Discover, Distill, and Demote per source entry, not globally, to maximize FCRM.
3
+ YOU MUST create topical, taxonomized, and tagged known:// entries to resolve the selected unknown:// entry.
4
+ YOU MUST reference all related source entries and prompts in the `# Related` list
5
+ YOU MUST ONLY populate known entries with promoted information, NOT from your own training data or opinion.
6
+ YOU MUST immediately demote unknowns, source entries, prompts, and log events after they are distilled, irrelevant, or resolved.
13
7
 
14
- ## Discovery Lifecycle: Promoting a source entry, creating a known entry, demoting the source entry, then archiving the resolved unknown
8
+ * Check the `tokens="N"` of the source entries against the `tokensFree="N"` constraint before promoting entries.
9
+ * You can use <get path="..." manifest/> to list paths and their token amounts for bulk operations without performing them.
10
+ * You can use <get path="..." line="X" limit="Y"/> to read subsets of entries that would exceed your `tokensFree` budget.
11
+ * Don't accidentally set the current prompt to `archived`.
15
12
 
16
- ### Discover
13
+ Example:
14
+ <get path="**" manifest>capital</get>
15
+ <get path="prompt://3" line="1" limit="100"/>
17
16
 
18
- <set path="trivia/capitals.csv" visibility="visible"/>
17
+ <set path="trivia/capitals.csv" visibility="visible"/>
19
18
 
20
- ### Distill
21
- <set path="known://countries/france/capital" summary="countries,france,capital,geography,trivia">
22
- # Capital of France
23
- The capital of France is Paris.
19
+ <set path="known://countries/france/capital" summary="countries,france,capital,geography,trivia">
20
+ # Related
21
+ [trivia question](prompt://3)
22
+ [unknown resolving](unknown://countries/france/capital)
23
+ [source entry](trivia/capitals.csv)
24
24
 
25
- { ... }
25
+ # Capital of France
26
+ The capital of France is Paris.
26
27
 
27
- # References
28
- [unknown resolving](unknown://countries/france/capital)
29
- [source entry](trivia/capitals.csv)
30
- </set>
28
+ {...}
29
+ </set>
31
30
 
32
- ### Demote
33
-
34
- <set path="trivia/capitals.csv" visibility="summarized"/>
35
- <set path="unknown://countries/france/capital" visibility="summarized"/>
36
- <set path="unknown://countries/poland/capital" summary="REJECTED: Irrelevant" visibility="summarized"/>
37
- <set path="https://en.wikipedia.org/wiki/Paris,_Texas" summary="REJECTED: Wrong Paris" visibility="summarized"/>
38
- <set path="log://turn_1/set/*" visibility="archived"/>
39
- <set path="log://turn_1/get/trivia/*" visibility="archived"/>
40
- <set path="log://turn_2/get/capital%20of%20france" visibility="archived"/>
31
+ <set path="prompt://3" visibility="summarized"/>
32
+ <set path="unknown://countries/france/capital" visibility="summarized"/>
33
+ <set path="unknown://countries/france/seat_of_government" summary="RESOLVED: Not necessary" visibility="summarized"/>
34
+ <set path="trivia/capitals.csv" visibility="summarized"/>
41
35
 
42
36
  ## Turn Termination (CHOOSE ONLY ONE):
43
-
44
- Definition Stage Return: <update status="154">returning to definition stage</update>
45
- Discovery Stage Continuation: <update status="155">referencing and resolving more unknowns</update>
46
- Discovery Stage Completion: <update status="158">all unknowns (if any) referenced or resolved by known entries</update>
37
+ * Decomposition Stage Return: <update status="154">additional unknowns identified; returning to Decomposition Stage</update>
38
+ * Distillation Stage Continuation: <update status="155">discovering and distilling more for the selected unknown</update>
39
+ * Distillation Stage Completion: <update status="156">this unknown's known entries written</update>
@@ -0,0 +1,22 @@
1
+ # Demotion Stage: YOU MUST demote all source entries, prompts, and log events that are now distilled or no longer relevant
2
+
3
+ Example:
4
+ <set path="prompt://2" summary="All information distilled into knowns" visibility="summarized"/>
5
+ <set path="trivia/capitals.csv" visibility="summarized"/>
6
+ <set path="unknown://countries/france/capital" visibility="summarized"/>
7
+ <set path="unknown://countries/poland/capital" summary="REJECTED: Irrelevant" visibility="summarized"/>
8
+ <set path="https://en.wikipedia.org/wiki/Paris,_Texas" summary="REJECTED: Wrong Paris" visibility="summarized"/>
9
+ <set path="log://turn_1/**" visibility="archived"/>
10
+ <set path="log://turn_2/**" visibility="archived"/>
11
+ <set path="log://turn_3/set/**" visibility="archived"/>
12
+ <set path="log://turn_3/get/**" visibility="archived"/>
13
+ <set path="log://turn_3/search/**" visibility="archived"/>
14
+
15
+ * You need room to think. Demote large prompts and source entries, then iterate them with <get path="..." line="N" limit="N"/> as necessary.
16
+ * When demoting prompts, prefer "summarized" to "archived" to avoid losing necessary context.
17
+
18
+ ## Turn Termination (CHOOSE ONLY ONE):
19
+ * Decomposition Stage Return: <update status="164">additional unknowns identified; returning to Decomposition Stage</update>
20
+ * Distillation Stage Return: <update status="165">more unknowns remain; returning to Distillation Stage</update>
21
+ * Demotion Stage Continuation: <update status="166">demoting more distilled or irrelevant entries, prompts, and log events</update>
22
+ * Demotion Stage Completion: <update status="167">all unknowns resolved and demoted; ready for Deployment Stage</update>
@@ -0,0 +1,17 @@
1
+ # Deployment Stage: YOU MUST act on the prompt.
2
+
3
+ YOU MUST attempt to deterministically verify your actions, outputs, or answers before declaring completion, if possible.
4
+
5
+ Example: verifying deliverable before completion
6
+ <set path="sum.js">console.log(process.argv.slice(2).reduce((a, b) => a + Number(b), 0));</set>
7
+ <sh>[ -f sum.js ] && node --version && node sum.js 2 2 | grep -qx 4</sh>
8
+ <update status="177">sum.js written, node available, ran cleanly, correct output?</update>
9
+
10
+ Example: <update status="200">Paris</update>
11
+
12
+ ## Turn Termination (CHOOSE ONLY ONE):
13
+ * Decomposition Stage Return: <update status="174">additional unknowns identified; returning to Decomposition Stage</update>
14
+ * Distillation Stage Return: <update status="175">selected unknown not yet resolved; returning to Distillation Stage</update>
15
+ * Demotion Stage Return: <update status="176">context not yet sufficiently demoted; returning to Demotion Stage</update>
16
+ * Deployment Stage Continuation: <update status="177">performing more actions</update>
17
+ * Deployment Stage Completion: <update status="200">{direct answer (summary of actions performed if prompt not a question)}</update>
@@ -1,8 +0,0 @@
1
- YOU MUST act on the prompt.
2
-
3
- Turn Termination (CHOOSE ONLY ONE):
4
-
5
- Definition Stage Return: <update status="184">returning to definition stage</update>
6
- Discovery Stage Return: <update status="185">returning to discovery stage</update>
7
- Deployment Stage Continuation: <update status="188">performing more actions</update>
8
- Deployment Stage Completion: <update status="200">summary of actions performed, or direct answer</update>
@@ -1,13 +1,17 @@
1
1
  # known {#known_plugin}
2
2
 
3
- Writes knowledge entries into the store at full visibility.
3
+ Writes knowledge entries into the store at full visibility, and renders
4
+ the project's data surface as the bifurcated `<summarized>` /
5
+ `<visible>` blocks at the top of the user message.
4
6
 
5
7
  ## Registration
6
8
 
7
9
  - **Tool**: `known`
8
10
  - **Category**: `data`
9
11
  - **Handler**: Upserts the entry body at the target path with status 200.
10
- - **Filter**: `assembly.system` at priority 100 — renders `<knowns>` section.
12
+ - **Filters**:
13
+ - `assembly.user` priority 50 — renders `<summarized>`.
14
+ - `assembly.user` priority 75 — renders `<visible>`.
11
15
 
12
16
  ## Projection
13
17
 
@@ -15,7 +19,23 @@ Shows `# known {path}` followed by the entry body.
15
19
 
16
20
  ## Assembly
17
21
 
18
- Filters turn_context rows where `category === "data"`. Renders all
19
- data entries (files, knowledge, skills, URLs) into the `<knowns>` section
20
- of the system message. Third-party plugins that register with
21
- `category: "data"` automatically appear here.
22
+ Filters `ctx.rows` where `category === "data"`. Two separate blocks
23
+ emit at the top of the user message in this order:
24
+
25
+ - `<summarized>` — each data entry whose visibility is `visible` or
26
+ `summarized`, rendered under its scheme tag with the plugin's
27
+ summary projection as body (truncated knowns, code symbols,
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.
32
+ - `<visible>` — each data entry whose visibility is `visible`,
33
+ rendered with the plugin's visible projection (full body) as the
34
+ tag body. A visible entry appears in *both* blocks: summary
35
+ projection up top, full body below.
36
+
37
+ This split lets `<summarized>` stay cache-stable across promote/demote
38
+ operations — only `<visible>` mutates when the model promotes a
39
+ summary or demotes a visible entry. Third-party plugins that register
40
+ with `category: "data"` automatically appear in both blocks under
41
+ their scheme tag.
@@ -12,10 +12,9 @@ export default class Known {
12
12
  core.on("handler", this.handler.bind(this));
13
13
  core.on("visible", this.full.bind(this));
14
14
  core.on("summarized", this.summary.bind(this));
15
- core.filter("assembly.system", this.assembleContext.bind(this), 100);
16
- // <known> is internal — written via <set path="known://...">. Hidden
17
- // from all model-facing tool lists. Handler still dispatches if the
18
- // model emits <known> directly out of habit.
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>.
19
18
  core.markHidden();
20
19
  }
21
20
 
@@ -23,23 +22,20 @@ export default class Known {
23
22
  const { entries: store, sequence: turn, runId, loopId } = rummy;
24
23
  if (!entry.body) return;
25
24
 
26
- // Size gate
27
25
  const entryTokens = countTokens(entry.body);
28
26
  if (entryTokens > MAX_ENTRY_TOKENS) {
29
- const rejectPath = await store.slugPath(runId, "known", entry.body);
30
27
  await store.set({
31
28
  runId,
32
29
  turn,
33
- path: rejectPath,
30
+ loopId,
31
+ path: entry.resultPath,
34
32
  body: `Entry too large (${entryTokens} tokens, max ${MAX_ENTRY_TOKENS}). Sort the information, ideas, or plans carefully into multiple entries.`,
35
33
  state: "failed",
36
34
  outcome: `overflow:${entryTokens}`,
37
- loopId,
38
35
  });
39
36
  return;
40
37
  }
41
38
 
42
- // Resolve path: explicit or auto-generated slug
43
39
  let knownPath = entry.attributes?.path;
44
40
  if (knownPath && !knownPath.includes("://")) {
45
41
  knownPath = `known://${knownPath}`;
@@ -53,9 +49,7 @@ export default class Known {
53
49
  );
54
50
  }
55
51
 
56
- // Dedup: if path exists, update rather than duplicate. An empty
57
- // new body means "preserve the existing entry's body" (e.g. the
58
- // model is updating attributes only).
52
+ // Dedup: existing path update; empty body preserves existing body.
59
53
  const existing = await store.getEntriesByPattern(runId, knownPath, null);
60
54
  if (existing.length > 0) {
61
55
  const nextBody = entry.body === "" ? existing[0].body : entry.body;
@@ -86,31 +80,45 @@ export default class Known {
86
80
  return entry.body;
87
81
  }
88
82
 
89
- // Summarized knowns keep the first 500 characters so the model
90
- // doesn't lose the plot when budget auto-demotion kicks in on its
91
- // own work. Anything larger gets capped so a pathologically big
92
- // known doesn't saturate the packet at summarized visibility
93
- // either. Matches the pattern on `<prompt>` summarized view.
83
+ // Summarized: first 500 chars; matches <prompt> summarized.
94
84
  summary(entry) {
95
85
  if (!entry.body) return "";
96
86
  if (entry.body.length <= 500) return entry.body;
97
87
  return `${entry.body.slice(0, 500)}\n[truncated — promote to see the full body]`;
98
88
  }
99
89
 
100
- async assembleContext(content, ctx) {
101
- const entries = ctx.rows.filter((r) => r.category === "data");
90
+ // Identity-keyed summary lines: every data entry the run is tracking
91
+ // at visibility=visible or visibility=summarized.
92
+ async assembleSummarized(content, ctx) {
93
+ const entries = ctx.rows.filter(
94
+ (r) =>
95
+ r.category === "data" &&
96
+ (r.visibility === "visible" || r.visibility === "summarized"),
97
+ );
98
+ if (entries.length === 0) return content;
99
+ const lines = entries.map((e) =>
100
+ renderContextTag(e, e.sBody != null ? e.sBody : e.body),
101
+ );
102
+ return `${content}<summarized>\n${lines.join("\n")}\n</summarized>\n`;
103
+ }
104
+
105
+ async assembleVisible(content, ctx) {
106
+ const entries = ctx.rows.filter(
107
+ (r) => r.category === "data" && r.visibility === "visible",
108
+ );
102
109
  if (entries.length === 0) return content;
103
- const demotedSet = new Set(ctx.demoted);
104
- const lines = entries.map((e) => renderContextTag(e, demotedSet));
105
- return `${content}\n\n<context>\n${lines.join("\n")}\n</context>`;
110
+ const lines = entries.map((e) =>
111
+ renderContextTag(e, e.vBody != null ? e.vBody : e.body),
112
+ );
113
+ return `${content}<visible>\n${lines.join("\n")}\n</visible>\n`;
106
114
  }
107
115
  }
108
116
 
109
- function renderContextTag(entry, demotedSet) {
110
- // schemeOf() returns NULL / "" for bare file paths; translate for the tag.
117
+ function renderContextTag(entry, projectedBody) {
111
118
  const tag = entry.scheme ? entry.scheme : "file";
112
119
  const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
113
120
  const tokens = entry.aTokens != null ? ` tokens="${entry.aTokens}"` : "";
121
+ const lines = entry.vLines != null ? ` lines="${entry.vLines}"` : "";
114
122
  const attrs =
115
123
  typeof entry.attributes === "string"
116
124
  ? JSON.parse(entry.attributes)
@@ -128,21 +136,16 @@ function renderContextTag(entry, demotedSet) {
128
136
  const stateAttr =
129
137
  entry.state && entry.state !== "resolved" ? ` state="${entry.state}"` : "";
130
138
  const outcomeAttr = entry.outcome ? ` outcome="${entry.outcome}"` : "";
131
- const visibility = entry.visibility
132
- ? ` visibility="${entry.visibility}"`
133
- : "";
134
- const flag = demotedSet?.has(entry.path) ? " demoted" : "";
135
- // Always render summary attribute on knowns — empty value hints the model
136
- // it forgot to add searchable keywords.
139
+ const visibility =
140
+ entry.visibility === "archived" ? ` visibility="archived"` : "";
137
141
  const summaryText =
138
142
  typeof attrs?.summary === "string"
139
143
  ? attrs.summary.replace(/"/g, "'").slice(0, 80)
140
144
  : "";
141
145
  const summary = ` summary="${summaryText}"`;
142
-
143
- const attrStr = `${turn}${status}${stateAttr}${outcomeAttr}${summary}${visibility}${tokens}${flag}`;
144
- if (entry.body) {
145
- return `<${tag} path="${entry.path}"${attrStr}>${entry.body}</${tag}>`;
146
+ const attrStr = `${turn}${status}${stateAttr}${outcomeAttr}${summary}${visibility}${tokens}${lines}`;
147
+ if (projectedBody) {
148
+ return `<${tag} path="${entry.path}"${attrStr}>${projectedBody}</${tag}>`;
146
149
  }
147
150
  return `<${tag} path="${entry.path}"${attrStr}/>`;
148
151
  }
@@ -29,7 +29,7 @@ size. Resolution:
29
29
  own body tokens.
30
30
  - `sh` and `env` own multiple streaming channels (`sh://turn_N/{slug}_N`)
31
31
  — no single target to point at. `tokens=` is omitted; the channels
32
- render their own tokens in `<context>`.
32
+ render their own tokens in `<visible>`.
33
33
 
34
34
  ## Behavior
35
35
 
@@ -43,6 +43,6 @@ Log entries (`log://turn_N/{action}/{slug}`) are audit records —
43
43
  summary, exit status, references to where the data lives — and never
44
44
  carry the payload itself. Payload for streaming actions lives under the
45
45
  producer's own scheme (`sh://`, `env://`, future `search://`, etc.) at
46
- `category=data`, and is rendered inside `<context>` by the known
46
+ `category=data`, and is rendered inside `<visible>` by the known
47
47
  plugin. Scheme determines category; data and logging never share a
48
48
  scheme. See [scheme_category_split](#scheme_category_split).
@@ -1,11 +1,6 @@
1
1
  import { stateToStatus } from "../../agent/httpStatus.js";
2
2
 
3
- // Schemes whose log body is an action summary, not the cost-bearing
4
- // content. For these, the action's cost lives on a separate data entry
5
- // (sh/env: streaming channels; set/mv/cp: the target entry). Report
6
- // tokens from the target when we can resolve it (set/mv/cp via
7
- // attrs.path); omit entirely for sh/env (multiple channels, no single
8
- // target to point at).
3
+ // sh/env span multiple channels; channels render their own tokens in <visible>.
9
4
  const STREAM_NO_TOKENS = new Set(["sh", "env"]);
10
5
 
11
6
  export default class Log {
@@ -17,10 +12,7 @@ export default class Log {
17
12
  }
18
13
 
19
14
  async assembleLog(content, ctx) {
20
- // Log includes action entries (scheme=log) AND prior prompts. The
21
- // most recent prompt is rendered separately by the prompt plugin
22
- // as `<prompt>`; everything older lives in the log so the model
23
- // can see the full question history across a sustained run.
15
+ // Includes prior prompts; the latest prompt is rendered separately as <prompt>.
24
16
  const latestPrompt = ctx.rows.findLast(
25
17
  (r) => r.category === "prompt" && r.scheme === "prompt",
26
18
  );
@@ -39,10 +31,7 @@ export default class Log {
39
31
  }
40
32
  }
41
33
 
42
- // Log paths are log://turn_N/action/slug. The second segment is the
43
- // action — the plugin/tool that produced this log entry (set, get,
44
- // search, update, error, etc.). Used as the XML tag name. Prompt
45
- // entries live at prompt://N; they render as <prompt> in history.
34
+ // Action segment of log://turn_N/action/slug XML tag.
46
35
  function actionFromPath(path) {
47
36
  if (path?.startsWith("prompt://")) return "prompt";
48
37
  const match = path?.match(/^log:\/\/turn_\d+\/([^/]+)\//);
@@ -63,23 +52,30 @@ function renderLogTag(entry, rowsByPath) {
63
52
  : entry.state
64
53
  ? stateToStatus(entry.state, entry.outcome)
65
54
  : null;
66
- const status = statusValue != null ? ` status="${statusValue}"` : "";
55
+ // Suppress status on prompts; uniform 200 carries no signal.
56
+ const status =
57
+ statusValue != null && action !== "prompt"
58
+ ? ` status="${statusValue}"`
59
+ : "";
67
60
  const outcomeAttr = entry.outcome ? ` outcome="${entry.outcome}"` : "";
68
- // `tokens=` is the promotion premium (aTokens) of the thing this tag
69
- // represents — what the model would free by demoting it. For actions
70
- // that reference a separate data entry (get/set/mv/cp), resolve via
71
- // attrs.path and report the target's aTokens. For actions whose log
72
- // body IS the cost-bearing content (search/update/error/ask_user,
73
- // plus <get> slice reads), use the log entry's own aTokens. sh/env
74
- // span multiple channel entries and are omitted — the channels
75
- // render their own tokens in <context>.
61
+ // tokens = aTokens of the thing this tag represents (target via attrs.path, else self).
76
62
  const isSlice = attrs?.lineStart != null;
77
63
  const targetEntry = attrs?.path ? rowsByPath.get(attrs.path) : null;
78
64
  let tokenSource = null;
79
- if (STREAM_NO_TOKENS.has(action)) tokenSource = null;
80
- else if (isSlice) tokenSource = entry.aTokens;
81
- else if (targetEntry) tokenSource = targetEntry.aTokens;
82
- else tokenSource = entry.aTokens;
65
+ let lineSource = null;
66
+ if (STREAM_NO_TOKENS.has(action)) {
67
+ tokenSource = null;
68
+ lineSource = null;
69
+ } else if (isSlice) {
70
+ tokenSource = entry.aTokens;
71
+ lineSource = entry.vLines;
72
+ } else if (targetEntry) {
73
+ tokenSource = targetEntry.aTokens;
74
+ lineSource = targetEntry.vLines;
75
+ } else {
76
+ tokenSource = entry.aTokens;
77
+ lineSource = entry.vLines;
78
+ }
83
79
  const tokens = tokenSource != null ? ` tokens="${tokenSource}"` : "";
84
80
  const summary =
85
81
  typeof attrs?.summary === "string"
@@ -89,16 +85,13 @@ function renderLogTag(entry, rowsByPath) {
89
85
  typeof attrs?.query === "string" ? ` query="${attrs.query}"` : "";
90
86
  const command =
91
87
  typeof attrs?.command === "string" ? ` command="${attrs.command}"` : "";
92
- // target= is the path the action touched (e.g. the file/known that was
93
- // set, the URL that was fetched). Plugins store it in attrs.path when
94
- // they write the log entry.
95
88
  const target = attrs?.path ? ` target="${attrs.path}"` : "";
96
- // Slice reads tag the log entry with lineStart/lineEnd/totalLines so
97
- // the <get> tag surfaces `lines="a-b/total"` — a concrete handle for
98
- // the model to re-issue or compare against another slice.
89
+ // Slice reads emit lines="a-b/total"; others emit simple lines="N".
99
90
  const lines = isSlice
100
91
  ? ` lines="${attrs.lineStart}-${attrs.lineEnd}/${attrs.totalLines}"`
101
- : "";
92
+ : lineSource != null
93
+ ? ` lines="${lineSource}"`
94
+ : "";
102
95
 
103
96
  const attrStr = `${target}${status}${outcomeAttr}${query}${command}${summary}${lines}${tokens}`;
104
97
 
@@ -1,17 +1,13 @@
1
+ import config from "../../agent/config.js";
1
2
  import msg from "../../agent/messages.js";
3
+ import { chatCompletionStream } from "../../llm/openaiStream.js";
4
+ import { retryWithBackoff } from "../../llm/retry.js";
2
5
 
3
- const FETCH_TIMEOUT = Number(process.env.RUMMY_FETCH_TIMEOUT);
4
- if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
6
+ const { FETCH_TIMEOUT } = config;
5
7
 
6
8
  const PROVIDER = "ollama";
7
9
 
8
- /**
9
- * Ollama LLM provider plugin. Registers with hooks.llm.providers if
10
- * OLLAMA_BASE_URL is set; inert otherwise. Handles model aliases of the
11
- * form `ollama/{modelName}` — e.g. `ollama/llama3.1:8b` or
12
- * `ollama/library/qwen:7b` (Ollama accepts both bare and
13
- * registry-qualified model names).
14
- */
10
+ // Inert unless OLLAMA_BASE_URL is set; ollama/{model[/registry]} aliases.
15
11
  export default class Ollama {
16
12
  #baseUrl;
17
13
 
@@ -41,70 +37,58 @@ export default class Ollama {
41
37
  ? AbortSignal.any([options.signal, timeoutSignal])
42
38
  : timeoutSignal;
43
39
 
44
- const response = await fetch(`${this.#baseUrl}/v1/chat/completions`, {
45
- method: "POST",
46
- headers: { "Content-Type": "application/json" },
47
- body: JSON.stringify(body),
48
- signal,
49
- });
50
-
51
- if (!response.ok) {
52
- const error = await response.text();
53
- throw new Error(
54
- msg("error.ollama_api", { status: `${response.status} - ${error}` }),
55
- );
56
- }
57
-
58
- const data = await response.json();
59
-
60
- for (const choice of data.choices) {
61
- const m = choice.message;
62
- if (!m) continue;
63
- const parts = [m.reasoning_content, m.reasoning, m.thinking].filter(
64
- Boolean,
65
- );
66
- m.reasoning_content =
67
- parts.length > 0 ? [...new Set(parts)].join("\n") : null;
40
+ try {
41
+ return await chatCompletionStream({
42
+ url: `${this.#baseUrl}/v1/chat/completions`,
43
+ headers: {},
44
+ body,
45
+ signal,
46
+ });
47
+ } catch (err) {
48
+ if (err.status) {
49
+ throw new Error(
50
+ msg("error.ollama_api", { status: `${err.status} - ${err.body}` }),
51
+ );
52
+ }
53
+ throw err;
68
54
  }
69
-
70
- return data;
71
55
  }
72
56
 
73
57
  async #getContextSize(model) {
74
- for (let attempt = 0; attempt < 3; attempt++) {
75
- try {
76
- const response = await fetch(`${this.#baseUrl}/api/show`, {
77
- method: "POST",
78
- headers: { "Content-Type": "application/json" },
79
- body: JSON.stringify({ model }),
80
- signal: AbortSignal.timeout(FETCH_TIMEOUT),
81
- });
82
- if (!response.ok) {
83
- throw new Error(
84
- msg("error.ollama_show_failed", {
85
- status: response.status,
86
- baseUrl: this.#baseUrl,
87
- }),
88
- );
89
- }
90
- const data = await response.json();
91
- if (data.model_info) {
92
- for (const [key, value] of Object.entries(data.model_info)) {
93
- if (key.endsWith(".context_length")) return value;
94
- }
95
- }
96
- throw new Error(msg("error.ollama_no_context_length", { model }));
97
- } catch (err) {
98
- if (err.message.includes("Ollama")) throw err;
99
- if (attempt < 2) {
100
- await new Promise((r) => setTimeout(r, (attempt + 1) * 2000));
101
- continue;
102
- }
58
+ const fetchContext = async () => {
59
+ const response = await fetch(`${this.#baseUrl}/api/show`, {
60
+ method: "POST",
61
+ headers: { "Content-Type": "application/json" },
62
+ body: JSON.stringify({ model }),
63
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
64
+ });
65
+ if (!response.ok) {
103
66
  throw new Error(
104
- msg("error.ollama_unreachable", { baseUrl: this.#baseUrl }),
105
- { cause: err },
67
+ msg("error.ollama_show_failed", {
68
+ status: response.status,
69
+ baseUrl: this.#baseUrl,
70
+ }),
106
71
  );
107
72
  }
73
+ const data = await response.json();
74
+ if (data.model_info) {
75
+ for (const [key, value] of Object.entries(data.model_info)) {
76
+ if (key.endsWith(".context_length")) return value;
77
+ }
78
+ }
79
+ throw new Error(msg("error.ollama_no_context_length", { model }));
80
+ };
81
+ try {
82
+ return await retryWithBackoff(fetchContext, {
83
+ deadlineMs: FETCH_TIMEOUT,
84
+ isRetryable: (err) => !err.message.includes("Ollama"),
85
+ });
86
+ } catch (err) {
87
+ if (err.message.includes("Ollama")) throw err;
88
+ throw new Error(
89
+ msg("error.ollama_unreachable", { baseUrl: this.#baseUrl }),
90
+ { cause: err },
91
+ );
108
92
  }
109
93
  }
110
94
  }