@possumtech/rummy 0.2.8 → 0.3.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 +13 -2
- package/EXCEPTIONS.md +46 -0
- package/PLUGINS.md +422 -188
- package/SPEC.md +440 -106
- package/migrations/001_initial_schema.sql +5 -3
- package/package.json +17 -5
- package/service.js +5 -3
- package/src/agent/AgentLoop.js +252 -55
- package/src/agent/ContextAssembler.js +20 -4
- package/src/agent/KnownStore.js +82 -25
- package/src/agent/ProjectAgent.js +4 -1
- package/src/agent/ResponseHealer.js +86 -32
- package/src/agent/TurnExecutor.js +542 -207
- package/src/agent/XmlParser.js +77 -41
- package/src/agent/known_store.sql +68 -4
- package/src/agent/schemes.sql +3 -0
- package/src/agent/tokens.js +7 -21
- package/src/agent/turns.sql +15 -1
- package/src/hooks/HookRegistry.js +7 -0
- package/src/hooks/Hooks.js +15 -0
- package/src/hooks/PluginContext.js +14 -1
- package/src/hooks/RummyContext.js +16 -4
- package/src/hooks/ToolRegistry.js +77 -19
- package/src/llm/LlmProvider.js +27 -8
- package/src/llm/OpenAiClient.js +20 -0
- package/src/llm/OpenRouterClient.js +24 -2
- package/src/llm/XaiClient.js +47 -2
- package/src/plugins/ask_user/README.md +4 -4
- package/src/plugins/ask_user/ask_user.js +5 -5
- package/src/plugins/ask_user/ask_userDoc.js +29 -0
- package/src/plugins/budget/README.md +31 -0
- package/src/plugins/budget/budget.js +55 -0
- package/src/plugins/cp/README.md +5 -4
- package/src/plugins/cp/cp.js +10 -6
- package/src/plugins/cp/cpDoc.js +29 -0
- package/src/plugins/engine/engine.sql +1 -8
- package/src/plugins/engine/turn_context.sql +4 -9
- package/src/plugins/env/README.md +3 -4
- package/src/plugins/env/env.js +5 -5
- package/src/plugins/env/envDoc.js +29 -0
- package/src/plugins/file/README.md +9 -12
- package/src/plugins/file/file.js +34 -35
- package/src/plugins/get/README.md +2 -2
- package/src/plugins/get/get.js +77 -6
- package/src/plugins/get/getDoc.js +51 -0
- package/src/plugins/hedberg/hedberg.js +2 -1
- package/src/plugins/hedberg/matcher.js +10 -29
- package/src/plugins/hedberg/normalize.js +28 -0
- package/src/plugins/hedberg/patterns.js +25 -27
- package/src/plugins/hedberg/sed.js +17 -10
- package/src/plugins/index.js +66 -14
- package/src/plugins/instructions/README.md +6 -2
- package/src/plugins/instructions/instructions.js +20 -4
- package/src/plugins/instructions/preamble.md +19 -5
- package/src/plugins/known/README.md +10 -7
- package/src/plugins/known/known.js +23 -17
- package/src/plugins/known/knownDoc.js +34 -0
- package/src/plugins/mv/README.md +5 -4
- package/src/plugins/mv/mv.js +27 -6
- package/src/plugins/mv/mvDoc.js +45 -0
- package/src/plugins/performed/README.md +15 -0
- package/src/plugins/performed/performed.js +45 -0
- package/src/plugins/persona/persona.js +78 -0
- package/src/plugins/previous/README.md +3 -2
- package/src/plugins/previous/previous.js +33 -24
- package/src/plugins/progress/README.md +1 -2
- package/src/plugins/progress/progress.js +33 -21
- package/src/plugins/prompt/README.md +5 -5
- package/src/plugins/prompt/prompt.js +15 -17
- package/src/plugins/rm/README.md +4 -4
- package/src/plugins/rm/rm.js +32 -20
- package/src/plugins/rm/rmDoc.js +30 -0
- package/src/plugins/rpc/README.md +15 -28
- package/src/plugins/rpc/rpc.js +42 -77
- package/src/plugins/set/README.md +13 -12
- package/src/plugins/set/set.js +107 -16
- package/src/plugins/set/setDoc.js +49 -0
- package/src/plugins/sh/README.md +4 -4
- package/src/plugins/sh/sh.js +5 -5
- package/src/plugins/sh/shDoc.js +29 -0
- package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
- package/src/plugins/summarize/README.md +6 -5
- package/src/plugins/summarize/summarize.js +7 -6
- package/src/plugins/summarize/summarizeDoc.js +33 -0
- package/src/plugins/telemetry/telemetry.js +16 -9
- package/src/plugins/think/README.md +20 -0
- package/src/plugins/think/think.js +5 -0
- package/src/plugins/unknown/README.md +6 -5
- package/src/plugins/unknown/unknown.js +12 -9
- package/src/plugins/unknown/unknownDoc.js +31 -0
- package/src/plugins/update/README.md +3 -8
- package/src/plugins/update/update.js +7 -6
- package/src/plugins/update/updateDoc.js +33 -0
- package/src/server/ClientConnection.js +59 -45
- package/src/server/RpcRegistry.js +52 -4
- package/src/sql/v_model_context.sql +10 -25
- package/src/plugins/ask_user/docs.md +0 -2
- package/src/plugins/cp/docs.md +0 -2
- package/src/plugins/current/README.md +0 -14
- package/src/plugins/current/current.js +0 -47
- package/src/plugins/env/docs.md +0 -4
- package/src/plugins/get/docs.md +0 -10
- package/src/plugins/known/docs.md +0 -3
- package/src/plugins/mv/docs.md +0 -2
- package/src/plugins/rm/docs.md +0 -6
- package/src/plugins/set/docs.md +0 -6
- package/src/plugins/sh/docs.md +0 -2
- package/src/plugins/skills/README.md +0 -25
- package/src/plugins/store/README.md +0 -20
- package/src/plugins/store/docs.md +0 -6
- package/src/plugins/store/store.js +0 -63
- package/src/plugins/summarize/docs.md +0 -4
- package/src/plugins/unknown/docs.md +0 -5
- package/src/plugins/update/docs.md +0 -4
package/src/agent/XmlParser.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { Parser } from "htmlparser2";
|
|
2
2
|
import { parseEditContent } from "../plugins/hedberg/edits.js";
|
|
3
|
-
import { normalizeAttrs } from "../plugins/hedberg/normalize.js";
|
|
3
|
+
import { normalizeAttrs, parseJsonEdit } from "../plugins/hedberg/normalize.js";
|
|
4
4
|
import { parseSed } from "../plugins/hedberg/sed.js";
|
|
5
5
|
|
|
6
|
-
const STORE_TOOLS = new Set([
|
|
7
|
-
|
|
8
|
-
"store",
|
|
9
|
-
"rm",
|
|
10
|
-
"set",
|
|
11
|
-
"mv",
|
|
12
|
-
"cp",
|
|
13
|
-
"search",
|
|
14
|
-
]);
|
|
15
|
-
const ALL_TOOLS = new Set([
|
|
6
|
+
const STORE_TOOLS = new Set(["get", "rm", "set", "mv", "cp", "search"]);
|
|
7
|
+
export const ALL_TOOLS = new Set([
|
|
16
8
|
...STORE_TOOLS,
|
|
17
9
|
"known",
|
|
18
10
|
"sh",
|
|
@@ -21,6 +13,9 @@ const ALL_TOOLS = new Set([
|
|
|
21
13
|
"summarize",
|
|
22
14
|
"update",
|
|
23
15
|
"unknown",
|
|
16
|
+
"think",
|
|
17
|
+
"thought",
|
|
18
|
+
"mcp",
|
|
24
19
|
]);
|
|
25
20
|
|
|
26
21
|
/**
|
|
@@ -51,31 +46,10 @@ function resolveCommand(name, attrs, rawBody) {
|
|
|
51
46
|
};
|
|
52
47
|
}
|
|
53
48
|
}
|
|
54
|
-
// JSON-style { search, replace }
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const json = JSON.parse(trimmed);
|
|
60
|
-
search = json.search;
|
|
61
|
-
replace = json.replace ?? "";
|
|
62
|
-
} catch {
|
|
63
|
-
// Try = style: { search="old", replace="new" }
|
|
64
|
-
const searchMatch = trimmed.match(/search\s*=\s*"([^"]*)"/);
|
|
65
|
-
const replaceMatch = trimmed.match(/replace\s*=\s*"([^"]*)"/);
|
|
66
|
-
if (searchMatch) {
|
|
67
|
-
search = searchMatch[1];
|
|
68
|
-
replace = replaceMatch?.[1] ?? "";
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (search != null) {
|
|
72
|
-
return {
|
|
73
|
-
name,
|
|
74
|
-
path: a.path,
|
|
75
|
-
search,
|
|
76
|
-
replace,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
49
|
+
// JSON-style { search, replace }
|
|
50
|
+
const jsonEdit = parseJsonEdit(trimmed);
|
|
51
|
+
if (jsonEdit) {
|
|
52
|
+
return { name, path: a.path, ...jsonEdit };
|
|
79
53
|
}
|
|
80
54
|
// Sed syntax: s/search/replace/flags — supports chained commands
|
|
81
55
|
if (trimmed.startsWith("s/")) {
|
|
@@ -116,9 +90,9 @@ function resolveCommand(name, attrs, rawBody) {
|
|
|
116
90
|
preview: a.preview,
|
|
117
91
|
};
|
|
118
92
|
}
|
|
119
|
-
// Plain write
|
|
93
|
+
// Plain write or fidelity change
|
|
120
94
|
const body = trimmed || a.body || "";
|
|
121
|
-
return { name,
|
|
95
|
+
return { name, ...a, body };
|
|
122
96
|
}
|
|
123
97
|
|
|
124
98
|
if (name === "summarize" || name === "update" || name === "unknown") {
|
|
@@ -129,10 +103,10 @@ function resolveCommand(name, attrs, rawBody) {
|
|
|
129
103
|
if (name === "known") {
|
|
130
104
|
const body = trimmed || a.body || "";
|
|
131
105
|
const path = a.path || null;
|
|
132
|
-
return { name, path, body };
|
|
106
|
+
return { name, ...a, path, body };
|
|
133
107
|
}
|
|
134
108
|
|
|
135
|
-
if (name === "get" || name === "
|
|
109
|
+
if (name === "get" || name === "rm") {
|
|
136
110
|
const path = a.path || trimmed || null;
|
|
137
111
|
return { name, path, body: a.body, preview: a.preview };
|
|
138
112
|
}
|
|
@@ -174,6 +148,9 @@ export default class XmlParser {
|
|
|
174
148
|
static parse(content) {
|
|
175
149
|
if (!content) return { commands: [], warnings: [], unparsed: "" };
|
|
176
150
|
|
|
151
|
+
// Normalize native tool call formats to rummy XML
|
|
152
|
+
const normalized = XmlParser.#normalizeToolCalls(content);
|
|
153
|
+
|
|
177
154
|
const commands = [];
|
|
178
155
|
const warnings = [];
|
|
179
156
|
const textChunks = [];
|
|
@@ -248,7 +225,7 @@ export default class XmlParser {
|
|
|
248
225
|
},
|
|
249
226
|
);
|
|
250
227
|
|
|
251
|
-
parser.write(
|
|
228
|
+
parser.write(normalized);
|
|
252
229
|
ended = true;
|
|
253
230
|
parser.end();
|
|
254
231
|
|
|
@@ -264,4 +241,63 @@ export default class XmlParser {
|
|
|
264
241
|
const unparsed = textChunks.join("").trim();
|
|
265
242
|
return { commands, warnings, unparsed };
|
|
266
243
|
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Normalize native tool call formats to rummy XML.
|
|
247
|
+
* Models sometimes emit their training-format tool calls instead of
|
|
248
|
+
* our XML tags. The intent is unambiguous — translate silently.
|
|
249
|
+
*/
|
|
250
|
+
static #normalizeToolCalls(content) {
|
|
251
|
+
// Gemma: ```tool_code\n<xml>...\n``` — strip code fences around valid XML
|
|
252
|
+
let result = content.replace(
|
|
253
|
+
/```(?:tool_code|tool_command|xml)\n([\s\S]*?)```/g,
|
|
254
|
+
(_, inner) => inner.trim(),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Qwen/gemma: <|tool_call>call:NAME{key:"value"}<tool_call|>
|
|
258
|
+
result = result.replace(
|
|
259
|
+
/<\|tool_call>call:(\w+)\{([^}]*)\}<(?:tool_call\||\|tool_call)>/g,
|
|
260
|
+
(_, name, params) => {
|
|
261
|
+
if (!ALL_TOOLS.has(name)) return _;
|
|
262
|
+
const valueMatch = params.match(/["']([^"']+)["']/);
|
|
263
|
+
const body = valueMatch?.[1] || "";
|
|
264
|
+
return `<${name}>${body}</${name}>`;
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// OpenAI function_call JSON: {"name":"search","arguments":{"query":"..."}}
|
|
269
|
+
result = result.replace(
|
|
270
|
+
/\{"name"\s*:\s*"(\w+)"\s*,\s*"arguments"\s*:\s*\{([^}]*)\}\}/g,
|
|
271
|
+
(_, name, args) => {
|
|
272
|
+
if (!ALL_TOOLS.has(name)) return _;
|
|
273
|
+
const pairs = [...args.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g)];
|
|
274
|
+
const body = pairs[0]?.[2] || "";
|
|
275
|
+
return `<${name}>${body}</${name}>`;
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Anthropic: <tool_use><name>search</name><input>{"query":"..."}</input></tool_use>
|
|
280
|
+
result = result.replace(
|
|
281
|
+
/<tool_use>\s*<name>(\w+)<\/name>\s*<input>\{([^}]*)\}<\/input>\s*<\/tool_use>/g,
|
|
282
|
+
(_, name, args) => {
|
|
283
|
+
if (!ALL_TOOLS.has(name)) return _;
|
|
284
|
+
const pairs = [...args.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g)];
|
|
285
|
+
const body = pairs[0]?.[2] || "";
|
|
286
|
+
return `<${name}>${body}</${name}>`;
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Mistral: [TOOL_CALLS] [{"name":"search","arguments":{"query":"..."}}]
|
|
291
|
+
result = result.replace(
|
|
292
|
+
/\[TOOL_CALLS\]\s*\[\{"name"\s*:\s*"(\w+)"\s*,\s*"arguments"\s*:\s*\{([^}]*)\}\}\]/g,
|
|
293
|
+
(_, name, args) => {
|
|
294
|
+
if (!ALL_TOOLS.has(name)) return _;
|
|
295
|
+
const pairs = [...args.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g)];
|
|
296
|
+
const body = pairs[0]?.[2] || "";
|
|
297
|
+
return `<${name}>${body}</${name}>`;
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
267
303
|
}
|
|
@@ -56,7 +56,15 @@ UPDATE known_entries
|
|
|
56
56
|
SET
|
|
57
57
|
fidelity = :fidelity
|
|
58
58
|
, tokens = CASE
|
|
59
|
-
WHEN :fidelity = '
|
|
59
|
+
WHEN :fidelity = 'archive'
|
|
60
|
+
THEN 0
|
|
61
|
+
WHEN :fidelity = 'index'
|
|
62
|
+
THEN 0
|
|
63
|
+
WHEN :fidelity = 'summary'
|
|
64
|
+
THEN COALESCE(
|
|
65
|
+
countTokens(json_extract(attributes, '$.summary')),
|
|
66
|
+
countTokens(substr(body, 1, 80))
|
|
67
|
+
)
|
|
60
68
|
ELSE tokens_full
|
|
61
69
|
END
|
|
62
70
|
, updated_at = CURRENT_TIMESTAMP
|
|
@@ -74,7 +82,7 @@ WHERE run_id = :run_id AND path = :path;
|
|
|
74
82
|
-- PREP: demote_path
|
|
75
83
|
UPDATE known_entries
|
|
76
84
|
SET
|
|
77
|
-
fidelity = '
|
|
85
|
+
fidelity = 'archive'
|
|
78
86
|
, tokens = 0
|
|
79
87
|
, updated_at = CURRENT_TIMESTAMP
|
|
80
88
|
WHERE run_id = :run_id AND path = :path;
|
|
@@ -84,7 +92,15 @@ UPDATE known_entries
|
|
|
84
92
|
SET
|
|
85
93
|
fidelity = :fidelity
|
|
86
94
|
, tokens = CASE
|
|
87
|
-
WHEN :fidelity = '
|
|
95
|
+
WHEN :fidelity = 'archive'
|
|
96
|
+
THEN 0
|
|
97
|
+
WHEN :fidelity = 'index'
|
|
98
|
+
THEN 0
|
|
99
|
+
WHEN :fidelity = 'summary'
|
|
100
|
+
THEN COALESCE(
|
|
101
|
+
countTokens(json_extract(attributes, '$.summary')),
|
|
102
|
+
countTokens(substr(body, 1, 80))
|
|
103
|
+
)
|
|
88
104
|
ELSE countTokens(body)
|
|
89
105
|
END
|
|
90
106
|
, updated_at = CURRENT_TIMESTAMP
|
|
@@ -133,7 +149,7 @@ WHERE
|
|
|
133
149
|
-- PREP: demote_by_pattern
|
|
134
150
|
UPDATE known_entries
|
|
135
151
|
SET
|
|
136
|
-
fidelity = '
|
|
152
|
+
fidelity = 'archive'
|
|
137
153
|
, tokens = 0
|
|
138
154
|
, updated_at = CURRENT_TIMESTAMP
|
|
139
155
|
WHERE
|
|
@@ -173,3 +189,51 @@ WHERE
|
|
|
173
189
|
run_id = :run_id
|
|
174
190
|
AND hedmatch(:path, path)
|
|
175
191
|
AND (:body IS NULL OR hedsearch(:body, body));
|
|
192
|
+
|
|
193
|
+
-- PREP: restore_summarized_prompts
|
|
194
|
+
-- Restore prompt entries demoted to summary by a recovery phase that was
|
|
195
|
+
-- interrupted (e.g. server crash). Safe to call unconditionally at loop
|
|
196
|
+
-- start: if the full prompt would overflow, Prompt Demotion handles it.
|
|
197
|
+
UPDATE known_entries
|
|
198
|
+
SET
|
|
199
|
+
fidelity = 'full'
|
|
200
|
+
, tokens = tokens_full
|
|
201
|
+
, updated_at = CURRENT_TIMESTAMP
|
|
202
|
+
WHERE run_id = :run_id AND scheme = 'prompt' AND fidelity = 'summary';
|
|
203
|
+
|
|
204
|
+
-- PREP: demote_previous_loop_logging
|
|
205
|
+
-- Demote full logging entries from all other loops to summary.
|
|
206
|
+
-- Fires at loop start so <previous> entries are already compact.
|
|
207
|
+
UPDATE known_entries
|
|
208
|
+
SET
|
|
209
|
+
fidelity = 'summary'
|
|
210
|
+
, tokens = COALESCE(
|
|
211
|
+
countTokens(json_extract(attributes, '$.summary'))
|
|
212
|
+
, countTokens(substr(body, 1, 80))
|
|
213
|
+
)
|
|
214
|
+
, updated_at = CURRENT_TIMESTAMP
|
|
215
|
+
WHERE
|
|
216
|
+
run_id = :run_id
|
|
217
|
+
AND (loop_id IS NULL OR loop_id != :loop_id)
|
|
218
|
+
AND fidelity = 'full'
|
|
219
|
+
AND scheme IN (SELECT name FROM schemes WHERE category = 'logging');
|
|
220
|
+
|
|
221
|
+
-- PREP: demote_turn_data_entries
|
|
222
|
+
-- Demote full data entries from a turn to summary with 413 status.
|
|
223
|
+
-- Fires when end-of-turn materialization exceeds the context ceiling.
|
|
224
|
+
UPDATE known_entries
|
|
225
|
+
SET
|
|
226
|
+
fidelity = 'summary'
|
|
227
|
+
, status = 413
|
|
228
|
+
, tokens = COALESCE(
|
|
229
|
+
countTokens(json_extract(attributes, '$.summary'))
|
|
230
|
+
, countTokens(substr(body, 1, 80))
|
|
231
|
+
)
|
|
232
|
+
, updated_at = CURRENT_TIMESTAMP
|
|
233
|
+
WHERE
|
|
234
|
+
run_id = :run_id
|
|
235
|
+
AND turn = :turn
|
|
236
|
+
AND fidelity = 'full'
|
|
237
|
+
AND status < 400
|
|
238
|
+
AND scheme IN (SELECT name FROM schemes WHERE category = 'data')
|
|
239
|
+
RETURNING path;
|
package/src/agent/schemes.sql
CHANGED
package/src/agent/tokens.js
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Token
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Token estimation. Conservative character-based approximation.
|
|
3
|
+
* RUMMY_TOKEN_DIVISOR controls characters per token.
|
|
4
|
+
* No external dependencies. The budget contract is exact.
|
|
5
|
+
* contextSize is the ceiling. countTokens is the measurement.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
const tiktoken = await import("tiktoken");
|
|
12
|
-
encoder = tiktoken.get_encoding("o200k_base");
|
|
13
|
-
} catch {
|
|
14
|
-
// tiktoken unavailable — use character-based estimate
|
|
15
|
-
}
|
|
8
|
+
const DIVISOR = Number(process.env.RUMMY_TOKEN_DIVISOR);
|
|
9
|
+
if (!DIVISOR) throw new Error("RUMMY_TOKEN_DIVISOR must be a non-zero number");
|
|
16
10
|
|
|
17
11
|
export function countTokens(text) {
|
|
18
12
|
if (!text) return 0;
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const tokens = encoder.encode(text);
|
|
22
|
-
return tokens.length;
|
|
23
|
-
} catch {
|
|
24
|
-
// Fallback on encoding error
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return Math.ceil(text.length / 4);
|
|
13
|
+
return Math.ceil(text.length / DIVISOR);
|
|
28
14
|
}
|
package/src/agent/turns.sql
CHANGED
|
@@ -6,7 +6,9 @@ RETURNING id, sequence;
|
|
|
6
6
|
-- PREP: update_turn_stats
|
|
7
7
|
UPDATE turns
|
|
8
8
|
SET
|
|
9
|
-
|
|
9
|
+
context_tokens = :context_tokens
|
|
10
|
+
, reasoning_content = :reasoning_content
|
|
11
|
+
, prompt_tokens = :prompt_tokens
|
|
10
12
|
, cached_tokens = :cached_tokens
|
|
11
13
|
, completion_tokens = :completion_tokens
|
|
12
14
|
, reasoning_tokens = :reasoning_tokens
|
|
@@ -25,6 +27,18 @@ SELECT
|
|
|
25
27
|
FROM turns
|
|
26
28
|
WHERE run_id = :run_id;
|
|
27
29
|
|
|
30
|
+
-- PREP: get_turn_context_tokens
|
|
31
|
+
SELECT context_tokens
|
|
32
|
+
FROM turns
|
|
33
|
+
WHERE run_id = :run_id AND sequence = :sequence;
|
|
34
|
+
|
|
35
|
+
-- PREP: get_last_context_tokens
|
|
36
|
+
SELECT context_tokens
|
|
37
|
+
FROM turns
|
|
38
|
+
WHERE run_id = :run_id AND context_tokens > 0
|
|
39
|
+
ORDER BY sequence DESC
|
|
40
|
+
LIMIT 1;
|
|
41
|
+
|
|
28
42
|
-- PREP: get_run_log
|
|
29
43
|
SELECT ke.path, ke.status, ke.body, ke.attributes
|
|
30
44
|
FROM known_entries AS ke
|
|
@@ -63,6 +63,13 @@ export default class HookRegistry {
|
|
|
63
63
|
this.#events.get(tag).sort((a, b) => a.priority - b.priority);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
removeEvent(tag, callback) {
|
|
67
|
+
const hooks = this.#events.get(tag);
|
|
68
|
+
if (!hooks) return;
|
|
69
|
+
const idx = hooks.findIndex((h) => h.callback === callback);
|
|
70
|
+
if (idx !== -1) hooks.splice(idx, 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
async emitEvent(tag, ...args) {
|
|
67
74
|
const hooks = this.#events.get(tag) || [];
|
|
68
75
|
for (const h of hooks) {
|
package/src/hooks/Hooks.js
CHANGED
|
@@ -11,6 +11,7 @@ export default function createHooks(debug = false) {
|
|
|
11
11
|
|
|
12
12
|
const createEvent = (tag) => ({
|
|
13
13
|
on: (callback, priority) => registry.addEvent(tag, callback, priority),
|
|
14
|
+
off: (callback) => registry.removeEvent(tag, callback),
|
|
14
15
|
emit: (...args) => registry.emitEvent(tag, ...args),
|
|
15
16
|
});
|
|
16
17
|
|
|
@@ -39,6 +40,7 @@ export default function createHooks(debug = false) {
|
|
|
39
40
|
},
|
|
40
41
|
},
|
|
41
42
|
run: {
|
|
43
|
+
created: createEvent("run.created"),
|
|
42
44
|
started: createEvent("run.started"),
|
|
43
45
|
progress: createEvent("run.progress"),
|
|
44
46
|
state: createEvent("run.state"),
|
|
@@ -47,10 +49,15 @@ export default function createHooks(debug = false) {
|
|
|
47
49
|
completed: createEvent("run.step.completed"),
|
|
48
50
|
},
|
|
49
51
|
},
|
|
52
|
+
loop: {
|
|
53
|
+
started: createEvent("loop.started"),
|
|
54
|
+
completed: createEvent("loop.completed"),
|
|
55
|
+
},
|
|
50
56
|
turn: {
|
|
51
57
|
started: createEvent("turn.started"),
|
|
52
58
|
response: createEvent("turn.response"),
|
|
53
59
|
proposing: createEvent("turn.proposing"),
|
|
60
|
+
completed: createEvent("turn.completed"),
|
|
54
61
|
},
|
|
55
62
|
assembly: {
|
|
56
63
|
system: createFilter("assembly.system"),
|
|
@@ -80,9 +87,17 @@ export default function createHooks(debug = false) {
|
|
|
80
87
|
tools: createFilter("prompt.tools"),
|
|
81
88
|
},
|
|
82
89
|
entry: {
|
|
90
|
+
recording: createFilter("entry.recording"),
|
|
83
91
|
created: createEvent("entry.created"),
|
|
84
92
|
changed: createEvent("entry.changed"),
|
|
85
93
|
},
|
|
94
|
+
tool: {
|
|
95
|
+
before: createEvent("tool.before"),
|
|
96
|
+
after: createEvent("tool.after"),
|
|
97
|
+
},
|
|
98
|
+
context: {
|
|
99
|
+
materialized: createEvent("context.materialized"),
|
|
100
|
+
},
|
|
86
101
|
action: {},
|
|
87
102
|
ui: {
|
|
88
103
|
render: createEvent("ui.render"),
|
|
@@ -48,7 +48,12 @@ export default class PluginContext {
|
|
|
48
48
|
return this.#schemes;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
registerScheme({ name, modelVisible = 1, category = "
|
|
51
|
+
registerScheme({ name, modelVisible = 1, category = "logging" } = {}) {
|
|
52
|
+
if (!PluginContext.CATEGORIES.has(category)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Invalid category "${category}". Must be one of: ${[...PluginContext.CATEGORIES].join(", ")}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
52
57
|
this.#schemes.push({
|
|
53
58
|
name: name || this.#name,
|
|
54
59
|
model_visible: modelVisible,
|
|
@@ -56,6 +61,14 @@ export default class PluginContext {
|
|
|
56
61
|
});
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
static CATEGORIES = Object.freeze(
|
|
65
|
+
new Set(["data", "logging", "unknown", "prompt"]),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
ensureTool() {
|
|
69
|
+
this.#hooks.tools.ensureTool(this.#name);
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
/**
|
|
60
73
|
* Register a named callback for this plugin.
|
|
61
74
|
* "handler" registers the tool handler.
|
|
@@ -55,8 +55,20 @@ export default class RummyContext {
|
|
|
55
55
|
return this.#context.loopId || null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
get
|
|
59
|
-
return this.#context.
|
|
58
|
+
get noRepo() {
|
|
59
|
+
return this.#context.noRepo === true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get noInteraction() {
|
|
63
|
+
return this.#context.noInteraction === true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get noWeb() {
|
|
67
|
+
return this.#context.noWeb === true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get toolSet() {
|
|
71
|
+
return this.#context.toolSet || null;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
get contextSize() {
|
|
@@ -89,7 +101,7 @@ export default class RummyContext {
|
|
|
89
101
|
|
|
90
102
|
// --- Tool methods (same operations the model uses) ---
|
|
91
103
|
|
|
92
|
-
async set({ path, body, status = 200, attributes } = {}) {
|
|
104
|
+
async set({ path, body, status = 200, fidelity, attributes } = {}) {
|
|
93
105
|
if (!path) {
|
|
94
106
|
const slugify = (await import("../sql/functions/slugify.js")).default;
|
|
95
107
|
const base = slugify(body || "");
|
|
@@ -101,7 +113,7 @@ export default class RummyContext {
|
|
|
101
113
|
path,
|
|
102
114
|
body || "",
|
|
103
115
|
status,
|
|
104
|
-
{ attributes, loopId: this.loopId },
|
|
116
|
+
{ fidelity, attributes, loopId: this.loopId },
|
|
105
117
|
);
|
|
106
118
|
return path;
|
|
107
119
|
}
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
// Tool display order: gather → reason → act → communicate.
|
|
2
|
+
// Position in the list implies priority to the model.
|
|
3
|
+
const TOOL_ORDER = [
|
|
4
|
+
"get",
|
|
5
|
+
"set",
|
|
6
|
+
"known",
|
|
7
|
+
"unknown",
|
|
8
|
+
"env",
|
|
9
|
+
"sh",
|
|
10
|
+
"rm",
|
|
11
|
+
"cp",
|
|
12
|
+
"mv",
|
|
13
|
+
"search",
|
|
14
|
+
"summarize",
|
|
15
|
+
"update",
|
|
16
|
+
"ask_user",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function sortByPriority(names) {
|
|
20
|
+
return names.toSorted((a, b) => {
|
|
21
|
+
const ia = TOOL_ORDER.indexOf(a);
|
|
22
|
+
const ib = TOOL_ORDER.indexOf(b);
|
|
23
|
+
if (ia === -1 && ib === -1) return a.localeCompare(b);
|
|
24
|
+
if (ia === -1) return 1;
|
|
25
|
+
if (ib === -1) return 1;
|
|
26
|
+
return ia - ib;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
1
30
|
export default class ToolRegistry {
|
|
2
31
|
#tools = new Map();
|
|
3
32
|
#handlers = new Map();
|
|
@@ -5,12 +34,9 @@ export default class ToolRegistry {
|
|
|
5
34
|
|
|
6
35
|
ensureTool(scheme) {
|
|
7
36
|
if (this.#tools.has(scheme)) return;
|
|
8
|
-
this.#tools.set(scheme, Object.freeze({
|
|
37
|
+
this.#tools.set(scheme, Object.freeze({}));
|
|
9
38
|
}
|
|
10
39
|
|
|
11
|
-
// Exception: old register() removed. Plugins use core.on("handler")/core.on("full").
|
|
12
|
-
// The only remaining caller pathway is ensureTool + onHandle + onView.
|
|
13
|
-
|
|
14
40
|
get(name) {
|
|
15
41
|
return this.#tools.get(name);
|
|
16
42
|
}
|
|
@@ -39,10 +65,33 @@ export default class ToolRegistry {
|
|
|
39
65
|
`Every tool must define how its entries appear in the model view.`,
|
|
40
66
|
);
|
|
41
67
|
}
|
|
68
|
+
|
|
69
|
+
const attrs =
|
|
70
|
+
typeof entry.attributes === "string"
|
|
71
|
+
? JSON.parse(entry.attributes)
|
|
72
|
+
: entry.attributes;
|
|
73
|
+
const summary = typeof attrs?.summary === "string" ? attrs.summary : null;
|
|
74
|
+
|
|
42
75
|
const fidelity = entry.fidelity || "full";
|
|
43
76
|
const fn = fidelityMap.get(fidelity);
|
|
44
|
-
if (!fn)
|
|
45
|
-
|
|
77
|
+
if (!fn) {
|
|
78
|
+
// No view for this fidelity — fall back on model-authored summary
|
|
79
|
+
return summary || "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const body = await fn(entry);
|
|
83
|
+
|
|
84
|
+
// Prepend summary keywords above plugin output at summary fidelity
|
|
85
|
+
if (fidelity === "summary" && summary && body) {
|
|
86
|
+
return `${summary}\n${body}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fall back to summary attribute when plugin returns empty
|
|
90
|
+
if (fidelity === "summary" && summary && !body) {
|
|
91
|
+
return summary;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return body;
|
|
46
95
|
}
|
|
47
96
|
|
|
48
97
|
hasView(scheme) {
|
|
@@ -59,22 +108,31 @@ export default class ToolRegistry {
|
|
|
59
108
|
}
|
|
60
109
|
}
|
|
61
110
|
|
|
62
|
-
get actTools() {
|
|
63
|
-
return new Set(
|
|
64
|
-
[...this.#tools.entries()]
|
|
65
|
-
.filter(([, def]) => def.category === "act")
|
|
66
|
-
.map(([name]) => name),
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
111
|
get names() {
|
|
71
|
-
return [...this.#tools.keys()];
|
|
112
|
+
return sortByPriority([...this.#tools.keys()]);
|
|
72
113
|
}
|
|
73
114
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Compute the active tool set for a loop.
|
|
117
|
+
* All exclusions — mode, flags — handled here. One mechanism.
|
|
118
|
+
*/
|
|
119
|
+
resolveForLoop(
|
|
120
|
+
mode,
|
|
121
|
+
{ noInteraction = false, noWeb = false, noProposals = false } = {},
|
|
122
|
+
) {
|
|
123
|
+
const excluded = new Set();
|
|
124
|
+
if (mode === "ask") excluded.add("sh");
|
|
125
|
+
if (noInteraction) excluded.add("ask_user");
|
|
126
|
+
if (noWeb) excluded.add("search");
|
|
127
|
+
if (noProposals) {
|
|
128
|
+
excluded.add("ask_user");
|
|
129
|
+
excluded.add("env");
|
|
130
|
+
excluded.add("sh");
|
|
131
|
+
}
|
|
132
|
+
const names = sortByPriority(
|
|
133
|
+
[...this.#tools.keys()].filter((n) => !excluded.has(n)),
|
|
134
|
+
);
|
|
135
|
+
return new Set(names);
|
|
78
136
|
}
|
|
79
137
|
|
|
80
138
|
entries() {
|
package/src/llm/LlmProvider.js
CHANGED
|
@@ -90,18 +90,37 @@ export default class LlmProvider {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async getContextSize(model) {
|
|
93
|
+
// DB is the authority — check models table first
|
|
94
|
+
if (this.#db) {
|
|
95
|
+
const row = await this.#db.get_model_by_alias.get({ alias: model });
|
|
96
|
+
if (row?.context_length) return row.context_length;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Fall back to API query
|
|
93
100
|
const resolvedModel = await this.resolve(model);
|
|
101
|
+
let size;
|
|
94
102
|
if (resolvedModel.startsWith("ollama/")) {
|
|
95
103
|
const localModel = resolvedModel.replace("ollama/", "");
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
if (resolvedModel.startsWith("x.ai/")) {
|
|
104
|
+
size = await this.#getOllama().getContextSize(localModel);
|
|
105
|
+
} else if (resolvedModel.startsWith("openai/")) {
|
|
106
|
+
size = await this.#getOpenAi().getContextSize(resolvedModel);
|
|
107
|
+
} else if (resolvedModel.startsWith("x.ai/")) {
|
|
102
108
|
const localModel = resolvedModel.replace("x.ai/", "");
|
|
103
|
-
|
|
109
|
+
size = await this.#getXai().getContextSize(localModel);
|
|
110
|
+
} else {
|
|
111
|
+
size = await this.#getOpenRouter().getContextSize(resolvedModel);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Cache back to DB for next time
|
|
115
|
+
if (this.#db && size) {
|
|
116
|
+
await this.#db.update_model_context_length
|
|
117
|
+
.run({
|
|
118
|
+
alias: model,
|
|
119
|
+
context_length: size,
|
|
120
|
+
})
|
|
121
|
+
.catch(() => {});
|
|
104
122
|
}
|
|
105
|
-
|
|
123
|
+
|
|
124
|
+
return size;
|
|
106
125
|
}
|
|
107
126
|
}
|
package/src/llm/OpenAiClient.js
CHANGED
|
@@ -49,6 +49,12 @@ export default class OpenAiClient {
|
|
|
49
49
|
);
|
|
50
50
|
msg.reasoning_content =
|
|
51
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
|
+
}
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
return data;
|
|
@@ -59,6 +65,20 @@ export default class OpenAiClient {
|
|
|
59
65
|
const headers = { "Content-Type": "application/json" };
|
|
60
66
|
if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
61
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
|
|
62
82
|
const response = await fetch(`${this.#baseUrl}/v1/models`, {
|
|
63
83
|
headers,
|
|
64
84
|
signal: AbortSignal.timeout(timeout),
|