@possumtech/rummy 2.1.0 → 2.2.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 +40 -15
- package/.xai.key +1 -0
- package/PLUGINS.md +169 -53
- package/README.md +38 -32
- package/SPEC.md +366 -179
- package/bin/digest.js +1097 -0
- package/biome/no-fallbacks.grit +2 -2
- package/gemini.key +1 -0
- package/lang/en.json +10 -1
- package/migrations/001_initial_schema.sql +9 -2
- package/package.json +19 -8
- package/service.js +1 -0
- package/src/agent/AgentLoop.js +76 -26
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/Entries.js +238 -60
- package/src/agent/ProjectAgent.js +44 -0
- package/src/agent/TurnExecutor.js +99 -30
- package/src/agent/XmlParser.js +206 -111
- package/src/agent/errors.js +35 -0
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +3 -42
- package/src/agent/materializeContext.js +30 -1
- package/src/agent/runs.sql +8 -18
- package/src/agent/tokens.js +0 -1
- package/src/agent/turns.sql +1 -0
- package/src/hooks/Hooks.js +26 -0
- package/src/hooks/RummyContext.js +12 -1
- package/src/lib/hedberg/README.md +60 -0
- package/src/lib/hedberg/hedberg.js +60 -0
- package/src/lib/hedberg/marker.js +158 -0
- package/src/{plugins → lib}/hedberg/matcher.js +1 -2
- package/src/llm/LlmProvider.js +41 -3
- package/src/llm/openaiStream.js +17 -0
- package/src/plugins/ask_user/ask_user.js +12 -2
- package/src/plugins/ask_user/ask_userDoc.md +1 -5
- package/src/plugins/budget/README.md +29 -24
- package/src/plugins/budget/budget.js +166 -110
- package/src/plugins/cli/README.md +3 -4
- package/src/plugins/cli/cli.js +31 -5
- package/src/plugins/cloudflare/cloudflare.js +136 -0
- package/src/plugins/cp/cp.js +41 -4
- package/src/plugins/cp/cpDoc.md +5 -6
- package/src/plugins/engine/engine.sql +1 -1
- package/src/plugins/env/README.md +5 -4
- package/src/plugins/env/env.js +7 -4
- package/src/plugins/env/envDoc.md +7 -8
- package/src/plugins/error/error.js +56 -15
- package/src/plugins/file/README.md +12 -3
- package/src/plugins/file/file.js +2 -2
- package/src/plugins/get/get.js +59 -36
- package/src/plugins/get/getDoc.md +10 -34
- package/src/plugins/google/google.js +115 -0
- package/src/plugins/hedberg/hedberg.js +13 -56
- package/src/plugins/helpers.js +66 -12
- package/src/plugins/index.js +1 -2
- package/src/plugins/instructions/README.md +44 -47
- package/src/plugins/instructions/instructions-system.md +44 -0
- package/src/plugins/instructions/instructions-user.md +53 -0
- package/src/plugins/instructions/instructions.js +58 -189
- package/src/plugins/known/README.md +6 -7
- package/src/plugins/known/known.js +24 -30
- package/src/plugins/log/log.js +41 -32
- package/src/plugins/mv/mv.js +40 -1
- package/src/plugins/mv/mvDoc.md +1 -8
- package/src/plugins/ollama/ollama.js +4 -3
- package/src/plugins/openai/openai.js +4 -3
- package/src/plugins/openrouter/openrouter.js +14 -4
- package/src/plugins/persona/README.md +11 -13
- package/src/plugins/persona/default.md +29 -0
- package/src/plugins/persona/persona.js +10 -66
- package/src/plugins/policy/policy.js +23 -22
- package/src/plugins/prompt/README.md +37 -27
- package/src/plugins/prompt/prompt.js +13 -19
- package/src/plugins/rm/rm.js +18 -0
- package/src/plugins/rm/rmDoc.md +5 -6
- package/src/plugins/rpc/rpc.js +3 -3
- package/src/plugins/set/set.js +205 -323
- package/src/plugins/set/setDoc.md +47 -17
- package/src/plugins/sh/README.md +6 -5
- package/src/plugins/sh/sh.js +8 -5
- package/src/plugins/sh/shDoc.md +7 -8
- package/src/plugins/skill/README.md +37 -14
- package/src/plugins/skill/skill.js +200 -101
- package/src/plugins/skill/skillDoc.js +3 -0
- package/src/plugins/skill/skillDoc.md +9 -0
- package/src/plugins/stream/README.md +7 -6
- package/src/plugins/stream/finalize.js +100 -0
- package/src/plugins/stream/stream.js +13 -45
- package/src/plugins/telemetry/telemetry.js +27 -4
- package/src/plugins/think/think.js +2 -3
- package/src/plugins/think/thinkDoc.md +2 -4
- package/src/plugins/unknown/README.md +1 -1
- package/src/plugins/unknown/unknown.js +17 -19
- package/src/plugins/update/update.js +4 -51
- package/src/plugins/update/updateDoc.md +21 -6
- package/src/plugins/xai/xai.js +68 -102
- package/src/plugins/yolo/yolo.js +102 -75
- package/src/sql/functions/hedmatch.js +1 -1
- package/src/sql/functions/hedreplace.js +1 -1
- package/src/sql/functions/hedsearch.js +1 -1
- package/src/sql/functions/slugify.js +16 -2
- package/BENCH_ENVIRONMENT.md +0 -230
- package/CLIENT_INTERFACE.md +0 -396
- package/last_run.txt +0 -5617
- package/scriptify/ask_run.js +0 -77
- package/scriptify/cache_probe.js +0 -66
- package/scriptify/cache_probe_grok.js +0 -74
- package/src/agent/budget.js +0 -33
- package/src/agent/config.js +0 -38
- package/src/plugins/hedberg/README.md +0 -71
- package/src/plugins/hedberg/docs.md +0 -0
- package/src/plugins/hedberg/edits.js +0 -55
- package/src/plugins/hedberg/normalize.js +0 -17
- package/src/plugins/hedberg/sed.js +0 -49
- package/src/plugins/instructions/instructions.md +0 -34
- package/src/plugins/instructions/instructions_104.md +0 -8
- package/src/plugins/instructions/instructions_105.md +0 -39
- package/src/plugins/instructions/instructions_106.md +0 -22
- package/src/plugins/instructions/instructions_107.md +0 -17
- package/src/plugins/instructions/instructions_108.md +0 -0
- package/src/plugins/known/knownDoc.js +0 -3
- package/src/plugins/known/knownDoc.md +0 -8
- package/src/plugins/unknown/unknownDoc.js +0 -3
- package/src/plugins/unknown/unknownDoc.md +0 -11
- package/turns/cli_1777462658211/turn_001.txt +0 -772
- package/turns/cli_1777462658211/turn_002.txt +0 -606
- package/turns/cli_1777462658211/turn_003.txt +0 -667
- package/turns/cli_1777462658211/turn_004.txt +0 -297
- package/turns/cli_1777462658211/turn_005.txt +0 -301
- package/turns/cli_1777462658211/turn_006.txt +0 -262
- package/turns/cli_1777465095132/turn_001.txt +0 -715
- package/turns/cli_1777465095132/turn_002.txt +0 -236
- package/turns/cli_1777465095132/turn_003.txt +0 -287
- package/turns/cli_1777465095132/turn_004.txt +0 -694
- package/turns/cli_1777465095132/turn_005.txt +0 -422
- package/turns/cli_1777465095132/turn_006.txt +0 -365
- package/turns/cli_1777465095132/turn_007.txt +0 -885
- package/turns/cli_1777465095132/turn_008.txt +0 -1277
- package/turns/cli_1777465095132/turn_009.txt +0 -736
- /package/src/{plugins → lib}/hedberg/patterns.js +0 -0
package/src/plugins/cp/cp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Entries from "../../agent/Entries.js";
|
|
2
|
+
import { storePatternResult } from "../helpers.js";
|
|
2
3
|
import docs from "./cpDoc.js";
|
|
3
4
|
|
|
4
5
|
export default class Cp {
|
|
@@ -24,25 +25,60 @@ export default class Cp {
|
|
|
24
25
|
? entry.attributes.visibility
|
|
25
26
|
: undefined;
|
|
26
27
|
|
|
28
|
+
// Manifest: list what would be copied without performing the cp.
|
|
29
|
+
if (entry.attributes.manifest !== undefined) {
|
|
30
|
+
const matches = await store.getEntriesByPattern(runId, path);
|
|
31
|
+
await storePatternResult(store, runId, turn, "cp", path, null, matches, {
|
|
32
|
+
manifest: true,
|
|
33
|
+
loopId,
|
|
34
|
+
attributes: { path, to },
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
const source = await store.getBody(runId, path);
|
|
28
40
|
if (source === null) return;
|
|
41
|
+
// Tags propagate: explicit `tags=` on the cp wins; otherwise the
|
|
42
|
+
// destination inherits the source entry's tags. Same shape as
|
|
43
|
+
// visibility — explicit attr overrides, default inherits.
|
|
44
|
+
let destTags = null;
|
|
45
|
+
if (typeof entry.attributes.tags === "string") {
|
|
46
|
+
destTags = entry.attributes.tags;
|
|
47
|
+
} else {
|
|
48
|
+
const sourceAttrs = await store.getAttributes(runId, path);
|
|
49
|
+
if (sourceAttrs && typeof sourceAttrs.tags === "string") {
|
|
50
|
+
destTags = sourceAttrs.tags;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
29
53
|
|
|
30
54
|
const destScheme = Entries.scheme(to);
|
|
31
55
|
const existing = await store.getBody(runId, to);
|
|
32
56
|
const warning =
|
|
33
|
-
existing !== null
|
|
34
|
-
? `Overwrote existing entry at ${to}`
|
|
35
|
-
: null;
|
|
57
|
+
existing !== null ? `Overwrote existing entry at ${to}` : null;
|
|
36
58
|
|
|
37
59
|
const body = `${path} ${to}`;
|
|
38
60
|
if (destScheme === null) {
|
|
61
|
+
// Bare-file destination: hand the shared materializer (set.js
|
|
62
|
+
// #materializeFile, gated on attrs.path + attrs.patched) the
|
|
63
|
+
// authoritative new body so it writes the source content to
|
|
64
|
+
// disk on accept. Without this the proposal accepted but no
|
|
65
|
+
// file landed — the model's "<cp src dest> then <set dest>
|
|
66
|
+
// SEARCH/REPLACE" sequence silently no-op'd at materialize.
|
|
39
67
|
await store.set({
|
|
40
68
|
runId,
|
|
41
69
|
turn,
|
|
42
70
|
path: entry.resultPath,
|
|
43
71
|
body,
|
|
44
72
|
state: "proposed",
|
|
45
|
-
attributes: {
|
|
73
|
+
attributes: {
|
|
74
|
+
from: path,
|
|
75
|
+
to,
|
|
76
|
+
isMove: false,
|
|
77
|
+
warning,
|
|
78
|
+
path: to,
|
|
79
|
+
patched: source,
|
|
80
|
+
visibility,
|
|
81
|
+
},
|
|
46
82
|
loopId,
|
|
47
83
|
});
|
|
48
84
|
} else {
|
|
@@ -53,6 +89,7 @@ export default class Cp {
|
|
|
53
89
|
body: source,
|
|
54
90
|
state: "resolved",
|
|
55
91
|
visibility,
|
|
92
|
+
attributes: destTags ? { tags: destTags } : null,
|
|
56
93
|
loopId,
|
|
57
94
|
});
|
|
58
95
|
await store.set({
|
package/src/plugins/cp/cpDoc.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
## <cp path="[source]">[destination]</cp> - Copy
|
|
1
|
+
## <cp path="[source]">[destination]</cp> - Copy an entry or file
|
|
2
2
|
|
|
3
|
-
Example: <cp path="
|
|
4
|
-
<!--
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<!-- Glob batch copy across known entries. -->
|
|
3
|
+
Example: <cp path="known://server/handler_main">src/main.c</cp>
|
|
4
|
+
<!-- Body is the destination path; cross-scheme copies are allowed. -->
|
|
5
|
+
Example: <cp path="known://countries/france/*">known://archive/countries/france/</cp>
|
|
6
|
+
<!-- Glob source + directory-shaped destination = batch copy preserving names. -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
-- PREP: get_promoted_entries
|
|
2
2
|
SELECT
|
|
3
3
|
ke.path, ke.scheme, ke.state, ke.outcome, ke.visibility, ke.turn
|
|
4
|
-
, countTokens(ke.body) AS tokens
|
|
4
|
+
, ke.refs, countTokens(ke.body) AS tokens
|
|
5
5
|
FROM known_entries AS ke
|
|
6
6
|
JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
|
|
7
7
|
WHERE
|
|
@@ -8,7 +8,7 @@ side effects.
|
|
|
8
8
|
## Registration
|
|
9
9
|
|
|
10
10
|
- **Tool**: `env`
|
|
11
|
-
- **Scheme**: `env` — `category: "
|
|
11
|
+
- **Scheme**: `env` — `category: "logging"` (channels are time-indexed activity, not state)
|
|
12
12
|
- **Handler**: Upserts the proposal entry at status 202 (proposed).
|
|
13
13
|
|
|
14
14
|
## Two namespaces per invocation
|
|
@@ -16,9 +16,10 @@ side effects.
|
|
|
16
16
|
- **Log entry**: `log://turn_N/env/{slug}` — scheme=`log`, category=`logging`.
|
|
17
17
|
The audit record (renders inside `<log>` as `<env>`).
|
|
18
18
|
- **Data channels**: `env://turn_N/{slug}_1` (stdout), `env://turn_N/{slug}_2`
|
|
19
|
-
(stderr) — scheme=`env`, category=`
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
(stderr) — scheme=`env`, category=`logging` (time-indexed activity).
|
|
20
|
+
Render inside `<log>` adjacent to their parent `<env>` action entry;
|
|
21
|
+
visibility controls whether the body is full or compact, not which
|
|
22
|
+
block they appear in.
|
|
22
23
|
|
|
23
24
|
The `env` scheme exists **only** for the data channels. See
|
|
24
25
|
[scheme_category_split](#scheme_category_split).
|
package/src/plugins/env/env.js
CHANGED
|
@@ -9,7 +9,10 @@ export default class Env {
|
|
|
9
9
|
constructor(core) {
|
|
10
10
|
this.#core = core;
|
|
11
11
|
// env vs sh: env is read-only (allowed in ask-mode); see plugin README.
|
|
12
|
-
|
|
12
|
+
// Streaming stdout/stderr is time-indexed activity output, not
|
|
13
|
+
// topic-indexed state — category="logging" so it renders in <log>
|
|
14
|
+
// adjacent to its action entry, not in <summary>/<visible>.
|
|
15
|
+
core.registerScheme({ category: "logging" });
|
|
13
16
|
core.on("handler", this.handler.bind(this));
|
|
14
17
|
core.on("visible", this.full.bind(this));
|
|
15
18
|
core.on("summarized", this.summary.bind(this));
|
|
@@ -25,7 +28,7 @@ export default class Env {
|
|
|
25
28
|
if (m?.[1] !== "env") return;
|
|
26
29
|
let command = "";
|
|
27
30
|
if (ctx.attrs?.command) command = ctx.attrs.command;
|
|
28
|
-
else if (ctx.attrs?.
|
|
31
|
+
else if (ctx.attrs?.tags) command = ctx.attrs.tags;
|
|
29
32
|
const turn = (await ctx.db.get_run_by_id.get({ id: ctx.runId })).next_turn;
|
|
30
33
|
const dataBase = logPathToDataBase(ctx.path);
|
|
31
34
|
for (const ch of [1, 2]) {
|
|
@@ -36,7 +39,7 @@ export default class Env {
|
|
|
36
39
|
body: "",
|
|
37
40
|
state: "streaming",
|
|
38
41
|
visibility: "summarized",
|
|
39
|
-
attributes: { command,
|
|
42
|
+
attributes: { command, tags: command, channel: ch },
|
|
40
43
|
});
|
|
41
44
|
}
|
|
42
45
|
await ctx.entries.set({
|
|
@@ -55,7 +58,7 @@ export default class Env {
|
|
|
55
58
|
path: entry.resultPath,
|
|
56
59
|
body: "",
|
|
57
60
|
state: "proposed",
|
|
58
|
-
attributes: { ...entry.attributes,
|
|
61
|
+
attributes: { ...entry.attributes, tags: entry.attributes.command },
|
|
59
62
|
loopId,
|
|
60
63
|
});
|
|
61
64
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
## <env>[command]</env> - Run an exploratory shell command
|
|
2
2
|
|
|
3
|
-
Example:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Example:
|
|
4
|
+
<env><<EOF
|
|
5
|
+
npm --version
|
|
6
|
+
node --version
|
|
7
|
+
git log --oneline -3
|
|
8
|
+
EOF</env>
|
|
9
|
+
<!-- Heredoc body is opaque — embed multi-line scripts and special characters without escaping. Output co-locates at env://turn_N/<slug>. -->
|
|
8
10
|
|
|
9
11
|
YOU MUST NOT use <env></env> to read or list files — use <get path="*"/> instead
|
|
10
|
-
<!-- Prevents cat/ls through shell. Forces file access through get. -->
|
|
11
|
-
|
|
12
12
|
YOU MUST NOT use <env></env> for commands with side effects
|
|
13
|
-
<!-- Separates exploration from action. env = observe only. -->
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SOFT_FAILURE_OUTCOMES } from "../../agent/errors.js";
|
|
2
|
+
import { SUMMARY_MAX_CHARS } from "../helpers.js";
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const
|
|
4
|
+
const MAX_STRIKES = Number(process.env.RUMMY_MAX_STRIKES);
|
|
5
|
+
const MIN_CYCLES = Number(process.env.RUMMY_MIN_CYCLES);
|
|
6
|
+
const MAX_CYCLE_PERIOD = Number(process.env.RUMMY_MAX_CYCLE_PERIOD);
|
|
6
7
|
|
|
7
8
|
function fingerprint(entry) {
|
|
8
9
|
const parts = Object.keys(entry.attributes)
|
|
@@ -40,18 +41,27 @@ export default class ErrorPlugin {
|
|
|
40
41
|
this.#core = core;
|
|
41
42
|
core.registerScheme({ category: "logging" });
|
|
42
43
|
core.on("visible", (entry) => `# error\n${entry.body}`);
|
|
43
|
-
core.on("summarized", (entry) => entry.body);
|
|
44
|
+
core.on("summarized", (entry) => entry.body.slice(0, SUMMARY_MAX_CHARS));
|
|
44
45
|
|
|
45
46
|
core.hooks.error.log.on(this.#onErrorLog.bind(this));
|
|
46
47
|
core.hooks.loop.started.on(this.#onLoopStarted.bind(this));
|
|
47
48
|
core.hooks.loop.completed.on(this.#onLoopCompleted.bind(this));
|
|
48
49
|
core.hooks.turn.started.on(this.#onTurnStarted.bind(this));
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
// Subscribe to the turn.verdict filter chain. Multi-plugin
|
|
52
|
+
// surface — strike streak, cycle detection, stagnation
|
|
53
|
+
// pressure all flow through here. Future voters (e.g. budget
|
|
54
|
+
// overflow termination, runaway-on-context-grow) participate
|
|
55
|
+
// via the same chain.
|
|
56
|
+
core.filter("turn.verdict", this.#verdict.bind(this));
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
#onLoopStarted({ loopId }) {
|
|
54
|
-
this.#loopState.set(loopId, {
|
|
60
|
+
this.#loopState.set(loopId, {
|
|
61
|
+
streak: 0,
|
|
62
|
+
history: [],
|
|
63
|
+
turnErrors: 0,
|
|
64
|
+
});
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
#onLoopCompleted({ loopId }) {
|
|
@@ -71,24 +81,43 @@ export default class ErrorPlugin {
|
|
|
71
81
|
message,
|
|
72
82
|
status,
|
|
73
83
|
attributes,
|
|
84
|
+
soft,
|
|
74
85
|
}) {
|
|
75
86
|
const statusValue = status ?? 400;
|
|
76
87
|
const path = await store.logPath(runId, turn, "error", message);
|
|
88
|
+
// Soft errors record but don't strike: the issue was already
|
|
89
|
+
// recovered (e.g. parser auto-corrected a closing-tag mismatch)
|
|
90
|
+
// and the entry exists only so the model can see what happened.
|
|
91
|
+
// state="resolved" keeps recordedFailed clean; skipping
|
|
92
|
+
// turnErrors++ keeps the strike machinery from firing. Per SPEC
|
|
93
|
+
// #entries, outcome is reserved for state ∈ {failed, cancelled}
|
|
94
|
+
// — soft entries land with outcome=null. Status carrier for
|
|
95
|
+
// rendering is attributes.status, consulted before outcome by
|
|
96
|
+
// log.js's renderLogTag.
|
|
77
97
|
await store.set({
|
|
78
98
|
runId,
|
|
79
99
|
turn,
|
|
80
100
|
path,
|
|
81
101
|
body: message,
|
|
82
|
-
state: "failed",
|
|
83
|
-
outcome: `status:${statusValue}`,
|
|
102
|
+
state: soft ? "resolved" : "failed",
|
|
103
|
+
outcome: soft ? null : `status:${statusValue}`,
|
|
84
104
|
loopId,
|
|
85
105
|
attributes: { ...attributes, status: statusValue },
|
|
86
106
|
});
|
|
107
|
+
if (soft) return;
|
|
87
108
|
const state = this.#loopState.get(loopId);
|
|
88
109
|
if (state) state.turnErrors++;
|
|
89
110
|
}
|
|
90
111
|
|
|
91
|
-
async #verdict(
|
|
112
|
+
async #verdict(
|
|
113
|
+
_currentVerdict,
|
|
114
|
+
{ store, runId, loopId, recorded, summaryText, turn: _turn },
|
|
115
|
+
) {
|
|
116
|
+
// _currentVerdict is the upstream filter's result. Today this is
|
|
117
|
+
// the only voter so it's always { continue: true }. When other
|
|
118
|
+
// plugins join the chain, they can short-circuit by setting
|
|
119
|
+
// continue=false; this implementation could honor that via an
|
|
120
|
+
// early return. Left noop for now to preserve current semantics.
|
|
92
121
|
const state = this.#loopState.get(loopId);
|
|
93
122
|
|
|
94
123
|
let cycleReason = null;
|
|
@@ -102,10 +131,20 @@ export default class ErrorPlugin {
|
|
|
102
131
|
state.turnErrors++;
|
|
103
132
|
}
|
|
104
133
|
|
|
134
|
+
// Some failure outcomes are findings the model should adapt to,
|
|
135
|
+
// not contract violations. `not_found` (model tried to act on an
|
|
136
|
+
// entry that doesn't exist) and `conflict` (SEARCH text didn't
|
|
137
|
+
// match current body) are recoverable: the model reads the new
|
|
138
|
+
// state and tries again. Striking on these punishes legitimate
|
|
139
|
+
// state-discovery and accumulates 499s on otherwise productive
|
|
140
|
+
// runs. Hard outcomes (validation, permission, exit:N) still strike.
|
|
105
141
|
let recordedFailed = false;
|
|
106
142
|
for (const e of recorded) {
|
|
107
143
|
const current = await store.getState(runId, e.path);
|
|
108
|
-
if (
|
|
144
|
+
if (
|
|
145
|
+
current?.state === "failed" &&
|
|
146
|
+
!SOFT_FAILURE_OUTCOMES.has(current.outcome)
|
|
147
|
+
) {
|
|
109
148
|
recordedFailed = true;
|
|
110
149
|
break;
|
|
111
150
|
}
|
|
@@ -139,10 +178,12 @@ export default class ErrorPlugin {
|
|
|
139
178
|
`Abandoned after ${state.streak} consecutive strikes.`,
|
|
140
179
|
};
|
|
141
180
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
// No reason on continue: the model sees the actual failure
|
|
182
|
+
// entries directly in <log> next turn. Hardcoding "Missing
|
|
183
|
+
// update" mislabels strikes that fire on validation /
|
|
184
|
+
// permission / dispatch failures or cycles, when the update
|
|
185
|
+
// itself was emitted correctly.
|
|
186
|
+
return { continue: true };
|
|
146
187
|
}
|
|
147
188
|
|
|
148
189
|
state.streak = 0;
|
|
@@ -15,8 +15,17 @@ Static methods `setConstraint` and `dropConstraint` manage per-project
|
|
|
15
15
|
file constraints in the database. Constraints are project-level config
|
|
16
16
|
(backbone), not tool dispatch. See [file_constraints](../../../SPEC.md#file_constraints).
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
-
|
|
18
|
+
Constraint type governs **membership** and **write permission**, not
|
|
19
|
+
in-context visibility. Visibility (visible / summarized / archived)
|
|
20
|
+
is per-entry and model-controlled — files default to `archived` on
|
|
21
|
+
ingestion; the model promotes via `<get>` / `<set visibility=...>`.
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
- `add` — file is part of the project; ingested as an entry; model
|
|
24
|
+
may write. Default for `setConstraint`.
|
|
25
|
+
- `readonly` — same ingestion; `<set>` is vetoed at the proposal-
|
|
26
|
+
accept gate.
|
|
27
|
+
- `ignore` — excluded from scans entirely. The file remains on disk
|
|
28
|
+
for `<sh>` / `<env>` invocation but is not present as an entry.
|
|
29
|
+
|
|
30
|
+
Promotion/demotion of an ingested file goes through the standard tool
|
|
22
31
|
handler chain via `dispatchTool`.
|
package/src/plugins/file/file.js
CHANGED
|
@@ -19,7 +19,7 @@ export default class File {
|
|
|
19
19
|
return "";
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
static async setConstraint(db, projectId, pattern, visibility = "
|
|
22
|
+
static async setConstraint(db, projectId, pattern, visibility = "add") {
|
|
23
23
|
const path = await normalizePath(db, projectId, pattern);
|
|
24
24
|
if (!path) return null;
|
|
25
25
|
|
|
@@ -47,7 +47,7 @@ export default class File {
|
|
|
47
47
|
// True if any readonly constraint matches; called from set-accept gate.
|
|
48
48
|
static async isReadonly(db, projectId, path) {
|
|
49
49
|
const rows = await db.get_file_constraints.all({ project_id: projectId });
|
|
50
|
-
const { hedmatch } = await import("
|
|
50
|
+
const { hedmatch } = await import("../../lib/hedberg/patterns.js");
|
|
51
51
|
return rows.some(
|
|
52
52
|
(r) => r.visibility === "readonly" && hedmatch(r.pattern, path),
|
|
53
53
|
);
|
package/src/plugins/get/get.js
CHANGED
|
@@ -19,7 +19,12 @@ export default class Get {
|
|
|
19
19
|
|
|
20
20
|
async handler(entry, rummy) {
|
|
21
21
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
22
|
-
|
|
22
|
+
// Search-by-tags: same `tags` attribute that <set> writes onto
|
|
23
|
+
// entries. Same name on both ends — no in/out semantic split.
|
|
24
|
+
const tagsAttr = entry.attributes.tags;
|
|
25
|
+
// Tags-only get defaults path to "**" so the model can recall by
|
|
26
|
+
// folksonomic tags without remembering exact paths.
|
|
27
|
+
const target = entry.attributes.path || (tagsAttr ? "**" : null);
|
|
23
28
|
if (!target) {
|
|
24
29
|
await store.set({
|
|
25
30
|
runId,
|
|
@@ -35,7 +40,13 @@ export default class Get {
|
|
|
35
40
|
const normalized = Entries.normalizePath(target);
|
|
36
41
|
const bodyFilter = entry.attributes.body;
|
|
37
42
|
const manifest = entry.attributes.manifest !== undefined;
|
|
38
|
-
const
|
|
43
|
+
const wantedTags = tagsAttr
|
|
44
|
+
? tagsAttr
|
|
45
|
+
.split(",")
|
|
46
|
+
.map((t) => t.trim().toLowerCase())
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
: null;
|
|
49
|
+
const isPattern = bodyFilter || normalized.includes("*") || !!wantedTags;
|
|
39
50
|
|
|
40
51
|
// Negative line = tail-from-end (line=-50 starts 50 from end).
|
|
41
52
|
const lineRaw = entry.attributes.line;
|
|
@@ -45,11 +56,23 @@ export default class Get {
|
|
|
45
56
|
? Math.max(1, parseInt(entry.attributes.limit, 10))
|
|
46
57
|
: null;
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
let matches = await store.getEntriesByPattern(
|
|
49
60
|
runId,
|
|
50
61
|
normalized,
|
|
51
62
|
bodyFilter,
|
|
52
63
|
);
|
|
64
|
+
if (wantedTags) {
|
|
65
|
+
matches = matches.filter((e) => {
|
|
66
|
+
if (!e.attributes) return false;
|
|
67
|
+
const attrs =
|
|
68
|
+
typeof e.attributes === "string"
|
|
69
|
+
? JSON.parse(e.attributes)
|
|
70
|
+
: e.attributes;
|
|
71
|
+
if (typeof attrs.tags !== "string") return false;
|
|
72
|
+
const tags = attrs.tags.toLowerCase();
|
|
73
|
+
return wantedTags.every((t) => tags.includes(t));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
53
76
|
|
|
54
77
|
// Manifest: list matches + full-body token costs; no promotion.
|
|
55
78
|
if (manifest) {
|
|
@@ -67,20 +90,11 @@ export default class Get {
|
|
|
67
90
|
}
|
|
68
91
|
|
|
69
92
|
// Partial read: line slice in the log entry; no promotion.
|
|
93
|
+
// Per getDoc: "line/limit works on any scheme — files, sh
|
|
94
|
+
// stdout, knowns, urls." Multi-match (glob, tags, or body
|
|
95
|
+
// filter narrowing) emits one slice section per match —
|
|
96
|
+
// model can scope further with body filter or tighter path.
|
|
70
97
|
if (line !== null || limit !== null) {
|
|
71
|
-
if (isPattern) {
|
|
72
|
-
await store.set({
|
|
73
|
-
runId,
|
|
74
|
-
turn,
|
|
75
|
-
path: entry.resultPath,
|
|
76
|
-
body: "line/limit requires a single path, not a glob or body filter",
|
|
77
|
-
state: "failed",
|
|
78
|
-
outcome: "validation",
|
|
79
|
-
loopId,
|
|
80
|
-
attributes: { path: target },
|
|
81
|
-
});
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
98
|
if (matches.length === 0) {
|
|
85
99
|
await store.set({
|
|
86
100
|
runId,
|
|
@@ -93,32 +107,25 @@ export default class Get {
|
|
|
93
107
|
});
|
|
94
108
|
return;
|
|
95
109
|
}
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const endLine = endIdx;
|
|
108
|
-
const header = `${target}\n[lines ${startLine}–${endLine} / ${total} total]`;
|
|
110
|
+
const sections = matches.map((match) => sliceSection(match, line, limit));
|
|
111
|
+
const body = sections.map((s) => s.text).join("\n\n");
|
|
112
|
+
const attributes = { path: target };
|
|
113
|
+
if (sections.length === 1) {
|
|
114
|
+
const only = sections[0];
|
|
115
|
+
attributes.lineStart = only.startLine;
|
|
116
|
+
attributes.lineEnd = only.endLine;
|
|
117
|
+
attributes.totalLines = only.total;
|
|
118
|
+
} else {
|
|
119
|
+
attributes.matchCount = sections.length;
|
|
120
|
+
}
|
|
109
121
|
await store.set({
|
|
110
122
|
runId,
|
|
111
123
|
turn,
|
|
112
124
|
path: entry.resultPath,
|
|
113
|
-
body
|
|
125
|
+
body,
|
|
114
126
|
state: "resolved",
|
|
115
127
|
loopId,
|
|
116
|
-
attributes
|
|
117
|
-
path: target,
|
|
118
|
-
lineStart: startLine,
|
|
119
|
-
lineEnd: endLine,
|
|
120
|
-
totalLines: total,
|
|
121
|
-
},
|
|
128
|
+
attributes,
|
|
122
129
|
});
|
|
123
130
|
return;
|
|
124
131
|
}
|
|
@@ -190,3 +197,19 @@ export default class Get {
|
|
|
190
197
|
return "";
|
|
191
198
|
}
|
|
192
199
|
}
|
|
200
|
+
|
|
201
|
+
function sliceSection(match, line, limit) {
|
|
202
|
+
const allLines = match.body.split("\n");
|
|
203
|
+
const total = allLines.length;
|
|
204
|
+
const startLine =
|
|
205
|
+
line == null
|
|
206
|
+
? 1
|
|
207
|
+
: line < 0
|
|
208
|
+
? Math.max(1, total + line + 1)
|
|
209
|
+
: Math.max(1, line);
|
|
210
|
+
const startIdx = startLine - 1;
|
|
211
|
+
const endIdx = limit !== null ? Math.min(startIdx + limit, total) : total;
|
|
212
|
+
const slice = allLines.slice(startIdx, endIdx).join("\n");
|
|
213
|
+
const text = `${match.path}\n[lines ${startLine}–${endIdx} / ${total} total]\n${slice}`;
|
|
214
|
+
return { text, startLine, endLine: endIdx, total };
|
|
215
|
+
}
|
|
@@ -1,38 +1,14 @@
|
|
|
1
|
-
## <get path="[path
|
|
2
|
-
|
|
3
|
-
Example: <get path="src/app.js"/>
|
|
4
|
-
<!-- Simplest form. Path attribute. Body is reserved for content filter. -->
|
|
1
|
+
## <get path="[path]"/> - Promote an entry
|
|
5
2
|
|
|
6
3
|
Example: <get path="known://*">auth</get>
|
|
7
|
-
<!--
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Example: <get path="src/**/*.js" manifest>authentication</get>
|
|
13
|
-
<!-- Full pattern: recursive glob + content filter. -->
|
|
14
|
-
|
|
4
|
+
<!-- Body is a content filter, not new content. Path glob + body keyword = filtered recall. -->
|
|
5
|
+
Example: <get path="src/**/!(*.test).js" manifest>auth</get>
|
|
6
|
+
<!-- Negation: !(pattern) excludes matches; combine with body filter for "auth in sources, not tests." -->
|
|
7
|
+
Example: <get path="log://turn_*/sh/**" manifest/>
|
|
8
|
+
<!-- ** crosses path separators (matches log://turn_5/sh/build_1, log://turn_5/sh/test/run_2); * matches one segment only. -->
|
|
15
9
|
Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>
|
|
16
|
-
<!--
|
|
17
|
-
|
|
18
|
-
Example: <get path="sh://turn_3/npm_test_1" line="-50"/>
|
|
19
|
-
<!-- Tail: negative line reads the last 50 lines. Works on any growing entry — streaming sh output, logs, knowns. -->
|
|
20
|
-
|
|
10
|
+
<!-- line/limit: read a slice without promoting. line=-50 tails the last 50 lines. -->
|
|
21
11
|
Example: <get path="https://en.wikipedia.org/wiki/Long_Page" line="1" limit="200"/>
|
|
22
|
-
<!-- URL
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<!-- Reinforces picomatch patterns work everywhere. -->
|
|
26
|
-
|
|
27
|
-
* Body text filters results by content match (can use glob, regex, jsonpath, or xpath patterns)
|
|
28
|
-
<!-- Body = filter, not just path. -->
|
|
29
|
-
|
|
30
|
-
* `line` and `limit` read a slice without promoting the entry, which costs as many tokens as the slice contains. Negative `line` reads from the end (tail).
|
|
31
|
-
<!-- Partial read is safe: context budget unaffected. Tail idiom enables watching growing entries. -->
|
|
32
|
-
|
|
33
|
-
* `manifest` lists the paths and their token amounts instead of performing the operation; useful for bulk and pattern matching tasks.
|
|
34
|
-
<!-- manifest = listing, not snippet. The natural-language reading of "preview" pulled small models toward content-sampling; for body samples use line/limit. -->
|
|
35
|
-
|
|
36
|
-
* Remember to <set path="..." visibility="summarize"/> when entries or log events are no longer relevant.
|
|
37
|
-
|
|
38
|
-
* Promotions don't appear until next turn — emit Stage Continuation (1xx), not Completion (200)
|
|
12
|
+
<!-- URL slice. line/limit works on any scheme — files, sh stdout, knowns, urls. -->
|
|
13
|
+
Example: <get path="*.md"/>
|
|
14
|
+
<!-- One glob per call fans out. For unrelated paths, emit one `<get>` per path — `path` is a single entry or one pattern, not a space-separated list. -->
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import msg from "../../agent/messages.js";
|
|
5
|
+
import { chatCompletionStream } from "../../llm/openaiStream.js";
|
|
6
|
+
|
|
7
|
+
const FETCH_TIMEOUT = Number(process.env.RUMMY_FETCH_TIMEOUT);
|
|
8
|
+
|
|
9
|
+
const PROVIDER = "google";
|
|
10
|
+
const BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
11
|
+
const COMPAT_URL = `${BASE_URL}/openai`;
|
|
12
|
+
|
|
13
|
+
// Repo-root-relative key file. Resolved relative to this source file so
|
|
14
|
+
// CWD changes during runs (programbench/tbench cd into workspaces) don't
|
|
15
|
+
// break the lookup. Plugin is inert if the file is missing. Tests may
|
|
16
|
+
// override the path via `RUMMY_GEMINI_KEY_FILE` to point at a tmpdir
|
|
17
|
+
// fixture; the env var is a path knob, not a runtime fallback.
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
function resolveKeyFile() {
|
|
20
|
+
return process.env.RUMMY_GEMINI_KEY_FILE
|
|
21
|
+
? process.env.RUMMY_GEMINI_KEY_FILE
|
|
22
|
+
: join(__dirname, "..", "..", "..", "gemini.key");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Inert unless gemini.key exists in repo root; google/{model} aliases.
|
|
26
|
+
//
|
|
27
|
+
// Uses Google AI Studio's OpenAI-compatible endpoint
|
|
28
|
+
// (`/v1beta/openai/chat/completions`) for completions so we share the
|
|
29
|
+
// streaming SSE accumulator with the other OpenAI-shaped providers.
|
|
30
|
+
// Context-size lookups go to the native endpoint
|
|
31
|
+
// (`/v1beta/models/{model}`) because the OpenAI-compat /models response
|
|
32
|
+
// drops `inputTokenLimit`.
|
|
33
|
+
//
|
|
34
|
+
// Auth is `Authorization: Bearer {key}` on both endpoints; the legacy
|
|
35
|
+
// `?key={key}` query-param form is supported by Google but the bearer
|
|
36
|
+
// form is consistent with our other plugins.
|
|
37
|
+
export default class Google {
|
|
38
|
+
#apiKey;
|
|
39
|
+
#contextCache = new Map();
|
|
40
|
+
|
|
41
|
+
constructor(core) {
|
|
42
|
+
const keyFile = resolveKeyFile();
|
|
43
|
+
if (!existsSync(keyFile)) return;
|
|
44
|
+
const raw = readFileSync(keyFile, "utf8").trim();
|
|
45
|
+
if (!raw) return;
|
|
46
|
+
this.#apiKey = raw;
|
|
47
|
+
|
|
48
|
+
const wireModel = (alias) => alias.split("/").slice(1).join("/");
|
|
49
|
+
|
|
50
|
+
core.hooks.llm.providers.push({
|
|
51
|
+
name: PROVIDER,
|
|
52
|
+
matches: (model) => model.split("/")[0] === PROVIDER,
|
|
53
|
+
completion: (messages, model, options) =>
|
|
54
|
+
this.#completion(messages, wireModel(model), options),
|
|
55
|
+
getContextSize: (model) => this.#getContextSize(wireModel(model)),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async #completion(messages, model, options = {}) {
|
|
60
|
+
const body = { model, messages };
|
|
61
|
+
if (options.maxTokens !== undefined) body.max_tokens = options.maxTokens;
|
|
62
|
+
if (options.temperature !== undefined)
|
|
63
|
+
body.temperature = options.temperature;
|
|
64
|
+
|
|
65
|
+
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT);
|
|
66
|
+
const signal = options.signal
|
|
67
|
+
? AbortSignal.any([options.signal, timeoutSignal])
|
|
68
|
+
: timeoutSignal;
|
|
69
|
+
|
|
70
|
+
const headers = { Authorization: `Bearer ${this.#apiKey}` };
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return await chatCompletionStream({
|
|
74
|
+
url: `${COMPAT_URL}/chat/completions`,
|
|
75
|
+
headers,
|
|
76
|
+
body,
|
|
77
|
+
signal,
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err.status === 401 || err.status === 403) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
msg("error.google_auth", { status: `${err.status} - ${err.body}` }),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (err.status) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
msg("error.google_api", { status: `${err.status} - ${err.body}` }),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async #getContextSize(model) {
|
|
95
|
+
if (this.#contextCache.has(model)) return this.#contextCache.get(model);
|
|
96
|
+
|
|
97
|
+
// Native /v1beta/models/{model} requires API key as `?key=` query
|
|
98
|
+
// parameter — Bearer auth (which works on the OpenAI-compat layer)
|
|
99
|
+
// returns 401 here. Different auth surface, same key.
|
|
100
|
+
const url = `${BASE_URL}/models/${model}?key=${encodeURIComponent(this.#apiKey)}`;
|
|
101
|
+
const res = await fetch(url, {
|
|
102
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
msg("error.google_models_failed", { model, status: res.status }),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const data = await res.json();
|
|
110
|
+
const ctx = data?.inputTokenLimit;
|
|
111
|
+
if (!ctx) throw new Error(msg("error.google_no_context_length", { model }));
|
|
112
|
+
this.#contextCache.set(model, ctx);
|
|
113
|
+
return ctx;
|
|
114
|
+
}
|
|
115
|
+
}
|