@possumtech/rummy 0.5.0 → 2.0.1
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 +42 -5
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +934 -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 +13 -11
- package/scriptify/ask_run.js +77 -0
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +476 -335
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +676 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -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 +280 -125
- package/src/agent/materializeContext.js +104 -0
- package/src/agent/runs.sql +29 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/tokens.js +6 -0
- 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 +139 -35
- package/src/hooks/ToolRegistry.js +21 -16
- package/src/llm/LlmProvider.js +66 -89
- package/src/llm/errors.js +21 -0
- package/src/llm/retry.js +63 -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 +306 -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 +244 -9
- package/src/plugins/instructions/instructions.md +33 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +38 -0
- package/src/plugins/instructions/instructions_106.md +21 -0
- package/src/plugins/instructions/instructions_107.md +10 -0
- package/src/plugins/instructions/instructions_108.md +0 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +68 -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 +129 -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 +64 -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 +525 -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 +83 -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/plugins/yolo/yolo.js +192 -0
- 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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# log {#log_plugin}
|
|
2
|
+
|
|
3
|
+
Assembles the `<log>` block in the user message: every
|
|
4
|
+
`category="logging"` entry across the entire run, rendered as XML tool
|
|
5
|
+
tags in v_model_context sort order.
|
|
6
|
+
|
|
7
|
+
## Registration
|
|
8
|
+
|
|
9
|
+
- **Filter**: `assembly.user` (priority 100) — contributes the `<log>`
|
|
10
|
+
block to the user packet.
|
|
11
|
+
|
|
12
|
+
## Rendering
|
|
13
|
+
|
|
14
|
+
Each logging entry renders with its scheme as the tag name (`<get>`,
|
|
15
|
+
`<set>`, `<search>`, `<rm>`, `<cp>`, `<mv>`, `<sh>`, `<env>`,
|
|
16
|
+
`<update>`, `<ask_user>`, `<error>`, `<budget>`). Attributes:
|
|
17
|
+
`path`, `turn`, `status`, `state`, `outcome`, `summary`, `visibility`,
|
|
18
|
+
`tokens`.
|
|
19
|
+
|
|
20
|
+
**`tokens=` invariant.** The value is always the full-visibility cost
|
|
21
|
+
of the thing the tag represents — never the log entry's own stub body
|
|
22
|
+
size. Resolution:
|
|
23
|
+
|
|
24
|
+
- If the log entry has `attrs.path` referencing a data entry (`get`,
|
|
25
|
+
`set`, `mv`, `cp`): `tokens=` is that target's tokens. Promotes the
|
|
26
|
+
audit record into a cost-accurate signal the model can plan against.
|
|
27
|
+
- If the action's log body itself IS the cost-bearing content
|
|
28
|
+
(`search`, `update`, `error`, `ask_user`): `tokens=` is the entry's
|
|
29
|
+
own body tokens.
|
|
30
|
+
- `sh` and `env` own multiple streaming channels (`sh://turn_N/{slug}_N`)
|
|
31
|
+
— no single target to point at. `tokens=` is omitted; the channels
|
|
32
|
+
render their own tokens in `<context>`.
|
|
33
|
+
|
|
34
|
+
## Behavior
|
|
35
|
+
|
|
36
|
+
No loop-boundary split. The `turn` attribute on every entry carries
|
|
37
|
+
when it happened; the model derives loop membership from the data if
|
|
38
|
+
it matters. One chronological log from turn 1 to now.
|
|
39
|
+
|
|
40
|
+
## Scheme invariant
|
|
41
|
+
|
|
42
|
+
Log entries (`log://turn_N/{action}/{slug}`) are audit records —
|
|
43
|
+
summary, exit status, references to where the data lives — and never
|
|
44
|
+
carry the payload itself. Payload for streaming actions lives under the
|
|
45
|
+
producer's own scheme (`sh://`, `env://`, future `search://`, etc.) at
|
|
46
|
+
`category=data`, and is rendered inside `<context>` by the known
|
|
47
|
+
plugin. Scheme determines category; data and logging never share a
|
|
48
|
+
scheme. See [scheme_category_split](#scheme_category_split).
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { stateToStatus } from "../../agent/httpStatus.js";
|
|
2
|
+
|
|
3
|
+
// Schemes whose log body is an action summary, not the cost-bearing
|
|
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).
|
|
9
|
+
const STREAM_NO_TOKENS = new Set(["sh", "env"]);
|
|
10
|
+
|
|
11
|
+
export default class Log {
|
|
12
|
+
#core;
|
|
13
|
+
|
|
14
|
+
constructor(core) {
|
|
15
|
+
this.#core = core;
|
|
16
|
+
core.filter("assembly.user", this.assembleLog.bind(this), 100);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async assembleLog(content, ctx) {
|
|
20
|
+
// Log includes action entries (scheme=log) AND prior prompts. The
|
|
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.
|
|
24
|
+
const latestPrompt = ctx.rows.findLast(
|
|
25
|
+
(r) => r.category === "prompt" && r.scheme === "prompt",
|
|
26
|
+
);
|
|
27
|
+
const entries = ctx.rows.filter((r) => {
|
|
28
|
+
if (r.category === "logging" && r.scheme === "log") return true;
|
|
29
|
+
if (r.category === "prompt" && r.scheme === "prompt") {
|
|
30
|
+
return r !== latestPrompt;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
});
|
|
34
|
+
if (entries.length === 0) return content;
|
|
35
|
+
const rowsByPath = new Map();
|
|
36
|
+
for (const r of ctx.rows) rowsByPath.set(r.path, r);
|
|
37
|
+
const lines = entries.map((e) => renderLogTag(e, rowsByPath));
|
|
38
|
+
return `${content}<log>\n${lines.join("\n")}\n</log>\n`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Log paths are log://turn_N/action/slug. The second segment is the
|
|
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.
|
|
46
|
+
function actionFromPath(path) {
|
|
47
|
+
if (path?.startsWith("prompt://")) return "prompt";
|
|
48
|
+
const match = path?.match(/^log:\/\/turn_\d+\/([^/]+)\//);
|
|
49
|
+
return match ? match[1] : "log";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderLogTag(entry, rowsByPath) {
|
|
53
|
+
const attrs =
|
|
54
|
+
typeof entry.attributes === "string"
|
|
55
|
+
? JSON.parse(entry.attributes)
|
|
56
|
+
: entry.attributes;
|
|
57
|
+
|
|
58
|
+
const action = actionFromPath(entry.path);
|
|
59
|
+
|
|
60
|
+
const statusValue =
|
|
61
|
+
attrs?.status != null
|
|
62
|
+
? attrs.status
|
|
63
|
+
: entry.state
|
|
64
|
+
? stateToStatus(entry.state, entry.outcome)
|
|
65
|
+
: null;
|
|
66
|
+
// Prompts are uniformly status=200 — uniform value carries no signal
|
|
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.
|
|
70
|
+
const status =
|
|
71
|
+
statusValue != null && action !== "prompt"
|
|
72
|
+
? ` status="${statusValue}"`
|
|
73
|
+
: "";
|
|
74
|
+
const outcomeAttr = entry.outcome ? ` outcome="${entry.outcome}"` : "";
|
|
75
|
+
// `tokens=` is the promotion premium (aTokens) of the thing this tag
|
|
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>.
|
|
83
|
+
const isSlice = attrs?.lineStart != null;
|
|
84
|
+
const targetEntry = attrs?.path ? rowsByPath.get(attrs.path) : null;
|
|
85
|
+
let tokenSource = null;
|
|
86
|
+
let lineSource = null;
|
|
87
|
+
if (STREAM_NO_TOKENS.has(action)) {
|
|
88
|
+
tokenSource = null;
|
|
89
|
+
lineSource = null;
|
|
90
|
+
} else if (isSlice) {
|
|
91
|
+
tokenSource = entry.aTokens;
|
|
92
|
+
lineSource = entry.vLines;
|
|
93
|
+
} else if (targetEntry) {
|
|
94
|
+
tokenSource = targetEntry.aTokens;
|
|
95
|
+
lineSource = targetEntry.vLines;
|
|
96
|
+
} else {
|
|
97
|
+
tokenSource = entry.aTokens;
|
|
98
|
+
lineSource = entry.vLines;
|
|
99
|
+
}
|
|
100
|
+
const tokens = tokenSource != null ? ` tokens="${tokenSource}"` : "";
|
|
101
|
+
const summary =
|
|
102
|
+
typeof attrs?.summary === "string"
|
|
103
|
+
? ` summary="${attrs.summary.slice(0, 80)}"`
|
|
104
|
+
: "";
|
|
105
|
+
const query =
|
|
106
|
+
typeof attrs?.query === "string" ? ` query="${attrs.query}"` : "";
|
|
107
|
+
const command =
|
|
108
|
+
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
|
+
const target = attrs?.path ? ` target="${attrs.path}"` : "";
|
|
113
|
+
// Slice reads tag the log entry with lineStart/lineEnd/totalLines so
|
|
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.
|
|
117
|
+
const lines = isSlice
|
|
118
|
+
? ` lines="${attrs.lineStart}-${attrs.lineEnd}/${attrs.totalLines}"`
|
|
119
|
+
: lineSource != null
|
|
120
|
+
? ` lines="${lineSource}"`
|
|
121
|
+
: "";
|
|
122
|
+
|
|
123
|
+
const attrStr = `${target}${status}${outcomeAttr}${query}${command}${summary}${lines}${tokens}`;
|
|
124
|
+
|
|
125
|
+
if (entry.body) {
|
|
126
|
+
return `<${action} path="${entry.path}"${attrStr}>${entry.body}</${action}>`;
|
|
127
|
+
}
|
|
128
|
+
return `<${action} path="${entry.path}"${attrStr}/>`;
|
|
129
|
+
}
|
package/src/plugins/mv/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# mv
|
|
1
|
+
# mv {#mv_plugin}
|
|
2
2
|
|
|
3
3
|
Moves (renames) an entry from one path to another within the K/V store.
|
|
4
4
|
|
|
@@ -15,5 +15,5 @@ Shows `mv {from} {to}`.
|
|
|
15
15
|
## Behavior
|
|
16
16
|
|
|
17
17
|
Warns if the destination already exists and will be overwritten. Uses
|
|
18
|
-
`
|
|
18
|
+
`Entries.scheme()` to determine scheme vs file paths. Source entry
|
|
19
19
|
is removed on successful scheme moves.
|
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.
|