@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/plugins/file/file.js
CHANGED
|
@@ -1,34 +1,45 @@
|
|
|
1
1
|
import { isAbsolute, relative } from "node:path";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* File plugin: projections and constraints for filesystem entries.
|
|
5
|
+
*
|
|
6
|
+
* Bare file paths (src/app.js) have scheme=NULL in the DB because
|
|
7
|
+
* schemeOf() only recognizes "://" patterns. The schemes table has
|
|
8
|
+
* a "file" entry so v_model_context can JOIN via COALESCE(scheme, 'file').
|
|
9
|
+
* This is the one exception to "every scheme has a plugin owner" —
|
|
10
|
+
* the file plugin owns the NULL scheme through the "file" registry entry.
|
|
11
|
+
*/
|
|
3
12
|
export default class File {
|
|
4
13
|
#core;
|
|
5
14
|
|
|
6
15
|
constructor(core) {
|
|
7
16
|
this.#core = core;
|
|
8
|
-
|
|
9
|
-
core.registerScheme({
|
|
10
|
-
core.registerScheme({ name: "
|
|
17
|
+
// "file" scheme covers bare paths (scheme IS NULL in DB)
|
|
18
|
+
core.registerScheme({ category: "data" });
|
|
19
|
+
core.registerScheme({ name: "http", category: "data" });
|
|
20
|
+
core.registerScheme({ name: "https", category: "data" });
|
|
11
21
|
core.on("full", this.full.bind(this));
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
22
|
+
core.on("summary", this.summary.bind(this));
|
|
23
|
+
// Default identity views for http/https — rummy.web overrides these
|
|
24
|
+
core.hooks.tools.onView("http", (entry) => entry.body);
|
|
25
|
+
core.hooks.tools.onView("https", (entry) => entry.body);
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
full(entry) {
|
|
20
29
|
return entry.body;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
summary() {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set a project-level file constraint. Backbone operation —
|
|
38
|
+
* constraints are project config, not tool dispatch.
|
|
39
|
+
*/
|
|
40
|
+
static async setConstraint(db, projectId, pattern, visibility = "active") {
|
|
30
41
|
const path = await normalizePath(db, projectId, pattern);
|
|
31
|
-
if (!path) return
|
|
42
|
+
if (!path) return null;
|
|
32
43
|
|
|
33
44
|
await db.upsert_file_constraint.run({
|
|
34
45
|
project_id: projectId,
|
|
@@ -36,34 +47,22 @@ export default class File {
|
|
|
36
47
|
visibility,
|
|
37
48
|
});
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
if (visibility === "active") {
|
|
41
|
-
for (const run of runs) {
|
|
42
|
-
await knownStore.promoteByPattern(run.id, path, null, 0);
|
|
43
|
-
}
|
|
44
|
-
} else if (visibility === "ignore") {
|
|
45
|
-
for (const run of runs) {
|
|
46
|
-
await knownStore.demoteByPattern(run.id, path, null);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return { status: "ok" };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
static async ignore(db, knownStore, projectId, pattern) {
|
|
54
|
-
return File.activate(db, knownStore, projectId, pattern, "ignore");
|
|
50
|
+
return path;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Remove a project-level file constraint.
|
|
55
|
+
*/
|
|
56
|
+
static async dropConstraint(db, projectId, pattern) {
|
|
58
57
|
const path = await normalizePath(db, projectId, pattern);
|
|
59
|
-
if (!path) return
|
|
58
|
+
if (!path) return null;
|
|
60
59
|
|
|
61
60
|
await db.delete_file_constraint.run({
|
|
62
61
|
project_id: projectId,
|
|
63
62
|
pattern: path,
|
|
64
63
|
});
|
|
65
64
|
|
|
66
|
-
return
|
|
65
|
+
return path;
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -5,8 +5,7 @@ Retrieves and promotes entries by path or glob pattern.
|
|
|
5
5
|
## Registration
|
|
6
6
|
|
|
7
7
|
- **Tool**: `get`
|
|
8
|
-
- **
|
|
9
|
-
- **Category**: ask
|
|
8
|
+
- **Category**: `logging`
|
|
10
9
|
- **Handler**: Fetches matching entries via `getEntriesByPattern`, promotes them with `promoteByPattern`, and records the result.
|
|
11
10
|
|
|
12
11
|
## Projection
|
|
@@ -17,3 +16,4 @@ Shows `get {path}` followed by the entry body.
|
|
|
17
16
|
|
|
18
17
|
- Pattern queries (globs or body filters) produce a summary of matched paths.
|
|
19
18
|
- Exact path queries report the path and token count, or "not found".
|
|
19
|
+
- Budget check: rejects with 413 if incoming tokens exceed remaining context.
|
package/src/plugins/get/get.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import KnownStore from "../../agent/KnownStore.js";
|
|
3
2
|
import { storePatternResult } from "../helpers.js";
|
|
3
|
+
import docs from "./getDoc.js";
|
|
4
4
|
|
|
5
5
|
export default class Get {
|
|
6
6
|
#core;
|
|
@@ -11,10 +11,10 @@ export default class Get {
|
|
|
11
11
|
core.on("handler", this.handler.bind(this));
|
|
12
12
|
core.on("full", this.full.bind(this));
|
|
13
13
|
core.on("summary", this.summary.bind(this));
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
14
|
+
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
15
|
+
docsMap.get = docs;
|
|
16
|
+
return docsMap;
|
|
17
|
+
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async handler(entry, rummy) {
|
|
@@ -35,6 +35,7 @@ export default class Get {
|
|
|
35
35
|
normalized,
|
|
36
36
|
bodyFilter,
|
|
37
37
|
);
|
|
38
|
+
|
|
38
39
|
await store.promoteByPattern(runId, normalized, bodyFilter, turn);
|
|
39
40
|
|
|
40
41
|
if (isPattern) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Tool doc for <get>. 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-form is the primary invocation (simplest)
|
|
6
|
+
["## <get>[path/to/file]</get> - Load a file or entry into context"],
|
|
7
|
+
|
|
8
|
+
// --- Examples: 3 examples covering single file, known recall, and content search
|
|
9
|
+
[
|
|
10
|
+
"Example: <get>src/app.js</get>",
|
|
11
|
+
"Simplest form. Body = path. Teaches that get is the default read tool.",
|
|
12
|
+
],
|
|
13
|
+
[
|
|
14
|
+
'Example: <get path="known://*">auth</get>',
|
|
15
|
+
"Keyword recall: glob in path, search term in body. Cross-scheme hedberg pattern.",
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
'Example: <get path="src/**/*.js" preview>TODO</get>',
|
|
19
|
+
"Full pattern: recursive glob + preview + content filter. Shows all 3 features at once.",
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
// --- Constraints: RFC-style. Each prevents a specific failure mode.
|
|
23
|
+
[
|
|
24
|
+
"* Paths accept globs: `src/**/*.js`, `known://api_*`",
|
|
25
|
+
"Reinforces picomatch patterns work everywhere, not just in examples.",
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"* `preview` shows matches without loading into context",
|
|
29
|
+
"Budget-awareness. Without this, models load everything and blow context.",
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"* Body text filters results by content match",
|
|
33
|
+
"Generalizes examples 2-3. Body = filter, not just path.",
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
'* Use <set path="..." fidelity="index"/> to archive loaded content',
|
|
37
|
+
"Lifecycle: get→set. Load, read, archive. Prevents context hoarding.",
|
|
38
|
+
],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export default LINES.map(([text]) => text).join("\n");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseEditContent } from "./edits.js";
|
|
2
2
|
import HeuristicMatcher, { generatePatch } from "./matcher.js";
|
|
3
|
-
import { normalizeAttrs } from "./normalize.js";
|
|
3
|
+
import { normalizeAttrs, parseJsonEdit } from "./normalize.js";
|
|
4
4
|
import { hedmatch, hedsearch } from "./patterns.js";
|
|
5
5
|
import { parseSed } from "./sed.js";
|
|
6
6
|
|
|
@@ -29,6 +29,7 @@ export default class Hedberg {
|
|
|
29
29
|
replace: Hedberg.replace,
|
|
30
30
|
parseSed,
|
|
31
31
|
parseEdits: parseEditContent,
|
|
32
|
+
parseJsonEdit,
|
|
32
33
|
normalizeAttrs,
|
|
33
34
|
generatePatch,
|
|
34
35
|
};
|
|
@@ -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,5 @@
|
|
|
1
1
|
import { DOMParser } from "@xmldom/xmldom";
|
|
2
|
+
import picomatch from "picomatch";
|
|
2
3
|
import xpath from "xpath";
|
|
3
4
|
|
|
4
5
|
export const deterministic = true;
|
|
@@ -131,26 +132,7 @@ function detect(pattern) {
|
|
|
131
132
|
|
|
132
133
|
// --- Compilation ---
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
let result = "";
|
|
136
|
-
for (let i = 0; i < glob.length; i++) {
|
|
137
|
-
const c = glob[i];
|
|
138
|
-
if (c === "*") result += ".*";
|
|
139
|
-
else if (c === "?") result += ".";
|
|
140
|
-
else if (c === "[") {
|
|
141
|
-
const close = glob.indexOf("]", i + 1);
|
|
142
|
-
if (close === -1) {
|
|
143
|
-
result += "\\[";
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
result += glob.slice(i, close + 1);
|
|
147
|
-
i = close;
|
|
148
|
-
} else if (/[.+^${}()|\\]/.test(c)) {
|
|
149
|
-
result += `\\${c}`;
|
|
150
|
-
} else result += c;
|
|
151
|
-
}
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
135
|
+
// Glob matching delegated to picomatch (standard, battle-tested).
|
|
154
136
|
|
|
155
137
|
function parseRegex(pattern) {
|
|
156
138
|
const lastSlash = pattern.lastIndexOf("/");
|
|
@@ -214,12 +196,28 @@ function compile(pattern) {
|
|
|
214
196
|
switch (type) {
|
|
215
197
|
case "literal":
|
|
216
198
|
return { type, pattern };
|
|
217
|
-
case "glob":
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
}
|
|
223
221
|
case "regex": {
|
|
224
222
|
const { body, flags } = parseRegex(pattern);
|
|
225
223
|
return {
|
|
@@ -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/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { readdir, stat } from "node:fs/promises";
|
|
4
|
-
import { basename, join } from "node:path";
|
|
4
|
+
import { basename, isAbsolute, join } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import PluginContext from "../hooks/PluginContext.js";
|
|
7
7
|
|
|
@@ -30,10 +30,6 @@ export async function registerPlugins(dirs = [], hooks) {
|
|
|
30
30
|
const AUDIT_SCHEMES = [
|
|
31
31
|
"instructions",
|
|
32
32
|
"system",
|
|
33
|
-
"prompt",
|
|
34
|
-
"ask",
|
|
35
|
-
"act",
|
|
36
|
-
"progress",
|
|
37
33
|
"reasoning",
|
|
38
34
|
"model",
|
|
39
35
|
"error",
|
|
@@ -42,6 +38,8 @@ const AUDIT_SCHEMES = [
|
|
|
42
38
|
"content",
|
|
43
39
|
];
|
|
44
40
|
|
|
41
|
+
const PROMPT_SCHEMES = ["prompt", "progress"];
|
|
42
|
+
|
|
45
43
|
/**
|
|
46
44
|
* After DB is ready, inject db and store into all PluginContext instances,
|
|
47
45
|
* upsert declared schemes, and bootstrap audit schemes.
|
|
@@ -50,10 +48,17 @@ export async function initPlugins(db, store, hooks) {
|
|
|
50
48
|
for (const name of AUDIT_SCHEMES) {
|
|
51
49
|
await db.upsert_scheme.run({
|
|
52
50
|
name,
|
|
53
|
-
model_visible:
|
|
51
|
+
model_visible: 0,
|
|
54
52
|
category: "audit",
|
|
55
53
|
});
|
|
56
54
|
}
|
|
55
|
+
for (const name of PROMPT_SCHEMES) {
|
|
56
|
+
await db.upsert_scheme.run({
|
|
57
|
+
name,
|
|
58
|
+
model_visible: 1,
|
|
59
|
+
category: "prompt",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
57
62
|
|
|
58
63
|
for (const ctx of instances.values()) {
|
|
59
64
|
ctx.db = db;
|
|
@@ -70,16 +75,19 @@ export async function initPlugins(db, store, hooks) {
|
|
|
70
75
|
for (const s of ctx.schemes) registered.add(s.name);
|
|
71
76
|
}
|
|
72
77
|
for (const name of AUDIT_SCHEMES) registered.add(name);
|
|
78
|
+
for (const name of PROMPT_SCHEMES) registered.add(name);
|
|
73
79
|
|
|
74
80
|
for (const toolName of hooks.tools.names) {
|
|
75
81
|
if (registered.has(toolName)) continue;
|
|
76
82
|
await db.upsert_scheme.run({
|
|
77
83
|
name: toolName,
|
|
78
84
|
model_visible: 1,
|
|
79
|
-
category: "
|
|
85
|
+
category: "logging",
|
|
80
86
|
});
|
|
81
87
|
}
|
|
82
88
|
}
|
|
89
|
+
|
|
90
|
+
if (store) store.loadSchemes(db);
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
function resolvePlugin(packageName) {
|
|
@@ -105,9 +113,20 @@ async function loadEnvPlugins(hooks) {
|
|
|
105
113
|
if (!key.startsWith("RUMMY_PLUGIN_") || !value) continue;
|
|
106
114
|
const name = key.replace("RUMMY_PLUGIN_", "").toLowerCase();
|
|
107
115
|
try {
|
|
108
|
-
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
|
+
);
|
|
109
124
|
if (typeof Plugin?.register === "function") {
|
|
110
|
-
await
|
|
125
|
+
await withTimeout(
|
|
126
|
+
Plugin.register(hooks),
|
|
127
|
+
PLUGIN_LOAD_TIMEOUT,
|
|
128
|
+
`Plugin register timed out: ${value}`,
|
|
129
|
+
);
|
|
111
130
|
} else if (typeof Plugin === "function") {
|
|
112
131
|
const ctx = new PluginContext(name, hooks);
|
|
113
132
|
new Plugin(ctx);
|
|
@@ -120,6 +139,19 @@ async function loadEnvPlugins(hooks) {
|
|
|
120
139
|
}
|
|
121
140
|
}
|
|
122
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
|
+
|
|
123
155
|
async function scanDir(dir, hooks, isRoot = false) {
|
|
124
156
|
if (!existsSync(dir)) return;
|
|
125
157
|
|
|
@@ -167,18 +199,29 @@ async function scanDir(dir, hooks, isRoot = false) {
|
|
|
167
199
|
await loadPlugin(fullPath, hooks);
|
|
168
200
|
}
|
|
169
201
|
} else if (stats.isDirectory()) {
|
|
202
|
+
if (existsSync(join(fullPath, "DISABLED"))) continue;
|
|
170
203
|
await scanDir(fullPath, hooks, false);
|
|
171
204
|
}
|
|
172
205
|
}
|
|
173
206
|
}
|
|
174
207
|
|
|
208
|
+
const PLUGIN_LOAD_TIMEOUT = 10000;
|
|
209
|
+
|
|
175
210
|
async function loadPlugin(filePath, hooks) {
|
|
176
211
|
try {
|
|
177
212
|
const url = pathToFileURL(filePath).href;
|
|
178
|
-
const { default: Plugin } = await
|
|
213
|
+
const { default: Plugin } = await withTimeout(
|
|
214
|
+
import(url),
|
|
215
|
+
PLUGIN_LOAD_TIMEOUT,
|
|
216
|
+
`Plugin import timed out: ${filePath}`,
|
|
217
|
+
);
|
|
179
218
|
|
|
180
219
|
if (typeof Plugin?.register === "function") {
|
|
181
|
-
await
|
|
220
|
+
await withTimeout(
|
|
221
|
+
Plugin.register(hooks),
|
|
222
|
+
PLUGIN_LOAD_TIMEOUT,
|
|
223
|
+
`Plugin register timed out: ${filePath}`,
|
|
224
|
+
);
|
|
182
225
|
} else if (typeof Plugin === "function") {
|
|
183
226
|
const name = basename(filePath, ".js");
|
|
184
227
|
const ctx = new PluginContext(name, hooks);
|
|
@@ -186,8 +229,17 @@ async function loadPlugin(filePath, hooks) {
|
|
|
186
229
|
instances.set(name, ctx);
|
|
187
230
|
}
|
|
188
231
|
} catch (err) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
232
|
+
console.warn(
|
|
233
|
+
`[RUMMY] Plugin load failed: ${basename(filePath)} — ${err.message}`,
|
|
234
|
+
);
|
|
192
235
|
}
|
|
193
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
|
+
const toolSet = rummy.toolSet
|
|
21
|
+
? [...rummy.toolSet]
|
|
22
|
+
: this.#core.hooks.tools.names;
|
|
20
23
|
await store.upsert(runId, turn, "instructions://system", "", 200, {
|
|
21
|
-
attributes: {
|
|
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.
|