@possumtech/rummy 0.4.0 → 2.0.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 +21 -4
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +850 -373
- package/bin/demo.js +166 -0
- package/bin/rummy.js +9 -3
- package/biome/no-fallbacks.grit +50 -0
- package/lang/en.json +2 -2
- package/migrations/001_initial_schema.sql +88 -37
- package/package.json +6 -4
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +460 -331
- package/src/agent/ContextAssembler.js +4 -2
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -379
- package/src/agent/XmlParser.js +242 -67
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +275 -118
- package/src/agent/materializeContext.js +102 -0
- package/src/agent/runs.sql +10 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +35 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +140 -37
- package/src/hooks/ToolRegistry.js +36 -35
- package/src/llm/LlmProvider.js +64 -90
- package/src/llm/errors.js +21 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -23
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -23
- package/src/plugins/budget/budget.js +261 -69
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +31 -13
- package/src/plugins/cp/cpDoc.js +2 -23
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +47 -8
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -7
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +125 -49
- package/src/plugins/get/getDoc.js +2 -43
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +43 -3
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +126 -12
- package/src/plugins/instructions/instructions.md +25 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +46 -0
- package/src/plugins/instructions/instructions_106.md +0 -0
- package/src/plugins/instructions/instructions_107.md +0 -0
- package/src/plugins/instructions/instructions_108.md +8 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +77 -45
- package/src/plugins/known/knownDoc.js +2 -29
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +109 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +57 -24
- package/src/plugins/mv/mvDoc.js +2 -29
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +63 -18
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +58 -14
- package/src/plugins/rm/rmDoc.js +2 -24
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +515 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -77
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +52 -8
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -17
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +148 -74
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +14 -1
- package/src/plugins/think/thinkDoc.js +2 -17
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +56 -21
- package/src/plugins/unknown/unknownDoc.js +2 -25
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +67 -5
- package/src/plugins/update/updateDoc.js +2 -27
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/functions/slugify.js +13 -1
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/src/agent/KnownStore.js +0 -338
- package/src/agent/ResponseHealer.js +0 -188
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -37
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -60
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -26
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -28
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# hedberg
|
|
1
|
+
# hedberg {#hedberg_plugin}
|
|
2
2
|
|
|
3
3
|
The interpretation boundary between stochastic model output and
|
|
4
4
|
deterministic system operations.
|
|
@@ -26,7 +26,6 @@ constructor(core) {
|
|
|
26
26
|
| `replace(body, search, replacement, opts?)` | Apply replacement (sed regex → literal → heuristic) |
|
|
27
27
|
| `parseSed(input)` | Parse sed syntax into `[{ search, replace, flags, sed }]` |
|
|
28
28
|
| `parseEdits(content)` | Detect edit format (merge conflict, udiff, Claude XML) |
|
|
29
|
-
| `normalizeAttrs(attrs)` | Heal model attribute names (value→body, unknown→path) |
|
|
30
29
|
| `generatePatch(path, old, new)` | Generate unified diff |
|
|
31
30
|
|
|
32
31
|
### Hedberg.replace(body, search, replacement, options?)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseEditContent } from "./edits.js";
|
|
2
2
|
import HeuristicMatcher, { generatePatch } from "./matcher.js";
|
|
3
|
-
import {
|
|
3
|
+
import { parseJsonEdit } from "./normalize.js";
|
|
4
4
|
import { hedmatch, hedsearch } from "./patterns.js";
|
|
5
5
|
import { parseSed } from "./sed.js";
|
|
6
6
|
|
|
@@ -14,7 +14,6 @@ import { parseSed } from "./sed.js";
|
|
|
14
14
|
* core.hedberg.replace(body, search, replacement, options?)
|
|
15
15
|
* core.hedberg.parseSed(input)
|
|
16
16
|
* core.hedberg.parseEdits(content)
|
|
17
|
-
* core.hedberg.normalizeAttrs(attrs)
|
|
18
17
|
* core.hedberg.generatePatch(path, old, new)
|
|
19
18
|
*/
|
|
20
19
|
export default class Hedberg {
|
|
@@ -30,7 +29,6 @@ export default class Hedberg {
|
|
|
30
29
|
parseSed,
|
|
31
30
|
parseEdits: parseEditContent,
|
|
32
31
|
parseJsonEdit,
|
|
33
|
-
normalizeAttrs,
|
|
34
32
|
generatePatch,
|
|
35
33
|
};
|
|
36
34
|
|
|
@@ -57,7 +55,13 @@ export default class Hedberg {
|
|
|
57
55
|
searchText,
|
|
58
56
|
flags.includes("g") ? flags : `${flags}g`,
|
|
59
57
|
);
|
|
60
|
-
|
|
58
|
+
// Unescape regex metacharacter escapes in the replacement string.
|
|
59
|
+
// The model writes `\[x\]` meaning literal `[x]` in both search
|
|
60
|
+
// and replace. RegExp handles this in search; in the replacement
|
|
61
|
+
// string we must strip the backslashes ourselves since
|
|
62
|
+
// String.replace only interprets `$` sequences, not `\`.
|
|
63
|
+
const unescaped = replaceText.replace(/\\([[\](){}.*+?^$|\\])/g, "$1");
|
|
64
|
+
patch = body.replace(re, unescaped);
|
|
61
65
|
if (patch === body) patch = null;
|
|
62
66
|
} catch {
|
|
63
67
|
// Invalid regex — fall through to literal/heuristic interpretation
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createTwoFilesPatch } from "diff";
|
|
2
2
|
|
|
3
|
-
export function generatePatch(
|
|
3
|
+
export function generatePatch(entryPath, oldContent, newContent) {
|
|
4
4
|
return createTwoFilesPatch(
|
|
5
|
-
`${
|
|
6
|
-
`${
|
|
5
|
+
`${entryPath}\told`,
|
|
6
|
+
`${entryPath}\tnew`,
|
|
7
7
|
oldContent,
|
|
8
8
|
newContent,
|
|
9
9
|
"",
|
|
@@ -13,37 +13,36 @@ export function generatePatch(filePath, oldContent, newContent) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export default class HeuristicMatcher {
|
|
16
|
-
static matchAndPatch(
|
|
16
|
+
static matchAndPatch(entryPath, entryBody, searchBlock, replaceBlock) {
|
|
17
17
|
// Unescape common regex escapes (models often escape brackets, parens, etc.)
|
|
18
18
|
const unescaped = searchBlock.replace(/\\([[\](){}.*+?^$|\\])/g, "$1");
|
|
19
|
-
if (unescaped !== searchBlock &&
|
|
19
|
+
if (unescaped !== searchBlock && entryBody.includes(unescaped)) {
|
|
20
20
|
searchBlock = unescaped;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const searchLines = searchBlock.split(/\r?\n/);
|
|
24
|
-
const fileLines =
|
|
24
|
+
const fileLines = entryBody.split(/\r?\n/);
|
|
25
25
|
|
|
26
26
|
// 1. Exact Match Attempt (line-boundary substring search)
|
|
27
|
-
let exactIdx =
|
|
27
|
+
let exactIdx = entryBody.indexOf(searchBlock);
|
|
28
28
|
let lastExactIdx = -1;
|
|
29
29
|
let exactCount = 0;
|
|
30
30
|
while (exactIdx !== -1) {
|
|
31
|
-
const atLineBoundary =
|
|
32
|
-
exactIdx === 0 || fileContent[exactIdx - 1] === "\n";
|
|
31
|
+
const atLineBoundary = exactIdx === 0 || entryBody[exactIdx - 1] === "\n";
|
|
33
32
|
if (atLineBoundary) {
|
|
34
33
|
exactCount++;
|
|
35
34
|
lastExactIdx = exactIdx;
|
|
36
35
|
}
|
|
37
|
-
exactIdx =
|
|
36
|
+
exactIdx = entryBody.indexOf(searchBlock, exactIdx + 1);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
if (exactCount > 0) {
|
|
41
40
|
const useIdx = lastExactIdx;
|
|
42
41
|
const newContent =
|
|
43
|
-
|
|
42
|
+
entryBody.slice(0, useIdx) +
|
|
44
43
|
replaceBlock +
|
|
45
|
-
|
|
46
|
-
const patch = generatePatch(
|
|
44
|
+
entryBody.slice(useIdx + searchBlock.length);
|
|
45
|
+
const patch = generatePatch(entryPath, entryBody, newContent);
|
|
47
46
|
const warning =
|
|
48
47
|
exactCount > 1
|
|
49
48
|
? `SEARCH block matched ${exactCount} locations. Edit was applied to the last occurrence. Use more surrounding context in future edits to avoid ambiguity.`
|
|
@@ -59,9 +58,9 @@ export default class HeuristicMatcher {
|
|
|
59
58
|
|
|
60
59
|
if (searchTokens.length === 0) {
|
|
61
60
|
// Empty SEARCH = append REPLACE to end of file
|
|
62
|
-
const trailing =
|
|
63
|
-
const newContent = `${
|
|
64
|
-
const patch = generatePatch(
|
|
61
|
+
const trailing = entryBody.endsWith("\n") ? "" : "\n";
|
|
62
|
+
const newContent = `${entryBody + trailing + replaceBlock}\n`;
|
|
63
|
+
const patch = generatePatch(entryPath, entryBody, newContent);
|
|
65
64
|
return { patch, newContent, warning: null, error: null };
|
|
66
65
|
}
|
|
67
66
|
|
|
@@ -149,7 +148,7 @@ export default class HeuristicMatcher {
|
|
|
149
148
|
];
|
|
150
149
|
const newContent = newFileLines.join("\n");
|
|
151
150
|
|
|
152
|
-
const patch = generatePatch(
|
|
151
|
+
const patch = generatePatch(entryPath, entryBody, newContent);
|
|
153
152
|
|
|
154
153
|
if (fuzzyAmbiguous) {
|
|
155
154
|
warning =
|
|
@@ -1,51 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Attribute normalization. Heals legacy and alternative attribute names
|
|
3
|
-
* from model output into canonical form.
|
|
4
|
-
*
|
|
5
|
-
* - value="" → body=""
|
|
6
|
-
* - file="" or key="" → path="" (first unrecognized attr becomes path)
|
|
7
|
-
* - preview="" → preview=true
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const KNOWN_ATTRS = new Set([
|
|
11
|
-
"path",
|
|
12
|
-
"body",
|
|
13
|
-
"preview",
|
|
14
|
-
"question",
|
|
15
|
-
"options",
|
|
16
|
-
"search",
|
|
17
|
-
"replace",
|
|
18
|
-
"to",
|
|
19
|
-
"results",
|
|
20
|
-
"command",
|
|
21
|
-
"warn",
|
|
22
|
-
"summary",
|
|
23
|
-
"fidelity",
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
export function normalizeAttrs(attrs) {
|
|
27
|
-
const out = { ...attrs };
|
|
28
|
-
if ("value" in out && !("body" in out)) {
|
|
29
|
-
out.body = out.value;
|
|
30
|
-
delete out.value;
|
|
31
|
-
}
|
|
32
|
-
if (!out.path) {
|
|
33
|
-
for (const [k, v] of Object.entries(out)) {
|
|
34
|
-
if (!KNOWN_ATTRS.has(k) && v) {
|
|
35
|
-
out.path = v;
|
|
36
|
-
delete out[k];
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
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);
|
|
46
|
-
return out;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
1
|
/**
|
|
50
2
|
* Parse JSON-style edit from body content.
|
|
51
3
|
* Accepts: {"search":"old","replace":"new"} and {search="old",replace="new"}
|
package/src/plugins/helpers.js
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read a sibling tooldoc markdown file and return its model-facing text.
|
|
7
|
+
* Strips HTML comments (rationale stays in source, never reaches the model)
|
|
8
|
+
* and collapses any blank-line runs left behind. Each plugin's Doc.js is a
|
|
9
|
+
* one-liner that defers to this so authors edit normal markdown instead of
|
|
10
|
+
* a JS array of [text, rationale] pairs.
|
|
11
|
+
*/
|
|
12
|
+
export function loadDoc(metaUrl, name) {
|
|
13
|
+
const dir = dirname(fileURLToPath(metaUrl));
|
|
14
|
+
return readFileSync(join(dir, name), "utf8")
|
|
15
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
16
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
17
|
+
.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Translate a log entry path into its companion data-scheme base path.
|
|
22
|
+
* `log://turn_N/{action}/{rest}` → `{action}://turn_N/{rest}`.
|
|
23
|
+
* Streaming producers (sh, env) create data channel entries under the
|
|
24
|
+
* producer scheme while the audit record lives in the log scheme; this
|
|
25
|
+
* helper bridges the two namespaces. Returns null for non-log paths.
|
|
26
|
+
*/
|
|
27
|
+
export function logPathToDataBase(logPath) {
|
|
28
|
+
const m = logPath?.match(/^log:\/\/turn_(\d+)\/([^/]+)\/(.+)$/);
|
|
29
|
+
if (!m) return null;
|
|
30
|
+
return `${m[2]}://turn_${m[1]}/${m[3]}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
1
33
|
/**
|
|
2
34
|
* Shared helper for pattern-based tool results.
|
|
3
35
|
* Used by get, set, store, and rm tools.
|
|
@@ -10,13 +42,21 @@ export async function storePatternResult(
|
|
|
10
42
|
path,
|
|
11
43
|
bodyFilter,
|
|
12
44
|
matches,
|
|
13
|
-
{ preview = false, loopId = null } = {},
|
|
45
|
+
{ preview = false, loopId = null, attributes = null } = {},
|
|
14
46
|
) {
|
|
15
|
-
const
|
|
47
|
+
const logSlug = await store.logPath(runId, turn, scheme, path);
|
|
16
48
|
const filter = bodyFilter ? ` body="${bodyFilter}"` : "";
|
|
17
49
|
const total = matches.reduce((s, m) => s + m.tokens, 0);
|
|
18
50
|
const listing = matches.map((m) => `${m.path} (${m.tokens})`).join("\n");
|
|
19
51
|
const prefix = preview ? "PREVIEW " : "";
|
|
20
52
|
const body = `${prefix}${scheme} path="${path}"${filter}: ${matches.length} matched (${total} tokens)\n${listing}`;
|
|
21
|
-
await store.
|
|
53
|
+
await store.set({
|
|
54
|
+
runId,
|
|
55
|
+
turn,
|
|
56
|
+
path: logSlug,
|
|
57
|
+
body,
|
|
58
|
+
state: "resolved",
|
|
59
|
+
loopId,
|
|
60
|
+
attributes,
|
|
61
|
+
});
|
|
22
62
|
}
|
package/src/plugins/index.js
CHANGED
|
@@ -11,20 +11,70 @@ function getGlobalPrefix() {
|
|
|
11
11
|
return globalPrefix;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const instances = new Map();
|
|
15
|
-
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Plugin loader:
|
|
16
|
+
* 1. Walk filesystem + env vars to collect plugin descriptors.
|
|
17
|
+
* 2. Import each and instantiate with a fresh PluginContext.
|
|
18
|
+
*
|
|
19
|
+
* Returns a Map of name → PluginContext for the caller to pass to
|
|
20
|
+
* initPlugins. No module-global state — each caller owns its set.
|
|
21
|
+
*
|
|
22
|
+
* Plugin constructors must be declarative (SPEC surfaces): they
|
|
23
|
+
* register schemes, hooks, filters, RPC methods — but don't dereference
|
|
24
|
+
* infrastructure that might not be ready yet. Because the plugin
|
|
25
|
+
* contract makes constructors side-effect-free on each other, load
|
|
26
|
+
* order doesn't matter and there is no dependency system.
|
|
19
27
|
*/
|
|
20
28
|
export async function registerPlugins(dirs = [], hooks) {
|
|
21
29
|
const uniqueDirs = [...new Set(dirs.map((d) => join(d)))];
|
|
22
30
|
|
|
31
|
+
const descriptors = [];
|
|
23
32
|
for (const dir of uniqueDirs) {
|
|
24
|
-
await
|
|
33
|
+
await collectFromDir(dir, true, descriptors);
|
|
34
|
+
}
|
|
35
|
+
await collectFromEnv(descriptors);
|
|
36
|
+
|
|
37
|
+
const resolved = [];
|
|
38
|
+
for (const d of descriptors) {
|
|
39
|
+
try {
|
|
40
|
+
const module = await withTimeout(
|
|
41
|
+
import(d.url),
|
|
42
|
+
PLUGIN_LOAD_TIMEOUT,
|
|
43
|
+
`Plugin import timed out: ${d.source}`,
|
|
44
|
+
);
|
|
45
|
+
resolved.push({ ...d, Plugin: module.default });
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.warn(`[RUMMY] Plugin import failed: ${d.name} — ${err.message}`);
|
|
48
|
+
}
|
|
25
49
|
}
|
|
26
50
|
|
|
27
|
-
|
|
51
|
+
const instances = new Map();
|
|
52
|
+
for (const r of resolved) {
|
|
53
|
+
try {
|
|
54
|
+
await instantiatePlugin(r, hooks, instances);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.warn(`[RUMMY] Plugin load failed: ${r.name} — ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return instances;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function instantiatePlugin({ name, Plugin, source }, hooks, instances) {
|
|
63
|
+
if (typeof Plugin?.register === "function") {
|
|
64
|
+
await withTimeout(
|
|
65
|
+
Plugin.register(hooks),
|
|
66
|
+
PLUGIN_LOAD_TIMEOUT,
|
|
67
|
+
`Plugin register timed out: ${source}`,
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (typeof Plugin !== "function") return;
|
|
72
|
+
const ctx = new PluginContext(name, hooks);
|
|
73
|
+
new Plugin(ctx);
|
|
74
|
+
instances.set(name, ctx);
|
|
75
|
+
if (source.startsWith("env:")) {
|
|
76
|
+
console.log(`[RUMMY] Plugin ${name}: ${source.slice(4)}`);
|
|
77
|
+
}
|
|
28
78
|
}
|
|
29
79
|
|
|
30
80
|
const AUDIT_SCHEMES = [
|
|
@@ -32,62 +82,98 @@ const AUDIT_SCHEMES = [
|
|
|
32
82
|
"system",
|
|
33
83
|
"reasoning",
|
|
34
84
|
"model",
|
|
35
|
-
"error",
|
|
36
85
|
"user",
|
|
37
86
|
"assistant",
|
|
38
87
|
"content",
|
|
39
88
|
];
|
|
40
89
|
|
|
41
|
-
const PROMPT_SCHEMES = ["prompt"
|
|
90
|
+
const PROMPT_SCHEMES = ["prompt"];
|
|
91
|
+
|
|
92
|
+
// Lifecycle schemes: client-addressable entries that reflect server
|
|
93
|
+
// state. Writable by system (internal bookkeeping), plugin (extensions),
|
|
94
|
+
// and client (RPC in Phase 4).
|
|
95
|
+
const LIFECYCLE_SCHEMES = ["run"];
|
|
96
|
+
|
|
97
|
+
// Unified log namespace for action history entries under
|
|
98
|
+
// log://turn_N/scheme/slug.
|
|
99
|
+
const LOG_SCHEMES = ["log"];
|
|
42
100
|
|
|
43
101
|
/**
|
|
44
|
-
* After DB is ready,
|
|
45
|
-
*
|
|
102
|
+
* After DB is ready, upsert declared schemes and bootstrap audit/prompt
|
|
103
|
+
* schemes. Takes the plugin collection returned by registerPlugins.
|
|
104
|
+
* Per-plugin store/db access is provided per-turn via RummyContext;
|
|
105
|
+
* PluginContext itself holds only name + hooks.
|
|
46
106
|
*/
|
|
47
|
-
export async function initPlugins(db,
|
|
107
|
+
export async function initPlugins(db, hooks, instances) {
|
|
48
108
|
for (const name of AUDIT_SCHEMES) {
|
|
109
|
+
// Audit schemes are written only by system-level code (reasoning,
|
|
110
|
+
// user/assistant/model messages, etc.). Closing the door on model
|
|
111
|
+
// writes and plugin writes here.
|
|
49
112
|
await db.upsert_scheme.run({
|
|
50
113
|
name,
|
|
51
114
|
model_visible: 0,
|
|
52
115
|
category: "audit",
|
|
116
|
+
default_scope: "run",
|
|
117
|
+
writable_by: JSON.stringify(["system"]),
|
|
53
118
|
});
|
|
54
119
|
}
|
|
55
120
|
for (const name of PROMPT_SCHEMES) {
|
|
121
|
+
// Prompt entries are created by the prompt plugin on user input;
|
|
122
|
+
// model doesn't emit <set path="prompt://...">.
|
|
56
123
|
await db.upsert_scheme.run({
|
|
57
124
|
name,
|
|
58
125
|
model_visible: 1,
|
|
59
126
|
category: "prompt",
|
|
127
|
+
default_scope: "run",
|
|
128
|
+
writable_by: JSON.stringify(["plugin"]),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
for (const name of LOG_SCHEMES) {
|
|
132
|
+
await db.upsert_scheme.run({
|
|
133
|
+
name,
|
|
134
|
+
model_visible: 1,
|
|
135
|
+
category: "logging",
|
|
136
|
+
default_scope: "run",
|
|
137
|
+
writable_by: JSON.stringify(["system", "plugin", "model"]),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
for (const name of LIFECYCLE_SCHEMES) {
|
|
141
|
+
// Lifecycle entries are client-addressable mirrors of server state.
|
|
142
|
+
// Not model-visible. System writes internally; plugins and clients
|
|
143
|
+
// write via the 6 primitives.
|
|
144
|
+
await db.upsert_scheme.run({
|
|
145
|
+
name,
|
|
146
|
+
model_visible: 0,
|
|
147
|
+
category: "logging",
|
|
148
|
+
default_scope: "run",
|
|
149
|
+
writable_by: JSON.stringify(["system", "plugin", "client"]),
|
|
60
150
|
});
|
|
61
151
|
}
|
|
62
152
|
|
|
63
153
|
for (const ctx of instances.values()) {
|
|
64
|
-
ctx.db = db;
|
|
65
|
-
ctx.entries = store;
|
|
66
154
|
for (const scheme of ctx.schemes) {
|
|
67
155
|
await db.upsert_scheme.run(scheme);
|
|
68
156
|
}
|
|
69
157
|
}
|
|
70
158
|
|
|
71
159
|
// Register default schemes for tools that plugins ensured but didn't registerScheme for
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
for (const
|
|
75
|
-
for (const s of ctx.schemes) registered.add(s.name);
|
|
76
|
-
}
|
|
77
|
-
for (const name of AUDIT_SCHEMES) registered.add(name);
|
|
78
|
-
for (const name of PROMPT_SCHEMES) registered.add(name);
|
|
79
|
-
|
|
80
|
-
for (const toolName of hooks.tools.names) {
|
|
81
|
-
if (registered.has(toolName)) continue;
|
|
82
|
-
await db.upsert_scheme.run({
|
|
83
|
-
name: toolName,
|
|
84
|
-
model_visible: 1,
|
|
85
|
-
category: "logging",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
160
|
+
const registered = new Set();
|
|
161
|
+
for (const ctx of instances.values()) {
|
|
162
|
+
for (const s of ctx.schemes) registered.add(s.name);
|
|
88
163
|
}
|
|
164
|
+
for (const name of AUDIT_SCHEMES) registered.add(name);
|
|
165
|
+
for (const name of PROMPT_SCHEMES) registered.add(name);
|
|
89
166
|
|
|
90
|
-
|
|
167
|
+
for (const toolName of hooks.tools.names) {
|
|
168
|
+
if (registered.has(toolName)) continue;
|
|
169
|
+
await db.upsert_scheme.run({
|
|
170
|
+
name: toolName,
|
|
171
|
+
model_visible: 1,
|
|
172
|
+
category: "logging",
|
|
173
|
+
default_scope: "run",
|
|
174
|
+
writable_by: JSON.stringify(["model", "plugin"]),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
91
177
|
}
|
|
92
178
|
|
|
93
179
|
function resolvePlugin(packageName) {
|
|
@@ -99,7 +185,7 @@ function resolvePlugin(packageName) {
|
|
|
99
185
|
throw new Error(`Package '${packageName}' not found locally or globally`);
|
|
100
186
|
}
|
|
101
187
|
|
|
102
|
-
async function
|
|
188
|
+
async function _importPlugin(packageName) {
|
|
103
189
|
const dir = resolvePlugin(packageName);
|
|
104
190
|
const pkg = JSON.parse(
|
|
105
191
|
(await import("node:fs")).readFileSync(join(dir, "package.json"), "utf8"),
|
|
@@ -108,133 +194,70 @@ async function importPlugin(packageName) {
|
|
|
108
194
|
return import(pathToFileURL(join(dir, entry)).href);
|
|
109
195
|
}
|
|
110
196
|
|
|
111
|
-
async function
|
|
197
|
+
async function collectFromEnv(descriptors) {
|
|
112
198
|
for (const [key, value] of Object.entries(process.env)) {
|
|
113
199
|
if (!key.startsWith("RUMMY_PLUGIN_") || !value) continue;
|
|
114
200
|
const name = key.replace("RUMMY_PLUGIN_", "").toLowerCase();
|
|
115
201
|
try {
|
|
116
|
-
const
|
|
117
|
-
?
|
|
118
|
-
:
|
|
119
|
-
|
|
120
|
-
importPromise,
|
|
121
|
-
PLUGIN_LOAD_TIMEOUT,
|
|
122
|
-
`Plugin import timed out: ${value}`,
|
|
123
|
-
);
|
|
124
|
-
if (typeof Plugin?.register === "function") {
|
|
125
|
-
await withTimeout(
|
|
126
|
-
Plugin.register(hooks),
|
|
127
|
-
PLUGIN_LOAD_TIMEOUT,
|
|
128
|
-
`Plugin register timed out: ${value}`,
|
|
129
|
-
);
|
|
130
|
-
} else if (typeof Plugin === "function") {
|
|
131
|
-
const ctx = new PluginContext(name, hooks);
|
|
132
|
-
new Plugin(ctx);
|
|
133
|
-
instances.set(name, ctx);
|
|
134
|
-
}
|
|
135
|
-
console.log(`[RUMMY] Plugin ${name}: ${value}`);
|
|
202
|
+
const url = isAbsolute(value)
|
|
203
|
+
? await resolveAbsoluteUrl(value)
|
|
204
|
+
: await resolvePackageUrl(value);
|
|
205
|
+
descriptors.push({ name, url, source: `env:${value}` });
|
|
136
206
|
} catch (err) {
|
|
137
207
|
console.warn(`[RUMMY] Plugin ${name} (${value}): ${err.message}`);
|
|
138
208
|
}
|
|
139
209
|
}
|
|
140
210
|
}
|
|
141
211
|
|
|
142
|
-
async function
|
|
212
|
+
async function resolvePackageUrl(packageName) {
|
|
213
|
+
const dir = resolvePlugin(packageName);
|
|
214
|
+
const pkg = JSON.parse(
|
|
215
|
+
(await import("node:fs")).readFileSync(join(dir, "package.json"), "utf8"),
|
|
216
|
+
);
|
|
217
|
+
const entry = pkg.exports?.["."] || pkg.main || "index.js";
|
|
218
|
+
return pathToFileURL(join(dir, entry)).href;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function resolveAbsoluteUrl(dir) {
|
|
143
222
|
const pkgPath = join(dir, "package.json");
|
|
144
223
|
if (!existsSync(pkgPath)) {
|
|
145
|
-
|
|
146
|
-
return import(pathToFileURL(dir).href);
|
|
224
|
+
return pathToFileURL(dir).href;
|
|
147
225
|
}
|
|
148
226
|
const pkg = JSON.parse(
|
|
149
227
|
(await import("node:fs")).readFileSync(pkgPath, "utf8"),
|
|
150
228
|
);
|
|
151
229
|
const entry = pkg.exports?.["."] || pkg.main || "index.js";
|
|
152
|
-
return
|
|
230
|
+
return pathToFileURL(join(dir, entry)).href;
|
|
153
231
|
}
|
|
154
232
|
|
|
155
|
-
async function
|
|
233
|
+
async function collectFromDir(dir, isRoot, descriptors) {
|
|
156
234
|
if (!existsSync(dir)) return;
|
|
235
|
+
if (!(await stat(dir)).isDirectory()) return;
|
|
157
236
|
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
dirStats = await stat(dir);
|
|
161
|
-
} catch (_err) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!dirStats.isDirectory()) {
|
|
166
|
-
if (process.env.RUMMY_DEBUG === "true") {
|
|
167
|
-
console.error(
|
|
168
|
-
`[RUMMY] Cannot scan plugin directory (not a directory): ${dir}`,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
let entries;
|
|
175
|
-
try {
|
|
176
|
-
entries = await readdir(dir);
|
|
177
|
-
} catch (err) {
|
|
178
|
-
if (process.env.RUMMY_DEBUG === "true") {
|
|
179
|
-
console.error(`[RUMMY] Failed to read directory ${dir}:`, err.message);
|
|
180
|
-
}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
for (const name of entries) {
|
|
237
|
+
for (const name of await readdir(dir)) {
|
|
185
238
|
if (name.endsWith(".test.js")) continue;
|
|
186
239
|
|
|
187
240
|
const fullPath = join(dir, name);
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
stats = await stat(fullPath);
|
|
191
|
-
} catch (_err) {
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
241
|
+
const stats = await stat(fullPath);
|
|
194
242
|
|
|
195
243
|
if (stats.isFile() && name.endsWith(".js")) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
244
|
+
const isEntryFile = name === "index.js" || name === `${basename(dir)}.js`;
|
|
245
|
+
if (isEntryFile || (isRoot && name !== "index.js")) {
|
|
246
|
+
descriptors.push({
|
|
247
|
+
name: basename(fullPath, ".js"),
|
|
248
|
+
url: pathToFileURL(fullPath).href,
|
|
249
|
+
source: fullPath,
|
|
250
|
+
});
|
|
200
251
|
}
|
|
201
252
|
} else if (stats.isDirectory()) {
|
|
202
253
|
if (existsSync(join(fullPath, "DISABLED"))) continue;
|
|
203
|
-
await
|
|
254
|
+
await collectFromDir(fullPath, false, descriptors);
|
|
204
255
|
}
|
|
205
256
|
}
|
|
206
257
|
}
|
|
207
258
|
|
|
208
259
|
const PLUGIN_LOAD_TIMEOUT = 10000;
|
|
209
260
|
|
|
210
|
-
async function loadPlugin(filePath, hooks) {
|
|
211
|
-
try {
|
|
212
|
-
const url = pathToFileURL(filePath).href;
|
|
213
|
-
const { default: Plugin } = await withTimeout(
|
|
214
|
-
import(url),
|
|
215
|
-
PLUGIN_LOAD_TIMEOUT,
|
|
216
|
-
`Plugin import timed out: ${filePath}`,
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
if (typeof Plugin?.register === "function") {
|
|
220
|
-
await withTimeout(
|
|
221
|
-
Plugin.register(hooks),
|
|
222
|
-
PLUGIN_LOAD_TIMEOUT,
|
|
223
|
-
`Plugin register timed out: ${filePath}`,
|
|
224
|
-
);
|
|
225
|
-
} else if (typeof Plugin === "function") {
|
|
226
|
-
const name = basename(filePath, ".js");
|
|
227
|
-
const ctx = new PluginContext(name, hooks);
|
|
228
|
-
const _instance = new Plugin(ctx);
|
|
229
|
-
instances.set(name, ctx);
|
|
230
|
-
}
|
|
231
|
-
} catch (err) {
|
|
232
|
-
console.warn(
|
|
233
|
-
`[RUMMY] Plugin load failed: ${basename(filePath)} — ${err.message}`,
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
261
|
function withTimeout(promise, ms, message) {
|
|
239
262
|
return Promise.race([
|
|
240
263
|
promise,
|
|
@@ -1,15 +1,41 @@
|
|
|
1
|
-
# instructions
|
|
1
|
+
# instructions {#instructions_plugin}
|
|
2
2
|
|
|
3
|
-
Projects the
|
|
3
|
+
Projects the model-facing instructions into the assembled packet.
|
|
4
|
+
Cleanly split into a stable system-side base and a dynamic user-side
|
|
5
|
+
phase directive so prompt caching holds across turns within a run.
|
|
4
6
|
|
|
5
7
|
## Registration
|
|
6
8
|
|
|
7
|
-
- **View**: `full` — renders
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
- **View**: `full` — renders the `instructions.md` base (identity +
|
|
10
|
+
`[%TOOLS%]` + `[%TOOLDOCS%]` + optional persona) for the
|
|
11
|
+
`instructions://system` entry. Stable across turns.
|
|
12
|
+
- **Event**: `turn.started` — writes `instructions://system` entry
|
|
13
|
+
with `{ persona, toolSet }` attributes.
|
|
14
|
+
- **Filter**: `instructions.toolDocs` — gathers docs from all tool
|
|
15
|
+
plugins into a docsMap.
|
|
16
|
+
- **Filter**: `assembly.user` (priority 250) — renders the current
|
|
17
|
+
phase's `instructions_10N.md` as `<instructions>` immediately
|
|
18
|
+
before `<prompt>`. Phase selected from the latest `<update status>`
|
|
19
|
+
emission in this turn's row set.
|
|
10
20
|
|
|
11
|
-
##
|
|
21
|
+
## Files
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
- `instructions.js` — plugin registration and assembly logic.
|
|
24
|
+
- `instructions.md` — the system-side base template. Static across
|
|
25
|
+
turns; only identity + `[%TOOLS%]` + `[%TOOLDOCS%]` placeholders.
|
|
26
|
+
- `instructions_104.md` … `instructions_108.md` — phase-specific
|
|
27
|
+
directives keyed by the 1XY status encoding (Define / Discover /
|
|
28
|
+
Distill / Demote / Deploy).
|
|
29
|
+
- `protocol.js` — placeholder module reserved for deterministic
|
|
30
|
+
protocol rule enforcement. Currently pass-through.
|
|
31
|
+
|
|
32
|
+
## Cache shape
|
|
33
|
+
|
|
34
|
+
- System message includes the base template + tool docs + persona.
|
|
35
|
+
Identical bytes every turn within a run → cache-stable.
|
|
36
|
+
- User message includes `<instructions>` at priority 250 — changes
|
|
37
|
+
as the phase advances, which is expected cache-turnover territory.
|
|
38
|
+
|
|
39
|
+
If you add a per-turn-dynamic piece to `instructions.md` by mistake,
|
|
40
|
+
the system prompt changes every turn and the cache prefix collapses.
|
|
41
|
+
Put anything turn-specific in a phase file instead.
|