@possumtech/rummy 0.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +21 -5
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +850 -373
- package/bin/demo.js +166 -0
- package/bin/rummy.js +9 -3
- package/biome/no-fallbacks.grit +50 -0
- package/lang/en.json +2 -2
- package/migrations/001_initial_schema.sql +88 -37
- package/package.json +6 -4
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +460 -330
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +229 -421
- package/src/agent/XmlParser.js +99 -33
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +275 -125
- package/src/agent/materializeContext.js +102 -0
- package/src/agent/runs.sql +10 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +29 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +135 -35
- package/src/hooks/ToolRegistry.js +21 -16
- package/src/llm/LlmProvider.js +64 -90
- package/src/llm/errors.js +21 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -25
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -25
- package/src/plugins/budget/budget.js +260 -88
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +29 -11
- package/src/plugins/cp/cpDoc.js +2 -15
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +45 -6
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -2
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +103 -48
- package/src/plugins/get/getDoc.js +2 -32
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +42 -2
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +122 -9
- package/src/plugins/instructions/instructions.md +25 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +46 -0
- package/src/plugins/instructions/instructions_106.md +0 -0
- package/src/plugins/instructions/instructions_107.md +0 -0
- package/src/plugins/instructions/instructions_108.md +8 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +67 -36
- package/src/plugins/known/knownDoc.js +2 -17
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +109 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +55 -22
- package/src/plugins/mv/mvDoc.js +2 -18
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +58 -16
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +56 -12
- package/src/plugins/rm/rmDoc.js +2 -20
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +515 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -75
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +50 -6
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -18
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +129 -80
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +2 -15
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +47 -19
- package/src/plugins/unknown/unknownDoc.js +2 -21
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +67 -5
- package/src/plugins/update/updateDoc.js +2 -30
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/FIDELITY_CONTRACT.md +0 -172
- package/src/agent/KnownStore.js +0 -337
- package/src/agent/ResponseHealer.js +0 -241
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -45
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -56
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -43
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -27
package/src/plugins/env/env.js
CHANGED
|
@@ -1,30 +1,69 @@
|
|
|
1
|
+
import { logPathToDataBase } from "../helpers.js";
|
|
1
2
|
import docs from "./envDoc.js";
|
|
2
3
|
|
|
4
|
+
const LOG_ACTION_RE = /^log:\/\/turn_\d+\/(\w+)\//;
|
|
5
|
+
|
|
3
6
|
export default class Env {
|
|
4
7
|
#core;
|
|
5
8
|
|
|
6
9
|
constructor(core) {
|
|
7
10
|
this.#core = core;
|
|
8
|
-
|
|
11
|
+
// `env` scheme holds the streamed stdout/stderr payload. See sh.js
|
|
12
|
+
// for the scheme/category split rationale. env differs from sh only
|
|
13
|
+
// in ask-mode policy (env is safe/read-only; sh has side effects).
|
|
14
|
+
core.registerScheme({ category: "data" });
|
|
9
15
|
core.on("handler", this.handler.bind(this));
|
|
10
|
-
core.on("
|
|
11
|
-
core.on("
|
|
16
|
+
core.on("visible", this.full.bind(this));
|
|
17
|
+
core.on("summarized", this.summary.bind(this));
|
|
12
18
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
13
19
|
docsMap.env = docs;
|
|
14
20
|
return docsMap;
|
|
15
21
|
});
|
|
22
|
+
core.on("proposal.accepted", this.#onAccepted.bind(this));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async #onAccepted(ctx) {
|
|
26
|
+
const m = LOG_ACTION_RE.exec(ctx.path);
|
|
27
|
+
if (m?.[1] !== "env") return;
|
|
28
|
+
let command = "";
|
|
29
|
+
if (ctx.attrs?.command) command = ctx.attrs.command;
|
|
30
|
+
else if (ctx.attrs?.summary) command = ctx.attrs.summary;
|
|
31
|
+
const turn = (await ctx.db.get_run_by_id.get({ id: ctx.runId })).next_turn;
|
|
32
|
+
const dataBase = logPathToDataBase(ctx.path);
|
|
33
|
+
for (const ch of [1, 2]) {
|
|
34
|
+
await ctx.entries.set({
|
|
35
|
+
runId: ctx.runId,
|
|
36
|
+
turn,
|
|
37
|
+
path: `${dataBase}_${ch}`,
|
|
38
|
+
body: "",
|
|
39
|
+
state: "streaming",
|
|
40
|
+
visibility: "summarized",
|
|
41
|
+
attributes: { command, summary: command, channel: ch },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
await ctx.entries.set({
|
|
45
|
+
runId: ctx.runId,
|
|
46
|
+
path: ctx.path,
|
|
47
|
+
state: "resolved",
|
|
48
|
+
body: `ran '${command}' (in progress). Output: ${dataBase}_1, ${dataBase}_2`,
|
|
49
|
+
});
|
|
16
50
|
}
|
|
17
51
|
|
|
18
52
|
async handler(entry, rummy) {
|
|
19
53
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
20
|
-
await store.
|
|
21
|
-
|
|
54
|
+
await store.set({
|
|
55
|
+
runId,
|
|
56
|
+
turn,
|
|
57
|
+
path: entry.resultPath,
|
|
58
|
+
body: "",
|
|
59
|
+
state: "proposed",
|
|
60
|
+
attributes: { ...entry.attributes, summary: entry.attributes.command },
|
|
22
61
|
loopId,
|
|
23
62
|
});
|
|
24
63
|
}
|
|
25
64
|
|
|
26
65
|
full(entry) {
|
|
27
|
-
return `# env ${entry.attributes.command
|
|
66
|
+
return `# env ${entry.attributes.command}\n${entry.body}`;
|
|
28
67
|
}
|
|
29
68
|
|
|
30
69
|
summary() {
|
|
@@ -1,24 +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
|
-
["## <env>[command]</env> - Run an exploratory shell command"],
|
|
6
|
-
[
|
|
7
|
-
"Example: <env>npm --version</env>",
|
|
8
|
-
"Version check. Safe, no side effects.",
|
|
9
|
-
],
|
|
10
|
-
[
|
|
11
|
-
"Example: <env>git log --oneline -5</env>",
|
|
12
|
-
"Git history. Shows env for read-only investigation.",
|
|
13
|
-
],
|
|
14
|
-
[
|
|
15
|
-
'* YOU MUST NOT use <env></env> to read or list files — use <get path="*"/> instead',
|
|
16
|
-
"Prevents cat/ls through shell. Forces file access through get.",
|
|
17
|
-
],
|
|
18
|
-
[
|
|
19
|
-
"* YOU MUST NOT use <env></env> for commands with side effects",
|
|
20
|
-
"Separates exploration from action. env = observe only.",
|
|
21
|
-
],
|
|
22
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
23
2
|
|
|
24
|
-
export default
|
|
3
|
+
export default loadDoc(import.meta.url, "envDoc.md");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## <env>[command]</env> - Run an exploratory shell command
|
|
2
|
+
|
|
3
|
+
Example: <env>npm --version</env>
|
|
4
|
+
<!-- Version check. Safe, no side effects. -->
|
|
5
|
+
|
|
6
|
+
Example: <env>git log --oneline -5</env>
|
|
7
|
+
<!-- Git history. Shows env for read-only investigation. -->
|
|
8
|
+
|
|
9
|
+
* 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
|
+
* YOU MUST NOT use <env></env> for commands with side effects
|
|
13
|
+
<!-- Separates exploration from action. env = observe only. -->
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# error {#error_plugin}
|
|
2
|
+
|
|
3
|
+
Subscribes to `error.log` hook and writes `error://` entries for any
|
|
4
|
+
runtime error a plugin or the turn executor wants surfaced to the
|
|
5
|
+
model.
|
|
6
|
+
|
|
7
|
+
## Registration
|
|
8
|
+
|
|
9
|
+
- **Scheme**: `error` (category: `logging`)
|
|
10
|
+
- **Hook subscriber**: `error.log` → writes entry at `error://<slug>`
|
|
11
|
+
with `state: "failed"`, `outcome: "validation"`.
|
|
12
|
+
|
|
13
|
+
## Projection
|
|
14
|
+
|
|
15
|
+
- **Promoted**: `# error\n{body}`
|
|
16
|
+
- **Demoted**: body only.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const MAX_STRIKES = Number(process.env.RUMMY_MAX_STRIKES);
|
|
2
|
+
const MIN_CYCLES = Number(process.env.RUMMY_MIN_CYCLES);
|
|
3
|
+
const MAX_CYCLE_PERIOD = Number(process.env.RUMMY_MAX_CYCLE_PERIOD);
|
|
4
|
+
|
|
5
|
+
const CONTRACT_REMINDER = "Missing update";
|
|
6
|
+
|
|
7
|
+
function fingerprint(entry) {
|
|
8
|
+
const parts = Object.keys(entry.attributes)
|
|
9
|
+
.toSorted()
|
|
10
|
+
.filter((k) => entry.attributes[k] != null)
|
|
11
|
+
.map((k) => `${k}=${entry.attributes[k]}`);
|
|
12
|
+
return `${entry.scheme}:${parts.join(",")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function detectCycle(history) {
|
|
16
|
+
for (let k = 1; k <= MAX_CYCLE_PERIOD; k++) {
|
|
17
|
+
const needed = k * MIN_CYCLES;
|
|
18
|
+
if (history.length < needed) continue;
|
|
19
|
+
const tail = history.slice(-needed);
|
|
20
|
+
const cycle = tail.slice(0, k);
|
|
21
|
+
let match = true;
|
|
22
|
+
outer: for (let rep = 0; rep < MIN_CYCLES; rep++) {
|
|
23
|
+
for (let j = 0; j < k; j++) {
|
|
24
|
+
if (tail[rep * k + j] !== cycle[j]) {
|
|
25
|
+
match = false;
|
|
26
|
+
break outer;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (match) return { detected: true, period: k, cycles: MIN_CYCLES };
|
|
31
|
+
}
|
|
32
|
+
return { detected: false };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default class ErrorPlugin {
|
|
36
|
+
#core;
|
|
37
|
+
#loopState = new Map();
|
|
38
|
+
|
|
39
|
+
constructor(core) {
|
|
40
|
+
this.#core = core;
|
|
41
|
+
core.registerScheme({ category: "logging" });
|
|
42
|
+
core.on("visible", (entry) => `# error\n${entry.body}`);
|
|
43
|
+
core.on("summarized", (entry) => entry.body);
|
|
44
|
+
|
|
45
|
+
core.hooks.error.log.on(this.#onErrorLog.bind(this));
|
|
46
|
+
core.hooks.loop.started.on(this.#onLoopStarted.bind(this));
|
|
47
|
+
core.hooks.loop.completed.on(this.#onLoopCompleted.bind(this));
|
|
48
|
+
core.hooks.turn.started.on(this.#onTurnStarted.bind(this));
|
|
49
|
+
|
|
50
|
+
core.hooks.error.verdict = this.#verdict.bind(this);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#onLoopStarted({ loopId }) {
|
|
54
|
+
this.#loopState.set(loopId, { streak: 0, history: [], turnErrors: 0 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#onLoopCompleted({ loopId }) {
|
|
58
|
+
this.#loopState.delete(loopId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#onTurnStarted({ rummy }) {
|
|
62
|
+
const state = this.#loopState.get(rummy.loopId);
|
|
63
|
+
state.turnErrors = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async #onErrorLog({
|
|
67
|
+
store,
|
|
68
|
+
runId,
|
|
69
|
+
turn,
|
|
70
|
+
loopId,
|
|
71
|
+
message,
|
|
72
|
+
status,
|
|
73
|
+
attributes,
|
|
74
|
+
}) {
|
|
75
|
+
const statusValue = status ?? 400;
|
|
76
|
+
const path = await store.logPath(runId, turn, "error", message);
|
|
77
|
+
await store.set({
|
|
78
|
+
runId,
|
|
79
|
+
turn,
|
|
80
|
+
path,
|
|
81
|
+
body: message,
|
|
82
|
+
state: "failed",
|
|
83
|
+
outcome: `status:${statusValue}`,
|
|
84
|
+
loopId,
|
|
85
|
+
attributes: { ...attributes, status: statusValue },
|
|
86
|
+
});
|
|
87
|
+
const state = this.#loopState.get(loopId);
|
|
88
|
+
if (state) state.turnErrors++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async #verdict({ store, runId, loopId, turn, recorded, summaryText }) {
|
|
92
|
+
const state = this.#loopState.get(loopId);
|
|
93
|
+
|
|
94
|
+
let cycleReason = null;
|
|
95
|
+
if (recorded && recorded.length > 0) {
|
|
96
|
+
const fp = recorded.map(fingerprint).toSorted().join("|");
|
|
97
|
+
state.history.push(fp);
|
|
98
|
+
const cycle = detectCycle(state.history);
|
|
99
|
+
if (cycle.detected) {
|
|
100
|
+
cycleReason = "Loop detected";
|
|
101
|
+
await this.#core.hooks.error.log.emit({
|
|
102
|
+
store,
|
|
103
|
+
runId,
|
|
104
|
+
turn,
|
|
105
|
+
loopId,
|
|
106
|
+
message: cycleReason,
|
|
107
|
+
status: 429,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const struck = state.turnErrors > 0;
|
|
113
|
+
|
|
114
|
+
if (summaryText && !struck) {
|
|
115
|
+
state.streak = 0;
|
|
116
|
+
const updateEntry = recorded?.findLast?.((e) => e.scheme === "update");
|
|
117
|
+
const terminalStatus = updateEntry?.attributes?.status ?? 200;
|
|
118
|
+
return { continue: false, status: terminalStatus };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (struck) {
|
|
122
|
+
state.streak++;
|
|
123
|
+
if (state.streak >= MAX_STRIKES) {
|
|
124
|
+
// On the abandoning strike, a same-turn terminal update
|
|
125
|
+
// is honored as completion rather than overridden by 499.
|
|
126
|
+
if (summaryText) {
|
|
127
|
+
state.streak = 0;
|
|
128
|
+
const updateEntry = recorded?.findLast?.(
|
|
129
|
+
(e) => e.scheme === "update",
|
|
130
|
+
);
|
|
131
|
+
const terminalStatus = updateEntry?.attributes?.status ?? 200;
|
|
132
|
+
return { continue: false, status: terminalStatus };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
continue: false,
|
|
136
|
+
status: 499,
|
|
137
|
+
reason:
|
|
138
|
+
cycleReason ||
|
|
139
|
+
`Abandoned after ${state.streak} consecutive strikes.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
continue: true,
|
|
144
|
+
reason: cycleReason || CONTRACT_REMINDER,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
state.streak = 0;
|
|
149
|
+
return { continue: true };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# file
|
|
1
|
+
# file {#file_plugin}
|
|
2
2
|
|
|
3
3
|
Owns file-related projections and file constraint management.
|
|
4
4
|
|
|
@@ -13,10 +13,10 @@ Owns file-related projections and file constraint management.
|
|
|
13
13
|
|
|
14
14
|
Static methods `setConstraint` and `dropConstraint` manage per-project
|
|
15
15
|
file constraints in the database. Constraints are project-level config
|
|
16
|
-
(backbone), not tool dispatch. See SPEC.md
|
|
16
|
+
(backbone), not tool dispatch. See [file_constraints](../../../SPEC.md#file_constraints).
|
|
17
17
|
|
|
18
|
-
- `active` / `readonly` — promoted into context.
|
|
19
|
-
- `ignore` — excluded from scans;
|
|
18
|
+
- `active` / `readonly` — promoted into context (visibility=visible).
|
|
19
|
+
- `ignore` — excluded from scans; summarizes existing entries.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
Promotion/demotion from constraints goes through the standard tool
|
|
22
|
+
handler chain via `dispatchTool`.
|
package/src/plugins/file/file.js
CHANGED
|
@@ -16,8 +16,8 @@ export default class File {
|
|
|
16
16
|
this.#core = core;
|
|
17
17
|
// "file" scheme covers bare paths (scheme IS NULL in DB)
|
|
18
18
|
core.registerScheme({ category: "data" });
|
|
19
|
-
core.on("
|
|
20
|
-
core.on("
|
|
19
|
+
core.on("visible", this.full.bind(this));
|
|
20
|
+
core.on("summarized", this.summary.bind(this));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
full(entry) {
|
|
@@ -59,6 +59,19 @@ export default class File {
|
|
|
59
59
|
|
|
60
60
|
return path;
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* True if `path` is covered by any readonly constraint for the project.
|
|
65
|
+
* Constraints can be globs; hedberg.match provides the pattern engine.
|
|
66
|
+
* Called from AgentLoop set-accept to refuse writes to protected paths.
|
|
67
|
+
*/
|
|
68
|
+
static async isReadonly(db, projectId, path) {
|
|
69
|
+
const rows = await db.get_file_constraints.all({ project_id: projectId });
|
|
70
|
+
const { hedmatch } = await import("./../hedberg/patterns.js");
|
|
71
|
+
return rows.some(
|
|
72
|
+
(r) => r.visibility === "readonly" && hedmatch(r.pattern, path),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
async function normalizePath(db, projectId, path) {
|
package/src/plugins/get/get.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Entries from "../../agent/Entries.js";
|
|
2
2
|
import { storePatternResult } from "../helpers.js";
|
|
3
3
|
import docs from "./getDoc.js";
|
|
4
4
|
|
|
@@ -9,8 +9,8 @@ export default class Get {
|
|
|
9
9
|
this.#core = core;
|
|
10
10
|
core.registerScheme();
|
|
11
11
|
core.on("handler", this.handler.bind(this));
|
|
12
|
-
core.on("
|
|
13
|
-
core.on("
|
|
12
|
+
core.on("visible", this.full.bind(this));
|
|
13
|
+
core.on("summarized", this.summary.bind(this));
|
|
14
14
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
15
15
|
docsMap.get = docs;
|
|
16
16
|
return docsMap;
|
|
@@ -21,21 +21,34 @@ export default class Get {
|
|
|
21
21
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
22
22
|
const target = entry.attributes.path;
|
|
23
23
|
if (!target) {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Route through the unified error channel so the message lands
|
|
25
|
+
// as `<error>` in <log> with a readable body AND the failure
|
|
26
|
+
// counts as a strike. The previous direct `store.set` wrote a
|
|
27
|
+
// blank-bodied failed entry whose `error` attribute was never
|
|
28
|
+
// rendered — model saw a vague 400 and repeated the mistake.
|
|
29
|
+
await rummy.hooks.error.log.emit({
|
|
30
|
+
store,
|
|
31
|
+
runId,
|
|
32
|
+
turn,
|
|
26
33
|
loopId,
|
|
34
|
+
message:
|
|
35
|
+
'Missing required "path" attribute on <get>. Use <get path="..."/>.',
|
|
36
|
+
status: 400,
|
|
27
37
|
});
|
|
28
38
|
return;
|
|
29
39
|
}
|
|
30
|
-
const normalized =
|
|
31
|
-
|
|
40
|
+
const normalized = Entries.normalizePath(target);
|
|
41
|
+
// XmlParser passes attributes through; `body` attr is optional.
|
|
42
|
+
const bodyFilter = entry.attributes.body;
|
|
32
43
|
const preview = entry.attributes.preview !== undefined;
|
|
33
44
|
const isPattern = bodyFilter || normalized.includes("*");
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
// Negative `line` is idiomatic tail-from-end: `line="-50"` means
|
|
47
|
+
// "start 50 lines from the end," enabling `tail -n N` behavior.
|
|
48
|
+
// Positive `line` is 1-indexed from start (classic). `limit` is
|
|
49
|
+
// always a positive count.
|
|
50
|
+
const lineRaw = entry.attributes.line;
|
|
51
|
+
const line = lineRaw != null ? parseInt(lineRaw, 10) : null;
|
|
39
52
|
const limit =
|
|
40
53
|
entry.attributes.limit != null
|
|
41
54
|
? Math.max(1, parseInt(entry.attributes.limit, 10))
|
|
@@ -48,7 +61,7 @@ export default class Get {
|
|
|
48
61
|
);
|
|
49
62
|
|
|
50
63
|
// Preview — list matches with their full-body token costs. No promotion,
|
|
51
|
-
// no
|
|
64
|
+
// no visibility change, no Token Budget spent. Model uses this to plan
|
|
52
65
|
// which entries to actually promote. getDoc promises this behavior; the
|
|
53
66
|
// prior implementation silently promoted anyway, burning the Token Budget
|
|
54
67
|
// on entries the model thought it was only inspecting.
|
|
@@ -66,62 +79,91 @@ export default class Get {
|
|
|
66
79
|
return;
|
|
67
80
|
}
|
|
68
81
|
|
|
69
|
-
// Partial read — no
|
|
82
|
+
// Partial read — no visibility promotion, returns a line slice as the log item.
|
|
70
83
|
if (line !== null || limit !== null) {
|
|
71
84
|
if (isPattern) {
|
|
72
|
-
await store.
|
|
85
|
+
await store.set({
|
|
73
86
|
runId,
|
|
74
87
|
turn,
|
|
75
|
-
entry.resultPath,
|
|
76
|
-
"line/limit requires a single path, not a glob or body filter",
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
path: entry.resultPath,
|
|
89
|
+
body: "line/limit requires a single path, not a glob or body filter",
|
|
90
|
+
state: "failed",
|
|
91
|
+
outcome: "validation",
|
|
92
|
+
loopId,
|
|
93
|
+
attributes: { path: target },
|
|
94
|
+
});
|
|
80
95
|
return;
|
|
81
96
|
}
|
|
82
97
|
if (matches.length === 0) {
|
|
83
|
-
await store.
|
|
98
|
+
await store.set({
|
|
84
99
|
runId,
|
|
85
100
|
turn,
|
|
86
|
-
entry.resultPath,
|
|
87
|
-
`${target} not found`,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
path: entry.resultPath,
|
|
102
|
+
body: `${target} not found`,
|
|
103
|
+
state: "resolved",
|
|
104
|
+
loopId,
|
|
105
|
+
attributes: { path: target },
|
|
106
|
+
});
|
|
91
107
|
return;
|
|
92
108
|
}
|
|
93
109
|
const allLines = matches[0].body.split("\n");
|
|
94
110
|
const total = allLines.length;
|
|
95
|
-
|
|
111
|
+
// Negative line offsets from the end: line=-50 starts 50 lines
|
|
112
|
+
// before the end. Clamped to 1 if the offset exceeds total.
|
|
113
|
+
const startLine =
|
|
114
|
+
line == null
|
|
115
|
+
? 1
|
|
116
|
+
: line < 0
|
|
117
|
+
? Math.max(1, total + line + 1)
|
|
118
|
+
: Math.max(1, line);
|
|
96
119
|
const startIdx = startLine - 1;
|
|
97
120
|
const endIdx = limit !== null ? Math.min(startIdx + limit, total) : total;
|
|
98
121
|
const slice = allLines.slice(startIdx, endIdx).join("\n");
|
|
99
122
|
const endLine = endIdx;
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
// Body leads with the source path so the model can re-issue
|
|
124
|
+
// a full or different-range read without guessing the URL.
|
|
125
|
+
// lineStart/lineEnd/totalLines ride attrs so renderLogTag can
|
|
126
|
+
// surface `lines="a-b/total"` without parsing the body.
|
|
127
|
+
const header = `${target}\n[lines ${startLine}–${endLine} / ${total} total]`;
|
|
128
|
+
await store.set({
|
|
102
129
|
runId,
|
|
103
130
|
turn,
|
|
104
|
-
entry.resultPath,
|
|
105
|
-
`${header}\n${slice}`,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
path: entry.resultPath,
|
|
132
|
+
body: `${header}\n${slice}`,
|
|
133
|
+
state: "resolved",
|
|
134
|
+
loopId,
|
|
135
|
+
attributes: {
|
|
136
|
+
path: target,
|
|
137
|
+
lineStart: startLine,
|
|
138
|
+
lineEnd: endLine,
|
|
139
|
+
totalLines: total,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
109
142
|
return;
|
|
110
143
|
}
|
|
111
144
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
const VALID_VISIBILITY = {
|
|
146
|
+
summarized: 1,
|
|
147
|
+
visible: 1,
|
|
115
148
|
archived: 1,
|
|
116
149
|
};
|
|
117
|
-
const
|
|
118
|
-
? entry.attributes.
|
|
150
|
+
const visibilityAttr = VALID_VISIBILITY[entry.attributes.visibility]
|
|
151
|
+
? entry.attributes.visibility
|
|
119
152
|
: null;
|
|
120
153
|
|
|
121
|
-
await store.
|
|
122
|
-
|
|
154
|
+
await store.get({
|
|
155
|
+
runId: runId,
|
|
156
|
+
turn: turn,
|
|
157
|
+
path: normalized,
|
|
158
|
+
bodyFilter: bodyFilter,
|
|
159
|
+
});
|
|
160
|
+
if (visibilityAttr) {
|
|
123
161
|
for (const match of matches)
|
|
124
|
-
await store.
|
|
162
|
+
await store.set({
|
|
163
|
+
runId: runId,
|
|
164
|
+
path: match.path,
|
|
165
|
+
visibility: visibilityAttr,
|
|
166
|
+
});
|
|
125
167
|
}
|
|
126
168
|
|
|
127
169
|
if (isPattern) {
|
|
@@ -135,14 +177,27 @@ export default class Get {
|
|
|
135
177
|
matches,
|
|
136
178
|
{ loopId, attributes: { path: target } },
|
|
137
179
|
);
|
|
180
|
+
} else if (matches.length === 0) {
|
|
181
|
+
await store.set({
|
|
182
|
+
runId,
|
|
183
|
+
turn,
|
|
184
|
+
path: entry.resultPath,
|
|
185
|
+
body: `${target} not found`,
|
|
186
|
+
state: "resolved",
|
|
187
|
+
loopId,
|
|
188
|
+
attributes: { path: target },
|
|
189
|
+
});
|
|
138
190
|
} else {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
191
|
+
// Log a concise record of the promotion. The promoted entry
|
|
192
|
+
// itself is visible in <context>; this log line is the model's
|
|
193
|
+
// proof in <log> that the get has already been done so it
|
|
194
|
+
// doesn't re-issue the same fetch on a later turn.
|
|
195
|
+
await store.set({
|
|
196
|
+
runId,
|
|
197
|
+
turn,
|
|
198
|
+
path: entry.resultPath,
|
|
199
|
+
body: `${target} promoted`,
|
|
200
|
+
state: "resolved",
|
|
146
201
|
loopId,
|
|
147
202
|
attributes: { path: target },
|
|
148
203
|
});
|
|
@@ -1,33 +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
|
-
["## <get>[path/to/file]</get> - Promote an entry"],
|
|
6
|
-
["Example: <get>src/app.js</get>", "Simplest form. Body = path."],
|
|
7
|
-
[
|
|
8
|
-
'Example: <get path="known://*">auth</get>',
|
|
9
|
-
"Keyword recall: glob in path, search term in body.",
|
|
10
|
-
],
|
|
11
|
-
[
|
|
12
|
-
'Example: <get path="src/**/*.js">authentication</get>',
|
|
13
|
-
"Full pattern: recursive glob + content filter.",
|
|
14
|
-
],
|
|
15
|
-
[
|
|
16
|
-
'Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>',
|
|
17
|
-
"Partial read. Returns lines 644–723 without promoting.",
|
|
18
|
-
],
|
|
19
|
-
[
|
|
20
|
-
"* Paths accept patterns: `src/**/*.js`, `known://api_*`",
|
|
21
|
-
"Reinforces picomatch patterns work everywhere.",
|
|
22
|
-
],
|
|
23
|
-
[
|
|
24
|
-
"* Body text filters results by content match",
|
|
25
|
-
"Body = filter, not just path.",
|
|
26
|
-
],
|
|
27
|
-
[
|
|
28
|
-
"* `line` and `limit` read a slice without promoting the entry, which costs as many tokens as the slice contains.",
|
|
29
|
-
"Partial read is safe: context budget unaffected.",
|
|
30
|
-
],
|
|
31
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
32
2
|
|
|
33
|
-
export default
|
|
3
|
+
export default loadDoc(import.meta.url, "getDoc.md");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
## <get>[path/to/file]</get> - Promote an entry
|
|
2
|
+
|
|
3
|
+
Example: <get>src/app.js</get>
|
|
4
|
+
<!-- Simplest form. Body = path. -->
|
|
5
|
+
|
|
6
|
+
Example: <get path="known://*">auth</get>
|
|
7
|
+
<!-- Keyword recall: glob in path, search term in body. -->
|
|
8
|
+
|
|
9
|
+
Example: <get path="src/**/*.js">authentication</get>
|
|
10
|
+
<!-- Full pattern: recursive glob + content filter. -->
|
|
11
|
+
|
|
12
|
+
Example: <get path="src/**/*.js" preview>authentication</get>
|
|
13
|
+
<!-- Full pattern: recursive glob + content filter. -->
|
|
14
|
+
|
|
15
|
+
Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>
|
|
16
|
+
<!-- Partial read. Returns lines 644–723 without promoting. -->
|
|
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
|
+
|
|
21
|
+
Example: <get path="https://en.wikipedia.org/wiki/Long_Page" line="1" limit="200"/>
|
|
22
|
+
<!-- URL partial read. When a page is too large to promote whole, read a slice. Pattern generalizes to every scheme. -->
|
|
23
|
+
|
|
24
|
+
* Paths accept patterns: `src/**/*.js`, `known://api_*`
|
|
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
|
+
* `preview` lists the paths and token budget impact of an operation without performing it.
|
|
34
|
+
<!-- Partial read is safe: context budget unaffected. Tail idiom enables watching growing entries. -->
|
|
35
|
+
|
|
36
|
+
* Remember to <set path="..." visibility="summarize"/> when entries or log events are no longer relevant.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# hedberg
|
|
1
|
+
# hedberg {#hedberg_plugin}
|
|
2
2
|
|
|
3
3
|
The interpretation boundary between stochastic model output and
|
|
4
4
|
deterministic system operations.
|
|
@@ -26,7 +26,6 @@ constructor(core) {
|
|
|
26
26
|
| `replace(body, search, replacement, opts?)` | Apply replacement (sed regex → literal → heuristic) |
|
|
27
27
|
| `parseSed(input)` | Parse sed syntax into `[{ search, replace, flags, sed }]` |
|
|
28
28
|
| `parseEdits(content)` | Detect edit format (merge conflict, udiff, Claude XML) |
|
|
29
|
-
| `normalizeAttrs(attrs)` | Heal model attribute names (value→body, unknown→path) |
|
|
30
29
|
| `generatePatch(path, old, new)` | Generate unified diff |
|
|
31
30
|
|
|
32
31
|
### Hedberg.replace(body, search, replacement, options?)
|