@possumtech/rummy 0.2.7 → 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 +12 -3
- package/EXCEPTIONS.md +46 -0
- package/PLUGINS.md +454 -197
- package/SPEC.md +284 -93
- package/migrations/001_initial_schema.sql +57 -70
- package/package.json +16 -10
- package/service.js +1 -1
- package/src/agent/AgentLoop.js +254 -70
- package/src/agent/ContextAssembler.js +18 -4
- package/src/agent/KnownStore.js +156 -23
- package/src/agent/ProjectAgent.js +5 -4
- package/src/agent/ResponseHealer.js +21 -1
- package/src/agent/TurnExecutor.js +393 -115
- package/src/agent/XmlParser.js +92 -39
- package/src/agent/known_checks.sql +5 -4
- package/src/agent/known_queries.sql +4 -3
- package/src/agent/known_store.sql +45 -15
- package/src/agent/loops.sql +63 -0
- package/src/agent/runs.sql +7 -7
- package/src/agent/schemes.sql +5 -2
- package/src/agent/tokens.js +6 -21
- package/src/agent/turns.sql +13 -4
- package/src/hooks/Hooks.js +18 -0
- package/src/hooks/PluginContext.js +14 -10
- package/src/hooks/RummyContext.js +30 -10
- 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 +8 -7
- 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 +16 -12
- package/src/plugins/cp/cpDoc.js +29 -0
- package/src/plugins/current/README.md +4 -4
- package/src/plugins/current/current.js +12 -10
- package/src/plugins/engine/engine.sql +5 -10
- package/src/plugins/engine/turn_context.sql +13 -13
- package/src/plugins/env/README.md +3 -4
- package/src/plugins/env/env.js +8 -7
- package/src/plugins/env/envDoc.js +29 -0
- package/src/plugins/file/README.md +9 -12
- package/src/plugins/file/file.js +34 -45
- package/src/plugins/get/README.md +2 -2
- package/src/plugins/get/get.js +28 -11
- package/src/plugins/get/getDoc.js +41 -0
- package/src/plugins/hedberg/docs.md +0 -9
- package/src/plugins/hedberg/hedberg.js +4 -6
- package/src/plugins/hedberg/matcher.js +1 -1
- package/src/plugins/hedberg/normalize.js +28 -0
- package/src/plugins/hedberg/patterns.js +31 -33
- package/src/plugins/hedberg/sed.js +17 -10
- package/src/plugins/helpers.js +2 -2
- package/src/plugins/index.js +93 -28
- package/src/plugins/instructions/README.md +6 -2
- package/src/plugins/instructions/instructions.js +21 -5
- package/src/plugins/instructions/preamble.md +9 -5
- package/src/plugins/known/README.md +10 -7
- package/src/plugins/known/known.js +33 -23
- package/src/plugins/known/knownDoc.js +33 -0
- package/src/plugins/mv/README.md +5 -4
- package/src/plugins/mv/mv.js +16 -12
- 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 +12 -8
- package/src/plugins/progress/progress.js +44 -12
- package/src/plugins/prompt/README.md +5 -5
- package/src/plugins/prompt/prompt.js +23 -19
- package/src/plugins/rm/README.md +4 -4
- package/src/plugins/rm/rm.js +29 -12
- package/src/plugins/rm/rmDoc.js +30 -0
- package/src/plugins/rpc/README.md +15 -28
- package/src/plugins/rpc/rpc.js +63 -107
- package/src/plugins/set/README.md +13 -12
- package/src/plugins/set/set.js +82 -21
- package/src/plugins/set/setDoc.js +45 -0
- package/src/plugins/sh/README.md +4 -4
- package/src/plugins/sh/sh.js +8 -7
- package/src/plugins/sh/shDoc.js +29 -0
- package/src/plugins/{skills/skills.js → skill/skill.js} +12 -54
- 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 +20 -8
- 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 +11 -8
- 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 +3 -5
- package/src/server/RpcRegistry.js +52 -4
- package/src/sql/v_model_context.sql +31 -39
- package/src/sql/v_run_log.sql +3 -3
- package/src/agent/prompt_queue.sql +0 -39
- package/src/plugins/ask_user/docs.md +0 -2
- package/src/plugins/cp/docs.md +0 -2
- package/src/plugins/env/docs.md +0 -2
- package/src/plugins/get/docs.md +0 -6
- package/src/plugins/known/docs.md +0 -3
- package/src/plugins/mv/docs.md +0 -2
- package/src/plugins/rm/docs.md +0 -4
- package/src/plugins/set/docs.md +0 -4
- 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 -5
- package/src/plugins/store/store.js +0 -52
- package/src/plugins/summarize/docs.md +0 -4
- package/src/plugins/unknown/docs.md +0 -5
- package/src/plugins/update/docs.md +0 -4
|
@@ -19,6 +19,8 @@ const KNOWN_ATTRS = new Set([
|
|
|
19
19
|
"results",
|
|
20
20
|
"command",
|
|
21
21
|
"warn",
|
|
22
|
+
"summary",
|
|
23
|
+
"fidelity",
|
|
22
24
|
]);
|
|
23
25
|
|
|
24
26
|
export function normalizeAttrs(attrs) {
|
|
@@ -37,5 +39,31 @@ export function normalizeAttrs(attrs) {
|
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
if ("preview" in out) out.preview = true;
|
|
42
|
+
// summary="..." is the description text, not a fidelity flag
|
|
43
|
+
if ("summary" in out && !out.summary) out.summary = true;
|
|
44
|
+
// file:// prefix — strip silently, bare paths are the convention
|
|
45
|
+
if (out.path?.startsWith("file://")) out.path = out.path.slice(7);
|
|
40
46
|
return out;
|
|
41
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse JSON-style edit from body content.
|
|
51
|
+
* Accepts: {"search":"old","replace":"new"} and {search="old",replace="new"}
|
|
52
|
+
* Returns { search, replace } or null.
|
|
53
|
+
*/
|
|
54
|
+
export function parseJsonEdit(text) {
|
|
55
|
+
const trimmed = text.trim();
|
|
56
|
+
if (!trimmed.startsWith("{") || !/search/.test(trimmed)) return null;
|
|
57
|
+
try {
|
|
58
|
+
const json = JSON.parse(trimmed);
|
|
59
|
+
if (json.search != null)
|
|
60
|
+
return { search: json.search, replace: json.replace ?? "" };
|
|
61
|
+
} catch {
|
|
62
|
+
const searchMatch = trimmed.match(/search\s*=\s*"([^"]*)"/);
|
|
63
|
+
const replaceMatch = trimmed.match(/replace\s*=\s*"([^"]*)"/);
|
|
64
|
+
if (searchMatch) {
|
|
65
|
+
return { search: searchMatch[1], replace: replaceMatch?.[1] ?? "" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
2
|
+
import picomatch from "picomatch";
|
|
3
|
+
import xpath from "xpath";
|
|
2
4
|
|
|
3
5
|
export const deterministic = true;
|
|
4
6
|
|
|
@@ -130,26 +132,7 @@ function detect(pattern) {
|
|
|
130
132
|
|
|
131
133
|
// --- Compilation ---
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
let result = "";
|
|
135
|
-
for (let i = 0; i < glob.length; i++) {
|
|
136
|
-
const c = glob[i];
|
|
137
|
-
if (c === "*") result += ".*";
|
|
138
|
-
else if (c === "?") result += ".";
|
|
139
|
-
else if (c === "[") {
|
|
140
|
-
const close = glob.indexOf("]", i + 1);
|
|
141
|
-
if (close === -1) {
|
|
142
|
-
result += "\\[";
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
result += glob.slice(i, close + 1);
|
|
146
|
-
i = close;
|
|
147
|
-
} else if (/[.+^${}()|\\]/.test(c)) {
|
|
148
|
-
result += `\\${c}`;
|
|
149
|
-
} else result += c;
|
|
150
|
-
}
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
135
|
+
// Glob matching delegated to picomatch (standard, battle-tested).
|
|
153
136
|
|
|
154
137
|
function parseRegex(pattern) {
|
|
155
138
|
const lastSlash = pattern.lastIndexOf("/");
|
|
@@ -213,12 +196,28 @@ function compile(pattern) {
|
|
|
213
196
|
switch (type) {
|
|
214
197
|
case "literal":
|
|
215
198
|
return { type, pattern };
|
|
216
|
-
case "glob":
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
199
|
+
case "glob": {
|
|
200
|
+
const escaped = pattern.replace(/([()])/g, "\\$1");
|
|
201
|
+
// Scheme paths have no directory structure — * matches everything
|
|
202
|
+
const opts = escaped.includes("://")
|
|
203
|
+
? {
|
|
204
|
+
dot: true,
|
|
205
|
+
nobrace: true,
|
|
206
|
+
noextglob: true,
|
|
207
|
+
bash: false,
|
|
208
|
+
regex: true,
|
|
209
|
+
}
|
|
210
|
+
: { dot: true, nobrace: true, noextglob: true };
|
|
211
|
+
|
|
212
|
+
// For scheme paths, convert single * after :// to ** so it crosses "/"
|
|
213
|
+
const prepared = escaped.includes("://")
|
|
214
|
+
? escaped.replace(/:\/\/\*(?!\*)/, "://**")
|
|
215
|
+
: escaped;
|
|
216
|
+
|
|
217
|
+
const isMatch = picomatch(prepared, opts);
|
|
218
|
+
const picoRe = picomatch.makeRe(prepared, opts);
|
|
219
|
+
return { type, isMatch, searchRe: picoRe };
|
|
220
|
+
}
|
|
222
221
|
case "regex": {
|
|
223
222
|
const { body, flags } = parseRegex(pattern);
|
|
224
223
|
return {
|
|
@@ -250,11 +249,10 @@ function compile(pattern) {
|
|
|
250
249
|
|
|
251
250
|
function evalXpath(expr, string) {
|
|
252
251
|
try {
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
const node =
|
|
257
|
-
if (!node) return null;
|
|
252
|
+
const doc = new DOMParser().parseFromString(string, "text/xml");
|
|
253
|
+
const nodes = xpath.select(expr, doc);
|
|
254
|
+
if (!nodes || nodes.length === 0) return null;
|
|
255
|
+
const node = nodes[0];
|
|
258
256
|
return { match: node.textContent, node };
|
|
259
257
|
} catch {
|
|
260
258
|
return null;
|
|
@@ -332,7 +330,7 @@ export function hedmatch(pattern, string) {
|
|
|
332
330
|
case "literal":
|
|
333
331
|
return string === compiled.pattern;
|
|
334
332
|
case "glob":
|
|
335
|
-
return compiled.
|
|
333
|
+
return compiled.isMatch(string);
|
|
336
334
|
case "regex":
|
|
337
335
|
return compiled.re.test(string);
|
|
338
336
|
case "sed":
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
* - Flag extraction (g, i, m, s, v)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
function splitSed(str) {
|
|
8
|
+
function splitSed(str, delim) {
|
|
9
9
|
const parts = [];
|
|
10
10
|
let current = "";
|
|
11
|
+
const escaped = `\\${delim}`;
|
|
11
12
|
for (let i = 0; i < str.length; i++) {
|
|
12
13
|
if (str[i] === "\\" && i + 1 < str.length) {
|
|
13
14
|
current += str[i] + str[i + 1];
|
|
14
15
|
i++;
|
|
15
|
-
} else if (str[i] ===
|
|
16
|
+
} else if (str[i] === delim) {
|
|
16
17
|
parts.push(current);
|
|
17
18
|
current = "";
|
|
18
19
|
} else {
|
|
@@ -20,26 +21,32 @@ function splitSed(str) {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
parts.push(current);
|
|
23
|
-
return parts;
|
|
24
|
+
return { parts, escaped };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function parseSed(input) {
|
|
27
|
-
|
|
28
|
+
// Sed allows any non-alphanumeric delimiter: s/old/new/, s|old|new|, s#old#new#
|
|
29
|
+
const match = input.match(/^s([^\w\s])/);
|
|
30
|
+
if (!match) return null;
|
|
28
31
|
|
|
32
|
+
const delim = match[1];
|
|
29
33
|
const blocks = [];
|
|
30
34
|
let remaining = input;
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const prefix = `s${delim}`;
|
|
36
|
+
|
|
37
|
+
while (remaining.startsWith(prefix)) {
|
|
38
|
+
const { parts, escaped } = splitSed(remaining.slice(2), delim);
|
|
33
39
|
if (parts.length < 2) break;
|
|
34
40
|
const flags = (parts[2] || "").match(/^[gimsv]*/)?.[0] || "";
|
|
41
|
+
const unesc = (s) => s.replaceAll(escaped, delim);
|
|
35
42
|
blocks.push({
|
|
36
|
-
search: parts[0]
|
|
37
|
-
replace: parts[1]
|
|
43
|
+
search: unesc(parts[0]),
|
|
44
|
+
replace: unesc(parts[1]),
|
|
38
45
|
flags,
|
|
39
46
|
sed: true,
|
|
40
47
|
});
|
|
41
|
-
const rest = parts.slice(2).join(
|
|
42
|
-
const next = rest.indexOf(
|
|
48
|
+
const rest = parts.slice(2).join(delim);
|
|
49
|
+
const next = rest.indexOf(prefix);
|
|
43
50
|
remaining = next >= 0 ? rest.slice(next) : "";
|
|
44
51
|
}
|
|
45
52
|
|
package/src/plugins/helpers.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function storePatternResult(
|
|
|
10
10
|
path,
|
|
11
11
|
bodyFilter,
|
|
12
12
|
matches,
|
|
13
|
-
preview = false,
|
|
13
|
+
{ preview = false, loopId = null } = {},
|
|
14
14
|
) {
|
|
15
15
|
const slug = await store.slugPath(runId, scheme, path);
|
|
16
16
|
const filter = bodyFilter ? ` body="${bodyFilter}"` : "";
|
|
@@ -18,5 +18,5 @@ export async function storePatternResult(
|
|
|
18
18
|
const listing = matches.map((m) => `${m.path} (${m.tokens_full})`).join("\n");
|
|
19
19
|
const prefix = preview ? "PREVIEW " : "";
|
|
20
20
|
const body = `${prefix}${scheme} path="${path}"${filter}: ${matches.length} matched (${total} tokens)\n${listing}`;
|
|
21
|
-
await store.upsert(runId, turn, slug, body,
|
|
21
|
+
await store.upsert(runId, turn, slug, body, 200, { loopId });
|
|
22
22
|
}
|
package/src/plugins/index.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { readdir, stat } from "node:fs/promises";
|
|
3
|
-
import { basename, join } from "node:path";
|
|
4
|
+
import { basename, isAbsolute, join } from "node:path";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
import PluginContext from "../hooks/PluginContext.js";
|
|
6
7
|
|
|
8
|
+
let globalPrefix;
|
|
9
|
+
function getGlobalPrefix() {
|
|
10
|
+
globalPrefix ??= execSync("npm prefix -g", { encoding: "utf8" }).trim();
|
|
11
|
+
return globalPrefix;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
const instances = new Map();
|
|
8
15
|
|
|
9
16
|
/**
|
|
@@ -23,10 +30,6 @@ export async function registerPlugins(dirs = [], hooks) {
|
|
|
23
30
|
const AUDIT_SCHEMES = [
|
|
24
31
|
"instructions",
|
|
25
32
|
"system",
|
|
26
|
-
"prompt",
|
|
27
|
-
"ask",
|
|
28
|
-
"act",
|
|
29
|
-
"progress",
|
|
30
33
|
"reasoning",
|
|
31
34
|
"model",
|
|
32
35
|
"error",
|
|
@@ -35,20 +38,26 @@ const AUDIT_SCHEMES = [
|
|
|
35
38
|
"content",
|
|
36
39
|
];
|
|
37
40
|
|
|
41
|
+
const PROMPT_SCHEMES = ["prompt", "progress"];
|
|
42
|
+
|
|
38
43
|
/**
|
|
39
44
|
* After DB is ready, inject db and store into all PluginContext instances,
|
|
40
45
|
* upsert declared schemes, and bootstrap audit schemes.
|
|
41
46
|
*/
|
|
42
47
|
export async function initPlugins(db, store, hooks) {
|
|
43
48
|
for (const name of AUDIT_SCHEMES) {
|
|
44
|
-
|
|
49
|
+
await db.upsert_scheme.run({
|
|
45
50
|
name,
|
|
46
|
-
|
|
47
|
-
model_visible: ["ask", "act", "progress"].includes(name) ? 1 : 0,
|
|
48
|
-
valid_states: JSON.stringify(["info"]),
|
|
51
|
+
model_visible: 0,
|
|
49
52
|
category: "audit",
|
|
50
|
-
};
|
|
51
|
-
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
for (const name of PROMPT_SCHEMES) {
|
|
56
|
+
await db.upsert_scheme.run({
|
|
57
|
+
name,
|
|
58
|
+
model_visible: 1,
|
|
59
|
+
category: "prompt",
|
|
60
|
+
});
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
for (const ctx of instances.values()) {
|
|
@@ -66,25 +75,37 @@ export async function initPlugins(db, store, hooks) {
|
|
|
66
75
|
for (const s of ctx.schemes) registered.add(s.name);
|
|
67
76
|
}
|
|
68
77
|
for (const name of AUDIT_SCHEMES) registered.add(name);
|
|
78
|
+
for (const name of PROMPT_SCHEMES) registered.add(name);
|
|
69
79
|
|
|
70
80
|
for (const toolName of hooks.tools.names) {
|
|
71
81
|
if (registered.has(toolName)) continue;
|
|
72
82
|
await db.upsert_scheme.run({
|
|
73
83
|
name: toolName,
|
|
74
|
-
fidelity: "full",
|
|
75
84
|
model_visible: 1,
|
|
76
|
-
|
|
77
|
-
"full",
|
|
78
|
-
"proposed",
|
|
79
|
-
"pass",
|
|
80
|
-
"rejected",
|
|
81
|
-
"error",
|
|
82
|
-
"info",
|
|
83
|
-
]),
|
|
84
|
-
category: "result",
|
|
85
|
+
category: "logging",
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
|
|
90
|
+
if (store) store.loadSchemes(db);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolvePlugin(packageName) {
|
|
94
|
+
// Check local node_modules first, then global
|
|
95
|
+
const localDir = join(process.cwd(), "node_modules", packageName);
|
|
96
|
+
if (existsSync(join(localDir, "package.json"))) return localDir;
|
|
97
|
+
const globalDir = join(getGlobalPrefix(), "lib", "node_modules", packageName);
|
|
98
|
+
if (existsSync(join(globalDir, "package.json"))) return globalDir;
|
|
99
|
+
throw new Error(`Package '${packageName}' not found locally or globally`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function importPlugin(packageName) {
|
|
103
|
+
const dir = resolvePlugin(packageName);
|
|
104
|
+
const pkg = JSON.parse(
|
|
105
|
+
(await import("node:fs")).readFileSync(join(dir, "package.json"), "utf8"),
|
|
106
|
+
);
|
|
107
|
+
const entry = pkg.exports?.["."] || pkg.main || "index.js";
|
|
108
|
+
return import(pathToFileURL(join(dir, entry)).href);
|
|
88
109
|
}
|
|
89
110
|
|
|
90
111
|
async function loadEnvPlugins(hooks) {
|
|
@@ -92,9 +113,20 @@ async function loadEnvPlugins(hooks) {
|
|
|
92
113
|
if (!key.startsWith("RUMMY_PLUGIN_") || !value) continue;
|
|
93
114
|
const name = key.replace("RUMMY_PLUGIN_", "").toLowerCase();
|
|
94
115
|
try {
|
|
95
|
-
const
|
|
116
|
+
const importPromise = isAbsolute(value)
|
|
117
|
+
? importAbsolute(value)
|
|
118
|
+
: importPlugin(value);
|
|
119
|
+
const { default: Plugin } = await withTimeout(
|
|
120
|
+
importPromise,
|
|
121
|
+
PLUGIN_LOAD_TIMEOUT,
|
|
122
|
+
`Plugin import timed out: ${value}`,
|
|
123
|
+
);
|
|
96
124
|
if (typeof Plugin?.register === "function") {
|
|
97
|
-
await
|
|
125
|
+
await withTimeout(
|
|
126
|
+
Plugin.register(hooks),
|
|
127
|
+
PLUGIN_LOAD_TIMEOUT,
|
|
128
|
+
`Plugin register timed out: ${value}`,
|
|
129
|
+
);
|
|
98
130
|
} else if (typeof Plugin === "function") {
|
|
99
131
|
const ctx = new PluginContext(name, hooks);
|
|
100
132
|
new Plugin(ctx);
|
|
@@ -107,6 +139,19 @@ async function loadEnvPlugins(hooks) {
|
|
|
107
139
|
}
|
|
108
140
|
}
|
|
109
141
|
|
|
142
|
+
async function importAbsolute(dir) {
|
|
143
|
+
const pkgPath = join(dir, "package.json");
|
|
144
|
+
if (!existsSync(pkgPath)) {
|
|
145
|
+
// Bare .js file
|
|
146
|
+
return import(pathToFileURL(dir).href);
|
|
147
|
+
}
|
|
148
|
+
const pkg = JSON.parse(
|
|
149
|
+
(await import("node:fs")).readFileSync(pkgPath, "utf8"),
|
|
150
|
+
);
|
|
151
|
+
const entry = pkg.exports?.["."] || pkg.main || "index.js";
|
|
152
|
+
return import(pathToFileURL(join(dir, entry)).href);
|
|
153
|
+
}
|
|
154
|
+
|
|
110
155
|
async function scanDir(dir, hooks, isRoot = false) {
|
|
111
156
|
if (!existsSync(dir)) return;
|
|
112
157
|
|
|
@@ -154,18 +199,29 @@ async function scanDir(dir, hooks, isRoot = false) {
|
|
|
154
199
|
await loadPlugin(fullPath, hooks);
|
|
155
200
|
}
|
|
156
201
|
} else if (stats.isDirectory()) {
|
|
202
|
+
if (existsSync(join(fullPath, "DISABLED"))) continue;
|
|
157
203
|
await scanDir(fullPath, hooks, false);
|
|
158
204
|
}
|
|
159
205
|
}
|
|
160
206
|
}
|
|
161
207
|
|
|
208
|
+
const PLUGIN_LOAD_TIMEOUT = 10000;
|
|
209
|
+
|
|
162
210
|
async function loadPlugin(filePath, hooks) {
|
|
163
211
|
try {
|
|
164
212
|
const url = pathToFileURL(filePath).href;
|
|
165
|
-
const { default: Plugin } = await
|
|
213
|
+
const { default: Plugin } = await withTimeout(
|
|
214
|
+
import(url),
|
|
215
|
+
PLUGIN_LOAD_TIMEOUT,
|
|
216
|
+
`Plugin import timed out: ${filePath}`,
|
|
217
|
+
);
|
|
166
218
|
|
|
167
219
|
if (typeof Plugin?.register === "function") {
|
|
168
|
-
await
|
|
220
|
+
await withTimeout(
|
|
221
|
+
Plugin.register(hooks),
|
|
222
|
+
PLUGIN_LOAD_TIMEOUT,
|
|
223
|
+
`Plugin register timed out: ${filePath}`,
|
|
224
|
+
);
|
|
169
225
|
} else if (typeof Plugin === "function") {
|
|
170
226
|
const name = basename(filePath, ".js");
|
|
171
227
|
const ctx = new PluginContext(name, hooks);
|
|
@@ -173,8 +229,17 @@ async function loadPlugin(filePath, hooks) {
|
|
|
173
229
|
instances.set(name, ctx);
|
|
174
230
|
}
|
|
175
231
|
} catch (err) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
232
|
+
console.warn(
|
|
233
|
+
`[RUMMY] Plugin load failed: ${basename(filePath)} — ${err.message}`,
|
|
234
|
+
);
|
|
179
235
|
}
|
|
180
236
|
}
|
|
237
|
+
|
|
238
|
+
function withTimeout(promise, ms, message) {
|
|
239
|
+
return Promise.race([
|
|
240
|
+
promise,
|
|
241
|
+
new Promise((_, reject) =>
|
|
242
|
+
setTimeout(() => reject(new Error(message)), ms),
|
|
243
|
+
),
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
@@ -4,8 +4,12 @@ Projects the system prompt instructions into model context.
|
|
|
4
4
|
|
|
5
5
|
## Registration
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **View**: `full` — renders preamble + tool docs + persona.
|
|
8
|
+
- **Event**: `turn.started` — writes `instructions://system` entry.
|
|
9
|
+
- **Filter**: `instructions.toolDocs` — gathers docs from all tool plugins.
|
|
8
10
|
|
|
9
11
|
## Behavior
|
|
10
12
|
|
|
11
|
-
Replaces the `[%TOOLS%]` placeholder in the
|
|
13
|
+
Replaces the `[%TOOLS%]` placeholder in the preamble with the active
|
|
14
|
+
tool list. Appends tool descriptions gathered via the `toolDocs` filter
|
|
15
|
+
and persona text when present in attributes.
|
|
@@ -17,20 +17,36 @@ export default class Instructions {
|
|
|
17
17
|
async onTurnStarted({ rummy }) {
|
|
18
18
|
const { entries: store, sequence: turn, runId } = rummy;
|
|
19
19
|
const runRow = await rummy.db.get_run_by_id.get({ id: runId });
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const toolSet = rummy.toolSet
|
|
21
|
+
? [...rummy.toolSet]
|
|
22
|
+
: this.#core.hooks.tools.names;
|
|
23
|
+
await store.upsert(runId, turn, "instructions://system", "", 200, {
|
|
24
|
+
attributes: {
|
|
25
|
+
persona: runRow?.persona || null,
|
|
26
|
+
toolSet,
|
|
27
|
+
},
|
|
22
28
|
});
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
async full(entry) {
|
|
26
32
|
const attrs = entry.attributes;
|
|
27
|
-
const
|
|
33
|
+
const activeTools = attrs.toolSet
|
|
34
|
+
? new Set(attrs.toolSet)
|
|
35
|
+
: new Set(this.#core.hooks.tools.names);
|
|
36
|
+
const sorted = this.#core.hooks.tools.names.filter((n) =>
|
|
37
|
+
activeTools.has(n),
|
|
38
|
+
);
|
|
39
|
+
const tools = sorted.join(", ");
|
|
28
40
|
let prompt = preamble.replace("[%TOOLS%]", tools);
|
|
29
41
|
const toolDocs = await this.#core.hooks.instructions.toolDocs.filter(
|
|
30
|
-
"",
|
|
31
42
|
{},
|
|
43
|
+
{ toolSet: activeTools },
|
|
32
44
|
);
|
|
33
|
-
|
|
45
|
+
const docsText = sorted
|
|
46
|
+
.filter((key) => toolDocs[key])
|
|
47
|
+
.map((key) => toolDocs[key])
|
|
48
|
+
.join("\n\n");
|
|
49
|
+
if (docsText) prompt += `\n\n${docsText}`;
|
|
34
50
|
if (attrs.persona) prompt += `\n\n## Persona\n\n${attrs.persona}`;
|
|
35
51
|
return prompt;
|
|
36
52
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
You are an assistant.
|
|
1
|
+
You are an assistant. YOU MUST gather information, then YOU MAY either answer questions or take action.
|
|
2
2
|
|
|
3
3
|
# Response Rules
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Required: YOU MUST respond with Tool Commands in the XML format. YOU MAY use multiple tools in your response.
|
|
6
|
+
Optional: YOU MAY think in an optional <think></think> tag before using any other Tool Commands.
|
|
7
|
+
Required: YOU MUST register all unknowns with <unknown>(specific thing I need to learn)</unknown>.
|
|
8
|
+
Required: YOU MUST register all new information, decisions, and plans with <known>(specific information, ideas, or plans)</known>.
|
|
9
|
+
Required: YOU MUST conclude every turn with EITHER <update/> if still working OR <summarize/> if done. Never both.
|
|
10
|
+
Required: Path and summary information is approximate. YOU MUST use <get> to verify before acting on summarized content.
|
|
11
|
+
Info: When information conflicts, later turns are more likely to be relevant and correct than earlier turns.
|
|
12
|
+
Info: Your context is limited but your storage is not. Organize and categorize your information, ideas, plans, and history to optimize your context.
|
|
8
13
|
|
|
9
14
|
# Tool Commands
|
|
10
15
|
|
|
11
16
|
Tools: [%TOOLS%]
|
|
12
|
-
Required: Either `<update/>` if still working or `<summarize/>` if done. Never both.
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# known
|
|
2
2
|
|
|
3
|
-
Writes
|
|
3
|
+
Writes knowledge entries into the store at full fidelity.
|
|
4
4
|
|
|
5
5
|
## Registration
|
|
6
6
|
|
|
7
7
|
- **Tool**: `known`
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
8
|
+
- **Category**: `data`
|
|
9
|
+
- **Handler**: Upserts the entry body at the target path with status 200.
|
|
10
|
+
- **Filter**: `assembly.system` at priority 100 — renders `<knowns>` section.
|
|
11
11
|
|
|
12
12
|
## Projection
|
|
13
13
|
|
|
14
|
-
Shows
|
|
14
|
+
Shows `# known {path}` followed by the entry body.
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Assembly
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Filters turn_context rows where `category === "data"`. Renders all
|
|
19
|
+
data entries (files, knowledge, skills, URLs) into the `<knowns>` section
|
|
20
|
+
of the system message. Third-party plugins that register with
|
|
21
|
+
`category: "data"` automatically appear here.
|
|
@@ -1,28 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import docs from "./knownDoc.js";
|
|
2
2
|
|
|
3
3
|
export default class Known {
|
|
4
4
|
#core;
|
|
5
5
|
|
|
6
6
|
constructor(core) {
|
|
7
7
|
this.#core = core;
|
|
8
|
-
core.registerScheme({
|
|
9
|
-
fidelity: "turn",
|
|
10
|
-
validStates: ["full", "stored"],
|
|
11
|
-
category: "knowledge",
|
|
12
|
-
});
|
|
8
|
+
core.registerScheme({ category: "data" });
|
|
13
9
|
core.on("handler", this.handler.bind(this));
|
|
14
10
|
core.on("full", this.full.bind(this));
|
|
15
11
|
core.filter("assembly.system", this.assembleKnown.bind(this), 100);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
12
|
+
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
13
|
+
docsMap.known = docs;
|
|
14
|
+
return docsMap;
|
|
15
|
+
});
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
async handler(entry, rummy) {
|
|
23
19
|
const { entries: store, sequence: turn, runId } = rummy;
|
|
24
20
|
const target = entry.attributes.path || entry.resultPath;
|
|
25
|
-
await store.upsert(runId, turn, target, entry.body,
|
|
21
|
+
await store.upsert(runId, turn, target, entry.body, 200);
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
full(entry) {
|
|
@@ -30,28 +26,42 @@ export default class Known {
|
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
async assembleKnown(content, ctx) {
|
|
33
|
-
const entries = ctx.rows.filter(
|
|
34
|
-
(r) =>
|
|
35
|
-
r.category === "file" ||
|
|
36
|
-
r.category === "file_index" ||
|
|
37
|
-
r.category === "known" ||
|
|
38
|
-
r.category === "known_index",
|
|
39
|
-
);
|
|
29
|
+
const entries = ctx.rows.filter((r) => r.category === "data");
|
|
40
30
|
if (entries.length === 0) return content;
|
|
41
31
|
|
|
42
32
|
// Rows arrive pre-sorted by SQL: skill → index → summary → full, then by recency
|
|
43
|
-
const
|
|
33
|
+
const demotedSet = new Set(ctx.demoted || []);
|
|
34
|
+
const panic = ctx.type === "panic";
|
|
35
|
+
const lines = entries.map((e) => renderKnownTag(e, demotedSet, panic));
|
|
44
36
|
return `${content}\n\n<knowns>\n${lines.join("\n")}\n</knowns>`;
|
|
45
37
|
}
|
|
46
38
|
}
|
|
47
39
|
|
|
48
|
-
function renderKnownTag(entry) {
|
|
40
|
+
function renderKnownTag(entry, demotedSet, panic = false) {
|
|
41
|
+
const tag = entry.scheme || "file";
|
|
42
|
+
const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
|
|
49
43
|
const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
|
|
50
|
-
const
|
|
44
|
+
const status = entry.status ? ` status="${entry.status}"` : "";
|
|
45
|
+
const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
|
|
46
|
+
const flag = demotedSet?.has(entry.path) ? " demoted" : "";
|
|
47
|
+
|
|
48
|
+
// Panic mode: index-only view so context fits in LLM window
|
|
49
|
+
if (panic) {
|
|
50
|
+
return `<${tag} path="${entry.path}"${turn}${fidelity}${tokens}/>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const attrs =
|
|
54
|
+
typeof entry.attributes === "string"
|
|
55
|
+
? JSON.parse(entry.attributes)
|
|
56
|
+
: entry.attributes;
|
|
57
|
+
const summary =
|
|
58
|
+
typeof attrs?.summary === "string"
|
|
59
|
+
? ` summary="${attrs.summary.slice(0, 80)}"`
|
|
60
|
+
: "";
|
|
51
61
|
|
|
52
62
|
if (entry.body) {
|
|
53
|
-
return
|
|
63
|
+
return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}>${entry.body}</${tag}>`;
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
return
|
|
66
|
+
return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}/>`;
|
|
57
67
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Tool doc for <known/>. Each entry: [text, rationale].
|
|
2
|
+
// Text goes to the model. Rationale stays in source.
|
|
3
|
+
// Changing ANY line requires reading ALL rationales first.
|
|
4
|
+
const LINES = [
|
|
5
|
+
// --- Syntax: body = the information to save
|
|
6
|
+
[
|
|
7
|
+
"## <known>[specific information, ideas, or plans]</known> - Sort and save what you learn for later recall",
|
|
8
|
+
],
|
|
9
|
+
// --- Examples: summary-with-keywords first (teaches the right pattern)
|
|
10
|
+
[
|
|
11
|
+
'Example: <known summary="hedberg,comedian,death,2005">Mitch Hedberg died on March 30, 2005</known>',
|
|
12
|
+
"Primary pattern: comma-separated keywords in summary. Path auto-generated from summary as known://hedberg/comedian/death/2005. Keywords become searchable path segments.",
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
'Example: <known path="known://people/rumsfeld" summary="defense,secretary,born,1932">Donald Rumsfeld was born in 1932 and served as Secretary of Defense</known>',
|
|
16
|
+
"Explicit path form: slashed path=category/key, summary=keywords. For when the model wants direct control over taxonomy.",
|
|
17
|
+
],
|
|
18
|
+
// --- Lifecycle
|
|
19
|
+
[
|
|
20
|
+
'* Recall with <get path="known://people/*">keyword</get>',
|
|
21
|
+
"Cross-tool lifecycle: glob by category, filter by keyword. Matches the slashed path convention.",
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"* `summary` keywords survive compression — write keywords you'll search for later",
|
|
25
|
+
"Teaches WHY summaries matter. Keywords become the path AND the compressed view.",
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"* YOU MUST sort and save all new information, ideas, and plans in their own <known> entries",
|
|
29
|
+
"Critical behavioral constraint. 'new' prevents re-saving known facts.",
|
|
30
|
+
],
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export default LINES.map(([text]) => text).join("\n");
|
package/src/plugins/mv/README.md
CHANGED
|
@@ -5,9 +5,8 @@ Moves (renames) an entry from one path to another within the K/V store.
|
|
|
5
5
|
## Registration
|
|
6
6
|
|
|
7
7
|
- **Tool**: `mv`
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **Handler**: Reads source body, writes to destination, removes source. K/V destinations resolve immediately (`pass`); file destinations produce a `proposed` entry.
|
|
8
|
+
- **Category**: `logging`
|
|
9
|
+
- **Handler**: Reads source body, writes to destination, removes source. Scheme destinations resolve immediately (status 200); file destinations produce status 202 (proposed).
|
|
11
10
|
|
|
12
11
|
## Projection
|
|
13
12
|
|
|
@@ -15,4 +14,6 @@ Shows `mv {from} {to}`.
|
|
|
15
14
|
|
|
16
15
|
## Behavior
|
|
17
16
|
|
|
18
|
-
Warns if the destination already exists and will be overwritten. Uses
|
|
17
|
+
Warns if the destination already exists and will be overwritten. Uses
|
|
18
|
+
`KnownStore.scheme()` to determine scheme vs file paths. Source entry
|
|
19
|
+
is removed on successful scheme moves.
|