@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
@@ -1,241 +0,0 @@
1
- const MAX_STALLS = Number(process.env.RUMMY_MAX_STALLS) || 3;
2
- const MIN_CYCLES = Number(process.env.RUMMY_MIN_CYCLES) || 3;
3
- const MAX_CYCLE_PERIOD = Number(process.env.RUMMY_MAX_CYCLE_PERIOD) || 4;
4
- const MAX_UPDATE_REPEATS = Number(process.env.RUMMY_MAX_UPDATE_REPEATS) || 3;
5
- const MAX_PATH_STAGNATION =
6
- Number(process.env.RUMMY_MAX_PATH_STAGNATION) || 5;
7
-
8
- /**
9
- * Build a stable fingerprint for a single recorded entry.
10
- * Uses scheme + original command target + all op-defining attributes.
11
- * Excludes body (content too granular; same operation ≠ same content).
12
- */
13
- function cmdFingerprint(entry) {
14
- const attrs = { ...(entry.attributes ?? {}) };
15
- delete attrs.body;
16
- const target =
17
- attrs.path ?? attrs.command ?? attrs.query ?? attrs.question ?? "";
18
- delete attrs.path;
19
- const extra = Object.keys(attrs)
20
- .toSorted()
21
- .filter((k) => attrs[k] != null)
22
- .map((k) => `${k}=${attrs[k]}`)
23
- .join(",");
24
- return `${entry.scheme}:${target}${extra ? `[${extra}]` : ""}`;
25
- }
26
-
27
- /**
28
- * Detect a repeating cycle in the fingerprint history.
29
- * Checks periods 1..MAX_CYCLE_PERIOD for MIN_CYCLES consecutive repetitions.
30
- * Catches AAAA (period 1), ABABAB (period 2), ABCABCABC (period 3), etc.
31
- */
32
- function detectCycle(history) {
33
- for (let k = 1; k <= MAX_CYCLE_PERIOD; k++) {
34
- const needed = k * MIN_CYCLES;
35
- if (history.length < needed) continue;
36
- const tail = history.slice(-needed);
37
- const cycle = tail.slice(0, k);
38
- let match = true;
39
- outer: for (let rep = 0; rep < MIN_CYCLES; rep++) {
40
- for (let j = 0; j < k; j++) {
41
- if (tail[rep * k + j] !== cycle[j]) {
42
- match = false;
43
- break outer;
44
- }
45
- }
46
- }
47
- if (match) return { detected: true, period: k, cycles: MIN_CYCLES };
48
- }
49
- return { detected: false };
50
- }
51
-
52
- /**
53
- * Extract the target paths a command touches for stagnation detection.
54
- * Same target logic as cmdFingerprint but returns the raw path for set
55
- * comparison across turns.
56
- */
57
- function cmdPaths(entry) {
58
- const attrs = entry.attributes ?? {};
59
- const paths = [];
60
- if (attrs.path) paths.push(attrs.path);
61
- if (attrs.to) paths.push(attrs.to);
62
- if (attrs.command) paths.push(attrs.command);
63
- if (attrs.query) paths.push(attrs.query);
64
- if (attrs.question) paths.push(attrs.question);
65
- return paths;
66
- }
67
-
68
- export default class ResponseHealer {
69
- #stallCount = 0;
70
- #turnHistory = [];
71
- #lastUpdateText = null;
72
- #updateRepeatCount = 0;
73
- #pathRuns = new Map(); // path → consecutive turns touched
74
-
75
- /**
76
- * Heal a missing status tag. Called when the model emits
77
- * neither <summarize/> nor <update/>.
78
- */
79
- /**
80
- * Heal a missing status tag. Called when the model emits
81
- * neither <summarize/> nor <update/>.
82
- *
83
- * Plain text with no commands = the model answered. Treat as summary.
84
- * Commands with no status tag = the model is working. Treat as update.
85
- */
86
- static healStatus(content, commands) {
87
- const trimmed = content.trim();
88
-
89
- // Detect malformed-glitch content — model attempted a tool invocation
90
- // (native call, malformed XML, etc.) that the parser couldn't dispatch.
91
- // This is NOT an answer; it's a glitch that deserves the 3-strikes
92
- // stall path so the model can recover. Without this check, the model
93
- // emits one malformed call and the run terminates after a single turn.
94
- const looksGlitched = /<\|tool_call>|<tool_call\|>/.test(trimmed);
95
-
96
- // No commands + plain text = answered. Treat as summary.
97
- if (commands.length === 0 && trimmed && !looksGlitched) {
98
- console.warn("[RUMMY] Healed: plain text response treated as summary");
99
- return { summaryText: trimmed.slice(0, 500), updateText: null };
100
- }
101
-
102
- // Only write/unknown commands + no investigation tools = completed action.
103
- // The model did the thing without saying <summarize>. Treat as summary.
104
- const hasInvestigation = commands.some((c) =>
105
- ["get", "env", "search", "ask_user"].includes(c.name),
106
- );
107
- if (!hasInvestigation && commands.length > 0) {
108
- const names = commands.map((c) => c.name).join(", ");
109
- console.warn(
110
- `[RUMMY] Healed: action-only response (${names}) treated as summary`,
111
- );
112
- return {
113
- summaryText: trimmed.slice(0, 500) || "Done.",
114
- updateText: null,
115
- };
116
- }
117
-
118
- console.warn(
119
- `[RUMMY] Healed: missing <update>/<summarize>. Tools: ${commands.map((c) => c.name).join(", ") || "none"}`,
120
- );
121
- return { summaryText: null, updateText: "..." };
122
- }
123
-
124
- /**
125
- * Detect cyclic tool patterns across turns.
126
- * Returns { continue: boolean, reason?: string }
127
- *
128
- * Appends this turn's fingerprint to history, then checks whether the
129
- * history ends in a repeating cycle of period 1..MAX_CYCLE_PERIOD with
130
- * at least MIN_CYCLES consecutive repetitions.
131
- *
132
- * Catches AAAA (period 1), ABABAB (period 2), ABCABC (period 3), etc.
133
- * Turns with no tool calls are skipped — they don't contribute to a cycle.
134
- */
135
- assessRepetition({ actionCalls, writeCalls }) {
136
- const commands = [...(actionCalls || []), ...(writeCalls || [])];
137
- if (commands.length === 0) return { continue: true };
138
-
139
- const fp = commands.map(cmdFingerprint).toSorted().join("|");
140
- this.#turnHistory.push(fp);
141
-
142
- const cycle = detectCycle(this.#turnHistory);
143
- if (cycle.detected) {
144
- const reason = `Cyclic tool pattern (period ${cycle.period}, ${cycle.cycles} repetitions)`;
145
- console.warn(`[RUMMY] Loop detected: ${reason}. Force-completing.`);
146
- return { continue: false, reason };
147
- }
148
-
149
- // Distinct-paths stagnation: the model might vary commands turn-to-turn
150
- // (avoiding exact-cycle detection) but still churn on a single path.
151
- // Track per-path consecutive touches; flag if any path is touched in
152
- // MAX_PATH_STAGNATION consecutive turns. Catches semantic stagnation
153
- // where the fingerprints differ in micro-detail but the work is stuck
154
- // on one entry (e.g. endlessly re-setting/re-getting the same plan).
155
- const touchedPaths = new Set();
156
- for (const cmd of commands) {
157
- for (const p of cmdPaths(cmd)) touchedPaths.add(p);
158
- }
159
- // Paths not touched this turn — run broken, remove from map.
160
- for (const path of [...this.#pathRuns.keys()]) {
161
- if (!touchedPaths.has(path)) this.#pathRuns.delete(path);
162
- }
163
- // Paths touched this turn — increment run.
164
- for (const path of touchedPaths) {
165
- this.#pathRuns.set(path, (this.#pathRuns.get(path) || 0) + 1);
166
- }
167
- for (const [path, run] of this.#pathRuns) {
168
- if (run >= MAX_PATH_STAGNATION) {
169
- const reason = `Path stagnation: ${path} touched ${run} consecutive turns`;
170
- console.warn(`[RUMMY] ${reason}. Force-completing.`);
171
- return { continue: false, reason };
172
- }
173
- }
174
-
175
- return { continue: true };
176
- }
177
-
178
- /**
179
- * Assess whether the run should continue.
180
- *
181
- * Returns { continue: boolean, reason?: string }
182
- *
183
- * Rules:
184
- * <summarize/> present → done (terminate)
185
- * <summarize/> + failed actions → overridden to <update> (continue)
186
- * <update/> present → continue (model says it's working)
187
- * neither present → warn, increment stall counter, continue
188
- * stall counter hits MAX_STALLS → force-complete
189
- */
190
- assessProgress({ summaryText, updateText, statusHealed, flags }) {
191
- if (summaryText) {
192
- this.#stallCount = 0;
193
- return { continue: false };
194
- }
195
-
196
- if (updateText && !statusHealed) {
197
- this.#stallCount = 0;
198
- // Track repeated update text — model stuck declaring readiness
199
- // But if the model created new entries this turn, it's making
200
- // progress even if the update text is the same.
201
- const madeProgress = flags?.hasWrites || flags?.hasReads;
202
- if (updateText === this.#lastUpdateText && !madeProgress) {
203
- this.#updateRepeatCount++;
204
- if (this.#updateRepeatCount >= MAX_UPDATE_REPEATS) {
205
- const reason = `Same <update/> repeated ${this.#updateRepeatCount} turns: "${updateText.slice(0, 60)}"`;
206
- console.warn(`[RUMMY] Stalled: ${reason}. Force-completing.`);
207
- return { continue: false, reason };
208
- }
209
- } else {
210
- this.#lastUpdateText = updateText;
211
- this.#updateRepeatCount = 1;
212
- }
213
- return { continue: true };
214
- }
215
-
216
- // Healed or neither — model is glitching
217
- this.#stallCount++;
218
-
219
- if (this.#stallCount >= MAX_STALLS) {
220
- const reason = `${this.#stallCount} turns with no <update/> or <summarize/>`;
221
- console.warn(`[RUMMY] Stalled: ${reason}. Force-completing.`);
222
- return { continue: false, reason };
223
- }
224
-
225
- console.warn(
226
- `[RUMMY] No <update/> or <summarize/> (stall ${this.#stallCount}/${MAX_STALLS})`,
227
- );
228
- return { continue: true };
229
- }
230
-
231
- /**
232
- * Reset state for a new run or after resolution resume.
233
- */
234
- reset() {
235
- this.#stallCount = 0;
236
- this.#turnHistory = [];
237
- this.#lastUpdateText = null;
238
- this.#updateRepeatCount = 0;
239
- this.#pathRuns = new Map();
240
- }
241
- }
@@ -1,100 +0,0 @@
1
- import msg from "../agent/messages.js";
2
-
3
- export default class OpenAiClient {
4
- #baseUrl;
5
- #apiKey;
6
-
7
- constructor(baseUrl, apiKey) {
8
- this.#baseUrl = String(baseUrl || "").replace(/\/v1\/?$/, "");
9
- this.#apiKey = apiKey || "";
10
- }
11
-
12
- async completion(messages, model, options = {}) {
13
- const body = { model, messages, think: true };
14
- if (options.temperature !== undefined)
15
- body.temperature = options.temperature;
16
-
17
- const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
18
- const timeoutSignal = AbortSignal.timeout(timeout);
19
- const signal = options.signal
20
- ? AbortSignal.any([options.signal, timeoutSignal])
21
- : timeoutSignal;
22
-
23
- const headers = { "Content-Type": "application/json" };
24
- if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
25
-
26
- const response = await fetch(`${this.#baseUrl}/v1/chat/completions`, {
27
- method: "POST",
28
- headers,
29
- body: JSON.stringify(body),
30
- signal,
31
- });
32
-
33
- if (!response.ok) {
34
- const error = await response.text();
35
- throw new Error(
36
- msg("error.openai_api", { status: `${response.status} - ${error}` }),
37
- );
38
- }
39
-
40
- const data = await response.json();
41
-
42
- for (const choice of data.choices || []) {
43
- const msg = choice.message;
44
- if (!msg) continue;
45
-
46
- // Normalize reasoning
47
- const parts = [msg.reasoning_content, msg.reasoning, msg.thinking].filter(
48
- Boolean,
49
- );
50
- msg.reasoning_content =
51
- parts.length > 0 ? [...new Set(parts)].join("\n") : null;
52
-
53
- if (process.env.RUMMY_DEBUG === "true" && msg.reasoning_content) {
54
- console.warn(
55
- `[RUMMY] Reasoning (${msg.reasoning_content.length} chars): ${msg.reasoning_content.slice(0, 120)}`,
56
- );
57
- }
58
- }
59
-
60
- return data;
61
- }
62
-
63
- async getContextSize(_model) {
64
- const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
65
- const headers = { "Content-Type": "application/json" };
66
- if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
67
-
68
- // Try /props first — llama.cpp exposes runtime n_ctx here
69
- try {
70
- const propsResponse = await fetch(`${this.#baseUrl}/props`, {
71
- headers,
72
- signal: AbortSignal.timeout(timeout),
73
- });
74
- if (propsResponse.ok) {
75
- const props = await propsResponse.json();
76
- const runtimeCtx = props?.default_generation_settings?.n_ctx;
77
- if (runtimeCtx) return runtimeCtx;
78
- }
79
- } catch {}
80
-
81
- // Fall back to /v1/models for training context
82
- const response = await fetch(`${this.#baseUrl}/v1/models`, {
83
- headers,
84
- signal: AbortSignal.timeout(timeout),
85
- });
86
- if (!response.ok) {
87
- throw new Error(
88
- msg("error.openai_models_failed", {
89
- status: response.status,
90
- baseUrl: this.#baseUrl,
91
- }),
92
- );
93
- }
94
- const data = await response.json();
95
- const model = data.data?.[0];
96
- const ctx = model?.meta?.n_ctx_train || model?.context_length;
97
- if (!ctx) throw new Error(msg("error.openai_no_context_length"));
98
- return ctx;
99
- }
100
- }
@@ -1,100 +0,0 @@
1
- import msg from "../agent/messages.js";
2
-
3
- const DEFAULT_CONTEXT_SIZE = 131072;
4
-
5
- export default class OpenRouterClient {
6
- #apiKey;
7
- #baseUrl;
8
-
9
- constructor(apiKey) {
10
- this.#apiKey = apiKey;
11
- this.#baseUrl = process.env.OPENROUTER_BASE_URL;
12
- }
13
-
14
- async completion(messages, model, options = {}) {
15
- if (!this.#apiKey) throw new Error(msg("error.openrouter_api_key_missing"));
16
- return this.#fetch(messages, model, options);
17
- }
18
-
19
- async #fetch(messages, model, options) {
20
- const body = { model, messages, include_reasoning: true };
21
- if (options.temperature !== undefined)
22
- body.temperature = options.temperature;
23
-
24
- const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
25
- const timeoutSignal = AbortSignal.timeout(timeout);
26
- const signal = options.signal
27
- ? AbortSignal.any([options.signal, timeoutSignal])
28
- : timeoutSignal;
29
-
30
- const response = await fetch(`${this.#baseUrl}/chat/completions`, {
31
- method: "POST",
32
- headers: {
33
- Authorization: `Bearer ${this.#apiKey}`,
34
- "Content-Type": "application/json",
35
- "HTTP-Referer": process.env.RUMMY_HTTP_REFERER,
36
- "X-Title": process.env.RUMMY_X_TITLE,
37
- },
38
- body: JSON.stringify(body),
39
- signal,
40
- });
41
-
42
- if (!response.ok) {
43
- const error = await response.text();
44
- if (response.status === 401 || response.status === 403) {
45
- throw new Error(
46
- msg("error.openrouter_auth", {
47
- status: `${response.status} - ${error}`,
48
- }),
49
- );
50
- }
51
- throw new Error(
52
- msg("error.openrouter_api", {
53
- status: `${response.status} - ${error}`,
54
- }),
55
- );
56
- }
57
- const data = await response.json();
58
-
59
- for (const choice of data.choices || []) {
60
- const cm = choice.message;
61
- if (!cm) continue;
62
- const parts = [
63
- cm.reasoning_content,
64
- cm.reasoning,
65
- cm.thinking,
66
- ...(cm.reasoning_details || []).map((d) => d.text),
67
- ].filter(Boolean);
68
- cm.reasoning_content =
69
- parts.length > 0 ? [...new Set(parts)].join("\n") : null;
70
- }
71
-
72
- return data;
73
- }
74
-
75
- #contextCache = new Map();
76
-
77
- async getContextSize(model) {
78
- if (process.env.RUMMY_CONTEXT_SIZE)
79
- return Number(process.env.RUMMY_CONTEXT_SIZE);
80
-
81
- if (this.#contextCache.has(model)) return this.#contextCache.get(model);
82
-
83
- try {
84
- const res = await fetch(`${this.#baseUrl}/models`, {
85
- headers: { Authorization: `Bearer ${this.#apiKey}` },
86
- signal: AbortSignal.timeout(5000),
87
- });
88
- if (res.ok) {
89
- const data = await res.json();
90
- const entry = data.data?.find((m) => m.id === model);
91
- if (entry?.context_length) {
92
- this.#contextCache.set(model, entry.context_length);
93
- return entry.context_length;
94
- }
95
- }
96
- } catch {}
97
-
98
- return DEFAULT_CONTEXT_SIZE;
99
- }
100
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * Pure recovery state transition — exported for testing.
3
- *
4
- * @param {object|null} recovery Current recovery state.
5
- * @param {{ assembledTokens: number, budgetRecovery?: { target: number, promptPath: string|null } }} result
6
- * @returns {{ next: object|null, action: null|'restore'|'hard413', promptPath: string|null }}
7
- */
8
- export function advanceRecovery(recovery, result) {
9
- // Initialise or update recovery state from a new Turn Demotion event.
10
- if (result.budgetRecovery) {
11
- if (!recovery) {
12
- recovery = {
13
- target: result.budgetRecovery.target,
14
- promptPath: result.budgetRecovery.promptPath,
15
- strikes: 0,
16
- lastTokens: result.assembledTokens,
17
- };
18
- } else {
19
- // Re-overflow during recovery: tighten target, don't count as strike.
20
- recovery = {
21
- ...recovery,
22
- target: Math.min(recovery.target, result.budgetRecovery.target),
23
- };
24
- }
25
- }
26
-
27
- if (recovery === null) return { next: null, action: null, promptPath: null };
28
-
29
- const current = result.assembledTokens;
30
-
31
- if (current <= recovery.target) {
32
- return { next: null, action: "restore", promptPath: recovery.promptPath };
33
- }
34
-
35
- const noProgress = current >= recovery.lastTokens && !result.budgetRecovery;
36
- const strikes = noProgress ? recovery.strikes + 1 : 0;
37
-
38
- if (strikes >= 3) {
39
- return { next: null, action: "hard413", promptPath: null };
40
- }
41
-
42
- return {
43
- next: { ...recovery, strikes, lastTokens: current },
44
- action: null,
45
- promptPath: null,
46
- };
47
- }
@@ -1,45 +0,0 @@
1
- You are a folksonomic knowledgebase assistant. Define what's unknown, gather knowns to resolve the unknown, act, then answer.
2
-
3
- Required: YOU MUST only respond with Tool Commands in the XML format (max 12/turn): [%TOOLS%]
4
-
5
- Required: YOU MUST register your unresolved questions as unknown:// entries, then resolve them.
6
- Example: <set path="unknown://{topic_or_question}" summary="keyword,keyword,keyword">specific question I need to research</set>
7
-
8
- Required: YOU MUST gather relevant facts, decisions, and information to store in known:// entries.
9
- Required: YOU MUST include navigable paths and specific, searchable summary tags to enable pattern search and promotion.
10
- Example: <set path="known://topic/subtopic1" summary="keyword,keyword,keyword">{known facts, decisions, or plans}</set>
11
-
12
- Required: YOU MUST add the paths of related entries to your entry, and edit existing related entries to add linkbacks.
13
- Example: <set path="known://topic/subtopic2" summary="keyword,keyword,keyword">{facts} Related: known://topic/subtopic1</set>
14
-
15
- Required: YOU MUST promote relevant entries to verify their contents. Paths and summaries are approximate and unreliable.
16
- Example: <get path="facts.txt"/>
17
- Required: YOU MUST demote entries after organizing and categorizing relevant information into known entries.
18
- Example: <set path="prompt://42" fidelity="demoted"/>
19
-
20
- Required: YOU MUST calculate and estimate the token totals (tokens="N") of entries before promoting and not exceed 50% of Token Budget.
21
- Warning: Promotions and new entries cost tokens. Demotions recover tokens. Exceeding your budget will result in a 413 Token Budget Error.
22
- Tip: Entries with higher turn numbers are more recent and relevant.
23
-
24
- Required: YOU MUST create and maintain a checklist to guide and track your progress. Only check items when they're completed.
25
- Required: YOU MUST adapt and expand this checklist for the specific context, entries, and prompt requirements.
26
- Example:
27
- <set path="known://rummy_plan" summary="plan,strategy,steps,roadmap">
28
- - [ ] identify and record unknown facts, unresolved decisions, and unclear plans
29
- - [ ] identify, organize, and categorize known facts, decisions, and plans before acting on prompt
30
- - [ ] identify relevant entries to verify, analyze, review, and record contents (don't assume from path or summary!)
31
- - [ ] after promoting an entry, organize and categorize findings into known entries
32
- - [ ] after the entry's information has been stored in known entries, demote it to optimize context relevance and token budget
33
- - [ ] iteratively analyze and explore until the unknowns that can be resolved are resolved
34
- - [ ] { specific action required by prompt }
35
- - [ ] ...
36
- - [ ] summarize when complete with summarize tag
37
- </set>
38
- Example: <set path="known://rummy_plan">s/- [ ] specific action required by prompt/- [x] specific action required by prompt/g</set>
39
-
40
- # Tool Usage
41
-
42
- Warning: YOU MUST NOT use shell commands for file operations. Files are entries that require Tool Command operations.
43
- Example: <set path="newFile.txt" summary="keyword,keyword,keyword">{new file contents}</set>
44
-
45
- [%TOOLDOCS%]
@@ -1,15 +0,0 @@
1
- # performed
2
-
3
- Renders the `<performed>` section of the user message — the active loop's
4
- tool results and lifecycle signals.
5
-
6
- ## Registration
7
-
8
- - **Filter**: `assembly.user` at priority 100
9
-
10
- ## Behavior
11
-
12
- Filters turn_context rows where `category === "logging"` and
13
- `source_turn >= loopStartTurn`. Renders each entry chronologically
14
- with turn, status, summary, fidelity, and tokens. Empty on the first
15
- turn of a loop.
@@ -1,45 +0,0 @@
1
- export default class Performed {
2
- #core;
3
-
4
- constructor(core) {
5
- this.#core = core;
6
- core.filter("assembly.user", this.assemblePerformed.bind(this), 100);
7
- }
8
-
9
- async assemblePerformed(content, ctx) {
10
- const entries = ctx.rows.filter(
11
- (r) =>
12
- r.category === "logging" &&
13
- r.source_turn >= ctx.loopStartTurn &&
14
- r.scheme !== "unknown",
15
- );
16
- if (entries.length === 0) return content;
17
-
18
- const lines = entries.map((e) => renderToolTag(e));
19
- return `${content}<performed>\n${lines.join("\n")}\n</performed>\n`;
20
- }
21
- }
22
-
23
- function renderToolTag(entry) {
24
- const attrs =
25
- typeof entry.attributes === "string"
26
- ? JSON.parse(entry.attributes)
27
- : entry.attributes;
28
-
29
- const target = attrs?.path || attrs?.file || attrs?.command || "";
30
- const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
31
- const status = entry.status ? ` status="${entry.status}"` : "";
32
- const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
33
- const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
34
- const summary =
35
- typeof attrs?.summary === "string"
36
- ? ` summary="${attrs.summary.slice(0, 80)}"`
37
- : "";
38
-
39
- const body = entry.body || null;
40
-
41
- if (body) {
42
- return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}>${body}</${entry.scheme}>`;
43
- }
44
- return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}/>`;
45
- }
@@ -1,16 +0,0 @@
1
- # previous
2
-
3
- Renders the `<previous>` section of the system message — completed loop
4
- history from prior ask/act invocations on this run.
5
-
6
- ## Registration
7
-
8
- - **Filter**: `assembly.system` at priority 200
9
- - **Condition**: Omitted when `loopStartTurn <= 1` (first loop has no history)
10
-
11
- ## Behavior
12
-
13
- Filters turn_context rows where `category` is `logging` or `prompt`
14
- and `source_turn < loopStartTurn`. Renders each entry chronologically
15
- with turn, status, summary, fidelity, and tokens. The model can target
16
- these entries by path with `<set>` or `<rm>` to free context space.
@@ -1,56 +0,0 @@
1
- export default class Previous {
2
- #core;
3
-
4
- constructor(core) {
5
- this.#core = core;
6
- core.filter("assembly.system", this.assemblePrevious.bind(this), 200);
7
- }
8
-
9
- async assemblePrevious(content, ctx) {
10
- if (ctx.loopStartTurn <= 1) return content;
11
-
12
- const entries = ctx.rows
13
- .filter(
14
- (r) =>
15
- (r.category === "logging" || r.category === "prompt") &&
16
- r.source_turn < ctx.loopStartTurn,
17
- )
18
- .toSorted((a, b) => {
19
- if (a.source_turn !== b.source_turn)
20
- return a.source_turn - b.source_turn;
21
- // Within the same turn: prompt first (cause before effect)
22
- if (a.category === "prompt" && b.category !== "prompt") return -1;
23
- if (b.category === "prompt" && a.category !== "prompt") return 1;
24
- return 0;
25
- });
26
- if (entries.length === 0) return content;
27
-
28
- const lines = await Promise.all(
29
- entries.map((e) => renderToolTag(e, this.#core)),
30
- );
31
- return `${content}\n\n<previous>\n${lines.join("\n")}\n</previous>`;
32
- }
33
- }
34
-
35
- async function renderToolTag(entry, _core) {
36
- const attrs =
37
- typeof entry.attributes === "string"
38
- ? JSON.parse(entry.attributes)
39
- : entry.attributes;
40
-
41
- const target = attrs?.path || attrs?.file || attrs?.command || "";
42
- const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
43
- const status = entry.status ? ` status="${entry.status}"` : "";
44
- const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
45
- const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
46
- const summary =
47
- typeof attrs?.summary === "string"
48
- ? ` summary="${attrs.summary.replace(/"/g, "'")}"`
49
- : "";
50
-
51
- // Trust the projected body. Plugin decided per-fidelity what to show.
52
- if (entry.body) {
53
- return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}>${entry.body}</${entry.scheme}>`;
54
- }
55
- return `<${entry.scheme} path="${target}"${turn}${status}${summary}${fidelity}${tokens}/>`;
56
- }
@@ -1,16 +0,0 @@
1
- # progress
2
-
3
- Renders the `<progress>` section of the user message — bridges the
4
- current work log to the active prompt.
5
-
6
- ## Registration
7
-
8
- - **Filter**: `assembly.user` at priority 200
9
-
10
- ## Behavior
11
-
12
- Emits `<progress turn="N">` carrying token budget and fidelity stats.
13
- On continuation turns with current entries: "The above actions were
14
- performed in response to the following prompt:"
15
-
16
- Progress text is the tuning knob for model orientation between turns.