@possumtech/rummy 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +11 -1
- package/EXCEPTIONS.md +46 -0
- package/PLUGINS.md +422 -188
- package/SPEC.md +284 -93
- package/migrations/001_initial_schema.sql +6 -4
- package/package.json +13 -5
- package/src/agent/AgentLoop.js +166 -15
- package/src/agent/ContextAssembler.js +18 -4
- package/src/agent/KnownStore.js +127 -13
- package/src/agent/ProjectAgent.js +4 -1
- package/src/agent/ResponseHealer.js +21 -1
- package/src/agent/TurnExecutor.js +365 -175
- package/src/agent/XmlParser.js +72 -39
- package/src/agent/known_store.sql +20 -4
- package/src/agent/schemes.sql +3 -0
- package/src/agent/tokens.js +6 -21
- package/src/agent/turns.sql +10 -1
- package/src/hooks/Hooks.js +18 -0
- package/src/hooks/PluginContext.js +14 -1
- package/src/hooks/RummyContext.js +16 -4
- package/src/hooks/ToolRegistry.js +83 -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/BudgetGuard.js +74 -0
- package/src/plugins/budget/README.md +43 -0
- package/src/plugins/budget/budget.js +79 -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/current/README.md +4 -4
- package/src/plugins/current/current.js +9 -6
- 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 +6 -5
- package/src/plugins/get/getDoc.js +41 -0
- package/src/plugins/hedberg/hedberg.js +2 -1
- 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 +9 -5
- package/src/plugins/known/README.md +10 -7
- package/src/plugins/known/known.js +29 -17
- package/src/plugins/known/knownDoc.js +33 -0
- package/src/plugins/mv/README.md +5 -4
- package/src/plugins/mv/mv.js +10 -6
- package/src/plugins/mv/mvDoc.js +31 -0
- package/src/plugins/persona/persona.js +78 -0
- package/src/plugins/previous/README.md +2 -2
- package/src/plugins/previous/previous.js +9 -6
- package/src/plugins/progress/progress.js +41 -15
- package/src/plugins/prompt/README.md +5 -5
- package/src/plugins/prompt/prompt.js +18 -13
- package/src/plugins/rm/README.md +4 -4
- package/src/plugins/rm/rm.js +5 -5
- 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 +60 -5
- package/src/plugins/set/setDoc.js +45 -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 +3 -1
- package/src/plugins/think/README.md +20 -0
- package/src/plugins/think/think.js +5 -0
- package/src/plugins/unknown/README.md +5 -5
- package/src/plugins/unknown/unknown.js +9 -7
- 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/RpcRegistry.js +52 -4
- package/src/sql/v_model_context.sql +16 -21
- package/src/plugins/ask_user/docs.md +0 -2
- package/src/plugins/cp/docs.md +0 -2
- 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,17 +1,9 @@
|
|
|
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
|
-
"get",
|
|
8
|
-
"store",
|
|
9
|
-
"rm",
|
|
10
|
-
"set",
|
|
11
|
-
"mv",
|
|
12
|
-
"cp",
|
|
13
|
-
"search",
|
|
14
|
-
]);
|
|
6
|
+
const STORE_TOOLS = new Set(["get", "rm", "set", "mv", "cp", "search"]);
|
|
15
7
|
const ALL_TOOLS = new Set([
|
|
16
8
|
...STORE_TOOLS,
|
|
17
9
|
"known",
|
|
@@ -51,31 +43,10 @@ function resolveCommand(name, attrs, rawBody) {
|
|
|
51
43
|
};
|
|
52
44
|
}
|
|
53
45
|
}
|
|
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
|
-
}
|
|
46
|
+
// JSON-style { search, replace }
|
|
47
|
+
const jsonEdit = parseJsonEdit(trimmed);
|
|
48
|
+
if (jsonEdit) {
|
|
49
|
+
return { name, path: a.path, ...jsonEdit };
|
|
79
50
|
}
|
|
80
51
|
// Sed syntax: s/search/replace/flags — supports chained commands
|
|
81
52
|
if (trimmed.startsWith("s/")) {
|
|
@@ -116,9 +87,9 @@ function resolveCommand(name, attrs, rawBody) {
|
|
|
116
87
|
preview: a.preview,
|
|
117
88
|
};
|
|
118
89
|
}
|
|
119
|
-
// Plain write
|
|
90
|
+
// Plain write or fidelity change
|
|
120
91
|
const body = trimmed || a.body || "";
|
|
121
|
-
return { name,
|
|
92
|
+
return { name, ...a, body };
|
|
122
93
|
}
|
|
123
94
|
|
|
124
95
|
if (name === "summarize" || name === "update" || name === "unknown") {
|
|
@@ -132,7 +103,7 @@ function resolveCommand(name, attrs, rawBody) {
|
|
|
132
103
|
return { name, path, body };
|
|
133
104
|
}
|
|
134
105
|
|
|
135
|
-
if (name === "get" || name === "
|
|
106
|
+
if (name === "get" || name === "rm") {
|
|
136
107
|
const path = a.path || trimmed || null;
|
|
137
108
|
return { name, path, body: a.body, preview: a.preview };
|
|
138
109
|
}
|
|
@@ -174,6 +145,9 @@ export default class XmlParser {
|
|
|
174
145
|
static parse(content) {
|
|
175
146
|
if (!content) return { commands: [], warnings: [], unparsed: "" };
|
|
176
147
|
|
|
148
|
+
// Normalize native tool call formats to rummy XML
|
|
149
|
+
const normalized = XmlParser.#normalizeToolCalls(content);
|
|
150
|
+
|
|
177
151
|
const commands = [];
|
|
178
152
|
const warnings = [];
|
|
179
153
|
const textChunks = [];
|
|
@@ -248,7 +222,7 @@ export default class XmlParser {
|
|
|
248
222
|
},
|
|
249
223
|
);
|
|
250
224
|
|
|
251
|
-
parser.write(
|
|
225
|
+
parser.write(normalized);
|
|
252
226
|
ended = true;
|
|
253
227
|
parser.end();
|
|
254
228
|
|
|
@@ -264,4 +238,63 @@ export default class XmlParser {
|
|
|
264
238
|
const unparsed = textChunks.join("").trim();
|
|
265
239
|
return { commands, warnings, unparsed };
|
|
266
240
|
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Normalize native tool call formats to rummy XML.
|
|
244
|
+
* Models sometimes emit their training-format tool calls instead of
|
|
245
|
+
* our XML tags. The intent is unambiguous — translate silently.
|
|
246
|
+
*/
|
|
247
|
+
static #normalizeToolCalls(content) {
|
|
248
|
+
// Gemma: ```tool_code\n<xml>...\n``` — strip code fences around valid XML
|
|
249
|
+
let result = content.replace(
|
|
250
|
+
/```(?:tool_code|tool_command|xml)\n([\s\S]*?)```/g,
|
|
251
|
+
(_, inner) => inner.trim(),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Qwen/gemma: <|tool_call>call:NAME{key:"value"}<tool_call|>
|
|
255
|
+
result = result.replace(
|
|
256
|
+
/<\|tool_call>call:(\w+)\{([^}]*)\}<(?:tool_call\||\|tool_call)>/g,
|
|
257
|
+
(_, name, params) => {
|
|
258
|
+
if (!ALL_TOOLS.has(name)) return _;
|
|
259
|
+
const valueMatch = params.match(/["']([^"']+)["']/);
|
|
260
|
+
const body = valueMatch?.[1] || "";
|
|
261
|
+
return `<${name}>${body}</${name}>`;
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// OpenAI function_call JSON: {"name":"search","arguments":{"query":"..."}}
|
|
266
|
+
result = result.replace(
|
|
267
|
+
/\{"name"\s*:\s*"(\w+)"\s*,\s*"arguments"\s*:\s*\{([^}]*)\}\}/g,
|
|
268
|
+
(_, name, args) => {
|
|
269
|
+
if (!ALL_TOOLS.has(name)) return _;
|
|
270
|
+
const pairs = [...args.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g)];
|
|
271
|
+
const body = pairs[0]?.[2] || "";
|
|
272
|
+
return `<${name}>${body}</${name}>`;
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Anthropic: <tool_use><name>search</name><input>{"query":"..."}</input></tool_use>
|
|
277
|
+
result = result.replace(
|
|
278
|
+
/<tool_use>\s*<name>(\w+)<\/name>\s*<input>\{([^}]*)\}<\/input>\s*<\/tool_use>/g,
|
|
279
|
+
(_, name, args) => {
|
|
280
|
+
if (!ALL_TOOLS.has(name)) return _;
|
|
281
|
+
const pairs = [...args.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g)];
|
|
282
|
+
const body = pairs[0]?.[2] || "";
|
|
283
|
+
return `<${name}>${body}</${name}>`;
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// Mistral: [TOOL_CALLS] [{"name":"search","arguments":{"query":"..."}}]
|
|
288
|
+
result = result.replace(
|
|
289
|
+
/\[TOOL_CALLS\]\s*\[\{"name"\s*:\s*"(\w+)"\s*,\s*"arguments"\s*:\s*\{([^}]*)\}\}\]/g,
|
|
290
|
+
(_, name, args) => {
|
|
291
|
+
if (!ALL_TOOLS.has(name)) return _;
|
|
292
|
+
const pairs = [...args.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g)];
|
|
293
|
+
const body = pairs[0]?.[2] || "";
|
|
294
|
+
return `<${name}>${body}</${name}>`;
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
267
300
|
}
|
|
@@ -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
|
package/src/agent/schemes.sql
CHANGED
package/src/agent/tokens.js
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
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);
|
|
16
9
|
|
|
17
10
|
export function countTokens(text) {
|
|
18
11
|
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);
|
|
12
|
+
return Math.ceil(text.length / DIVISOR);
|
|
28
13
|
}
|
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,13 @@ SELECT
|
|
|
25
27
|
FROM turns
|
|
26
28
|
WHERE run_id = :run_id;
|
|
27
29
|
|
|
30
|
+
-- PREP: get_last_context_tokens
|
|
31
|
+
SELECT context_tokens
|
|
32
|
+
FROM turns
|
|
33
|
+
WHERE run_id = :run_id AND context_tokens > 0
|
|
34
|
+
ORDER BY sequence DESC
|
|
35
|
+
LIMIT 1;
|
|
36
|
+
|
|
28
37
|
-- PREP: get_run_log
|
|
29
38
|
SELECT ke.path, ke.status, ke.body, ke.attributes
|
|
30
39
|
FROM known_entries AS ke
|
package/src/hooks/Hooks.js
CHANGED
|
@@ -39,6 +39,7 @@ export default function createHooks(debug = false) {
|
|
|
39
39
|
},
|
|
40
40
|
},
|
|
41
41
|
run: {
|
|
42
|
+
created: createEvent("run.created"),
|
|
42
43
|
started: createEvent("run.started"),
|
|
43
44
|
progress: createEvent("run.progress"),
|
|
44
45
|
state: createEvent("run.state"),
|
|
@@ -47,10 +48,15 @@ export default function createHooks(debug = false) {
|
|
|
47
48
|
completed: createEvent("run.step.completed"),
|
|
48
49
|
},
|
|
49
50
|
},
|
|
51
|
+
loop: {
|
|
52
|
+
started: createEvent("loop.started"),
|
|
53
|
+
completed: createEvent("loop.completed"),
|
|
54
|
+
},
|
|
50
55
|
turn: {
|
|
51
56
|
started: createEvent("turn.started"),
|
|
52
57
|
response: createEvent("turn.response"),
|
|
53
58
|
proposing: createEvent("turn.proposing"),
|
|
59
|
+
completed: createEvent("turn.completed"),
|
|
54
60
|
},
|
|
55
61
|
assembly: {
|
|
56
62
|
system: createFilter("assembly.system"),
|
|
@@ -67,6 +73,10 @@ export default function createHooks(debug = false) {
|
|
|
67
73
|
started: createEvent("act.started"),
|
|
68
74
|
completed: createEvent("act.completed"),
|
|
69
75
|
},
|
|
76
|
+
panic: {
|
|
77
|
+
started: createEvent("panic.started"),
|
|
78
|
+
completed: createEvent("panic.completed"),
|
|
79
|
+
},
|
|
70
80
|
llm: {
|
|
71
81
|
request: {
|
|
72
82
|
started: createEvent("llm.request.started"),
|
|
@@ -80,9 +90,17 @@ export default function createHooks(debug = false) {
|
|
|
80
90
|
tools: createFilter("prompt.tools"),
|
|
81
91
|
},
|
|
82
92
|
entry: {
|
|
93
|
+
recording: createFilter("entry.recording"),
|
|
83
94
|
created: createEvent("entry.created"),
|
|
84
95
|
changed: createEvent("entry.changed"),
|
|
85
96
|
},
|
|
97
|
+
tool: {
|
|
98
|
+
before: createEvent("tool.before"),
|
|
99
|
+
after: createEvent("tool.after"),
|
|
100
|
+
},
|
|
101
|
+
context: {
|
|
102
|
+
materialized: createEvent("context.materialized"),
|
|
103
|
+
},
|
|
86
104
|
action: {},
|
|
87
105
|
ui: {
|
|
88
106
|
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,37 @@ 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, noBench = false } = {},
|
|
122
|
+
) {
|
|
123
|
+
const excluded = new Set();
|
|
124
|
+
if (mode === "ask") excluded.add("sh");
|
|
125
|
+
if (mode === "panic") {
|
|
126
|
+
excluded.add("sh");
|
|
127
|
+
excluded.add("env");
|
|
128
|
+
excluded.add("search");
|
|
129
|
+
excluded.add("ask_user");
|
|
130
|
+
}
|
|
131
|
+
if (noInteraction) excluded.add("ask_user");
|
|
132
|
+
if (noWeb) excluded.add("search");
|
|
133
|
+
if (noBench) {
|
|
134
|
+
excluded.add("ask_user");
|
|
135
|
+
excluded.add("env");
|
|
136
|
+
excluded.add("sh");
|
|
137
|
+
}
|
|
138
|
+
const names = sortByPriority(
|
|
139
|
+
[...this.#tools.keys()].filter((n) => !excluded.has(n)),
|
|
140
|
+
);
|
|
141
|
+
return new Set(names);
|
|
78
142
|
}
|
|
79
143
|
|
|
80
144
|
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),
|
|
@@ -72,7 +72,29 @@ export default class OpenRouterClient {
|
|
|
72
72
|
return data;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
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;
|
|
77
99
|
}
|
|
78
100
|
}
|