@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.
- package/.env.example +42 -5
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +934 -373
- package/bin/demo.js +166 -0
- package/bin/rummy.js +9 -3
- package/biome/no-fallbacks.grit +50 -0
- package/lang/en.json +2 -2
- package/migrations/001_initial_schema.sql +88 -37
- package/package.json +13 -11
- package/scriptify/ask_run.js +77 -0
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +476 -335
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +676 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -421
- package/src/agent/XmlParser.js +99 -33
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +280 -125
- package/src/agent/materializeContext.js +104 -0
- package/src/agent/runs.sql +29 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/tokens.js +6 -0
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +29 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +139 -35
- package/src/hooks/ToolRegistry.js +21 -16
- package/src/llm/LlmProvider.js +66 -89
- package/src/llm/errors.js +21 -0
- package/src/llm/retry.js +63 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -25
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -25
- package/src/plugins/budget/budget.js +306 -88
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +29 -11
- package/src/plugins/cp/cpDoc.js +2 -15
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +45 -6
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -2
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +103 -48
- package/src/plugins/get/getDoc.js +2 -32
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +42 -2
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +244 -9
- package/src/plugins/instructions/instructions.md +33 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +38 -0
- package/src/plugins/instructions/instructions_106.md +21 -0
- package/src/plugins/instructions/instructions_107.md +10 -0
- package/src/plugins/instructions/instructions_108.md +0 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +68 -36
- package/src/plugins/known/knownDoc.js +2 -17
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +129 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +55 -22
- package/src/plugins/mv/mvDoc.js +2 -18
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +64 -16
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +56 -12
- package/src/plugins/rm/rmDoc.js +2 -20
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +525 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -75
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +50 -6
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -18
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +129 -80
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +2 -15
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +47 -19
- package/src/plugins/unknown/unknownDoc.js +2 -21
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +83 -5
- package/src/plugins/update/updateDoc.js +2 -30
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/plugins/yolo/yolo.js +192 -0
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/FIDELITY_CONTRACT.md +0 -172
- package/src/agent/KnownStore.js +0 -337
- package/src/agent/ResponseHealer.js +0 -241
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -45
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -56
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -43
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- 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
|
-
}
|
package/src/llm/OpenAiClient.js
DELETED
|
@@ -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.
|