@possumtech/rummy 0.5.0 → 2.0.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 (157) hide show
  1. package/.env.example +42 -5
  2. package/PLUGINS.md +389 -194
  3. package/README.md +25 -8
  4. package/SPEC.md +934 -373
  5. package/bin/demo.js +166 -0
  6. package/bin/rummy.js +9 -3
  7. package/biome/no-fallbacks.grit +50 -0
  8. package/lang/en.json +2 -2
  9. package/migrations/001_initial_schema.sql +88 -37
  10. package/package.json +13 -11
  11. package/scriptify/ask_run.js +77 -0
  12. package/service.js +50 -9
  13. package/src/agent/AgentLoop.js +476 -335
  14. package/src/agent/ContextAssembler.js +4 -4
  15. package/src/agent/Entries.js +676 -0
  16. package/src/agent/ProjectAgent.js +30 -18
  17. package/src/agent/TurnExecutor.js +232 -421
  18. package/src/agent/XmlParser.js +99 -33
  19. package/src/agent/budget.js +56 -0
  20. package/src/agent/errors.js +22 -0
  21. package/src/agent/httpStatus.js +39 -0
  22. package/src/agent/known_checks.sql +8 -4
  23. package/src/agent/known_queries.sql +9 -13
  24. package/src/agent/known_store.sql +280 -125
  25. package/src/agent/materializeContext.js +104 -0
  26. package/src/agent/runs.sql +29 -7
  27. package/src/agent/schemes.sql +14 -3
  28. package/src/agent/tokens.js +6 -0
  29. package/src/agent/turns.sql +9 -9
  30. package/src/hooks/HookRegistry.js +6 -5
  31. package/src/hooks/Hooks.js +44 -3
  32. package/src/hooks/PluginContext.js +29 -21
  33. package/src/{server → hooks}/RpcRegistry.js +2 -1
  34. package/src/hooks/RummyContext.js +139 -35
  35. package/src/hooks/ToolRegistry.js +21 -16
  36. package/src/llm/LlmProvider.js +66 -89
  37. package/src/llm/errors.js +21 -0
  38. package/src/llm/retry.js +63 -0
  39. package/src/plugins/ask_user/README.md +1 -1
  40. package/src/plugins/ask_user/ask_user.js +37 -12
  41. package/src/plugins/ask_user/ask_userDoc.js +2 -25
  42. package/src/plugins/ask_user/ask_userDoc.md +10 -0
  43. package/src/plugins/budget/README.md +27 -25
  44. package/src/plugins/budget/budget.js +306 -88
  45. package/src/plugins/cp/README.md +2 -2
  46. package/src/plugins/cp/cp.js +29 -11
  47. package/src/plugins/cp/cpDoc.js +2 -15
  48. package/src/plugins/cp/cpDoc.md +7 -0
  49. package/src/plugins/engine/README.md +2 -2
  50. package/src/plugins/engine/engine.sql +4 -4
  51. package/src/plugins/engine/turn_context.sql +10 -10
  52. package/src/plugins/env/README.md +20 -5
  53. package/src/plugins/env/env.js +45 -6
  54. package/src/plugins/env/envDoc.js +2 -23
  55. package/src/plugins/env/envDoc.md +13 -0
  56. package/src/plugins/error/README.md +16 -0
  57. package/src/plugins/error/error.js +151 -0
  58. package/src/plugins/file/README.md +6 -6
  59. package/src/plugins/file/file.js +15 -2
  60. package/src/plugins/get/README.md +1 -1
  61. package/src/plugins/get/get.js +103 -48
  62. package/src/plugins/get/getDoc.js +2 -32
  63. package/src/plugins/get/getDoc.md +36 -0
  64. package/src/plugins/hedberg/README.md +1 -2
  65. package/src/plugins/hedberg/hedberg.js +8 -4
  66. package/src/plugins/hedberg/matcher.js +16 -17
  67. package/src/plugins/hedberg/normalize.js +0 -48
  68. package/src/plugins/helpers.js +42 -2
  69. package/src/plugins/index.js +146 -123
  70. package/src/plugins/instructions/README.md +35 -9
  71. package/src/plugins/instructions/instructions.js +244 -9
  72. package/src/plugins/instructions/instructions.md +33 -0
  73. package/src/plugins/instructions/instructions_104.md +7 -0
  74. package/src/plugins/instructions/instructions_105.md +38 -0
  75. package/src/plugins/instructions/instructions_106.md +21 -0
  76. package/src/plugins/instructions/instructions_107.md +10 -0
  77. package/src/plugins/instructions/instructions_108.md +0 -0
  78. package/src/plugins/instructions/protocol.js +12 -0
  79. package/src/plugins/known/README.md +2 -2
  80. package/src/plugins/known/known.js +68 -36
  81. package/src/plugins/known/knownDoc.js +2 -17
  82. package/src/plugins/known/knownDoc.md +8 -0
  83. package/src/plugins/log/README.md +48 -0
  84. package/src/plugins/log/log.js +129 -0
  85. package/src/plugins/mv/README.md +2 -2
  86. package/src/plugins/mv/mv.js +55 -22
  87. package/src/plugins/mv/mvDoc.js +2 -18
  88. package/src/plugins/mv/mvDoc.md +10 -0
  89. package/src/plugins/ollama/README.md +15 -0
  90. package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
  91. package/src/plugins/openai/README.md +17 -0
  92. package/src/plugins/openai/openai.js +120 -0
  93. package/src/plugins/openrouter/README.md +27 -0
  94. package/src/plugins/openrouter/openrouter.js +121 -0
  95. package/src/plugins/persona/README.md +20 -0
  96. package/src/plugins/persona/persona.js +9 -16
  97. package/src/plugins/policy/README.md +21 -0
  98. package/src/plugins/policy/policy.js +29 -14
  99. package/src/plugins/prompt/README.md +1 -1
  100. package/src/plugins/prompt/prompt.js +64 -16
  101. package/src/plugins/rm/README.md +1 -1
  102. package/src/plugins/rm/rm.js +56 -12
  103. package/src/plugins/rm/rmDoc.js +2 -20
  104. package/src/plugins/rm/rmDoc.md +13 -0
  105. package/src/plugins/rpc/README.md +2 -2
  106. package/src/plugins/rpc/rpc.js +525 -296
  107. package/src/plugins/set/README.md +1 -1
  108. package/src/plugins/set/set.js +318 -75
  109. package/src/plugins/set/setDoc.js +2 -35
  110. package/src/plugins/set/setDoc.md +22 -0
  111. package/src/plugins/sh/README.md +28 -5
  112. package/src/plugins/sh/sh.js +50 -6
  113. package/src/plugins/sh/shDoc.js +2 -23
  114. package/src/plugins/sh/shDoc.md +13 -0
  115. package/src/plugins/skill/README.md +23 -0
  116. package/src/plugins/skill/skill.js +14 -18
  117. package/src/plugins/stream/README.md +101 -0
  118. package/src/plugins/stream/stream.js +290 -0
  119. package/src/plugins/telemetry/README.md +1 -1
  120. package/src/plugins/telemetry/telemetry.js +129 -80
  121. package/src/plugins/think/README.md +1 -1
  122. package/src/plugins/think/think.js +12 -0
  123. package/src/plugins/think/thinkDoc.js +2 -15
  124. package/src/plugins/think/thinkDoc.md +7 -0
  125. package/src/plugins/unknown/README.md +3 -3
  126. package/src/plugins/unknown/unknown.js +47 -19
  127. package/src/plugins/unknown/unknownDoc.js +2 -21
  128. package/src/plugins/unknown/unknownDoc.md +11 -0
  129. package/src/plugins/update/README.md +1 -1
  130. package/src/plugins/update/update.js +83 -5
  131. package/src/plugins/update/updateDoc.js +2 -30
  132. package/src/plugins/update/updateDoc.md +8 -0
  133. package/src/plugins/xai/README.md +23 -0
  134. package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
  135. package/src/plugins/yolo/yolo.js +192 -0
  136. package/src/server/ClientConnection.js +64 -37
  137. package/src/server/SocketServer.js +23 -10
  138. package/src/server/protocol.js +11 -0
  139. package/src/sql/v_model_context.sql +27 -31
  140. package/src/sql/v_run_log.sql +9 -14
  141. package/EXCEPTIONS.md +0 -46
  142. package/FIDELITY_CONTRACT.md +0 -172
  143. package/src/agent/KnownStore.js +0 -337
  144. package/src/agent/ResponseHealer.js +0 -241
  145. package/src/llm/OpenAiClient.js +0 -100
  146. package/src/llm/OpenRouterClient.js +0 -100
  147. package/src/plugins/budget/recovery.js +0 -47
  148. package/src/plugins/instructions/preamble.md +0 -45
  149. package/src/plugins/performed/README.md +0 -15
  150. package/src/plugins/performed/performed.js +0 -45
  151. package/src/plugins/previous/README.md +0 -16
  152. package/src/plugins/previous/previous.js +0 -56
  153. package/src/plugins/progress/README.md +0 -16
  154. package/src/plugins/progress/progress.js +0 -43
  155. package/src/plugins/summarize/README.md +0 -19
  156. package/src/plugins/summarize/summarize.js +0 -32
  157. package/src/plugins/summarize/summarizeDoc.js +0 -27
