@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/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) {
|
|
@@ -30,12 +30,81 @@ export default class Get {
|
|
|
30
30
|
const normalized = KnownStore.normalizePath(target);
|
|
31
31
|
const bodyFilter = entry.attributes.body || null;
|
|
32
32
|
const isPattern = bodyFilter || normalized.includes("*");
|
|
33
|
+
|
|
34
|
+
const line =
|
|
35
|
+
entry.attributes.line != null
|
|
36
|
+
? Math.max(1, parseInt(entry.attributes.line, 10))
|
|
37
|
+
: null;
|
|
38
|
+
const limit =
|
|
39
|
+
entry.attributes.limit != null
|
|
40
|
+
? Math.max(1, parseInt(entry.attributes.limit, 10))
|
|
41
|
+
: null;
|
|
42
|
+
|
|
33
43
|
const matches = await store.getEntriesByPattern(
|
|
34
44
|
runId,
|
|
35
45
|
normalized,
|
|
36
46
|
bodyFilter,
|
|
37
47
|
);
|
|
48
|
+
|
|
49
|
+
// Partial read — no fidelity promotion, returns a line slice as the log item.
|
|
50
|
+
if (line !== null || limit !== null) {
|
|
51
|
+
if (isPattern) {
|
|
52
|
+
await store.upsert(
|
|
53
|
+
runId,
|
|
54
|
+
turn,
|
|
55
|
+
entry.resultPath,
|
|
56
|
+
"line/limit requires a single path, not a glob or body filter",
|
|
57
|
+
400,
|
|
58
|
+
{ loopId },
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (matches.length === 0) {
|
|
63
|
+
await store.upsert(
|
|
64
|
+
runId,
|
|
65
|
+
turn,
|
|
66
|
+
entry.resultPath,
|
|
67
|
+
`${target} not found`,
|
|
68
|
+
200,
|
|
69
|
+
{ loopId },
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const allLines = matches[0].body.split("\n");
|
|
74
|
+
const total = allLines.length;
|
|
75
|
+
const startLine = line ?? 1;
|
|
76
|
+
const startIdx = startLine - 1;
|
|
77
|
+
const endIdx = limit !== null ? Math.min(startIdx + limit, total) : total;
|
|
78
|
+
const slice = allLines.slice(startIdx, endIdx).join("\n");
|
|
79
|
+
const endLine = endIdx;
|
|
80
|
+
const header = `[lines ${startLine}–${endLine} / ${total} total]`;
|
|
81
|
+
await store.upsert(
|
|
82
|
+
runId,
|
|
83
|
+
turn,
|
|
84
|
+
entry.resultPath,
|
|
85
|
+
`${header}\n${slice}`,
|
|
86
|
+
200,
|
|
87
|
+
{ loopId },
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const VALID_FIDELITY = {
|
|
93
|
+
stored: 1,
|
|
94
|
+
summary: 1,
|
|
95
|
+
index: 1,
|
|
96
|
+
full: 1,
|
|
97
|
+
archive: 1,
|
|
98
|
+
};
|
|
99
|
+
const fidelityAttr = VALID_FIDELITY[entry.attributes.fidelity]
|
|
100
|
+
? entry.attributes.fidelity
|
|
101
|
+
: null;
|
|
102
|
+
|
|
38
103
|
await store.promoteByPattern(runId, normalized, bodyFilter, turn);
|
|
104
|
+
if (fidelityAttr) {
|
|
105
|
+
for (const match of matches)
|
|
106
|
+
await store.setFidelity(runId, match.path, fidelityAttr);
|
|
107
|
+
}
|
|
39
108
|
|
|
40
109
|
if (isPattern) {
|
|
41
110
|
await storePatternResult(
|
|
@@ -52,7 +121,9 @@ export default class Get {
|
|
|
52
121
|
const total = matches.reduce((s, m) => s + m.tokens_full, 0);
|
|
53
122
|
const paths = matches.map((m) => m.path).join(", ");
|
|
54
123
|
const body =
|
|
55
|
-
matches.length > 0
|
|
124
|
+
matches.length > 0
|
|
125
|
+
? `${paths} loaded into <knowns> (${total} tokens)`
|
|
126
|
+
: `${target} not found`;
|
|
56
127
|
await store.upsert(runId, turn, entry.resultPath, body, 200, {
|
|
57
128
|
loopId,
|
|
58
129
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
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>authentication</get>',
|
|
19
|
+
"Full pattern: recursive glob + preview + content filter. Shows all 3 features at once. Body is a filter keyword, never file content.",
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
// --- Partial read: line/limit — show before constraints so model sees it as a first-class pattern
|
|
23
|
+
[
|
|
24
|
+
'Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>',
|
|
25
|
+
"Partial read. Returns lines 644–723 as the log item without promoting the entry to full. Use summary fidelity to find line numbers, then target the symbol directly.",
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
// --- Constraints: RFC-style. Each prevents a specific failure mode.
|
|
29
|
+
[
|
|
30
|
+
"* Paths accept patterns: `src/**/*.js`, `known://api_*`",
|
|
31
|
+
"Reinforces picomatch patterns work everywhere, not just in examples.",
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
"* `preview` shows matches without loading into context",
|
|
35
|
+
"Budget-awareness. Without this, models load everything and blow context.",
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
"* Body text filters results by content match",
|
|
39
|
+
"Generalizes examples 2-3. Body = filter, not just path.",
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
"* `line` and `limit` read a slice without promoting — patterns not allowed",
|
|
43
|
+
"The no-promotion constraint is what makes partial read safe: context budget is unaffected.",
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
'* Use <set path="..." fidelity="archive"/> to remove loaded content from context',
|
|
47
|
+
"Lifecycle: get→set. Load, read, archive. Prevents context hoarding.",
|
|
48
|
+
],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
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
|
};
|
|
@@ -1,34 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
1
|
+
import { createTwoFilesPatch } from "diff";
|
|
5
2
|
|
|
6
3
|
export function generatePatch(filePath, oldContent, newContent) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
`diff -u --label "${filePath}\told" --label "${filePath}\tnew" "${oldPath}" "${newPath}"`,
|
|
17
|
-
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] },
|
|
18
|
-
);
|
|
19
|
-
return result;
|
|
20
|
-
} catch (err) {
|
|
21
|
-
// diff exits 1 when files differ — that's the success case
|
|
22
|
-
if (err.stdout) return err.stdout;
|
|
23
|
-
return "";
|
|
24
|
-
} finally {
|
|
25
|
-
try {
|
|
26
|
-
unlinkSync(oldPath);
|
|
27
|
-
} catch {}
|
|
28
|
-
try {
|
|
29
|
-
unlinkSync(newPath);
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
4
|
+
return createTwoFilesPatch(
|
|
5
|
+
`${filePath}\told`,
|
|
6
|
+
`${filePath}\tnew`,
|
|
7
|
+
oldContent,
|
|
8
|
+
newContent,
|
|
9
|
+
"",
|
|
10
|
+
"",
|
|
11
|
+
{ context: 3 },
|
|
12
|
+
);
|
|
32
13
|
}
|
|
33
14
|
|
|
34
15
|
export default class HeuristicMatcher {
|
|
@@ -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,26 @@
|
|
|
1
|
-
You are
|
|
1
|
+
You are a folksonomic memory agent. YOU MUST organize all information into searchable taxonomies with navigable path hierarchies and searchable summary tags, then YOU MAY answer questions and/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
|
+
|
|
7
|
+
Optional: YOU MAY think in an optional <think></think> tag before using any other Tool Commands.
|
|
8
|
+
|
|
9
|
+
Required: YOU MUST register all unknowns with <unknown>[specific thing I need to learn]</unknown>.
|
|
10
|
+
|
|
11
|
+
Required: YOU MUST register all new facts, decisions, and plans with <known path="topic/subtopic" summary="keyword,keyword,keyword">[specific facts, decisions, or plans]</known>.
|
|
12
|
+
Required: Every <known> MUST include summary="keyword,keyword" tags.
|
|
13
|
+
Info: Paths are addresses for tools. Summary tags tell you what's inside.
|
|
14
|
+
Info: Path and summary information is approximate. YOU MUST use <get/> to verify before acting on summarized content.
|
|
15
|
+
Info: When information conflicts, later turns are more likely to be relevant and correct than earlier turns.
|
|
16
|
+
Info: Your context is limited but your archive is not. Organize and categorize your facts, decisions, plans, and history to optimize your context.
|
|
17
|
+
|
|
18
|
+
Required: YOU MUST promote all relevant "summary" entries to "full".
|
|
19
|
+
Required: YOU MUST demote all irrelevant "full" entries to "summary".
|
|
20
|
+
|
|
21
|
+
Required: YOU MUST conclude every turn with EITHER <update></update> if still working OR <summarize></summarize> if done. Never both.
|
|
22
|
+
Required: YOU MUST use one and only one <update></update> or <summarize></summarize> tag, and only at the end.
|
|
8
23
|
|
|
9
24
|
# Tool Commands
|
|
10
25
|
|
|
11
26
|
Tools: [%TOOLS%]
|
|
12
|
-
Required: Either `<update/>` if still working or `<summarize/>` if done. Never both.
|