@possumtech/rummy 0.5.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 -5
- 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 -330
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +229 -421
- package/src/agent/XmlParser.js +99 -33
- 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 -125
- 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 +29 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +135 -35
- package/src/hooks/ToolRegistry.js +21 -16
- 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 -25
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -25
- package/src/plugins/budget/budget.js +260 -88
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +29 -11
- package/src/plugins/cp/cpDoc.js +2 -15
- 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 +45 -6
- 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 -2
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +103 -48
- package/src/plugins/get/getDoc.js +2 -32
- 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 +42 -2
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +122 -9
- 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 +67 -36
- package/src/plugins/known/knownDoc.js +2 -17
- 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 +55 -22
- package/src/plugins/mv/mvDoc.js +2 -18
- 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 +58 -16
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +56 -12
- package/src/plugins/rm/rmDoc.js +2 -20
- 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 -75
- 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 +50 -6
- 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 -18
- 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 +129 -80
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +2 -15
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +47 -19
- package/src/plugins/unknown/unknownDoc.js +2 -21
- 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 -30
- 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/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/FIDELITY_CONTRACT.md +0 -172
- package/src/agent/KnownStore.js +0 -337
- package/src/agent/ResponseHealer.js +0 -241
- 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 -45
- 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 -56
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -43
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -27
package/src/plugins/mv/mv.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Entries from "../../agent/Entries.js";
|
|
2
2
|
import docs from "./mvDoc.js";
|
|
3
3
|
|
|
4
|
+
const LOG_ACTION_RE = /^log:\/\/turn_\d+\/(\w+)\//;
|
|
5
|
+
|
|
4
6
|
export default class Mv {
|
|
5
7
|
#core;
|
|
6
8
|
|
|
@@ -8,43 +10,56 @@ export default class Mv {
|
|
|
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.mv = 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] !== "mv") return;
|
|
25
|
+
if (!ctx.attrs?.isMove || !ctx.attrs?.from) return;
|
|
26
|
+
await ctx.entries.rm({ runId: ctx.runId, path: ctx.attrs.from });
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
async handler(entry, rummy) {
|
|
20
30
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
21
31
|
const { path, to } = entry.attributes;
|
|
22
|
-
const VALID = {
|
|
23
|
-
const
|
|
24
|
-
? entry.attributes.
|
|
32
|
+
const VALID = { visible: 1, summarized: 1, archived: 1 };
|
|
33
|
+
const visibility = VALID[entry.attributes.visibility]
|
|
34
|
+
? entry.attributes.visibility
|
|
25
35
|
: undefined;
|
|
26
36
|
|
|
27
|
-
//
|
|
28
|
-
if (
|
|
37
|
+
// Visibility-in-place: no destination, change visibility of matched entries
|
|
38
|
+
if (visibility && !to) {
|
|
29
39
|
const matches = await store.getEntriesByPattern(runId, path);
|
|
30
40
|
for (const match of matches)
|
|
31
|
-
await store.
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
await store.set({
|
|
42
|
+
runId: runId,
|
|
43
|
+
path: match.path,
|
|
44
|
+
visibility: visibility,
|
|
45
|
+
});
|
|
46
|
+
const label = `set to ${visibility}`;
|
|
47
|
+
await store.set({
|
|
34
48
|
runId,
|
|
35
49
|
turn,
|
|
36
|
-
entry.resultPath,
|
|
37
|
-
`${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
path: entry.resultPath,
|
|
51
|
+
body: `${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
52
|
+
state: "resolved",
|
|
53
|
+
visibility: "archived",
|
|
54
|
+
loopId,
|
|
55
|
+
});
|
|
41
56
|
return;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
const source = await store.getBody(runId, path);
|
|
45
60
|
if (source === null) return;
|
|
46
61
|
|
|
47
|
-
const destScheme =
|
|
62
|
+
const destScheme = Entries.scheme(to);
|
|
48
63
|
const existing = await store.getBody(runId, to);
|
|
49
64
|
const warning =
|
|
50
65
|
existing !== null && destScheme !== null
|
|
@@ -53,14 +68,32 @@ export default class Mv {
|
|
|
53
68
|
|
|
54
69
|
const body = `${path} ${to}`;
|
|
55
70
|
if (destScheme === null) {
|
|
56
|
-
await store.
|
|
71
|
+
await store.set({
|
|
72
|
+
runId,
|
|
73
|
+
turn,
|
|
74
|
+
path: entry.resultPath,
|
|
75
|
+
body,
|
|
76
|
+
state: "proposed",
|
|
57
77
|
attributes: { from: path, to, isMove: true, warning },
|
|
58
78
|
loopId,
|
|
59
79
|
});
|
|
60
80
|
} else {
|
|
61
|
-
await store.
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
await store.set({
|
|
82
|
+
runId,
|
|
83
|
+
turn,
|
|
84
|
+
path: to,
|
|
85
|
+
body: source,
|
|
86
|
+
state: "resolved",
|
|
87
|
+
visibility,
|
|
88
|
+
loopId,
|
|
89
|
+
});
|
|
90
|
+
await store.rm({ runId: runId, path: path });
|
|
91
|
+
await store.set({
|
|
92
|
+
runId,
|
|
93
|
+
turn,
|
|
94
|
+
path: entry.resultPath,
|
|
95
|
+
body,
|
|
96
|
+
state: "resolved",
|
|
64
97
|
attributes: { from: path, to, isMove: true, warning },
|
|
65
98
|
loopId,
|
|
66
99
|
});
|
|
@@ -68,7 +101,7 @@ export default class Mv {
|
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
full(entry) {
|
|
71
|
-
return `# mv ${entry.attributes.from
|
|
104
|
+
return `# mv ${entry.attributes.from} ${entry.attributes.to}`;
|
|
72
105
|
}
|
|
73
106
|
|
|
74
107
|
summary() {
|
package/src/plugins/mv/mvDoc.js
CHANGED
|
@@ -1,19 +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
|
-
[
|
|
6
|
-
'## <mv path="[source]">[destination]</mv> - Move or rename a file or entry',
|
|
7
|
-
],
|
|
8
|
-
[
|
|
9
|
-
'Example: <mv path="known://active_task">known://completed_task</mv>',
|
|
10
|
-
"Entry rename. Most common mv use case.",
|
|
11
|
-
],
|
|
12
|
-
['Example: <mv path="src/old_name.js">src/new_name.js</mv>', "File rename."],
|
|
13
|
-
[
|
|
14
|
-
'Example: <mv path="known://project/*" fidelity="demoted"/>',
|
|
15
|
-
"Batch fidelity change via pattern. No destination = fidelity in place.",
|
|
16
|
-
],
|
|
17
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
18
2
|
|
|
19
|
-
export default
|
|
3
|
+
export default loadDoc(import.meta.url, "mvDoc.md");
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## <mv path="[source]">[destination]</mv> - Move or rename a file or entry
|
|
2
|
+
|
|
3
|
+
Example: <mv path="known://active_task">known://completed_task</mv>
|
|
4
|
+
<!-- Entry rename. Most common mv use case. -->
|
|
5
|
+
|
|
6
|
+
Example: <mv path="src/old_name.js">src/new_name.js</mv>
|
|
7
|
+
<!-- File rename. -->
|
|
8
|
+
|
|
9
|
+
Example: <mv path="known://project/*" visibility="summarized"/>
|
|
10
|
+
<!-- Batch visibility change via pattern. No destination = visibility in place. -->
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ollama
|
|
2
|
+
|
|
3
|
+
Ollama LLM provider. Handles model aliases prefixed with `ollama/`
|
|
4
|
+
(e.g. `ollama/llama3.1:8b`).
|
|
5
|
+
|
|
6
|
+
## Env
|
|
7
|
+
|
|
8
|
+
- `OLLAMA_BASE_URL` — base URL (e.g. `http://localhost:11434`).
|
|
9
|
+
Plugin is inert if unset.
|
|
10
|
+
|
|
11
|
+
## Context Size
|
|
12
|
+
|
|
13
|
+
Calls `/api/show` for the requested model and scans `model_info` for
|
|
14
|
+
any `*.context_length` key. Retries up to 3× with exponential backoff
|
|
15
|
+
on non-Ollama transient errors.
|
|
@@ -1,19 +1,42 @@
|
|
|
1
|
-
import msg from "
|
|
1
|
+
import msg from "../../agent/messages.js";
|
|
2
2
|
|
|
3
|
-
|
|
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 = "ollama";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ollama LLM provider plugin. Registers with hooks.llm.providers if
|
|
10
|
+
* OLLAMA_BASE_URL is set; inert otherwise. Handles model aliases of the
|
|
11
|
+
* form `ollama/{modelName}` — e.g. `ollama/llama3.1:8b` or
|
|
12
|
+
* `ollama/library/qwen:7b` (Ollama accepts both bare and
|
|
13
|
+
* registry-qualified model names).
|
|
14
|
+
*/
|
|
15
|
+
export default class Ollama {
|
|
4
16
|
#baseUrl;
|
|
5
17
|
|
|
6
|
-
constructor(
|
|
18
|
+
constructor(core) {
|
|
19
|
+
const baseUrl = process.env.OLLAMA_BASE_URL;
|
|
20
|
+
if (!baseUrl) return;
|
|
7
21
|
this.#baseUrl = baseUrl;
|
|
22
|
+
|
|
23
|
+
const wireModel = (alias) => alias.split("/").slice(1).join("/");
|
|
24
|
+
|
|
25
|
+
core.hooks.llm.providers.push({
|
|
26
|
+
name: PROVIDER,
|
|
27
|
+
matches: (model) => model.split("/")[0] === PROVIDER,
|
|
28
|
+
completion: (messages, model, options) =>
|
|
29
|
+
this.#completion(messages, wireModel(model), options),
|
|
30
|
+
getContextSize: (model) => this.#getContextSize(wireModel(model)),
|
|
31
|
+
});
|
|
8
32
|
}
|
|
9
33
|
|
|
10
|
-
async completion(messages, model, options = {}) {
|
|
34
|
+
async #completion(messages, model, options = {}) {
|
|
11
35
|
const body = { model, messages, think: true };
|
|
12
36
|
if (options.temperature !== undefined)
|
|
13
37
|
body.temperature = options.temperature;
|
|
14
38
|
|
|
15
|
-
const
|
|
16
|
-
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
39
|
+
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT);
|
|
17
40
|
const signal = options.signal
|
|
18
41
|
? AbortSignal.any([options.signal, timeoutSignal])
|
|
19
42
|
: timeoutSignal;
|
|
@@ -34,29 +57,27 @@ export default class OllamaClient {
|
|
|
34
57
|
|
|
35
58
|
const data = await response.json();
|
|
36
59
|
|
|
37
|
-
for (const choice of data.choices
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
40
|
-
const parts = [
|
|
60
|
+
for (const choice of data.choices) {
|
|
61
|
+
const m = choice.message;
|
|
62
|
+
if (!m) continue;
|
|
63
|
+
const parts = [m.reasoning_content, m.reasoning, m.thinking].filter(
|
|
41
64
|
Boolean,
|
|
42
65
|
);
|
|
43
|
-
|
|
66
|
+
m.reasoning_content =
|
|
44
67
|
parts.length > 0 ? [...new Set(parts)].join("\n") : null;
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
return data;
|
|
48
71
|
}
|
|
49
72
|
|
|
50
|
-
async getContextSize(model) {
|
|
73
|
+
async #getContextSize(model) {
|
|
51
74
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
52
75
|
try {
|
|
53
76
|
const response = await fetch(`${this.#baseUrl}/api/show`, {
|
|
54
77
|
method: "POST",
|
|
55
78
|
headers: { "Content-Type": "application/json" },
|
|
56
79
|
body: JSON.stringify({ model }),
|
|
57
|
-
signal: AbortSignal.timeout(
|
|
58
|
-
Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000,
|
|
59
|
-
),
|
|
80
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
60
81
|
});
|
|
61
82
|
if (!response.ok) {
|
|
62
83
|
throw new Error(
|
|
@@ -67,9 +88,10 @@ export default class OllamaClient {
|
|
|
67
88
|
);
|
|
68
89
|
}
|
|
69
90
|
const data = await response.json();
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
if (data.model_info) {
|
|
92
|
+
for (const [key, value] of Object.entries(data.model_info)) {
|
|
93
|
+
if (key.endsWith(".context_length")) return value;
|
|
94
|
+
}
|
|
73
95
|
}
|
|
74
96
|
throw new Error(msg("error.ollama_no_context_length", { model }));
|
|
75
97
|
} catch (err) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# openai
|
|
2
|
+
|
|
3
|
+
OpenAI-compatible LLM provider. Handles any model whose alias doesn't
|
|
4
|
+
carry a provider prefix — the default fallback provider. Works with
|
|
5
|
+
OpenAI itself, llama.cpp, vLLM, and any other service that implements
|
|
6
|
+
the `/v1/chat/completions` and `/v1/models` shape.
|
|
7
|
+
|
|
8
|
+
## Env
|
|
9
|
+
|
|
10
|
+
- `OPENAI_BASE_URL` — base URL (e.g. `https://api.openai.com` or
|
|
11
|
+
`http://localhost:8080`). Plugin is inert if unset.
|
|
12
|
+
- `OPENAI_API_KEY` — bearer token (optional for local servers).
|
|
13
|
+
|
|
14
|
+
## Context Size
|
|
15
|
+
|
|
16
|
+
Probes `/props` first (llama.cpp runtime) for `n_ctx`, falls back to
|
|
17
|
+
`/v1/models` for the training context length.
|
|
@@ -0,0 +1,120 @@
|
|
|
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 = "openai";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OpenAI-compatible LLM provider plugin. Registers with hooks.llm.providers
|
|
10
|
+
* if OPENAI_BASE_URL is set in env; silently inert otherwise. Handles
|
|
11
|
+
* model aliases of the form `openai/{modelName}` — the first path
|
|
12
|
+
* segment picks the provider, the rest is whatever the API expects.
|
|
13
|
+
*/
|
|
14
|
+
export default class OpenAi {
|
|
15
|
+
#baseUrl;
|
|
16
|
+
#apiKey;
|
|
17
|
+
|
|
18
|
+
constructor(core) {
|
|
19
|
+
const baseUrl = process.env.OPENAI_BASE_URL;
|
|
20
|
+
if (!baseUrl) return;
|
|
21
|
+
this.#baseUrl = String(baseUrl).replace(/\/v1\/?$/, "");
|
|
22
|
+
this.#apiKey = process.env.OPENAI_API_KEY;
|
|
23
|
+
|
|
24
|
+
const wireModel = (alias) => alias.split("/").slice(1).join("/");
|
|
25
|
+
|
|
26
|
+
core.hooks.llm.providers.push({
|
|
27
|
+
name: PROVIDER,
|
|
28
|
+
matches: (model) => model.split("/")[0] === PROVIDER,
|
|
29
|
+
completion: (messages, model, options) =>
|
|
30
|
+
this.#completion(messages, wireModel(model), options),
|
|
31
|
+
getContextSize: (model) => this.#getContextSize(wireModel(model)),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async #completion(messages, model, options = {}) {
|
|
36
|
+
const body = { model, messages, think: true };
|
|
37
|
+
if (options.temperature !== undefined)
|
|
38
|
+
body.temperature = options.temperature;
|
|
39
|
+
|
|
40
|
+
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT);
|
|
41
|
+
const signal = options.signal
|
|
42
|
+
? AbortSignal.any([options.signal, timeoutSignal])
|
|
43
|
+
: timeoutSignal;
|
|
44
|
+
|
|
45
|
+
const headers = { "Content-Type": "application/json" };
|
|
46
|
+
if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
47
|
+
|
|
48
|
+
const response = await fetch(`${this.#baseUrl}/v1/chat/completions`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers,
|
|
51
|
+
body: JSON.stringify(body),
|
|
52
|
+
signal,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const error = await response.text();
|
|
57
|
+
throw new Error(
|
|
58
|
+
msg("error.openai_api", { status: `${response.status} - ${error}` }),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
|
|
64
|
+
for (const choice of data.choices) {
|
|
65
|
+
const m = choice.message;
|
|
66
|
+
if (!m) continue;
|
|
67
|
+
const parts = [m.reasoning_content, m.reasoning, m.thinking].filter(
|
|
68
|
+
Boolean,
|
|
69
|
+
);
|
|
70
|
+
m.reasoning_content =
|
|
71
|
+
parts.length > 0 ? [...new Set(parts)].join("\n") : null;
|
|
72
|
+
|
|
73
|
+
// Full reasoning dump is centralized in telemetry.js on every
|
|
74
|
+
// provider — keeping it out of provider plugins avoids double
|
|
75
|
+
// printing and per-provider drift.
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async #getContextSize(_model) {
|
|
82
|
+
const headers = { "Content-Type": "application/json" };
|
|
83
|
+
if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
84
|
+
|
|
85
|
+
// Try /props first — llama.cpp exposes runtime n_ctx here.
|
|
86
|
+
try {
|
|
87
|
+
const propsResponse = await fetch(`${this.#baseUrl}/props`, {
|
|
88
|
+
headers,
|
|
89
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
90
|
+
});
|
|
91
|
+
if (propsResponse.ok) {
|
|
92
|
+
const props = await propsResponse.json();
|
|
93
|
+
const runtimeCtx = props?.default_generation_settings?.n_ctx;
|
|
94
|
+
if (runtimeCtx) return runtimeCtx;
|
|
95
|
+
}
|
|
96
|
+
} catch (_err) {
|
|
97
|
+
// /props is a llama.cpp extension; absent on vanilla OpenAI.
|
|
98
|
+
// Fall through to /v1/models for the training-context-size hint.
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fall back to /v1/models for training context.
|
|
102
|
+
const response = await fetch(`${this.#baseUrl}/v1/models`, {
|
|
103
|
+
headers,
|
|
104
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
msg("error.openai_models_failed", {
|
|
109
|
+
status: response.status,
|
|
110
|
+
baseUrl: this.#baseUrl,
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const data = await response.json();
|
|
115
|
+
const model = data.data?.[0];
|
|
116
|
+
const ctx = model?.meta?.n_ctx_train || model?.context_length;
|
|
117
|
+
if (!ctx) throw new Error(msg("error.openai_no_context_length"));
|
|
118
|
+
return ctx;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# openrouter
|
|
2
|
+
|
|
3
|
+
OpenRouter LLM provider. Handles model aliases prefixed with
|
|
4
|
+
`openrouter/` (e.g. `openrouter/anthropic/claude-3-opus`). Strips the
|
|
5
|
+
provider segment and passes the rest (`publisher/model`) straight to
|
|
6
|
+
OpenRouter's API.
|
|
7
|
+
|
|
8
|
+
## Env
|
|
9
|
+
|
|
10
|
+
- `OPENROUTER_BASE_URL` — base URL (e.g. `https://openrouter.ai/api/v1`).
|
|
11
|
+
Plugin is inert if `OPENROUTER_API_KEY` or base URL is unset.
|
|
12
|
+
- `OPENROUTER_API_KEY` — bearer token.
|
|
13
|
+
- `RUMMY_HTTP_REFERER` / `RUMMY_X_TITLE` — attribution headers
|
|
14
|
+
OpenRouter uses for rankings.
|
|
15
|
+
|
|
16
|
+
## Reasoning Normalization
|
|
17
|
+
|
|
18
|
+
OpenRouter's response shape varies by underlying provider. The plugin
|
|
19
|
+
merges `reasoning_content` / `reasoning` / `thinking` /
|
|
20
|
+
`reasoning_details[].text` into a deduplicated `reasoning_content`
|
|
21
|
+
string on each choice's message.
|
|
22
|
+
|
|
23
|
+
## Context Size
|
|
24
|
+
|
|
25
|
+
Calls `/models` and reads `context_length` on the matching entry.
|
|
26
|
+
Cached per model for the plugin lifetime. If the endpoint fails or the
|
|
27
|
+
model is missing, the call throws — no hardcoded fallback.
|
|
@@ -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.
|