@possumtech/rummy 0.2.6 → 0.2.8
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 +2 -3
- package/PLUGINS.md +105 -82
- package/bin/rummy.js +9 -2
- package/migrations/001_initial_schema.sql +53 -68
- package/package.json +4 -6
- package/service.js +2 -2
- package/src/agent/AgentLoop.js +91 -58
- package/src/agent/ContextAssembler.js +2 -2
- package/src/agent/KnownStore.js +30 -11
- package/src/agent/ProjectAgent.js +1 -3
- package/src/agent/TurnExecutor.js +119 -31
- package/src/agent/XmlParser.js +20 -0
- package/src/agent/known_checks.sql +5 -4
- package/src/agent/known_queries.sql +4 -3
- package/src/agent/known_store.sql +29 -15
- package/src/agent/loops.sql +63 -0
- package/src/agent/runs.sql +7 -7
- package/src/agent/schemes.sql +2 -2
- package/src/agent/turns.sql +3 -3
- package/src/hooks/PluginContext.js +1 -10
- package/src/hooks/RummyContext.js +16 -8
- package/src/plugins/ask_user/ask_user.js +3 -2
- package/src/plugins/cp/cp.js +7 -7
- package/src/plugins/current/current.js +3 -4
- package/src/plugins/engine/engine.sql +5 -3
- package/src/plugins/engine/turn_context.sql +9 -4
- package/src/plugins/env/docs.md +2 -0
- package/src/plugins/env/env.js +3 -2
- package/src/plugins/file/file.js +9 -19
- package/src/plugins/get/docs.md +7 -3
- package/src/plugins/get/get.js +22 -6
- package/src/plugins/hedberg/docs.md +0 -9
- package/src/plugins/hedberg/hedberg.js +2 -5
- package/src/plugins/hedberg/matcher.js +1 -1
- package/src/plugins/hedberg/patterns.js +6 -6
- package/src/plugins/helpers.js +2 -2
- package/src/plugins/index.js +28 -15
- package/src/plugins/instructions/instructions.js +1 -1
- package/src/plugins/known/known.js +9 -11
- package/src/plugins/mv/mv.js +7 -7
- package/src/plugins/previous/previous.js +6 -5
- package/src/plugins/progress/progress.js +6 -0
- package/src/plugins/prompt/prompt.js +9 -10
- package/src/plugins/rm/docs.md +3 -1
- package/src/plugins/rm/rm.js +24 -7
- package/src/plugins/rpc/rpc.js +33 -42
- package/src/plugins/set/docs.md +3 -1
- package/src/plugins/set/set.js +22 -16
- package/src/plugins/sh/sh.js +3 -2
- package/src/plugins/skills/skills.js +3 -4
- package/src/plugins/store/docs.md +2 -1
- package/src/plugins/store/store.js +14 -3
- package/src/plugins/summarize/summarize.js +1 -1
- package/src/plugins/telemetry/telemetry.js +17 -7
- package/src/plugins/unknown/unknown.js +3 -2
- package/src/plugins/update/update.js +1 -1
- package/src/server/ClientConnection.js +3 -5
- package/src/sql/v_model_context.sql +20 -23
- package/src/sql/v_run_log.sql +3 -3
- package/src/agent/prompt_queue.sql +0 -39
|
@@ -51,6 +51,10 @@ export default class RummyContext {
|
|
|
51
51
|
return this.#context.turnId || null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
get loopId() {
|
|
55
|
+
return this.#context.loopId || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
54
58
|
get noContext() {
|
|
55
59
|
return this.#context.noContext === true;
|
|
56
60
|
}
|
|
@@ -85,7 +89,7 @@ export default class RummyContext {
|
|
|
85
89
|
|
|
86
90
|
// --- Tool methods (same operations the model uses) ---
|
|
87
91
|
|
|
88
|
-
async set({ path, body,
|
|
92
|
+
async set({ path, body, status = 200, attributes } = {}) {
|
|
89
93
|
if (!path) {
|
|
90
94
|
const slugify = (await import("../sql/functions/slugify.js")).default;
|
|
91
95
|
const base = slugify(body || "");
|
|
@@ -96,8 +100,8 @@ export default class RummyContext {
|
|
|
96
100
|
this.sequence,
|
|
97
101
|
path,
|
|
98
102
|
body || "",
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
status,
|
|
104
|
+
{ attributes, loopId: this.loopId },
|
|
101
105
|
);
|
|
102
106
|
return path;
|
|
103
107
|
}
|
|
@@ -117,14 +121,18 @@ export default class RummyContext {
|
|
|
117
121
|
async mv(from, to) {
|
|
118
122
|
const body = await this.entries.getBody(this.runId, from);
|
|
119
123
|
if (body === null) return;
|
|
120
|
-
await this.entries.upsert(this.runId, this.sequence, to, body,
|
|
124
|
+
await this.entries.upsert(this.runId, this.sequence, to, body, 200, {
|
|
125
|
+
loopId: this.loopId,
|
|
126
|
+
});
|
|
121
127
|
await this.entries.remove(this.runId, from);
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
async cp(from, to) {
|
|
125
131
|
const body = await this.entries.getBody(this.runId, from);
|
|
126
132
|
if (body === null) return;
|
|
127
|
-
await this.entries.upsert(this.runId, this.sequence, to, body,
|
|
133
|
+
await this.entries.upsert(this.runId, this.sequence, to, body, 200, {
|
|
134
|
+
loopId: this.loopId,
|
|
135
|
+
});
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
// --- Plugin-only methods (superset) ---
|
|
@@ -137,9 +145,9 @@ export default class RummyContext {
|
|
|
137
145
|
return this.entries.getAttributes(this.runId, path);
|
|
138
146
|
}
|
|
139
147
|
|
|
140
|
-
async
|
|
148
|
+
async getStatus(path) {
|
|
141
149
|
const row = await this.entries.getState(this.runId, path);
|
|
142
|
-
return row?.
|
|
150
|
+
return row?.status ?? null;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
async getEntry(path) {
|
|
@@ -161,7 +169,7 @@ export default class RummyContext {
|
|
|
161
169
|
|
|
162
170
|
async log(message) {
|
|
163
171
|
const path = `content://${Date.now()}`;
|
|
164
|
-
await this.entries.upsert(this.runId, this.sequence, path, message,
|
|
172
|
+
await this.entries.upsert(this.runId, this.sequence, path, message, 200);
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
// --- Node tree methods ---
|
|
@@ -16,7 +16,7 @@ export default class AskUser {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async handler(entry, rummy) {
|
|
19
|
-
const { entries: store, sequence: turn, runId } = rummy;
|
|
19
|
+
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
20
20
|
const { question, options: rawOptions } = entry.attributes;
|
|
21
21
|
|
|
22
22
|
const optionText = rawOptions || entry.body || "";
|
|
@@ -28,8 +28,9 @@ export default class AskUser {
|
|
|
28
28
|
.filter(Boolean)
|
|
29
29
|
: [];
|
|
30
30
|
|
|
31
|
-
await store.upsert(runId, turn, entry.resultPath, entry.body,
|
|
31
|
+
await store.upsert(runId, turn, entry.resultPath, entry.body, 202, {
|
|
32
32
|
attributes: { question, options },
|
|
33
|
+
loopId,
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
|
package/src/plugins/cp/cp.js
CHANGED
|
@@ -6,9 +6,7 @@ export default class Cp {
|
|
|
6
6
|
|
|
7
7
|
constructor(core) {
|
|
8
8
|
this.#core = core;
|
|
9
|
-
core.registerScheme(
|
|
10
|
-
validStates: ["full", "proposed", "pass", "rejected", "error", "pattern"],
|
|
11
|
-
});
|
|
9
|
+
core.registerScheme();
|
|
12
10
|
core.on("handler", this.handler.bind(this));
|
|
13
11
|
core.on("full", this.full.bind(this));
|
|
14
12
|
core.on("summary", this.summary.bind(this));
|
|
@@ -19,7 +17,7 @@ export default class Cp {
|
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
async handler(entry, rummy) {
|
|
22
|
-
const { entries: store, sequence: turn, runId } = rummy;
|
|
20
|
+
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
23
21
|
const { path, to } = entry.attributes;
|
|
24
22
|
|
|
25
23
|
const source = await store.getBody(runId, path);
|
|
@@ -34,13 +32,15 @@ export default class Cp {
|
|
|
34
32
|
|
|
35
33
|
const body = `${path} ${to}`;
|
|
36
34
|
if (destScheme === null) {
|
|
37
|
-
await store.upsert(runId, turn, entry.resultPath, body,
|
|
35
|
+
await store.upsert(runId, turn, entry.resultPath, body, 202, {
|
|
38
36
|
attributes: { from: path, to, isMove: false, warning },
|
|
37
|
+
loopId,
|
|
39
38
|
});
|
|
40
39
|
} else {
|
|
41
|
-
await store.upsert(runId, turn, to, source,
|
|
42
|
-
await store.upsert(runId, turn, entry.resultPath, body,
|
|
40
|
+
await store.upsert(runId, turn, to, source, 200, { loopId });
|
|
41
|
+
await store.upsert(runId, turn, entry.resultPath, body, 200, {
|
|
43
42
|
attributes: { from: path, to, isMove: false, warning },
|
|
43
|
+
loopId,
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -15,26 +15,25 @@ export default class Current {
|
|
|
15
15
|
if (entries.length === 0) return content;
|
|
16
16
|
|
|
17
17
|
const lines = await Promise.all(
|
|
18
|
-
entries.map((e) => renderToolTag(e,
|
|
18
|
+
entries.map((e) => renderToolTag(e, this.#core)),
|
|
19
19
|
);
|
|
20
20
|
return `${content}<current>\n${lines.join("\n")}\n</current>\n`;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
async function renderToolTag(entry,
|
|
24
|
+
async function renderToolTag(entry, core) {
|
|
25
25
|
const attrs =
|
|
26
26
|
typeof entry.attributes === "string"
|
|
27
27
|
? JSON.parse(entry.attributes)
|
|
28
28
|
: entry.attributes;
|
|
29
29
|
|
|
30
30
|
const path = `${entry.scheme}://${attrs?.path || attrs?.file || attrs?.command || ""}`;
|
|
31
|
-
const status = entry.
|
|
31
|
+
const status = entry.status ? ` status="${entry.status}"` : "";
|
|
32
32
|
|
|
33
33
|
let body;
|
|
34
34
|
try {
|
|
35
35
|
body = await core.hooks.tools.view(entry.scheme, {
|
|
36
36
|
...entry,
|
|
37
|
-
fidelity,
|
|
38
37
|
attributes: attrs,
|
|
39
38
|
});
|
|
40
39
|
} catch {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
-- PREP: get_promoted_entries
|
|
2
|
-
SELECT
|
|
2
|
+
SELECT
|
|
3
|
+
ke.path, ke.scheme, ke.status, ke.fidelity, ke.turn
|
|
4
|
+
, ke.tokens, ke.refs
|
|
3
5
|
FROM known_entries AS ke
|
|
4
6
|
JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
|
|
5
7
|
WHERE
|
|
6
8
|
ke.run_id = :run_id
|
|
7
|
-
AND ke.
|
|
9
|
+
AND ke.fidelity IN ('full', 'summary')
|
|
8
10
|
AND s.model_visible = 1
|
|
9
11
|
ORDER BY ke.turn, ke.refs, ke.tokens DESC;
|
|
10
12
|
|
|
@@ -14,5 +16,5 @@ FROM known_entries AS ke
|
|
|
14
16
|
JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
|
|
15
17
|
WHERE
|
|
16
18
|
ke.run_id = :run_id
|
|
17
|
-
AND ke.
|
|
19
|
+
AND ke.fidelity IN ('full', 'summary')
|
|
18
20
|
AND s.model_visible = 1;
|
|
@@ -4,22 +4,27 @@ WHERE run_id = :run_id AND turn = :turn;
|
|
|
4
4
|
|
|
5
5
|
-- PREP: get_model_context
|
|
6
6
|
SELECT
|
|
7
|
-
ordinal, path, scheme, fidelity,
|
|
7
|
+
ordinal, path, scheme, fidelity, status, body
|
|
8
|
+
, tokens, attributes, category, turn
|
|
8
9
|
FROM v_model_context
|
|
9
10
|
WHERE run_id = :run_id
|
|
10
11
|
ORDER BY ordinal;
|
|
11
12
|
|
|
12
13
|
-- PREP: insert_turn_context
|
|
13
14
|
INSERT INTO turn_context (
|
|
14
|
-
run_id, turn, ordinal, path, fidelity,
|
|
15
|
+
run_id, loop_id, turn, ordinal, path, fidelity, status
|
|
16
|
+
, body, tokens, attributes, category, source_turn
|
|
15
17
|
)
|
|
16
18
|
VALUES (
|
|
17
|
-
:run_id, :turn, :ordinal, :path, :fidelity
|
|
19
|
+
:run_id, :loop_id, :turn, :ordinal, :path, :fidelity
|
|
20
|
+
, :status, :body, :tokens
|
|
18
21
|
, COALESCE(:attributes, '{}'), :category, :source_turn
|
|
19
22
|
);
|
|
20
23
|
|
|
21
24
|
-- PREP: get_turn_context
|
|
22
|
-
SELECT
|
|
25
|
+
SELECT
|
|
26
|
+
ordinal, path, scheme, fidelity, status, body
|
|
27
|
+
, tokens, attributes, category, source_turn
|
|
23
28
|
FROM turn_context
|
|
24
29
|
WHERE run_id = :run_id AND turn = :turn
|
|
25
30
|
ORDER BY ordinal;
|
package/src/plugins/env/docs.md
CHANGED
package/src/plugins/env/env.js
CHANGED
|
@@ -16,9 +16,10 @@ export default class Env {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async handler(entry, rummy) {
|
|
19
|
-
const { entries: store, sequence: turn, runId } = rummy;
|
|
20
|
-
await store.upsert(runId, turn, entry.resultPath, entry.body,
|
|
19
|
+
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
20
|
+
await store.upsert(runId, turn, entry.resultPath, entry.body, 202, {
|
|
21
21
|
attributes: entry.attributes,
|
|
22
|
+
loopId,
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
|
package/src/plugins/file/file.js
CHANGED
|
@@ -5,23 +5,9 @@ export default class File {
|
|
|
5
5
|
|
|
6
6
|
constructor(core) {
|
|
7
7
|
this.#core = core;
|
|
8
|
-
core.registerScheme({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
category: "file",
|
|
12
|
-
});
|
|
13
|
-
core.registerScheme({
|
|
14
|
-
name: "http",
|
|
15
|
-
fidelity: "turn",
|
|
16
|
-
validStates: ["full", "summary", "stored"],
|
|
17
|
-
category: "file",
|
|
18
|
-
});
|
|
19
|
-
core.registerScheme({
|
|
20
|
-
name: "https",
|
|
21
|
-
fidelity: "turn",
|
|
22
|
-
validStates: ["full", "summary", "stored"],
|
|
23
|
-
category: "file",
|
|
24
|
-
});
|
|
8
|
+
core.registerScheme({ category: "file" });
|
|
9
|
+
core.registerScheme({ name: "http", category: "file" });
|
|
10
|
+
core.registerScheme({ name: "https", category: "file" });
|
|
25
11
|
core.on("full", this.full.bind(this));
|
|
26
12
|
|
|
27
13
|
// Register identity projections for schemes that just pass through body
|
|
@@ -50,8 +36,12 @@ export default class File {
|
|
|
50
36
|
visibility,
|
|
51
37
|
});
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
const runs = await db.get_all_runs.all({ project_id: projectId });
|
|
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") {
|
|
55
45
|
for (const run of runs) {
|
|
56
46
|
await knownStore.demoteByPattern(run.id, path, null);
|
|
57
47
|
}
|
package/src/plugins/get/docs.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
## <get>[path/to/file]</get> - Load a file or entry into context
|
|
2
2
|
Example: <get>docs/example.txt</get>
|
|
3
3
|
Example: <get>known://auth_flow</get>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
4
|
+
Example: <get path="src/**/*.js" preview/> (list matching files without loading)
|
|
5
|
+
Example: <get path="src/*.js" body="TODO" preview/> (find files containing TODO)
|
|
6
|
+
* Paths accept globs: `src/**/*.js`, `known://api_*`
|
|
7
|
+
* Adding `preview` shows matches without loading into context
|
|
8
|
+
* Use `body` attribute to filter by content
|
|
9
|
+
* Use "known://" paths to recall stored information
|
|
10
|
+
* When irrelevant or resolved, use <store/> to remove from context
|
package/src/plugins/get/get.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import KnownStore from "../../agent/KnownStore.js";
|
|
2
3
|
import { storePatternResult } from "../helpers.js";
|
|
3
4
|
|
|
4
5
|
export default class Get {
|
|
@@ -6,7 +7,7 @@ export default class Get {
|
|
|
6
7
|
|
|
7
8
|
constructor(core) {
|
|
8
9
|
this.#core = core;
|
|
9
|
-
core.registerScheme(
|
|
10
|
+
core.registerScheme();
|
|
10
11
|
core.on("handler", this.handler.bind(this));
|
|
11
12
|
core.on("full", this.full.bind(this));
|
|
12
13
|
core.on("summary", this.summary.bind(this));
|
|
@@ -17,12 +18,24 @@ export default class Get {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async handler(entry, rummy) {
|
|
20
|
-
const { entries: store, sequence: turn, runId } = rummy;
|
|
21
|
+
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
21
22
|
const target = entry.attributes.path;
|
|
23
|
+
if (!target) {
|
|
24
|
+
await store.upsert(runId, turn, entry.resultPath, "", 400, {
|
|
25
|
+
attributes: { error: "path is required" },
|
|
26
|
+
loopId,
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const normalized = KnownStore.normalizePath(target);
|
|
22
31
|
const bodyFilter = entry.attributes.body || null;
|
|
23
|
-
const isPattern = bodyFilter ||
|
|
24
|
-
const matches = await store.getEntriesByPattern(
|
|
25
|
-
|
|
32
|
+
const isPattern = bodyFilter || normalized.includes("*");
|
|
33
|
+
const matches = await store.getEntriesByPattern(
|
|
34
|
+
runId,
|
|
35
|
+
normalized,
|
|
36
|
+
bodyFilter,
|
|
37
|
+
);
|
|
38
|
+
await store.promoteByPattern(runId, normalized, bodyFilter, turn);
|
|
26
39
|
|
|
27
40
|
if (isPattern) {
|
|
28
41
|
await storePatternResult(
|
|
@@ -33,13 +46,16 @@ export default class Get {
|
|
|
33
46
|
target,
|
|
34
47
|
bodyFilter,
|
|
35
48
|
matches,
|
|
49
|
+
{ loopId },
|
|
36
50
|
);
|
|
37
51
|
} else {
|
|
38
52
|
const total = matches.reduce((s, m) => s + m.tokens_full, 0);
|
|
39
53
|
const paths = matches.map((m) => m.path).join(", ");
|
|
40
54
|
const body =
|
|
41
55
|
matches.length > 0 ? `${paths} ${total} tokens` : `${target} not found`;
|
|
42
|
-
await store.upsert(runId, turn, entry.resultPath, body,
|
|
56
|
+
await store.upsert(runId, turn, entry.resultPath, body, 200, {
|
|
57
|
+
loopId,
|
|
58
|
+
});
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Advanced Patterns
|
|
2
|
-
* Paths accept globs: `src/**/*.js`, `known://api_*`
|
|
3
|
-
* Body attributes filter by content: `<get path="src/*.js" body="TODO"/>`
|
|
4
|
-
* Regex patterns use /slashes/: `<get path="/\.test\.js$/" preview/>`
|
|
5
|
-
* Adding `preview` shows matches without making changes
|
|
6
|
-
* Chain multiple replacements: `s/old/new/ s/foo/bar/`
|
|
7
|
-
Example: <get path="src/**/*.js" body="TODO" preview/> (list js files containing TODO)
|
|
8
|
-
Example: <store path="src/**/*.test.js"/> (store all test files)
|
|
9
|
-
Example: <rm path="known://temp_*" preview/> (preview which temp entries would be deleted)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { parseEditContent } from "./edits.js";
|
|
3
2
|
import HeuristicMatcher, { generatePatch } from "./matcher.js";
|
|
4
3
|
import { normalizeAttrs } from "./normalize.js";
|
|
@@ -34,10 +33,8 @@ export default class Hedberg {
|
|
|
34
33
|
generatePatch,
|
|
35
34
|
};
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
content ? `${content}\n\n${docs}` : docs,
|
|
40
|
-
);
|
|
36
|
+
// Patterns documentation distributed to individual tool docs.
|
|
37
|
+
// Hedberg has no model-facing docs of its own.
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
/**
|
|
@@ -120,7 +120,7 @@ export default class HeuristicMatcher {
|
|
|
120
120
|
patch: null,
|
|
121
121
|
warning: null,
|
|
122
122
|
error:
|
|
123
|
-
"
|
|
123
|
+
"SEARCH blocks are matched literally, not as a pattern. Could not find the SEARCH block in the file.",
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
2
|
+
import xpath from "xpath";
|
|
2
3
|
|
|
3
4
|
export const deterministic = true;
|
|
4
5
|
|
|
@@ -250,11 +251,10 @@ function compile(pattern) {
|
|
|
250
251
|
|
|
251
252
|
function evalXpath(expr, string) {
|
|
252
253
|
try {
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
const node =
|
|
257
|
-
if (!node) return null;
|
|
254
|
+
const doc = new DOMParser().parseFromString(string, "text/xml");
|
|
255
|
+
const nodes = xpath.select(expr, doc);
|
|
256
|
+
if (!nodes || nodes.length === 0) return null;
|
|
257
|
+
const node = nodes[0];
|
|
258
258
|
return { match: node.textContent, node };
|
|
259
259
|
} catch {
|
|
260
260
|
return null;
|
package/src/plugins/helpers.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function storePatternResult(
|
|
|
10
10
|
path,
|
|
11
11
|
bodyFilter,
|
|
12
12
|
matches,
|
|
13
|
-
preview = false,
|
|
13
|
+
{ preview = false, loopId = null } = {},
|
|
14
14
|
) {
|
|
15
15
|
const slug = await store.slugPath(runId, scheme, path);
|
|
16
16
|
const filter = bodyFilter ? ` body="${bodyFilter}"` : "";
|
|
@@ -18,5 +18,5 @@ export async function storePatternResult(
|
|
|
18
18
|
const listing = matches.map((m) => `${m.path} (${m.tokens_full})`).join("\n");
|
|
19
19
|
const prefix = preview ? "PREVIEW " : "";
|
|
20
20
|
const body = `${prefix}${scheme} path="${path}"${filter}: ${matches.length} matched (${total} tokens)\n${listing}`;
|
|
21
|
-
await store.upsert(runId, turn, slug, body,
|
|
21
|
+
await store.upsert(runId, turn, slug, body, 200, { loopId });
|
|
22
22
|
}
|
package/src/plugins/index.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { readdir, stat } from "node:fs/promises";
|
|
3
4
|
import { basename, join } from "node:path";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
import PluginContext from "../hooks/PluginContext.js";
|
|
6
7
|
|
|
8
|
+
let globalPrefix;
|
|
9
|
+
function getGlobalPrefix() {
|
|
10
|
+
globalPrefix ??= execSync("npm prefix -g", { encoding: "utf8" }).trim();
|
|
11
|
+
return globalPrefix;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
const instances = new Map();
|
|
8
15
|
|
|
9
16
|
/**
|
|
@@ -41,14 +48,11 @@ const AUDIT_SCHEMES = [
|
|
|
41
48
|
*/
|
|
42
49
|
export async function initPlugins(db, store, hooks) {
|
|
43
50
|
for (const name of AUDIT_SCHEMES) {
|
|
44
|
-
|
|
51
|
+
await db.upsert_scheme.run({
|
|
45
52
|
name,
|
|
46
|
-
fidelity: ["ask", "act", "progress"].includes(name) ? "full" : "null",
|
|
47
53
|
model_visible: ["ask", "act", "progress"].includes(name) ? 1 : 0,
|
|
48
|
-
valid_states: JSON.stringify(["info"]),
|
|
49
54
|
category: "audit",
|
|
50
|
-
};
|
|
51
|
-
await db.upsert_scheme.run(scheme);
|
|
55
|
+
});
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
for (const ctx of instances.values()) {
|
|
@@ -71,28 +75,37 @@ export async function initPlugins(db, store, hooks) {
|
|
|
71
75
|
if (registered.has(toolName)) continue;
|
|
72
76
|
await db.upsert_scheme.run({
|
|
73
77
|
name: toolName,
|
|
74
|
-
fidelity: "full",
|
|
75
78
|
model_visible: 1,
|
|
76
|
-
valid_states: JSON.stringify([
|
|
77
|
-
"full",
|
|
78
|
-
"proposed",
|
|
79
|
-
"pass",
|
|
80
|
-
"rejected",
|
|
81
|
-
"error",
|
|
82
|
-
"info",
|
|
83
|
-
]),
|
|
84
79
|
category: "result",
|
|
85
80
|
});
|
|
86
81
|
}
|
|
87
82
|
}
|
|
88
83
|
}
|
|
89
84
|
|
|
85
|
+
function resolvePlugin(packageName) {
|
|
86
|
+
// Check local node_modules first, then global
|
|
87
|
+
const localDir = join(process.cwd(), "node_modules", packageName);
|
|
88
|
+
if (existsSync(join(localDir, "package.json"))) return localDir;
|
|
89
|
+
const globalDir = join(getGlobalPrefix(), "lib", "node_modules", packageName);
|
|
90
|
+
if (existsSync(join(globalDir, "package.json"))) return globalDir;
|
|
91
|
+
throw new Error(`Package '${packageName}' not found locally or globally`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function importPlugin(packageName) {
|
|
95
|
+
const dir = resolvePlugin(packageName);
|
|
96
|
+
const pkg = JSON.parse(
|
|
97
|
+
(await import("node:fs")).readFileSync(join(dir, "package.json"), "utf8"),
|
|
98
|
+
);
|
|
99
|
+
const entry = pkg.exports?.["."] || pkg.main || "index.js";
|
|
100
|
+
return import(pathToFileURL(join(dir, entry)).href);
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
async function loadEnvPlugins(hooks) {
|
|
91
104
|
for (const [key, value] of Object.entries(process.env)) {
|
|
92
105
|
if (!key.startsWith("RUMMY_PLUGIN_") || !value) continue;
|
|
93
106
|
const name = key.replace("RUMMY_PLUGIN_", "").toLowerCase();
|
|
94
107
|
try {
|
|
95
|
-
const { default: Plugin } = await
|
|
108
|
+
const { default: Plugin } = await importPlugin(value);
|
|
96
109
|
if (typeof Plugin?.register === "function") {
|
|
97
110
|
await Plugin.register(hooks);
|
|
98
111
|
} else if (typeof Plugin === "function") {
|
|
@@ -17,7 +17,7 @@ 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
|
-
await store.upsert(runId, turn, "instructions://system", "",
|
|
20
|
+
await store.upsert(runId, turn, "instructions://system", "", 200, {
|
|
21
21
|
attributes: { persona: runRow?.persona || null },
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -5,11 +5,7 @@ export default class Known {
|
|
|
5
5
|
|
|
6
6
|
constructor(core) {
|
|
7
7
|
this.#core = core;
|
|
8
|
-
core.registerScheme({
|
|
9
|
-
fidelity: "turn",
|
|
10
|
-
validStates: ["full", "stored"],
|
|
11
|
-
category: "knowledge",
|
|
12
|
-
});
|
|
8
|
+
core.registerScheme({ category: "knowledge" });
|
|
13
9
|
core.on("handler", this.handler.bind(this));
|
|
14
10
|
core.on("full", this.full.bind(this));
|
|
15
11
|
core.filter("assembly.system", this.assembleKnown.bind(this), 100);
|
|
@@ -22,7 +18,7 @@ export default class Known {
|
|
|
22
18
|
async handler(entry, rummy) {
|
|
23
19
|
const { entries: store, sequence: turn, runId } = rummy;
|
|
24
20
|
const target = entry.attributes.path || entry.resultPath;
|
|
25
|
-
await store.upsert(runId, turn, target, entry.body,
|
|
21
|
+
await store.upsert(runId, turn, target, entry.body, 200);
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
full(entry) {
|
|
@@ -40,18 +36,20 @@ export default class Known {
|
|
|
40
36
|
if (entries.length === 0) return content;
|
|
41
37
|
|
|
42
38
|
// Rows arrive pre-sorted by SQL: skill → index → summary → full, then by recency
|
|
43
|
-
const
|
|
39
|
+
const demotedSet = new Set(ctx.demoted || []);
|
|
40
|
+
const lines = entries.map((e) => renderKnownTag(e, demotedSet));
|
|
44
41
|
return `${content}\n\n<knowns>\n${lines.join("\n")}\n</knowns>`;
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
function renderKnownTag(entry) {
|
|
45
|
+
function renderKnownTag(entry, demotedSet) {
|
|
49
46
|
const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
|
|
50
|
-
const
|
|
47
|
+
const status = entry.status ? ` status="${entry.status}"` : "";
|
|
48
|
+
const flag = demotedSet?.has(entry.path) ? " demoted" : "";
|
|
51
49
|
|
|
52
50
|
if (entry.body) {
|
|
53
|
-
return `<known path="${entry.path}"${
|
|
51
|
+
return `<known path="${entry.path}"${status}${tokens}${flag}>${entry.body}</known>`;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
return `<known path="${entry.path}"${
|
|
54
|
+
return `<known path="${entry.path}"${status}${tokens}${flag}/>`;
|
|
57
55
|
}
|
package/src/plugins/mv/mv.js
CHANGED
|
@@ -6,9 +6,7 @@ export default class Mv {
|
|
|
6
6
|
|
|
7
7
|
constructor(core) {
|
|
8
8
|
this.#core = core;
|
|
9
|
-
core.registerScheme(
|
|
10
|
-
validStates: ["full", "proposed", "pass", "rejected", "error", "pattern"],
|
|
11
|
-
});
|
|
9
|
+
core.registerScheme();
|
|
12
10
|
core.on("handler", this.handler.bind(this));
|
|
13
11
|
core.on("full", this.full.bind(this));
|
|
14
12
|
core.on("summary", this.summary.bind(this));
|
|
@@ -19,7 +17,7 @@ export default class Mv {
|
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
async handler(entry, rummy) {
|
|
22
|
-
const { entries: store, sequence: turn, runId } = rummy;
|
|
20
|
+
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
23
21
|
const { path, to } = entry.attributes;
|
|
24
22
|
|
|
25
23
|
const source = await store.getBody(runId, path);
|
|
@@ -34,14 +32,16 @@ export default class Mv {
|
|
|
34
32
|
|
|
35
33
|
const body = `${path} ${to}`;
|
|
36
34
|
if (destScheme === null) {
|
|
37
|
-
await store.upsert(runId, turn, entry.resultPath, body,
|
|
35
|
+
await store.upsert(runId, turn, entry.resultPath, body, 202, {
|
|
38
36
|
attributes: { from: path, to, isMove: true, warning },
|
|
37
|
+
loopId,
|
|
39
38
|
});
|
|
40
39
|
} else {
|
|
41
|
-
await store.upsert(runId, turn, to, source,
|
|
40
|
+
await store.upsert(runId, turn, to, source, 200, { loopId });
|
|
42
41
|
await store.remove(runId, path);
|
|
43
|
-
await store.upsert(runId, turn, entry.resultPath, body,
|
|
42
|
+
await store.upsert(runId, turn, entry.resultPath, body, 200, {
|
|
44
43
|
attributes: { from: path, to, isMove: true, warning },
|
|
44
|
+
loopId,
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -11,32 +11,33 @@ export default class Previous {
|
|
|
11
11
|
|
|
12
12
|
const entries = ctx.rows.filter(
|
|
13
13
|
(r) =>
|
|
14
|
-
(r.category === "result" ||
|
|
14
|
+
(r.category === "result" ||
|
|
15
|
+
r.category === "structural" ||
|
|
16
|
+
r.category === "prompt") &&
|
|
15
17
|
r.source_turn < ctx.loopStartTurn,
|
|
16
18
|
);
|
|
17
19
|
if (entries.length === 0) return content;
|
|
18
20
|
|
|
19
21
|
const lines = await Promise.all(
|
|
20
|
-
entries.map((e) => renderToolTag(e,
|
|
22
|
+
entries.map((e) => renderToolTag(e, this.#core)),
|
|
21
23
|
);
|
|
22
24
|
return `${content}\n\n<previous>\n${lines.join("\n")}\n</previous>`;
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
async function renderToolTag(entry,
|
|
28
|
+
async function renderToolTag(entry, core) {
|
|
27
29
|
const attrs =
|
|
28
30
|
typeof entry.attributes === "string"
|
|
29
31
|
? JSON.parse(entry.attributes)
|
|
30
32
|
: entry.attributes;
|
|
31
33
|
|
|
32
34
|
const path = `${entry.scheme}://${attrs?.path || attrs?.file || attrs?.command || ""}`;
|
|
33
|
-
const status = entry.
|
|
35
|
+
const status = entry.status ? ` status="${entry.status}"` : "";
|
|
34
36
|
|
|
35
37
|
let body;
|
|
36
38
|
try {
|
|
37
39
|
body = await core.hooks.tools.view(entry.scheme, {
|
|
38
40
|
...entry,
|
|
39
|
-
fidelity,
|
|
40
41
|
attributes: attrs,
|
|
41
42
|
});
|
|
42
43
|
} catch {
|