@@ -0,0 +1,48 @@
1
+ # log {#log_plugin}
2
+
3
+ Assembles the `<log>` block in the user message: every
4
+ `category="logging"` entry across the entire run, rendered as XML tool
5
+ tags in v_model_context sort order.
6
+
7
+ ## Registration
8
+
9
+ - **Filter**: `assembly.user` (priority 100) — contributes the `<log>`
10
+ block to the user packet.
11
+
12
+ ## Rendering
13
+
14
+ Each logging entry renders with its scheme as the tag name (`<get>`,
15
+ `<set>`, `<search>`, `<rm>`, `<cp>`, `<mv>`, `<sh>`, `<env>`,
16
+ `<update>`, `<ask_user>`, `<error>`, `<budget>`). Attributes:
17
+ `path`, `turn`, `status`, `state`, `outcome`, `summary`, `visibility`,
18
+ `tokens`.
19
+
20
+ **`tokens=` invariant.** The value is always the full-visibility cost
21
+ of the thing the tag represents — never the log entry's own stub body
22
+ size. Resolution:
23
+
24
+ - If the log entry has `attrs.path` referencing a data entry (`get`,
25
+ `set`, `mv`, `cp`): `tokens=` is that target's tokens. Promotes the
26
+ audit record into a cost-accurate signal the model can plan against.
27
+ - If the action's log body itself IS the cost-bearing content
28
+ (`search`, `update`, `error`, `ask_user`): `tokens=` is the entry's
29
+ own body tokens.
30
+ - `sh` and `env` own multiple streaming channels (`sh://turn_N/{slug}_N`)
31
+ — no single target to point at. `tokens=` is omitted; the channels
32
+ render their own tokens in `<context>`.
33
+
34
+ ## Behavior
35
+
36
+ No loop-boundary split. The `turn` attribute on every entry carries
37
+ when it happened; the model derives loop membership from the data if
38
+ it matters. One chronological log from turn 1 to now.
39
+
40
+ ## Scheme invariant
41
+
42
+ Log entries (`log://turn_N/{action}/{slug}`) are audit records —
43
+ summary, exit status, references to where the data lives — and never
44
+ carry the payload itself. Payload for streaming actions lives under the
45
+ producer's own scheme (`sh://`, `env://`, future `search://`, etc.) at
46
+ `category=data`, and is rendered inside `<context>` by the known
47
+ plugin. Scheme determines category; data and logging never share a
48
+ scheme. See [scheme_category_split](#scheme_category_split).
@@ -0,0 +1,129 @@
1
+ import { stateToStatus } from "../../agent/httpStatus.js";
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).
9
+ const STREAM_NO_TOKENS = new Set(["sh", "env"]);
10
+
11
+ export default class Log {
12
+ #core;
13
+
14
+ constructor(core) {
15
+ this.#core = core;
16
+ core.filter("assembly.user", this.assembleLog.bind(this), 100);
17
+ }
18
+
19
+ 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.
24
+ const latestPrompt = ctx.rows.findLast(
25
+ (r) => r.category === "prompt" && r.scheme === "prompt",
26
+ );
27
+ const entries = ctx.rows.filter((r) => {
28
+ if (r.category === "logging" && r.scheme === "log") return true;
29
+ if (r.category === "prompt" && r.scheme === "prompt") {
30
+ return r !== latestPrompt;
31
+ }
32
+ return false;
33
+ });
34
+ if (entries.length === 0) return content;
35
+ const rowsByPath = new Map();
36
+ for (const r of ctx.rows) rowsByPath.set(r.path, r);
37
+ const lines = entries.map((e) => renderLogTag(e, rowsByPath));
38
+ return `${content}<log>\n${lines.join("\n")}\n</log>\n`;
39
+ }
40
+ }
41
+
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.
46
+ function actionFromPath(path) {
47
+ if (path?.startsWith("prompt://")) return "prompt";
48
+ const match = path?.match(/^log:\/\/turn_\d+\/([^/]+)\//);
49
+ return match ? match[1] : "log";
50
+ }
51
+
52
+ function renderLogTag(entry, rowsByPath) {
53
+ const attrs =
54
+ typeof entry.attributes === "string"
55
+ ? JSON.parse(entry.attributes)
56
+ : entry.attributes;
57
+
58
+ const action = actionFromPath(entry.path);
59
+
60
+ const statusValue =
61
+ attrs?.status != null
62
+ ? attrs.status
63
+ : entry.state
64
+ ? stateToStatus(entry.state, entry.outcome)
65
+ : null;
66
+ // Prompts are uniformly status=200 — uniform value carries no signal
67
+ // and read as "settled, no action needed." Suppress so cultivation
68
+ // vocabulary (vary, demote, archive) applies to prompts the same
69
+ // way it applies to other log entries.
70
+ const status =
71
+ statusValue != null && action !== "prompt"
72
+ ? ` status="${statusValue}"`
73
+ : "";
74
+ const outcomeAttr = entry.outcome ? ` outcome="${entry.outcome}"` : "";
75
+ // `tokens=` is the promotion premium (aTokens) of the thing this tag
76
+ // represents — what the model would free by demoting it. For actions
77
+ // that reference a separate data entry (get/set/mv/cp), resolve via
78
+ // attrs.path and report the target's aTokens. For actions whose log
79
+ // body IS the cost-bearing content (search/update/error/ask_user,
80
+ // plus <get> slice reads), use the log entry's own aTokens. sh/env
81
+ // span multiple channel entries and are omitted — the channels
82
+ // render their own tokens in <context>.
83
+ const isSlice = attrs?.lineStart != null;
84
+ const targetEntry = attrs?.path ? rowsByPath.get(attrs.path) : null;
85
+ let tokenSource = null;
86
+ let lineSource = null;
87
+ if (STREAM_NO_TOKENS.has(action)) {
88
+ tokenSource = null;
89
+ lineSource = null;
90
+ } else if (isSlice) {
91
+ tokenSource = entry.aTokens;
92
+ lineSource = entry.vLines;
93
+ } else if (targetEntry) {
94
+ tokenSource = targetEntry.aTokens;
95
+ lineSource = targetEntry.vLines;
96
+ } else {
97
+ tokenSource = entry.aTokens;
98
+ lineSource = entry.vLines;
99
+ }
100
+ const tokens = tokenSource != null ? ` tokens="${tokenSource}"` : "";
101
+ const summary =
102
+ typeof attrs?.summary === "string"
103
+ ? ` summary="${attrs.summary.slice(0, 80)}"`
104
+ : "";
105
+ const query =
106
+ typeof attrs?.query === "string" ? ` query="${attrs.query}"` : "";
107
+ const command =
108
+ typeof attrs?.command === "string" ? ` command="${attrs.command}"` : "";
109
+ // target= is the path the action touched (e.g. the file/known that was
110
+ // set, the URL that was fetched). Plugins store it in attrs.path when
111
+ // they write the log entry.
112
+ const target = attrs?.path ? ` target="${attrs.path}"` : "";
113
+ // Slice reads tag the log entry with lineStart/lineEnd/totalLines so
114
+ // the <get> tag surfaces `lines="a-b/total"` — a concrete handle for
115
+ // the model to re-issue or compare against another slice. Non-slice
116
+ // entries surface the simple `lines="N"` from the projected body.
117
+ const lines = isSlice
118
+ ? ` lines="${attrs.lineStart}-${attrs.lineEnd}/${attrs.totalLines}"`
119
+ : lineSource != null
120
+ ? ` lines="${lineSource}"`
121
+ : "";
122
+
123
+ const attrStr = `${target}${status}${outcomeAttr}${query}${command}${summary}${lines}${tokens}`;
124
+
125
+ if (entry.body) {
126
+ return `<${action} path="${entry.path}"${attrStr}>${entry.body}</${action}>`;
127
+ }
128
+ return `<${action} path="${entry.path}"${attrStr}/>`;
129
+ }
@@ -1,4 +1,4 @@
1
- # mv
1
+ # mv {#mv_plugin}
2
2
 
3
3
  Moves (renames) an entry from one path to another within the K/V store.
4
4
 
@@ -15,5 +15,5 @@ Shows `mv {from} {to}`.
15
15
  ## Behavior
16
16
 
17
17
  Warns if the destination already exists and will be overwritten. Uses
18
- `KnownStore.scheme()` to determine scheme vs file paths. Source entry
18
+ `Entries.scheme()` to determine scheme vs file paths. Source entry
19
19
  is removed on successful scheme moves.
@@ -1,6 +1,8 @@
1
- import KnownStore from "../../agent/KnownStore.js";
1
+ import Entries from "../../agent/Entries.js";
2
2
  import docs from "./mvDoc.js";
3
3
 
4
+ const LOG_ACTION_RE = /^log:\/\/turn_\d+\/(\w+)\//;
5
+
4
6
  export default class Mv {
5
7
  #core;
6
8
 
@@ -8,43 +10,56 @@ export default class Mv {
8
10
  this.#core = core;
9
11
  core.registerScheme();
10
12
  core.on("handler", this.handler.bind(this));
11
- core.on("promoted", this.full.bind(this));
12
- core.on("demoted", this.summary.bind(this));
13
+ core.on("visible", this.full.bind(this));
14
+ core.on("summarized", this.summary.bind(this));
13
15
  core.filter("instructions.toolDocs", async (docsMap) => {
14
16
  docsMap.mv = docs;
15
17
  return docsMap;
16
18
  });
19
+ core.on("proposal.accepted", this.#onAccepted.bind(this));
20
+ }
21
+
22
+ async #onAccepted(ctx) {
23
+ const m = LOG_ACTION_RE.exec(ctx.path);
24
+ if (m?.[1] !== "mv") return;
25
+ if (!ctx.attrs?.isMove || !ctx.attrs?.from) return;
26
+ await ctx.entries.rm({ runId: ctx.runId, path: ctx.attrs.from });
17
27
  }
18
28
 
19
29
  async handler(entry, rummy) {
20
30
  const { entries: store, sequence: turn, runId, loopId } = rummy;
21
31
  const { path, to } = entry.attributes;
22
- const VALID = { stored: 1, summary: 1, index: 1, full: 1, archive: 1 };
23
- const fidelity = VALID[entry.attributes.fidelity]
24
- ? entry.attributes.fidelity
32
+ const VALID = { visible: 1, summarized: 1, archived: 1 };
33
+ const visibility = VALID[entry.attributes.visibility]
34
+ ? entry.attributes.visibility
25
35
  : undefined;
26
36
 
27
- // Fidelity-in-place: no destination, change visibility of matched entries
28
- if (fidelity && !to) {
37
+ // Visibility-in-place: no destination, change visibility of matched entries
38
+ if (visibility && !to) {
29
39
  const matches = await store.getEntriesByPattern(runId, path);
30
40
  for (const match of matches)
31
- await store.setFidelity(runId, match.path, fidelity);
32
- const label = `set to ${fidelity}`;
33
- await store.upsert(
41
+ await store.set({
42
+ runId: runId,
43
+ path: match.path,
44
+ visibility: visibility,
45
+ });
46
+ const label = `set to ${visibility}`;
47
+ await store.set({
34
48
  runId,
35
49
  turn,
36
- entry.resultPath,
37
- `${matches.map((m) => m.path).join(", ")} ${label}`,
38
- 200,
39
- { fidelity: "archived", loopId },
40
- );
50
+ path: entry.resultPath,
51
+ body: `${matches.map((m) => m.path).join(", ")} ${label}`,
52
+ state: "resolved",
53
+ visibility: "archived",
54
+ loopId,
55
+ });
41
56
  return;
42
57
  }
43
58
 
44
59
  const source = await store.getBody(runId, path);
45
60
  if (source === null) return;
46
61
 
47
- const destScheme = KnownStore.scheme(to);
62
+ const destScheme = Entries.scheme(to);
48
63
  const existing = await store.getBody(runId, to);
49
64
  const warning =
50
65
  existing !== null && destScheme !== null
@@ -53,14 +68,32 @@ export default class Mv {
53
68
 
54
69
  const body = `${path} ${to}`;
55
70
  if (destScheme === null) {
56
- await store.upsert(runId, turn, entry.resultPath, body, 202, {
71
+ await store.set({
72
+ runId,
73
+ turn,
74
+ path: entry.resultPath,
75
+ body,
76
+ state: "proposed",
57
77
  attributes: { from: path, to, isMove: true, warning },
58
78
  loopId,
59
79
  });
60
80
  } else {
61
- await store.upsert(runId, turn, to, source, 200, { fidelity, loopId });
62
- await store.remove(runId, path);
63
- await store.upsert(runId, turn, entry.resultPath, body, 200, {
81
+ await store.set({
82
+ runId,
83
+ turn,
84
+ path: to,
85
+ body: source,
86
+ state: "resolved",
87
+ visibility,
88
+ loopId,
89
+ });
90
+ await store.rm({ runId: runId, path: path });
91
+ await store.set({
92
+ runId,
93
+ turn,
94
+ path: entry.resultPath,
95
+ body,
96
+ state: "resolved",
64
97
  attributes: { from: path, to, isMove: true, warning },
65
98
  loopId,
66
99
  });
@@ -68,7 +101,7 @@ export default class Mv {
68
101
  }
69
102
 
70
103
  full(entry) {
71
- return `# mv ${entry.attributes.from || ""} ${entry.attributes.to || ""}`;
104
+ return `# mv ${entry.attributes.from} ${entry.attributes.to}`;
72
105
  }
73
106
 
74
107
  summary() {
@@ -1,19 +1,3 @@
1
- // Tool doc for <mv>. Each entry: [text, rationale].
2
- // Text goes to the model. Rationale stays in source.
3
- // Changing ANY line requires reading ALL rationales first.
4
- const LINES = [
5
- [
6
- '## <mv path="[source]">[destination]</mv> - Move or rename a file or entry',
7
- ],
8
- [
9
- 'Example: <mv path="known://active_task">known://completed_task</mv>',
10
- "Entry rename. Most common mv use case.",
11
- ],
12
- ['Example: <mv path="src/old_name.js">src/new_name.js</mv>', "File rename."],
13
- [
14
- 'Example: <mv path="known://project/*" fidelity="demoted"/>',
15
- "Batch fidelity change via pattern. No destination = fidelity in place.",
16
- ],
17
- ];
1
+ import { loadDoc } from "../helpers.js";
18
2
 
19
- export default LINES.map(([text]) => text).join("\n");
3
+ export default loadDoc(import.meta.url, "mvDoc.md");
@@ -0,0 +1,10 @@
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
+ 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. -->
@@ -0,0 +1,15 @@
1
+ # ollama
2
+
3
+ Ollama LLM provider. Handles model aliases prefixed with `ollama/`
4
+ (e.g. `ollama/llama3.1:8b`).
5
+
6
+ ## Env
7
+
8
+ - `OLLAMA_BASE_URL` — base URL (e.g. `http://localhost:11434`).
9
+ Plugin is inert if unset.
10
+
11
+ ## Context Size
12
+
13
+ Calls `/api/show` for the requested model and scans `model_info` for
14
+ any `*.context_length` key. Retries up to 3× with exponential backoff
15
+ on non-Ollama transient errors.
@@ -1,19 +1,42 @@
1
- import msg from "../agent/messages.js";
1
+ import msg from "../../agent/messages.js";
2
2
 
3
- export default class OllamaClient {
3
+ const FETCH_TIMEOUT = Number(process.env.RUMMY_FETCH_TIMEOUT);
4
+ if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
5
+
6
+ const PROVIDER = "ollama";
7
+
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
+ */
15
+ export default class Ollama {
4
16
  #baseUrl;
5
17
 
6
- constructor(baseUrl) {
18
+ constructor(core) {
19
+ const baseUrl = process.env.OLLAMA_BASE_URL;
20
+ if (!baseUrl) return;
7
21
  this.#baseUrl = baseUrl;
22
+
23
+ const wireModel = (alias) => alias.split("/").slice(1).join("/");
24
+
25
+ core.hooks.llm.providers.push({
26
+ name: PROVIDER,
27
+ matches: (model) => model.split("/")[0] === PROVIDER,
28
+ completion: (messages, model, options) =>
29
+ this.#completion(messages, wireModel(model), options),
30
+ getContextSize: (model) => this.#getContextSize(wireModel(model)),
31
+ });
8
32
  }
9
33
 
10
- async completion(messages, model, options = {}) {
34
+ async #completion(messages, model, options = {}) {
11
35
  const body = { model, messages, think: true };
12
36
  if (options.temperature !== undefined)
13
37
  body.temperature = options.temperature;
14
38
 
15
- const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
16
- const timeoutSignal = AbortSignal.timeout(timeout);
39
+ const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT);
17
40
  const signal = options.signal
18
41
  ? AbortSignal.any([options.signal, timeoutSignal])
19
42
  : timeoutSignal;
@@ -34,29 +57,27 @@ export default class OllamaClient {
34
57
 
35
58
  const data = await response.json();
36
59
 
37
- for (const choice of data.choices || []) {
38
- const msg = choice.message;
39
- if (!msg) continue;
40
- const parts = [msg.reasoning_content, msg.reasoning, msg.thinking].filter(
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(
41
64
  Boolean,
42
65
  );
43
- msg.reasoning_content =
66
+ m.reasoning_content =
44
67
  parts.length > 0 ? [...new Set(parts)].join("\n") : null;
45
68
  }
46
69
 
47
70
  return data;
48
71
  }
49
72
 
50
- async getContextSize(model) {
73
+ async #getContextSize(model) {
51
74
  for (let attempt = 0; attempt < 3; attempt++) {
52
75
  try {
53
76
  const response = await fetch(`${this.#baseUrl}/api/show`, {
54
77
  method: "POST",
55
78
  headers: { "Content-Type": "application/json" },
56
79
  body: JSON.stringify({ model }),
57
- signal: AbortSignal.timeout(
58
- Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000,
59
- ),
80
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
60
81
  });
61
82
  if (!response.ok) {
62
83
  throw new Error(
@@ -67,9 +88,10 @@ export default class OllamaClient {
67
88
  );
68
89
  }
69
90
  const data = await response.json();
70
- const info = data.model_info || {};
71
- for (const [key, value] of Object.entries(info)) {
72
- if (key.endsWith(".context_length")) return value;
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
+ }
73
95
  }
74
96
  throw new Error(msg("error.ollama_no_context_length", { model }));
75
97
  } catch (err) {
@@ -0,0 +1,17 @@
1
+ # openai
2
+
3
+ OpenAI-compatible LLM provider. Handles any model whose alias doesn't
4
+ carry a provider prefix — the default fallback provider. Works with
5
+ OpenAI itself, llama.cpp, vLLM, and any other service that implements
6
+ the `/v1/chat/completions` and `/v1/models` shape.
7
+
8
+ ## Env
9
+
10
+ - `OPENAI_BASE_URL` — base URL (e.g. `https://api.openai.com` or
11
+ `http://localhost:8080`). Plugin is inert if unset.
12
+ - `OPENAI_API_KEY` — bearer token (optional for local servers).
13
+
14
+ ## Context Size
15
+
16
+ Probes `/props` first (llama.cpp runtime) for `n_ctx`, falls back to
17
+ `/v1/models` for the training context length.
@@ -0,0 +1,120 @@
1
+ import msg from "../../agent/messages.js";
2
+
3
+ const FETCH_TIMEOUT = Number(process.env.RUMMY_FETCH_TIMEOUT);
4
+ if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
5
+
6
+ const PROVIDER = "openai";
7
+
8
+ /**
9
+ * OpenAI-compatible LLM provider plugin. Registers with hooks.llm.providers
10
+ * if OPENAI_BASE_URL is set in env; silently inert otherwise. Handles
11
+ * model aliases of the form `openai/{modelName}` — the first path
12
+ * segment picks the provider, the rest is whatever the API expects.
13
+ */
14
+ export default class OpenAi {
15
+ #baseUrl;
16
+ #apiKey;
17
+
18
+ constructor(core) {
19
+ const baseUrl = process.env.OPENAI_BASE_URL;
20
+ if (!baseUrl) return;
21
+ this.#baseUrl = String(baseUrl).replace(/\/v1\/?$/, "");
22
+ this.#apiKey = process.env.OPENAI_API_KEY;
23
+
24
+ const wireModel = (alias) => alias.split("/").slice(1).join("/");
25
+
26
+ core.hooks.llm.providers.push({
27
+ name: PROVIDER,
28
+ matches: (model) => model.split("/")[0] === PROVIDER,
29
+ completion: (messages, model, options) =>
30
+ this.#completion(messages, wireModel(model), options),
31
+ getContextSize: (model) => this.#getContextSize(wireModel(model)),
32
+ });
33
+ }
34
+
35
+ async #completion(messages, model, options = {}) {
36
+ const body = { model, messages, think: true };
37
+ if (options.temperature !== undefined)
38
+ body.temperature = options.temperature;
39
+
40
+ const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT);
41
+ const signal = options.signal
42
+ ? AbortSignal.any([options.signal, timeoutSignal])
43
+ : timeoutSignal;
44
+
45
+ const headers = { "Content-Type": "application/json" };
46
+ if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
47
+
48
+ const response = await fetch(`${this.#baseUrl}/v1/chat/completions`, {
49
+ method: "POST",
50
+ headers,
51
+ body: JSON.stringify(body),
52
+ signal,
53
+ });
54
+
55
+ if (!response.ok) {
56
+ const error = await response.text();
57
+ throw new Error(
58
+ msg("error.openai_api", { status: `${response.status} - ${error}` }),
59
+ );
60
+ }
61
+
62
+ const data = await response.json();
63
+
64
+ for (const choice of data.choices) {
65
+ const m = choice.message;
66
+ if (!m) continue;
67
+ const parts = [m.reasoning_content, m.reasoning, m.thinking].filter(
68
+ Boolean,
69
+ );
70
+ m.reasoning_content =
71
+ parts.length > 0 ? [...new Set(parts)].join("\n") : null;
72
+
73
+ // Full reasoning dump is centralized in telemetry.js on every
74
+ // provider — keeping it out of provider plugins avoids double
75
+ // printing and per-provider drift.
76
+ }
77
+
78
+ return data;
79
+ }
80
+
81
+ async #getContextSize(_model) {
82
+ const headers = { "Content-Type": "application/json" };
83
+ if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
84
+
85
+ // Try /props first — llama.cpp exposes runtime n_ctx here.
86
+ try {
87
+ const propsResponse = await fetch(`${this.#baseUrl}/props`, {
88
+ headers,
89
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
90
+ });
91
+ if (propsResponse.ok) {
92
+ const props = await propsResponse.json();
93
+ const runtimeCtx = props?.default_generation_settings?.n_ctx;
94
+ if (runtimeCtx) return runtimeCtx;
95
+ }
96
+ } catch (_err) {
97
+ // /props is a llama.cpp extension; absent on vanilla OpenAI.
98
+ // Fall through to /v1/models for the training-context-size hint.
99
+ }
100
+
101
+ // Fall back to /v1/models for training context.
102
+ const response = await fetch(`${this.#baseUrl}/v1/models`, {
103
+ headers,
104
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
105
+ });
106
+ if (!response.ok) {
107
+ throw new Error(
108
+ msg("error.openai_models_failed", {
109
+ status: response.status,
110
+ baseUrl: this.#baseUrl,
111
+ }),
112
+ );
113
+ }
114
+ const data = await response.json();
115
+ const model = data.data?.[0];
116
+ const ctx = model?.meta?.n_ctx_train || model?.context_length;
117
+ if (!ctx) throw new Error(msg("error.openai_no_context_length"));
118
+ return ctx;
119
+ }
120
+ }
@@ -0,0 +1,27 @@
1
+ # openrouter
2
+
3
+ OpenRouter LLM provider. Handles model aliases prefixed with
4
+ `openrouter/` (e.g. `openrouter/anthropic/claude-3-opus`). Strips the
5
+ provider segment and passes the rest (`publisher/model`) straight to
6
+ OpenRouter's API.
7
+
8
+ ## Env
9
+
10
+ - `OPENROUTER_BASE_URL` — base URL (e.g. `https://openrouter.ai/api/v1`).
11
+ Plugin is inert if `OPENROUTER_API_KEY` or base URL is unset.
12
+ - `OPENROUTER_API_KEY` — bearer token.
13
+ - `RUMMY_HTTP_REFERER` / `RUMMY_X_TITLE` — attribution headers
14
+ OpenRouter uses for rankings.
15
+
16
+ ## Reasoning Normalization
17
+
18
+ OpenRouter's response shape varies by underlying provider. The plugin
19
+ merges `reasoning_content` / `reasoning` / `thinking` /
20
+ `reasoning_details[].text` into a deduplicated `reasoning_content`
21
+ string on each choice's message.
22
+
23
+ ## Context Size
24
+
25
+ Calls `/models` and reads `context_length` on the matching entry.
26
+ Cached per model for the plugin lifetime. If the endpoint fails or the
27
+ model is missing, the call throws — no hardcoded fallback.