@possumtech/rummy 2.0.1 → 2.1.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 +12 -7
- package/BENCH_ENVIRONMENT.md +230 -0
- package/CLIENT_INTERFACE.md +396 -0
- package/PLUGINS.md +93 -1
- package/SPEC.md +305 -28
- package/bin/postinstall.js +2 -2
- package/bin/rummy.js +2 -2
- package/last_run.txt +5617 -0
- package/migrations/001_initial_schema.sql +2 -1
- package/package.json +6 -2
- package/scriptify/cache_probe.js +66 -0
- package/scriptify/cache_probe_grok.js +74 -0
- package/service.js +22 -11
- package/src/agent/AgentLoop.js +33 -139
- package/src/agent/ContextAssembler.js +2 -9
- package/src/agent/Entries.js +36 -101
- package/src/agent/ProjectAgent.js +2 -9
- package/src/agent/TurnExecutor.js +45 -83
- package/src/agent/XmlParser.js +247 -273
- package/src/agent/budget.js +5 -28
- package/src/agent/config.js +38 -0
- package/src/agent/errors.js +7 -13
- package/src/agent/httpStatus.js +1 -19
- package/src/agent/known_store.sql +7 -2
- package/src/agent/materializeContext.js +12 -17
- package/src/agent/pathEncode.js +5 -0
- package/src/agent/rummyHome.js +9 -0
- package/src/agent/runs.sql +18 -0
- package/src/agent/tokens.js +2 -8
- package/src/hooks/HookRegistry.js +1 -16
- package/src/hooks/Hooks.js +8 -33
- package/src/hooks/PluginContext.js +3 -21
- package/src/hooks/RpcRegistry.js +1 -4
- package/src/hooks/RummyContext.js +2 -16
- package/src/hooks/ToolRegistry.js +5 -15
- package/src/llm/LlmProvider.js +28 -23
- package/src/llm/errors.js +41 -4
- package/src/llm/openaiStream.js +125 -0
- package/src/llm/retry.js +61 -15
- package/src/plugins/budget/budget.js +14 -81
- package/src/plugins/cli/README.md +87 -0
- package/src/plugins/cli/bin.js +61 -0
- package/src/plugins/cli/cli.js +120 -0
- package/src/plugins/env/README.md +2 -1
- package/src/plugins/env/env.js +4 -6
- package/src/plugins/env/envDoc.md +2 -2
- package/src/plugins/error/error.js +23 -23
- package/src/plugins/file/file.js +2 -22
- package/src/plugins/get/get.js +12 -34
- package/src/plugins/get/getDoc.md +5 -3
- package/src/plugins/hedberg/edits.js +1 -11
- package/src/plugins/hedberg/hedberg.js +3 -26
- package/src/plugins/hedberg/normalize.js +1 -5
- package/src/plugins/hedberg/patterns.js +4 -15
- package/src/plugins/hedberg/sed.js +1 -7
- package/src/plugins/helpers.js +28 -20
- package/src/plugins/index.js +25 -41
- package/src/plugins/instructions/README.md +18 -0
- package/src/plugins/instructions/instructions.js +13 -76
- package/src/plugins/instructions/instructions.md +19 -18
- package/src/plugins/instructions/instructions_104.md +5 -4
- package/src/plugins/instructions/instructions_105.md +16 -15
- package/src/plugins/instructions/instructions_106.md +15 -14
- package/src/plugins/instructions/instructions_107.md +13 -6
- package/src/plugins/known/README.md +26 -6
- package/src/plugins/known/known.js +36 -34
- package/src/plugins/log/README.md +2 -2
- package/src/plugins/log/log.js +6 -33
- package/src/plugins/ollama/ollama.js +50 -66
- package/src/plugins/openai/openai.js +26 -44
- package/src/plugins/openrouter/openrouter.js +28 -52
- package/src/plugins/policy/README.md +8 -2
- package/src/plugins/policy/policy.js +8 -21
- package/src/plugins/prompt/README.md +22 -0
- package/src/plugins/prompt/prompt.js +8 -16
- package/src/plugins/rm/rm.js +5 -2
- package/src/plugins/rm/rmDoc.md +4 -4
- package/src/plugins/rpc/README.md +2 -1
- package/src/plugins/rpc/rpc.js +51 -47
- package/src/plugins/set/README.md +5 -1
- package/src/plugins/set/set.js +23 -33
- package/src/plugins/set/setDoc.md +1 -1
- package/src/plugins/sh/README.md +2 -1
- package/src/plugins/sh/sh.js +5 -11
- package/src/plugins/sh/shDoc.md +2 -2
- package/src/plugins/stream/README.md +6 -5
- package/src/plugins/stream/stream.js +6 -35
- package/src/plugins/telemetry/telemetry.js +26 -19
- package/src/plugins/think/think.js +4 -7
- package/src/plugins/unknown/unknown.js +8 -13
- package/src/plugins/update/update.js +36 -35
- package/src/plugins/update/updateDoc.md +3 -3
- package/src/plugins/xai/xai.js +30 -20
- package/src/plugins/yolo/yolo.js +8 -41
- package/src/server/ClientConnection.js +17 -47
- package/src/server/SocketServer.js +14 -14
- package/src/server/protocol.js +1 -10
- package/src/sql/functions/slugify.js +5 -7
- package/src/sql/v_model_context.sql +4 -11
- package/turns/cli_1777462658211/turn_001.txt +772 -0
- package/turns/cli_1777462658211/turn_002.txt +606 -0
- package/turns/cli_1777462658211/turn_003.txt +667 -0
- package/turns/cli_1777462658211/turn_004.txt +297 -0
- package/turns/cli_1777462658211/turn_005.txt +301 -0
- package/turns/cli_1777462658211/turn_006.txt +262 -0
- package/turns/cli_1777465095132/turn_001.txt +715 -0
- package/turns/cli_1777465095132/turn_002.txt +236 -0
- package/turns/cli_1777465095132/turn_003.txt +287 -0
- package/turns/cli_1777465095132/turn_004.txt +694 -0
- package/turns/cli_1777465095132/turn_005.txt +422 -0
- package/turns/cli_1777465095132/turn_006.txt +365 -0
- package/turns/cli_1777465095132/turn_007.txt +885 -0
- package/turns/cli_1777465095132/turn_008.txt +1277 -0
- package/turns/cli_1777465095132/turn_009.txt +736 -0
|
@@ -12,10 +12,9 @@ export default class Known {
|
|
|
12
12
|
core.on("handler", this.handler.bind(this));
|
|
13
13
|
core.on("visible", this.full.bind(this));
|
|
14
14
|
core.on("summarized", this.summary.bind(this));
|
|
15
|
-
core.filter("assembly.
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
// model emits <known> directly out of habit.
|
|
15
|
+
core.filter("assembly.user", this.assembleSummarized.bind(this), 50);
|
|
16
|
+
core.filter("assembly.user", this.assembleVisible.bind(this), 75);
|
|
17
|
+
// Hidden tool: written via <set path="known://...">; handler tolerates direct <known>.
|
|
19
18
|
core.markHidden();
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -23,23 +22,20 @@ export default class Known {
|
|
|
23
22
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
24
23
|
if (!entry.body) return;
|
|
25
24
|
|
|
26
|
-
// Size gate
|
|
27
25
|
const entryTokens = countTokens(entry.body);
|
|
28
26
|
if (entryTokens > MAX_ENTRY_TOKENS) {
|
|
29
|
-
const rejectPath = await store.slugPath(runId, "known", entry.body);
|
|
30
27
|
await store.set({
|
|
31
28
|
runId,
|
|
32
29
|
turn,
|
|
33
|
-
|
|
30
|
+
loopId,
|
|
31
|
+
path: entry.resultPath,
|
|
34
32
|
body: `Entry too large (${entryTokens} tokens, max ${MAX_ENTRY_TOKENS}). Sort the information, ideas, or plans carefully into multiple entries.`,
|
|
35
33
|
state: "failed",
|
|
36
34
|
outcome: `overflow:${entryTokens}`,
|
|
37
|
-
loopId,
|
|
38
35
|
});
|
|
39
36
|
return;
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
// Resolve path: explicit or auto-generated slug
|
|
43
39
|
let knownPath = entry.attributes?.path;
|
|
44
40
|
if (knownPath && !knownPath.includes("://")) {
|
|
45
41
|
knownPath = `known://${knownPath}`;
|
|
@@ -53,9 +49,7 @@ export default class Known {
|
|
|
53
49
|
);
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
// Dedup:
|
|
57
|
-
// new body means "preserve the existing entry's body" (e.g. the
|
|
58
|
-
// model is updating attributes only).
|
|
52
|
+
// Dedup: existing path → update; empty body preserves existing body.
|
|
59
53
|
const existing = await store.getEntriesByPattern(runId, knownPath, null);
|
|
60
54
|
if (existing.length > 0) {
|
|
61
55
|
const nextBody = entry.body === "" ? existing[0].body : entry.body;
|
|
@@ -86,28 +80,41 @@ export default class Known {
|
|
|
86
80
|
return entry.body;
|
|
87
81
|
}
|
|
88
82
|
|
|
89
|
-
// Summarized
|
|
90
|
-
// doesn't lose the plot when budget auto-demotion kicks in on its
|
|
91
|
-
// own work. Anything larger gets capped so a pathologically big
|
|
92
|
-
// known doesn't saturate the packet at summarized visibility
|
|
93
|
-
// either. Matches the pattern on `<prompt>` summarized view.
|
|
83
|
+
// Summarized: first 500 chars; matches <prompt> summarized.
|
|
94
84
|
summary(entry) {
|
|
95
85
|
if (!entry.body) return "";
|
|
96
86
|
if (entry.body.length <= 500) return entry.body;
|
|
97
87
|
return `${entry.body.slice(0, 500)}\n[truncated — promote to see the full body]`;
|
|
98
88
|
}
|
|
99
89
|
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
// Identity-keyed summary lines: every data entry the run is tracking
|
|
91
|
+
// at visibility=visible or visibility=summarized.
|
|
92
|
+
async assembleSummarized(content, ctx) {
|
|
93
|
+
const entries = ctx.rows.filter(
|
|
94
|
+
(r) =>
|
|
95
|
+
r.category === "data" &&
|
|
96
|
+
(r.visibility === "visible" || r.visibility === "summarized"),
|
|
97
|
+
);
|
|
98
|
+
if (entries.length === 0) return content;
|
|
99
|
+
const lines = entries.map((e) =>
|
|
100
|
+
renderContextTag(e, e.sBody != null ? e.sBody : e.body),
|
|
101
|
+
);
|
|
102
|
+
return `${content}<summarized>\n${lines.join("\n")}\n</summarized>\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async assembleVisible(content, ctx) {
|
|
106
|
+
const entries = ctx.rows.filter(
|
|
107
|
+
(r) => r.category === "data" && r.visibility === "visible",
|
|
108
|
+
);
|
|
102
109
|
if (entries.length === 0) return content;
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
const lines = entries.map((e) =>
|
|
111
|
+
renderContextTag(e, e.vBody != null ? e.vBody : e.body),
|
|
112
|
+
);
|
|
113
|
+
return `${content}<visible>\n${lines.join("\n")}\n</visible>\n`;
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
|
|
109
|
-
function renderContextTag(entry,
|
|
110
|
-
// schemeOf() returns NULL / "" for bare file paths; translate for the tag.
|
|
117
|
+
function renderContextTag(entry, projectedBody) {
|
|
111
118
|
const tag = entry.scheme ? entry.scheme : "file";
|
|
112
119
|
const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
|
|
113
120
|
const tokens = entry.aTokens != null ? ` tokens="${entry.aTokens}"` : "";
|
|
@@ -129,21 +136,16 @@ function renderContextTag(entry, demotedSet) {
|
|
|
129
136
|
const stateAttr =
|
|
130
137
|
entry.state && entry.state !== "resolved" ? ` state="${entry.state}"` : "";
|
|
131
138
|
const outcomeAttr = entry.outcome ? ` outcome="${entry.outcome}"` : "";
|
|
132
|
-
const visibility =
|
|
133
|
-
? ` visibility="
|
|
134
|
-
: "";
|
|
135
|
-
const flag = demotedSet?.has(entry.path) ? " demoted" : "";
|
|
136
|
-
// Always render summary attribute on knowns — empty value hints the model
|
|
137
|
-
// it forgot to add searchable keywords.
|
|
139
|
+
const visibility =
|
|
140
|
+
entry.visibility === "archived" ? ` visibility="archived"` : "";
|
|
138
141
|
const summaryText =
|
|
139
142
|
typeof attrs?.summary === "string"
|
|
140
143
|
? attrs.summary.replace(/"/g, "'").slice(0, 80)
|
|
141
144
|
: "";
|
|
142
145
|
const summary = ` summary="${summaryText}"`;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return `<${tag} path="${entry.path}"${attrStr}>${entry.body}</${tag}>`;
|
|
146
|
+
const attrStr = `${turn}${status}${stateAttr}${outcomeAttr}${summary}${visibility}${tokens}${lines}`;
|
|
147
|
+
if (projectedBody) {
|
|
148
|
+
return `<${tag} path="${entry.path}"${attrStr}>${projectedBody}</${tag}>`;
|
|
147
149
|
}
|
|
148
150
|
return `<${tag} path="${entry.path}"${attrStr}/>`;
|
|
149
151
|
}
|
|
@@ -29,7 +29,7 @@ size. Resolution:
|
|
|
29
29
|
own body tokens.
|
|
30
30
|
- `sh` and `env` own multiple streaming channels (`sh://turn_N/{slug}_N`)
|
|
31
31
|
— no single target to point at. `tokens=` is omitted; the channels
|
|
32
|
-
render their own tokens in `<
|
|
32
|
+
render their own tokens in `<visible>`.
|
|
33
33
|
|
|
34
34
|
## Behavior
|
|
35
35
|
|
|
@@ -43,6 +43,6 @@ Log entries (`log://turn_N/{action}/{slug}`) are audit records —
|
|
|
43
43
|
summary, exit status, references to where the data lives — and never
|
|
44
44
|
carry the payload itself. Payload for streaming actions lives under the
|
|
45
45
|
producer's own scheme (`sh://`, `env://`, future `search://`, etc.) at
|
|
46
|
-
`category=data`, and is rendered inside `<
|
|
46
|
+
`category=data`, and is rendered inside `<visible>` by the known
|
|
47
47
|
plugin. Scheme determines category; data and logging never share a
|
|
48
48
|
scheme. See [scheme_category_split](#scheme_category_split).
|
package/src/plugins/log/log.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { stateToStatus } from "../../agent/httpStatus.js";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
// content. For these, the action's cost lives on a separate data entry
|
|
5
|
-
// (sh/env: streaming channels; set/mv/cp: the target entry). Report
|
|
6
|
-
// tokens from the target when we can resolve it (set/mv/cp via
|
|
7
|
-
// attrs.path); omit entirely for sh/env (multiple channels, no single
|
|
8
|
-
// target to point at).
|
|
3
|
+
// sh/env span multiple channels; channels render their own tokens in <visible>.
|
|
9
4
|
const STREAM_NO_TOKENS = new Set(["sh", "env"]);
|
|
10
5
|
|
|
11
6
|
export default class Log {
|
|
@@ -17,10 +12,7 @@ export default class Log {
|
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
async assembleLog(content, ctx) {
|
|
20
|
-
//
|
|
21
|
-
// most recent prompt is rendered separately by the prompt plugin
|
|
22
|
-
// as `<prompt>`; everything older lives in the log so the model
|
|
23
|
-
// can see the full question history across a sustained run.
|
|
15
|
+
// Includes prior prompts; the latest prompt is rendered separately as <prompt>.
|
|
24
16
|
const latestPrompt = ctx.rows.findLast(
|
|
25
17
|
(r) => r.category === "prompt" && r.scheme === "prompt",
|
|
26
18
|
);
|
|
@@ -39,10 +31,7 @@ export default class Log {
|
|
|
39
31
|
}
|
|
40
32
|
}
|
|
41
33
|
|
|
42
|
-
//
|
|
43
|
-
// action — the plugin/tool that produced this log entry (set, get,
|
|
44
|
-
// search, update, error, etc.). Used as the XML tag name. Prompt
|
|
45
|
-
// entries live at prompt://N; they render as <prompt> in history.
|
|
34
|
+
// Action segment of log://turn_N/action/slug → XML tag.
|
|
46
35
|
function actionFromPath(path) {
|
|
47
36
|
if (path?.startsWith("prompt://")) return "prompt";
|
|
48
37
|
const match = path?.match(/^log:\/\/turn_\d+\/([^/]+)\//);
|
|
@@ -63,23 +52,13 @@ function renderLogTag(entry, rowsByPath) {
|
|
|
63
52
|
: entry.state
|
|
64
53
|
? stateToStatus(entry.state, entry.outcome)
|
|
65
54
|
: null;
|
|
66
|
-
//
|
|
67
|
-
// and read as "settled, no action needed." Suppress so cultivation
|
|
68
|
-
// vocabulary (vary, demote, archive) applies to prompts the same
|
|
69
|
-
// way it applies to other log entries.
|
|
55
|
+
// Suppress status on prompts; uniform 200 carries no signal.
|
|
70
56
|
const status =
|
|
71
57
|
statusValue != null && action !== "prompt"
|
|
72
58
|
? ` status="${statusValue}"`
|
|
73
59
|
: "";
|
|
74
60
|
const outcomeAttr = entry.outcome ? ` outcome="${entry.outcome}"` : "";
|
|
75
|
-
//
|
|
76
|
-
// represents — what the model would free by demoting it. For actions
|
|
77
|
-
// that reference a separate data entry (get/set/mv/cp), resolve via
|
|
78
|
-
// attrs.path and report the target's aTokens. For actions whose log
|
|
79
|
-
// body IS the cost-bearing content (search/update/error/ask_user,
|
|
80
|
-
// plus <get> slice reads), use the log entry's own aTokens. sh/env
|
|
81
|
-
// span multiple channel entries and are omitted — the channels
|
|
82
|
-
// render their own tokens in <context>.
|
|
61
|
+
// tokens = aTokens of the thing this tag represents (target via attrs.path, else self).
|
|
83
62
|
const isSlice = attrs?.lineStart != null;
|
|
84
63
|
const targetEntry = attrs?.path ? rowsByPath.get(attrs.path) : null;
|
|
85
64
|
let tokenSource = null;
|
|
@@ -106,14 +85,8 @@ function renderLogTag(entry, rowsByPath) {
|
|
|
106
85
|
typeof attrs?.query === "string" ? ` query="${attrs.query}"` : "";
|
|
107
86
|
const command =
|
|
108
87
|
typeof attrs?.command === "string" ? ` command="${attrs.command}"` : "";
|
|
109
|
-
// target= is the path the action touched (e.g. the file/known that was
|
|
110
|
-
// set, the URL that was fetched). Plugins store it in attrs.path when
|
|
111
|
-
// they write the log entry.
|
|
112
88
|
const target = attrs?.path ? ` target="${attrs.path}"` : "";
|
|
113
|
-
// Slice reads
|
|
114
|
-
// the <get> tag surfaces `lines="a-b/total"` — a concrete handle for
|
|
115
|
-
// the model to re-issue or compare against another slice. Non-slice
|
|
116
|
-
// entries surface the simple `lines="N"` from the projected body.
|
|
89
|
+
// Slice reads emit lines="a-b/total"; others emit simple lines="N".
|
|
117
90
|
const lines = isSlice
|
|
118
91
|
? ` lines="${attrs.lineStart}-${attrs.lineEnd}/${attrs.totalLines}"`
|
|
119
92
|
: lineSource != null
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
+
import config from "../../agent/config.js";
|
|
1
2
|
import msg from "../../agent/messages.js";
|
|
3
|
+
import { chatCompletionStream } from "../../llm/openaiStream.js";
|
|
4
|
+
import { retryWithBackoff } from "../../llm/retry.js";
|
|
2
5
|
|
|
3
|
-
const FETCH_TIMEOUT =
|
|
4
|
-
if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
|
|
6
|
+
const { FETCH_TIMEOUT } = config;
|
|
5
7
|
|
|
6
8
|
const PROVIDER = "ollama";
|
|
7
9
|
|
|
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
|
-
*/
|
|
10
|
+
// Inert unless OLLAMA_BASE_URL is set; ollama/{model[/registry]} aliases.
|
|
15
11
|
export default class Ollama {
|
|
16
12
|
#baseUrl;
|
|
17
13
|
|
|
@@ -41,70 +37,58 @@ export default class Ollama {
|
|
|
41
37
|
? AbortSignal.any([options.signal, timeoutSignal])
|
|
42
38
|
: timeoutSignal;
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const data = await response.json();
|
|
59
|
-
|
|
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(
|
|
64
|
-
Boolean,
|
|
65
|
-
);
|
|
66
|
-
m.reasoning_content =
|
|
67
|
-
parts.length > 0 ? [...new Set(parts)].join("\n") : null;
|
|
40
|
+
try {
|
|
41
|
+
return await chatCompletionStream({
|
|
42
|
+
url: `${this.#baseUrl}/v1/chat/completions`,
|
|
43
|
+
headers: {},
|
|
44
|
+
body,
|
|
45
|
+
signal,
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err.status) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
msg("error.ollama_api", { status: `${err.status} - ${err.body}` }),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
68
54
|
}
|
|
69
|
-
|
|
70
|
-
return data;
|
|
71
55
|
}
|
|
72
56
|
|
|
73
57
|
async #getContextSize(model) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (!response.ok) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
msg("error.ollama_show_failed", {
|
|
85
|
-
status: response.status,
|
|
86
|
-
baseUrl: this.#baseUrl,
|
|
87
|
-
}),
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
const data = await response.json();
|
|
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
|
-
}
|
|
95
|
-
}
|
|
96
|
-
throw new Error(msg("error.ollama_no_context_length", { model }));
|
|
97
|
-
} catch (err) {
|
|
98
|
-
if (err.message.includes("Ollama")) throw err;
|
|
99
|
-
if (attempt < 2) {
|
|
100
|
-
await new Promise((r) => setTimeout(r, (attempt + 1) * 2000));
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
58
|
+
const fetchContext = async () => {
|
|
59
|
+
const response = await fetch(`${this.#baseUrl}/api/show`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
body: JSON.stringify({ model }),
|
|
63
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
103
66
|
throw new Error(
|
|
104
|
-
msg("error.
|
|
105
|
-
|
|
67
|
+
msg("error.ollama_show_failed", {
|
|
68
|
+
status: response.status,
|
|
69
|
+
baseUrl: this.#baseUrl,
|
|
70
|
+
}),
|
|
106
71
|
);
|
|
107
72
|
}
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
if (data.model_info) {
|
|
75
|
+
for (const [key, value] of Object.entries(data.model_info)) {
|
|
76
|
+
if (key.endsWith(".context_length")) return value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new Error(msg("error.ollama_no_context_length", { model }));
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
return await retryWithBackoff(fetchContext, {
|
|
83
|
+
deadlineMs: FETCH_TIMEOUT,
|
|
84
|
+
isRetryable: (err) => !err.message.includes("Ollama"),
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err.message.includes("Ollama")) throw err;
|
|
88
|
+
throw new Error(
|
|
89
|
+
msg("error.ollama_unreachable", { baseUrl: this.#baseUrl }),
|
|
90
|
+
{ cause: err },
|
|
91
|
+
);
|
|
108
92
|
}
|
|
109
93
|
}
|
|
110
94
|
}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
+
import config from "../../agent/config.js";
|
|
1
2
|
import msg from "../../agent/messages.js";
|
|
3
|
+
import { chatCompletionStream } from "../../llm/openaiStream.js";
|
|
2
4
|
|
|
3
|
-
const FETCH_TIMEOUT =
|
|
4
|
-
if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
|
|
5
|
+
const { FETCH_TIMEOUT } = config;
|
|
5
6
|
|
|
6
7
|
const PROVIDER = "openai";
|
|
7
8
|
|
|
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
|
-
*/
|
|
9
|
+
// Inert unless OPENAI_BASE_URL is set; openai/{model} aliases.
|
|
14
10
|
export default class OpenAi {
|
|
15
11
|
#baseUrl;
|
|
16
12
|
#apiKey;
|
|
@@ -42,47 +38,36 @@ export default class OpenAi {
|
|
|
42
38
|
? AbortSignal.any([options.signal, timeoutSignal])
|
|
43
39
|
: timeoutSignal;
|
|
44
40
|
|
|
45
|
-
const headers = {
|
|
41
|
+
const headers = {};
|
|
46
42
|
if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
44
|
+
try {
|
|
45
|
+
return await chatCompletionStream({
|
|
46
|
+
url: `${this.#baseUrl}/v1/chat/completions`,
|
|
47
|
+
headers,
|
|
48
|
+
body,
|
|
49
|
+
signal,
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.status) {
|
|
53
|
+
const wrapped = new Error(
|
|
54
|
+
msg("error.openai_api", { status: `${err.status} - ${err.body}` }),
|
|
55
|
+
{ cause: err },
|
|
56
|
+
);
|
|
57
|
+
wrapped.status = err.status;
|
|
58
|
+
wrapped.body = err.body;
|
|
59
|
+
wrapped.retryAfter = err.retryAfter;
|
|
60
|
+
throw wrapped;
|
|
61
|
+
}
|
|
62
|
+
throw err;
|
|
76
63
|
}
|
|
77
|
-
|
|
78
|
-
return data;
|
|
79
64
|
}
|
|
80
65
|
|
|
81
66
|
async #getContextSize(_model) {
|
|
82
67
|
const headers = { "Content-Type": "application/json" };
|
|
83
68
|
if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
84
69
|
|
|
85
|
-
//
|
|
70
|
+
// llama.cpp /props returns runtime n_ctx; absent on vanilla OpenAI.
|
|
86
71
|
try {
|
|
87
72
|
const propsResponse = await fetch(`${this.#baseUrl}/props`, {
|
|
88
73
|
headers,
|
|
@@ -93,10 +78,7 @@ export default class OpenAi {
|
|
|
93
78
|
const runtimeCtx = props?.default_generation_settings?.n_ctx;
|
|
94
79
|
if (runtimeCtx) return runtimeCtx;
|
|
95
80
|
}
|
|
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
|
-
}
|
|
81
|
+
} catch (_err) {}
|
|
100
82
|
|
|
101
83
|
// Fall back to /v1/models for training context.
|
|
102
84
|
const response = await fetch(`${this.#baseUrl}/v1/models`, {
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
+
import config from "../../agent/config.js";
|
|
1
2
|
import msg from "../../agent/messages.js";
|
|
3
|
+
import { chatCompletionStream } from "../../llm/openaiStream.js";
|
|
2
4
|
|
|
3
|
-
const FETCH_TIMEOUT =
|
|
4
|
-
if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
|
|
5
|
+
const { FETCH_TIMEOUT } = config;
|
|
5
6
|
|
|
6
7
|
const PROVIDER = "openrouter";
|
|
7
8
|
|
|
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
|
-
*/
|
|
9
|
+
// Inert unless OPENROUTER_API_KEY+OPENROUTER_BASE_URL set; openrouter/{publisher}/{model} aliases.
|
|
18
10
|
export default class OpenRouter {
|
|
19
11
|
#apiKey;
|
|
20
12
|
#baseUrl;
|
|
@@ -48,52 +40,36 @@ export default class OpenRouter {
|
|
|
48
40
|
? AbortSignal.any([options.signal, timeoutSignal])
|
|
49
41
|
: timeoutSignal;
|
|
50
42
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
});
|
|
43
|
+
const headers = {
|
|
44
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
45
|
+
"HTTP-Referer": process.env.RUMMY_HTTP_REFERER,
|
|
46
|
+
"X-Title": process.env.RUMMY_X_TITLE,
|
|
47
|
+
};
|
|
62
48
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
try {
|
|
50
|
+
return await chatCompletionStream({
|
|
51
|
+
url: `${this.#baseUrl}/chat/completions`,
|
|
52
|
+
headers,
|
|
53
|
+
body,
|
|
54
|
+
signal,
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err.status === 401 || err.status === 403) {
|
|
66
58
|
throw new Error(
|
|
67
59
|
msg("error.openrouter_auth", {
|
|
68
|
-
status: `${
|
|
60
|
+
status: `${err.status} - ${err.body}`,
|
|
69
61
|
}),
|
|
70
62
|
);
|
|
71
63
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
64
|
+
if (err.status) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
msg("error.openrouter_api", {
|
|
67
|
+
status: `${err.status} - ${err.body}`,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
throw err;
|
|
94
72
|
}
|
|
95
|
-
|
|
96
|
-
return data;
|
|
97
73
|
}
|
|
98
74
|
|
|
99
75
|
async #getContextSize(model) {
|
|
@@ -101,7 +77,7 @@ export default class OpenRouter {
|
|
|
101
77
|
|
|
102
78
|
const res = await fetch(`${this.#baseUrl}/models`, {
|
|
103
79
|
headers: { Authorization: `Bearer ${this.#apiKey}` },
|
|
104
|
-
signal: AbortSignal.timeout(
|
|
80
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
105
81
|
});
|
|
106
82
|
if (!res.ok) {
|
|
107
83
|
throw new Error(
|
|
@@ -6,8 +6,14 @@ was started in `ask` mode.
|
|
|
6
6
|
|
|
7
7
|
## Registration
|
|
8
8
|
|
|
9
|
-
- **Filter**: `entry.recording` (priority 1) —
|
|
10
|
-
|
|
9
|
+
- **Filter**: `entry.recording` (priority 1) — the validation /
|
|
10
|
+
transform hook in TurnExecutor's RECORD phase. Runs after the
|
|
11
|
+
command is parsed but before the audit row is committed. Returning
|
|
12
|
+
an object with `state: "failed"` (or `"cancelled"`) short-circuits
|
|
13
|
+
recording and skips DISPATCH for that command. Plugins may also
|
|
14
|
+
return a transformed entry (modified body, attributes, path) for
|
|
15
|
+
the recorder to commit. Filter signature:
|
|
16
|
+
`(entry, { store, runId, turn, loopId, mode })`.
|
|
11
17
|
|
|
12
18
|
## Rejections (ask mode only)
|
|
13
19
|
|
|
@@ -1,39 +1,28 @@
|
|
|
1
1
|
import Entries from "../../agent/Entries.js";
|
|
2
2
|
|
|
3
3
|
export default class Policy {
|
|
4
|
-
#core;
|
|
5
|
-
|
|
6
4
|
constructor(core) {
|
|
7
|
-
this.#core = core;
|
|
8
5
|
core.filter("entry.recording", this.#enforceAskMode.bind(this), 1);
|
|
9
6
|
}
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
store: ctx.store,
|
|
14
|
-
runId: ctx.runId,
|
|
15
|
-
turn: ctx.turn,
|
|
16
|
-
loopId: ctx.loopId,
|
|
17
|
-
message,
|
|
18
|
-
});
|
|
8
|
+
#fail(entry, body) {
|
|
9
|
+
return { ...entry, body, state: "failed", outcome: "permission" };
|
|
19
10
|
}
|
|
20
11
|
|
|
21
12
|
async #enforceAskMode(entry, ctx) {
|
|
22
13
|
if (ctx.mode !== "ask") return entry;
|
|
23
14
|
|
|
24
15
|
if (entry.scheme === "sh") {
|
|
25
|
-
|
|
26
|
-
return { ...entry, state: "failed", outcome: "permission" };
|
|
16
|
+
return this.#fail(entry, "Rejected <sh> in ask mode");
|
|
27
17
|
}
|
|
28
18
|
|
|
29
19
|
if (entry.scheme === "set" && entry.attributes?.path) {
|
|
30
20
|
const scheme = Entries.scheme(entry.attributes.path);
|
|
31
21
|
if (scheme === null && entry.body) {
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
return this.#fail(
|
|
23
|
+
entry,
|
|
34
24
|
`Rejected file edit to ${entry.attributes.path} in ask mode`,
|
|
35
25
|
);
|
|
36
|
-
return { ...entry, state: "failed", outcome: "permission" };
|
|
37
26
|
}
|
|
38
27
|
}
|
|
39
28
|
|
|
@@ -41,19 +30,17 @@ export default class Policy {
|
|
|
41
30
|
const pathAttr = entry.attributes?.path || entry.path;
|
|
42
31
|
const scheme = Entries.scheme(pathAttr);
|
|
43
32
|
if (scheme === null) {
|
|
44
|
-
|
|
45
|
-
return { ...entry, state: "failed", outcome: "permission" };
|
|
33
|
+
return this.#fail(entry, `Rejected file rm of ${pathAttr} in ask mode`);
|
|
46
34
|
}
|
|
47
35
|
}
|
|
48
36
|
|
|
49
37
|
if (entry.scheme === "mv" || entry.scheme === "cp") {
|
|
50
38
|
const destScheme = Entries.scheme(entry.attributes?.to);
|
|
51
39
|
if (destScheme === null) {
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
return this.#fail(
|
|
41
|
+
entry,
|
|
54
42
|
`Rejected ${entry.scheme} to file ${entry.attributes?.to} in ask mode`,
|
|
55
43
|
);
|
|
56
|
-
return { ...entry, state: "failed", outcome: "permission" };
|
|
57
44
|
}
|
|
58
45
|
}
|
|
59
46
|
|