@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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import msg from "../../agent/messages.js";
|
|
2
|
+
|
|
3
|
+
const FETCH_TIMEOUT = Number(process.env.RUMMY_FETCH_TIMEOUT);
|
|
4
|
+
if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
|
|
5
|
+
|
|
6
|
+
const PROVIDER = "openrouter";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OpenRouter LLM provider plugin. Handles model aliases of the form
|
|
10
|
+
* `openrouter/{publisher}/{modelName}`. Strips only the provider
|
|
11
|
+
* segment — OpenRouter's own API expects the `publisher/model` form,
|
|
12
|
+
* so that's exactly what's passed through to it (e.g.
|
|
13
|
+
* `openrouter/anthropic/claude-3-opus` → API receives
|
|
14
|
+
* `anthropic/claude-3-opus`).
|
|
15
|
+
*
|
|
16
|
+
* Inert if OPENROUTER_API_KEY / OPENROUTER_BASE_URL aren't set.
|
|
17
|
+
*/
|
|
18
|
+
export default class OpenRouter {
|
|
19
|
+
#apiKey;
|
|
20
|
+
#baseUrl;
|
|
21
|
+
#contextCache = new Map();
|
|
22
|
+
|
|
23
|
+
constructor(core) {
|
|
24
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
25
|
+
const baseUrl = process.env.OPENROUTER_BASE_URL;
|
|
26
|
+
if (!apiKey || !baseUrl) return;
|
|
27
|
+
this.#apiKey = apiKey;
|
|
28
|
+
this.#baseUrl = baseUrl;
|
|
29
|
+
|
|
30
|
+
const wireModel = (alias) => alias.split("/").slice(1).join("/");
|
|
31
|
+
|
|
32
|
+
core.hooks.llm.providers.push({
|
|
33
|
+
name: PROVIDER,
|
|
34
|
+
matches: (model) => model.split("/")[0] === PROVIDER,
|
|
35
|
+
completion: (messages, model, options) =>
|
|
36
|
+
this.#completion(messages, wireModel(model), options),
|
|
37
|
+
getContextSize: (model) => this.#getContextSize(wireModel(model)),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async #completion(messages, model, options = {}) {
|
|
42
|
+
const body = { model, messages, include_reasoning: true };
|
|
43
|
+
if (options.temperature !== undefined)
|
|
44
|
+
body.temperature = options.temperature;
|
|
45
|
+
|
|
46
|
+
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT);
|
|
47
|
+
const signal = options.signal
|
|
48
|
+
? AbortSignal.any([options.signal, timeoutSignal])
|
|
49
|
+
: timeoutSignal;
|
|
50
|
+
|
|
51
|
+
const response = await fetch(`${this.#baseUrl}/chat/completions`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
"HTTP-Referer": process.env.RUMMY_HTTP_REFERER,
|
|
57
|
+
"X-Title": process.env.RUMMY_X_TITLE,
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify(body),
|
|
60
|
+
signal,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const error = await response.text();
|
|
65
|
+
if (response.status === 401 || response.status === 403) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
msg("error.openrouter_auth", {
|
|
68
|
+
status: `${response.status} - ${error}`,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
msg("error.openrouter_api", {
|
|
74
|
+
status: `${response.status} - ${error}`,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
|
|
80
|
+
for (const choice of data.choices) {
|
|
81
|
+
const cm = choice.message;
|
|
82
|
+
if (!cm) continue;
|
|
83
|
+
const details = cm.reasoning_details
|
|
84
|
+
? cm.reasoning_details.map((d) => d.text)
|
|
85
|
+
: [];
|
|
86
|
+
const parts = [
|
|
87
|
+
cm.reasoning_content,
|
|
88
|
+
cm.reasoning,
|
|
89
|
+
cm.thinking,
|
|
90
|
+
...details,
|
|
91
|
+
].filter(Boolean);
|
|
92
|
+
cm.reasoning_content =
|
|
93
|
+
parts.length > 0 ? [...new Set(parts)].join("\n") : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async #getContextSize(model) {
|
|
100
|
+
if (this.#contextCache.has(model)) return this.#contextCache.get(model);
|
|
101
|
+
|
|
102
|
+
const res = await fetch(`${this.#baseUrl}/models`, {
|
|
103
|
+
headers: { Authorization: `Bearer ${this.#apiKey}` },
|
|
104
|
+
signal: AbortSignal.timeout(5000),
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`OpenRouter /models returned ${res.status}; cannot resolve context size for "${model}".`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const data = await res.json();
|
|
112
|
+
const entry = data.data?.find((m) => m.id === model);
|
|
113
|
+
if (!entry?.context_length) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`OpenRouter /models has no context_length for "${model}".`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
this.#contextCache.set(model, entry.context_length);
|
|
119
|
+
return entry.context_length;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# persona {#persona_plugin}
|
|
2
|
+
|
|
3
|
+
Runtime persona management. A persona is free-form text that gets
|
|
4
|
+
prepended to the model's system prompt for a run.
|
|
5
|
+
|
|
6
|
+
## Files
|
|
7
|
+
|
|
8
|
+
- **persona.js** — RPC registration and persona file loading.
|
|
9
|
+
|
|
10
|
+
## RPC Methods
|
|
11
|
+
|
|
12
|
+
| Method | Params | Notes |
|
|
13
|
+
|--------|--------|-------|
|
|
14
|
+
| `persona/set` | `{ run, name?, text? }` | Set persona by filename (`${RUMMY_HOME}/personas/<name>.md`) or raw text. Pass neither to clear. |
|
|
15
|
+
| `listPersonas` | — | Return `[{name, path}]` for available persona files. |
|
|
16
|
+
|
|
17
|
+
## Behavior
|
|
18
|
+
|
|
19
|
+
Persona is stored on the run row (`runs.persona`). The instructions
|
|
20
|
+
plugin reads it during system-prompt assembly.
|
|
@@ -20,10 +20,13 @@ export default class Persona {
|
|
|
20
20
|
text = await loadFile(params.name);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// "Pass neither to clear" — empty string counts as clear too.
|
|
24
|
+
let persona = null;
|
|
25
|
+
if (text) persona = text;
|
|
23
26
|
await ctx.db.update_run_config.run({
|
|
24
27
|
id: runRow.id,
|
|
25
28
|
temperature: null,
|
|
26
|
-
persona
|
|
29
|
+
persona,
|
|
27
30
|
context_limit: null,
|
|
28
31
|
model: null,
|
|
29
32
|
});
|
|
@@ -44,14 +47,10 @@ export default class Persona {
|
|
|
44
47
|
handler: async () => {
|
|
45
48
|
const dir = configDir();
|
|
46
49
|
if (!dir) return [];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.map((f) => ({ name: f.replace(".md", ""), path: join(dir, f) }));
|
|
52
|
-
} catch {
|
|
53
|
-
return [];
|
|
54
|
-
}
|
|
50
|
+
const files = await fs.readdir(dir);
|
|
51
|
+
return files
|
|
52
|
+
.filter((f) => f.endsWith(".md"))
|
|
53
|
+
.map((f) => ({ name: f.replace(".md", ""), path: join(dir, f) }));
|
|
55
54
|
},
|
|
56
55
|
description: "List available persona files. Returns [{ name, path }].",
|
|
57
56
|
requiresInit: true,
|
|
@@ -68,11 +67,5 @@ function configDir() {
|
|
|
68
67
|
async function loadFile(name) {
|
|
69
68
|
const dir = configDir();
|
|
70
69
|
if (!dir) throw new Error("RUMMY_HOME not configured");
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
return await fs.readFile(path, "utf8");
|
|
74
|
-
} catch (err) {
|
|
75
|
-
if (err.code === "ENOENT") throw new Error(`Not found: ${path}`);
|
|
76
|
-
throw err;
|
|
77
|
-
}
|
|
70
|
+
return fs.readFile(join(dir, `${name}.md`), "utf8");
|
|
78
71
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# policy {#policy_plugin}
|
|
2
|
+
|
|
3
|
+
Per-invocation enforcement of ask-mode restrictions. Rejects
|
|
4
|
+
model-emitted commands that would mutate the filesystem when the run
|
|
5
|
+
was started in `ask` mode.
|
|
6
|
+
|
|
7
|
+
## Registration
|
|
8
|
+
|
|
9
|
+
- **Filter**: `entry.recording` (priority 1) — runs before a command
|
|
10
|
+
becomes an entry.
|
|
11
|
+
|
|
12
|
+
## Rejections (ask mode only)
|
|
13
|
+
|
|
14
|
+
- `<sh>` — any shell command.
|
|
15
|
+
- `<set path="file.txt">` — file-scheme writes (bare path, non-scheme).
|
|
16
|
+
- `<rm path="file.txt">` — file-scheme deletes.
|
|
17
|
+
- `<mv>` / `<cp>` into a file-scheme destination.
|
|
18
|
+
|
|
19
|
+
Each rejection logs via `error.log` and returns an entry with
|
|
20
|
+
`state: "failed"`, `outcome: "permission"` so it still appears in the
|
|
21
|
+
turn's audit trail.
|
|
@@ -1,44 +1,59 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Entries from "../../agent/Entries.js";
|
|
2
2
|
|
|
3
3
|
export default class Policy {
|
|
4
|
+
#core;
|
|
5
|
+
|
|
4
6
|
constructor(core) {
|
|
7
|
+
this.#core = core;
|
|
5
8
|
core.filter("entry.recording", this.#enforceAskMode.bind(this), 1);
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
async #reject(ctx, message) {
|
|
12
|
+
await this.#core.hooks.error.log.emit({
|
|
13
|
+
store: ctx.store,
|
|
14
|
+
runId: ctx.runId,
|
|
15
|
+
turn: ctx.turn,
|
|
16
|
+
loopId: ctx.loopId,
|
|
17
|
+
message,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
async #enforceAskMode(entry, ctx) {
|
|
9
22
|
if (ctx.mode !== "ask") return entry;
|
|
10
23
|
|
|
11
24
|
if (entry.scheme === "sh") {
|
|
12
|
-
|
|
13
|
-
return { ...entry,
|
|
25
|
+
await this.#reject(ctx, "Rejected <sh> in ask mode");
|
|
26
|
+
return { ...entry, state: "failed", outcome: "permission" };
|
|
14
27
|
}
|
|
15
28
|
|
|
16
29
|
if (entry.scheme === "set" && entry.attributes?.path) {
|
|
17
|
-
const scheme =
|
|
30
|
+
const scheme = Entries.scheme(entry.attributes.path);
|
|
18
31
|
if (scheme === null && entry.body) {
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
await this.#reject(
|
|
33
|
+
ctx,
|
|
34
|
+
`Rejected file edit to ${entry.attributes.path} in ask mode`,
|
|
21
35
|
);
|
|
22
|
-
return { ...entry,
|
|
36
|
+
return { ...entry, state: "failed", outcome: "permission" };
|
|
23
37
|
}
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
if (entry.scheme === "rm") {
|
|
27
41
|
const pathAttr = entry.attributes?.path || entry.path;
|
|
28
|
-
const scheme =
|
|
42
|
+
const scheme = Entries.scheme(pathAttr);
|
|
29
43
|
if (scheme === null) {
|
|
30
|
-
|
|
31
|
-
return { ...entry,
|
|
44
|
+
await this.#reject(ctx, `Rejected file rm of ${pathAttr} in ask mode`);
|
|
45
|
+
return { ...entry, state: "failed", outcome: "permission" };
|
|
32
46
|
}
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
if (entry.scheme === "mv" || entry.scheme === "cp") {
|
|
36
|
-
const destScheme =
|
|
50
|
+
const destScheme = Entries.scheme(entry.attributes?.to);
|
|
37
51
|
if (destScheme === null) {
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
await this.#reject(
|
|
53
|
+
ctx,
|
|
54
|
+
`Rejected ${entry.scheme} to file ${entry.attributes?.to} in ask mode`,
|
|
40
55
|
);
|
|
41
|
-
return { ...entry,
|
|
56
|
+
return { ...entry, state: "failed", outcome: "permission" };
|
|
42
57
|
}
|
|
43
58
|
}
|
|
44
59
|
|
|
@@ -3,16 +3,17 @@ export default class Prompt {
|
|
|
3
3
|
|
|
4
4
|
constructor(core) {
|
|
5
5
|
this.#core = core;
|
|
6
|
-
core.hooks.tools.onView("prompt", (entry) =>
|
|
7
|
-
|
|
6
|
+
core.hooks.tools.onView("prompt", (entry) => entry.body, "visible");
|
|
7
|
+
core.hooks.tools.onView(
|
|
8
|
+
"prompt",
|
|
9
|
+
(entry) => {
|
|
8
10
|
const limit = 500;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
11
|
+
const full = entry.body;
|
|
12
|
+
if (full.length <= limit) return full;
|
|
13
|
+
return `${full.slice(0, limit)}\n[truncated — promote to see the complete prompt]`;
|
|
14
|
+
},
|
|
15
|
+
"summarized",
|
|
16
|
+
);
|
|
16
17
|
core.on("turn.started", this.onTurnStarted.bind(this));
|
|
17
18
|
core.filter("assembly.user", this.assemblePrompt.bind(this), 300);
|
|
18
19
|
}
|
|
@@ -21,15 +22,23 @@ export default class Prompt {
|
|
|
21
22
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
22
23
|
|
|
23
24
|
if (!isContinuation && prompt) {
|
|
24
|
-
|
|
25
|
+
// prompt:// writable_by: ["plugin"] — explicit for clarity.
|
|
26
|
+
await store.set({
|
|
27
|
+
runId,
|
|
28
|
+
turn,
|
|
29
|
+
path: `prompt://${turn}`,
|
|
30
|
+
body: prompt,
|
|
31
|
+
state: "resolved",
|
|
25
32
|
attributes: { mode },
|
|
26
33
|
loopId,
|
|
34
|
+
writer: "plugin",
|
|
27
35
|
});
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
async assemblePrompt(content, ctx) {
|
|
32
|
-
const
|
|
40
|
+
const { rows, contextSize, toolSet } = ctx;
|
|
41
|
+
const promptEntry = rows.findLast(
|
|
33
42
|
(r) => r.category === "prompt" && r.scheme === "prompt",
|
|
34
43
|
);
|
|
35
44
|
|
|
@@ -37,15 +46,51 @@ export default class Prompt {
|
|
|
37
46
|
typeof promptEntry?.attributes === "string"
|
|
38
47
|
? JSON.parse(promptEntry.attributes)
|
|
39
48
|
: promptEntry?.attributes;
|
|
40
|
-
const mode = attrs?.mode
|
|
41
|
-
const body = promptEntry
|
|
42
|
-
const
|
|
43
|
-
?
|
|
44
|
-
:
|
|
45
|
-
const
|
|
49
|
+
const mode = attrs?.mode ? attrs.mode : ctx.type;
|
|
50
|
+
const body = promptEntry ? promptEntry.body : "";
|
|
51
|
+
const activeTools = toolSet
|
|
52
|
+
? new Set(toolSet)
|
|
53
|
+
: new Set(this.#core.hooks.tools.names);
|
|
54
|
+
const commands = this.#core.hooks.tools.advertisedNames
|
|
55
|
+
.filter((n) => activeTools.has(n))
|
|
56
|
+
.join(",");
|
|
46
57
|
let warn = "";
|
|
47
58
|
if (mode === "ask") warn = ' warn="File editing disallowed."';
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
// Surface the most recent prior-turn budget demotion as a
|
|
61
|
+
// `reverted="N"` attribute on <prompt>. Historical error
|
|
62
|
+
// entries sit in <log> but read as ambient noise; this signal
|
|
63
|
+
// is dynamic and always fresh — the model sees that its
|
|
64
|
+
// promotions last turn were reverted, in the same spot where
|
|
65
|
+
// it reads budget numbers.
|
|
66
|
+
let reverted = "";
|
|
67
|
+
const priorTurn = ctx.turn - 1;
|
|
68
|
+
if (priorTurn >= 1) {
|
|
69
|
+
const priorDemotion = rows.find((r) => {
|
|
70
|
+
if (!r.path.startsWith(`log://turn_${priorTurn}/error/`)) return false;
|
|
71
|
+
const attrs =
|
|
72
|
+
typeof r.attributes === "string"
|
|
73
|
+
? JSON.parse(r.attributes)
|
|
74
|
+
: r.attributes;
|
|
75
|
+
return attrs?.status === 413 && attrs?.demotedCount > 0;
|
|
76
|
+
});
|
|
77
|
+
if (priorDemotion) {
|
|
78
|
+
const attrs =
|
|
79
|
+
typeof priorDemotion.attributes === "string"
|
|
80
|
+
? JSON.parse(priorDemotion.attributes)
|
|
81
|
+
: priorDemotion.attributes;
|
|
82
|
+
reverted = ` reverted="${attrs.demotedCount}"`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const path = promptEntry ? ` path="${promptEntry.path}"` : "";
|
|
87
|
+
const visibility = promptEntry?.visibility
|
|
88
|
+
? ` visibility="${promptEntry.visibility}"`
|
|
89
|
+
: "";
|
|
90
|
+
const tokens =
|
|
91
|
+
promptEntry?.aTokens != null
|
|
92
|
+
? ` tokens="${promptEntry.aTokens}"`
|
|
93
|
+
: "";
|
|
94
|
+
return `${content}<prompt mode="${mode}"${path} commands="${commands}"${warn}${reverted}${visibility}${tokens}>${body}</prompt>`;
|
|
50
95
|
}
|
|
51
96
|
}
|
package/src/plugins/rm/README.md
CHANGED
package/src/plugins/rm/rm.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Entries from "../../agent/Entries.js";
|
|
2
2
|
import docs from "./rmDoc.js";
|
|
3
3
|
|
|
4
|
+
const LOG_ACTION_RE = /^log:\/\/turn_\d+\/(\w+)\//;
|
|
5
|
+
|
|
4
6
|
export default class Rm {
|
|
5
7
|
#core;
|
|
6
8
|
|
|
@@ -8,25 +10,50 @@ export default class Rm {
|
|
|
8
10
|
this.#core = core;
|
|
9
11
|
core.registerScheme();
|
|
10
12
|
core.on("handler", this.handler.bind(this));
|
|
11
|
-
core.on("
|
|
12
|
-
core.on("
|
|
13
|
+
core.on("visible", this.full.bind(this));
|
|
14
|
+
core.on("summarized", this.summary.bind(this));
|
|
13
15
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
14
16
|
docsMap.rm = docs;
|
|
15
17
|
return docsMap;
|
|
16
18
|
});
|
|
19
|
+
core.on("proposal.accepted", this.#onAccepted.bind(this));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async #onAccepted(ctx) {
|
|
23
|
+
const m = LOG_ACTION_RE.exec(ctx.path);
|
|
24
|
+
if (m?.[1] !== "rm") return;
|
|
25
|
+
const target = ctx.attrs?.path;
|
|
26
|
+
if (!target) return;
|
|
27
|
+
await ctx.entries.rm({ runId: ctx.runId, path: target });
|
|
28
|
+
if (ctx.projectRoot) {
|
|
29
|
+
const { unlink } = await import("node:fs/promises");
|
|
30
|
+
const { join } = await import("node:path");
|
|
31
|
+
try {
|
|
32
|
+
await unlink(join(ctx.projectRoot, target));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// File may already be absent — entry rm'd regardless.
|
|
35
|
+
if (err.code !== "ENOENT") throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
17
38
|
}
|
|
18
39
|
|
|
19
40
|
async handler(entry, rummy) {
|
|
20
41
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
21
42
|
const target = entry.attributes.path;
|
|
22
43
|
if (!target) {
|
|
23
|
-
await store.
|
|
44
|
+
await store.set({
|
|
45
|
+
runId,
|
|
46
|
+
turn,
|
|
47
|
+
path: entry.resultPath,
|
|
48
|
+
body: "",
|
|
49
|
+
state: "failed",
|
|
50
|
+
outcome: "validation",
|
|
24
51
|
attributes: { error: "path is required" },
|
|
25
52
|
loopId,
|
|
26
53
|
});
|
|
27
54
|
return;
|
|
28
55
|
}
|
|
29
|
-
const normalized =
|
|
56
|
+
const normalized = Entries.normalizePath(target);
|
|
30
57
|
const matches = await store.getEntriesByPattern(
|
|
31
58
|
runId,
|
|
32
59
|
normalized,
|
|
@@ -34,7 +61,13 @@ export default class Rm {
|
|
|
34
61
|
);
|
|
35
62
|
|
|
36
63
|
if (matches.length === 0) {
|
|
37
|
-
await store.
|
|
64
|
+
await store.set({
|
|
65
|
+
runId,
|
|
66
|
+
turn,
|
|
67
|
+
path: entry.resultPath,
|
|
68
|
+
body: "",
|
|
69
|
+
state: "failed",
|
|
70
|
+
outcome: "not_found",
|
|
38
71
|
attributes: { path: target, error: `${target} not found` },
|
|
39
72
|
loopId,
|
|
40
73
|
});
|
|
@@ -45,24 +78,35 @@ export default class Rm {
|
|
|
45
78
|
const schemeMatches = matches.filter((m) => m.scheme !== null);
|
|
46
79
|
|
|
47
80
|
// Scheme entries: remove all, write one aggregate result entry
|
|
48
|
-
for (const match of schemeMatches)
|
|
81
|
+
for (const match of schemeMatches)
|
|
82
|
+
await store.rm({ runId: runId, path: match.path });
|
|
49
83
|
if (schemeMatches.length > 0) {
|
|
50
84
|
const paths = schemeMatches.map((m) => m.path).join("\n");
|
|
51
|
-
await store.
|
|
85
|
+
await store.set({
|
|
86
|
+
runId,
|
|
87
|
+
turn,
|
|
88
|
+
path: entry.resultPath,
|
|
89
|
+
body: paths,
|
|
90
|
+
state: "resolved",
|
|
52
91
|
attributes: { path: target },
|
|
53
92
|
loopId,
|
|
54
93
|
});
|
|
55
94
|
}
|
|
56
95
|
|
|
57
|
-
// File entries: individual
|
|
96
|
+
// File entries: individual proposals (require user resolution)
|
|
58
97
|
if (fileMatches.length > 0 && schemeMatches.length > 0)
|
|
59
|
-
await store.
|
|
98
|
+
await store.rm({ runId: runId, path: entry.resultPath });
|
|
60
99
|
for (const match of fileMatches) {
|
|
61
100
|
const resultPath =
|
|
62
101
|
schemeMatches.length === 0 && fileMatches.length === 1
|
|
63
102
|
? entry.resultPath
|
|
64
|
-
: await store.
|
|
65
|
-
await store.
|
|
103
|
+
: await store.logPath(runId, turn, "rm", match.path);
|
|
104
|
+
await store.set({
|
|
105
|
+
runId,
|
|
106
|
+
turn,
|
|
107
|
+
path: resultPath,
|
|
108
|
+
body: match.path,
|
|
109
|
+
state: "proposed",
|
|
66
110
|
attributes: { path: match.path },
|
|
67
111
|
loopId,
|
|
68
112
|
});
|
|
@@ -74,7 +118,7 @@ export default class Rm {
|
|
|
74
118
|
return entry.body ? `${header}\n${entry.body}` : header;
|
|
75
119
|
}
|
|
76
120
|
|
|
77
|
-
summary(
|
|
78
|
-
return
|
|
121
|
+
summary() {
|
|
122
|
+
return "";
|
|
79
123
|
}
|
|
80
124
|
}
|
package/src/plugins/rm/rmDoc.js
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
// Text goes to the model. Rationale stays in source.
|
|
3
|
-
// Changing ANY line requires reading ALL rationales first.
|
|
4
|
-
const LINES = [
|
|
5
|
-
['## <rm path="[path]"/> - Remove a file or entry'],
|
|
6
|
-
['Example: <rm path="src/config.js"/>', "File removal. Simplest form."],
|
|
7
|
-
[
|
|
8
|
-
'Example: <rm path="known://config/deprecated_service"/>',
|
|
9
|
-
"Shows topic-hierarchy path convention.",
|
|
10
|
-
],
|
|
11
|
-
[
|
|
12
|
-
'Example: <rm path="known://temp_*" preview/>',
|
|
13
|
-
"Preview before deleting. Safety pattern for bulk operations.",
|
|
14
|
-
],
|
|
15
|
-
[
|
|
16
|
-
'* Permanent. Prefer <set fidelity="archive"/> to preserve for later retrieval',
|
|
17
|
-
"Nudges toward archive over rm.",
|
|
18
|
-
],
|
|
19
|
-
[
|
|
20
|
-
"* Use `preview` to check matches before pattern-based bulk deletion",
|
|
21
|
-
"Reinforces preview safety pattern.",
|
|
22
|
-
],
|
|
23
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
24
2
|
|
|
25
|
-
export default
|
|
3
|
+
export default loadDoc(import.meta.url, "rmDoc.md");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## <rm path="[path]"/> - Remove a file or entry
|
|
2
|
+
|
|
3
|
+
Example: <rm path="src/config.js"/>
|
|
4
|
+
<!-- File removal. Simplest form. -->
|
|
5
|
+
|
|
6
|
+
Example: <rm path="known://temp_*" preview/>
|
|
7
|
+
<!-- Preview before deleting. Safety pattern for bulk operations. -->
|
|
8
|
+
|
|
9
|
+
* Permanent. Prefer <set path="..." visibility="archived"/> to preserve for later retrieval
|
|
10
|
+
<!-- Nudges toward archive over rm. Path attr included so the model sees a complete invocation shape, not a fragment. -->
|
|
11
|
+
|
|
12
|
+
* `preview` shows what paths would be affected without performing the operation.
|
|
13
|
+
<!-- Canonical preview teaching lives here — rm is the most intuitive 'check before committing' case. Model generalizes to cp/mv/get by analogy. Advanced uses (e.g. archive rediscovery via <get preview>) belong in persona/skill docs, not here. -->
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# rpc
|
|
1
|
+
# rpc {#rpc_plugin}
|
|
2
2
|
|
|
3
3
|
Registers core RPC methods and provides automatic tool dispatch for
|
|
4
4
|
all registered tools.
|
|
@@ -29,4 +29,4 @@ all registered tools.
|
|
|
29
29
|
- `getRuns`, `getRun`
|
|
30
30
|
|
|
31
31
|
### Notifications
|
|
32
|
-
- `run/state`, `run/progress`, `ui/render`, `ui/notify`
|
|
32
|
+
- `run/state`, `run/progress`, `run/proposal`, `ui/render`, `ui/notify`, `stream/cancelled`
|