@possumtech/rummy 2.2.1 → 2.3.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/package.json +14 -6
- package/service.js +18 -10
- package/src/agent/AgentLoop.js +2 -11
- package/src/agent/ContextAssembler.js +34 -3
- package/src/agent/Entries.js +16 -89
- package/src/agent/ProjectAgent.js +1 -16
- package/src/agent/TurnExecutor.js +12 -52
- package/src/agent/XmlParser.js +30 -117
- package/src/agent/errors.js +3 -22
- package/src/agent/materializeContext.js +3 -11
- package/src/hooks/Hooks.js +0 -29
- package/src/lib/hedberg/hedberg.js +4 -14
- package/src/lib/hedberg/marker.js +15 -59
- package/src/llm/LlmProvider.js +13 -26
- package/src/llm/errors.js +3 -11
- package/src/llm/openaiStream.js +6 -46
- package/src/plugins/ask_user/ask_user.js +12 -17
- package/src/plugins/budget/README.md +46 -8
- package/src/plugins/budget/budget.js +23 -42
- package/src/plugins/cp/cp.js +28 -18
- package/src/plugins/env/env.js +11 -7
- package/src/plugins/error/error.js +8 -37
- package/src/plugins/get/get.js +42 -24
- package/src/plugins/google/google.js +23 -3
- package/src/plugins/helpers.js +34 -50
- package/src/plugins/instructions/README.md +2 -2
- package/src/plugins/instructions/instructions-user.md +1 -1
- package/src/plugins/instructions/instructions.js +19 -6
- package/src/plugins/known/known.js +1 -8
- package/src/plugins/log/log.js +15 -1
- package/src/plugins/mv/mv.js +29 -19
- package/src/plugins/persona/persona.js +4 -4
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +1 -1
- package/src/plugins/rm/rm.js +26 -15
- package/src/plugins/rm/rmDoc.md +0 -2
- package/src/plugins/set/set.js +37 -84
- package/src/plugins/set/setDoc.md +16 -16
- package/src/plugins/sh/sh.js +10 -8
- package/src/plugins/skill/skillDoc.md +1 -1
- package/src/plugins/unknown/README.md +1 -1
- package/src/plugins/unknown/unknown.js +2 -6
- package/src/plugins/update/update.js +3 -2
- package/src/plugins/update/updateDoc.md +1 -1
- package/.env.example +0 -152
- package/.xai.key +0 -1
- package/PLUGINS.md +0 -962
- package/SPEC.md +0 -1897
- package/biome/no-fallbacks.grit +0 -50
- package/gemini.key +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SOFT_FAILURE_OUTCOMES } from "../../agent/errors.js";
|
|
2
|
-
import {
|
|
2
|
+
import { projectEmission, summarizeEmission } from "../helpers.js";
|
|
3
3
|
|
|
4
4
|
const MAX_STRIKES = Number(process.env.RUMMY_MAX_STRIKES);
|
|
5
5
|
const MIN_CYCLES = Number(process.env.RUMMY_MIN_CYCLES);
|
|
@@ -40,19 +40,14 @@ export default class ErrorPlugin {
|
|
|
40
40
|
constructor(core) {
|
|
41
41
|
this.#core = core;
|
|
42
42
|
core.registerScheme({ category: "logging" });
|
|
43
|
-
core.on("visible", (entry) =>
|
|
44
|
-
core.on("summarized", (entry) => entry.body
|
|
43
|
+
core.on("visible", (entry) => projectEmission(entry.body));
|
|
44
|
+
core.on("summarized", (entry) => summarizeEmission(entry.body));
|
|
45
45
|
|
|
46
46
|
core.hooks.error.log.on(this.#onErrorLog.bind(this));
|
|
47
47
|
core.hooks.loop.started.on(this.#onLoopStarted.bind(this));
|
|
48
48
|
core.hooks.loop.completed.on(this.#onLoopCompleted.bind(this));
|
|
49
49
|
core.hooks.turn.started.on(this.#onTurnStarted.bind(this));
|
|
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
51
|
core.filter("turn.verdict", this.#verdict.bind(this));
|
|
57
52
|
}
|
|
58
53
|
|
|
@@ -85,15 +80,8 @@ export default class ErrorPlugin {
|
|
|
85
80
|
}) {
|
|
86
81
|
const statusValue = status ?? 400;
|
|
87
82
|
const path = await store.logPath(runId, turn, "error", message);
|
|
88
|
-
// Soft errors record
|
|
89
|
-
//
|
|
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.
|
|
83
|
+
// Soft errors record without striking — recovered issues the model
|
|
84
|
+
// should see but not be punished for. SPEC #entries.
|
|
97
85
|
await store.set({
|
|
98
86
|
runId,
|
|
99
87
|
turn,
|
|
@@ -113,31 +101,19 @@ export default class ErrorPlugin {
|
|
|
113
101
|
_currentVerdict,
|
|
114
102
|
{ store, runId, loopId, recorded, summaryText, turn: _turn },
|
|
115
103
|
) {
|
|
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.
|
|
121
104
|
const state = this.#loopState.get(loopId);
|
|
122
105
|
|
|
123
106
|
let cycleReason = null;
|
|
124
|
-
// Empty turns share a blank fingerprint; intentional.
|
|
125
107
|
const fp = recorded.map(fingerprint).toSorted().join("|");
|
|
126
108
|
state.history.push(fp);
|
|
127
109
|
const cycle = detectCycle(state.history);
|
|
128
110
|
if (cycle.detected) {
|
|
129
111
|
cycleReason = "Loop detected";
|
|
130
|
-
// Silent strike: increment turn-errors without a model-facing entry.
|
|
131
112
|
state.turnErrors++;
|
|
132
113
|
}
|
|
133
114
|
|
|
134
|
-
//
|
|
135
|
-
//
|
|
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.
|
|
115
|
+
// Soft outcomes (not_found, conflict) are state-discovery findings
|
|
116
|
+
// the model adapts to; only hard failures count toward the strike.
|
|
141
117
|
let recordedFailed = false;
|
|
142
118
|
for (const e of recorded) {
|
|
143
119
|
const current = await store.getState(runId, e.path);
|
|
@@ -161,7 +137,7 @@ export default class ErrorPlugin {
|
|
|
161
137
|
if (struck) {
|
|
162
138
|
state.streak++;
|
|
163
139
|
if (state.streak >= MAX_STRIKES) {
|
|
164
|
-
//
|
|
140
|
+
// Same-turn terminal update wins over 499.
|
|
165
141
|
if (summaryText) {
|
|
166
142
|
state.streak = 0;
|
|
167
143
|
const updateEntry = recorded?.findLast?.(
|
|
@@ -178,11 +154,6 @@ export default class ErrorPlugin {
|
|
|
178
154
|
`Abandoned after ${state.streak} consecutive strikes.`,
|
|
179
155
|
};
|
|
180
156
|
}
|
|
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
157
|
return { continue: true };
|
|
187
158
|
}
|
|
188
159
|
|
package/src/plugins/get/get.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import Entries from "../../agent/Entries.js";
|
|
2
|
-
import {
|
|
2
|
+
import { countTokens } from "../../agent/tokens.js";
|
|
3
|
+
import {
|
|
4
|
+
projectEmission,
|
|
5
|
+
storePatternResult,
|
|
6
|
+
summarizeEmission,
|
|
7
|
+
} from "../helpers.js";
|
|
3
8
|
import docs from "./getDoc.js";
|
|
4
9
|
|
|
5
10
|
export default class Get {
|
|
@@ -19,11 +24,8 @@ export default class Get {
|
|
|
19
24
|
|
|
20
25
|
async handler(entry, rummy) {
|
|
21
26
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
22
|
-
// Search-by-tags: same `tags` attribute that <set> writes onto
|
|
23
|
-
// entries. Same name on both ends — no in/out semantic split.
|
|
24
27
|
const tagsAttr = entry.attributes.tags;
|
|
25
|
-
// Tags-only get defaults path to "**"
|
|
26
|
-
// folksonomic tags without remembering exact paths.
|
|
28
|
+
// Tags-only get defaults path to "**" for tag-only recall.
|
|
27
29
|
const target = entry.attributes.path || (tagsAttr ? "**" : null);
|
|
28
30
|
if (!target) {
|
|
29
31
|
await store.set({
|
|
@@ -74,7 +76,6 @@ export default class Get {
|
|
|
74
76
|
});
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
// Manifest: list matches + full-body token costs; no promotion.
|
|
78
79
|
if (manifest) {
|
|
79
80
|
await storePatternResult(
|
|
80
81
|
store,
|
|
@@ -89,27 +90,36 @@ export default class Get {
|
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// Partial read:
|
|
93
|
-
//
|
|
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.
|
|
93
|
+
// Partial read: slice into attrs.slice, no promotion. Multi-match
|
|
94
|
+
// emits one section per match.
|
|
97
95
|
if (line !== null || limit !== null) {
|
|
98
96
|
if (matches.length === 0) {
|
|
99
97
|
await store.set({
|
|
100
98
|
runId,
|
|
101
99
|
turn,
|
|
102
100
|
path: entry.resultPath,
|
|
103
|
-
body:
|
|
101
|
+
body: "",
|
|
104
102
|
state: "resolved",
|
|
103
|
+
outcome: "not_found",
|
|
105
104
|
loopId,
|
|
106
|
-
attributes: {
|
|
105
|
+
attributes: {
|
|
106
|
+
path: target,
|
|
107
|
+
line,
|
|
108
|
+
limit,
|
|
109
|
+
error: `${target} not found`,
|
|
110
|
+
},
|
|
107
111
|
});
|
|
108
112
|
return;
|
|
109
113
|
}
|
|
110
114
|
const sections = matches.map((match) => sliceSection(match, line, limit));
|
|
111
|
-
const
|
|
112
|
-
const attributes = {
|
|
115
|
+
const sliceBody = sections.map((s) => s.text).join("\n\n");
|
|
116
|
+
const attributes = {
|
|
117
|
+
path: target,
|
|
118
|
+
line,
|
|
119
|
+
limit,
|
|
120
|
+
beforeActionTokens: 0,
|
|
121
|
+
afterActionTokens: countTokens(sliceBody),
|
|
122
|
+
};
|
|
113
123
|
if (sections.length === 1) {
|
|
114
124
|
const only = sections[0];
|
|
115
125
|
attributes.lineStart = only.startLine;
|
|
@@ -122,7 +132,7 @@ export default class Get {
|
|
|
122
132
|
runId,
|
|
123
133
|
turn,
|
|
124
134
|
path: entry.resultPath,
|
|
125
|
-
body,
|
|
135
|
+
body: sliceBody,
|
|
126
136
|
state: "resolved",
|
|
127
137
|
loopId,
|
|
128
138
|
attributes,
|
|
@@ -170,31 +180,39 @@ export default class Get {
|
|
|
170
180
|
runId,
|
|
171
181
|
turn,
|
|
172
182
|
path: entry.resultPath,
|
|
173
|
-
body:
|
|
183
|
+
body: "",
|
|
174
184
|
state: "resolved",
|
|
185
|
+
outcome: "not_found",
|
|
175
186
|
loopId,
|
|
176
|
-
attributes: { path: target },
|
|
187
|
+
attributes: { path: target, error: `${target} not found` },
|
|
177
188
|
});
|
|
178
189
|
} else {
|
|
179
|
-
|
|
190
|
+
const promotedTokens = matches.reduce(
|
|
191
|
+
(n, m) => n + countTokens(m.body),
|
|
192
|
+
0,
|
|
193
|
+
);
|
|
180
194
|
await store.set({
|
|
181
195
|
runId,
|
|
182
196
|
turn,
|
|
183
197
|
path: entry.resultPath,
|
|
184
|
-
body:
|
|
198
|
+
body: "",
|
|
185
199
|
state: "resolved",
|
|
186
200
|
loopId,
|
|
187
|
-
attributes: {
|
|
201
|
+
attributes: {
|
|
202
|
+
path: target,
|
|
203
|
+
beforeActionTokens: 0,
|
|
204
|
+
afterActionTokens: promotedTokens,
|
|
205
|
+
},
|
|
188
206
|
});
|
|
189
207
|
}
|
|
190
208
|
}
|
|
191
209
|
|
|
192
210
|
full(entry) {
|
|
193
|
-
return
|
|
211
|
+
return projectEmission(entry.body);
|
|
194
212
|
}
|
|
195
213
|
|
|
196
|
-
summary() {
|
|
197
|
-
return
|
|
214
|
+
summary(entry) {
|
|
215
|
+
return summarizeEmission(entry.body);
|
|
198
216
|
}
|
|
199
217
|
}
|
|
200
218
|
|
|
@@ -10,6 +10,21 @@ const PROVIDER = "google";
|
|
|
10
10
|
const BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
11
11
|
const COMPAT_URL = `${BASE_URL}/openai`;
|
|
12
12
|
|
|
13
|
+
// Documented input-token limits, prefix-matched. The native introspection
|
|
14
|
+
// endpoint (/v1beta/models/{model}) requires a key permission separate from
|
|
15
|
+
// generateContent — keys provisioned for chat-only return 403 here, crashing
|
|
16
|
+
// any run that depends on the lookup. Trust the docs first; hit the API only
|
|
17
|
+
// for unknown models.
|
|
18
|
+
const KNOWN_CONTEXT = [
|
|
19
|
+
["gemini-3.1-flash-lite", 1_048_576],
|
|
20
|
+
["gemini-3.1-flash", 1_048_576],
|
|
21
|
+
["gemini-3.1-pro", 1_048_576],
|
|
22
|
+
["gemini-3.0", 1_048_576],
|
|
23
|
+
["gemini-2.5", 1_048_576],
|
|
24
|
+
["gemini-2.0", 1_048_576],
|
|
25
|
+
["gemini-1.5", 1_048_576],
|
|
26
|
+
];
|
|
27
|
+
|
|
13
28
|
// Repo-root-relative key file. Resolved relative to this source file so
|
|
14
29
|
// CWD changes during runs (programbench/tbench cd into workspaces) don't
|
|
15
30
|
// break the lookup. Plugin is inert if the file is missing. Tests may
|
|
@@ -94,9 +109,14 @@ export default class Google {
|
|
|
94
109
|
async #getContextSize(model) {
|
|
95
110
|
if (this.#contextCache.has(model)) return this.#contextCache.get(model);
|
|
96
111
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
const known = KNOWN_CONTEXT.find(([prefix]) => model.startsWith(prefix));
|
|
113
|
+
if (known) {
|
|
114
|
+
this.#contextCache.set(model, known[1]);
|
|
115
|
+
return known[1];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// /v1beta/models/{model} requires `?key=` (Bearer 401s here) AND a
|
|
119
|
+
// key scope that includes models.get — chat-only keys return 403.
|
|
100
120
|
const url = `${BASE_URL}/models/${model}?key=${encodeURIComponent(this.#apiKey)}`;
|
|
101
121
|
const res = await fetch(url, {
|
|
102
122
|
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
package/src/plugins/helpers.js
CHANGED
|
@@ -2,30 +2,32 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
-
// Hard
|
|
6
|
-
// source of truth — every plugin's `summarized` view must produce output
|
|
7
|
-
// ≤ this many characters; materializeContext's defensive cap fires when
|
|
8
|
-
// a plugin breaks the contract. Change this number, everything downstream
|
|
9
|
-
// stays consistent (no "450 here, 480 there, 500 over yonder" drift).
|
|
5
|
+
// Hard ceiling on summarized projections. materializeContext enforces.
|
|
10
6
|
export const SUMMARY_MAX_CHARS = 500;
|
|
11
7
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
8
|
+
// Tab-indent every line so a column-zero `:::path` in the body can't
|
|
9
|
+
// prematurely close the outer heredoc envelope.
|
|
10
|
+
export function projectEmission(source) {
|
|
11
|
+
if (!source) return "";
|
|
12
|
+
return source
|
|
13
|
+
.split("\n")
|
|
14
|
+
.map((line) => `\t${line}`)
|
|
15
|
+
.join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Same tab-indent recap signal as `projectEmission`, capped at
|
|
19
|
+
// SUMMARY_MAX_CHARS post-projection so tabs don't push past the contract.
|
|
20
|
+
export function summarizeEmission(body) {
|
|
21
|
+
if (!body) return "";
|
|
22
|
+
const projected = projectEmission(body);
|
|
23
|
+
return projected.length > SUMMARY_MAX_CHARS
|
|
24
|
+
? projected.slice(0, SUMMARY_MAX_CHARS)
|
|
25
|
+
: projected;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Heredoc fence (path is the terminator) — distinct from XML so model
|
|
29
|
+
// emissions and entry projections can't collide. JSON meta sorted for
|
|
30
|
+
// prefix-cache stability.
|
|
29
31
|
export function renderEntry(path, metadata, body) {
|
|
30
32
|
const meta = canonicalJson(metadata);
|
|
31
33
|
if (!body) {
|
|
@@ -35,7 +37,6 @@ export function renderEntry(path, metadata, body) {
|
|
|
35
37
|
return `${meta} <<:::${path}\n${body}${trailingNewline}:::${path}`;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// JSON.stringify with sorted top-level keys for byte-stable output.
|
|
39
40
|
function canonicalJson(obj) {
|
|
40
41
|
const keys = Object.keys(obj).sort();
|
|
41
42
|
const sorted = {};
|
|
@@ -43,39 +44,28 @@ function canonicalJson(obj) {
|
|
|
43
44
|
return JSON.stringify(sorted);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
// Read sibling tooldoc .md
|
|
47
|
+
// Read sibling tooldoc .md, strip HTML comments (rationale stays out of
|
|
48
|
+
// the model packet) and collapse blank-line runs.
|
|
47
49
|
export function loadDoc(metaUrl, name) {
|
|
48
50
|
const dir = dirname(fileURLToPath(metaUrl));
|
|
49
51
|
return readFileSync(join(dir, name), "utf8")
|
|
52
|
+
.replace(/^[ \t]*<!--[\s\S]*?-->[ \t]*\n?/gm, "")
|
|
50
53
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
51
54
|
.replace(/\n{3,}/g, "\n\n")
|
|
52
55
|
.trim();
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
// log://turn_N/{action}/{rest} → {action}://turn_N/{rest}; null if not a log path.
|
|
56
58
|
export function logPathToDataBase(logPath) {
|
|
57
59
|
const m = logPath?.match(/^log:\/\/turn_(\d+)\/([^/]+)\/(.+)$/);
|
|
58
60
|
if (!m) return null;
|
|
59
61
|
return `${m[2]}://turn_${m[1]}/${m[3]}`;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
// (see materializeContext) and protects against few-newline output like
|
|
66
|
-
// terminal-control programs (cmatrix, htop) emitting one giant ANSI line.
|
|
67
|
-
//
|
|
68
|
-
// Output stays as a flat string (not a renderEntry block) because the
|
|
69
|
-
// caller (log.assembleLog) wraps each log entry in renderEntry with its
|
|
70
|
-
// own metadata; this is the BODY of that block. Effectively double
|
|
71
|
-
// fencing — `<<:::log://turn_3/sh/foo` outer, then this header inside —
|
|
72
|
-
// but that's correct: the outer fence labels "this is sh activity at
|
|
73
|
-
// turn 3", and the body inside is the slice of stdout the model sees.
|
|
74
|
-
export function streamSummary(label, entry, MAX_LINES = 20) {
|
|
64
|
+
// Tail-truncate stream output to last MAX_LINES, then chop to
|
|
65
|
+
// SUMMARY_MAX_CHARS for one-line giants (ANSI/cmatrix shape).
|
|
66
|
+
export function streamSummary(_label, entry, MAX_LINES = 20) {
|
|
75
67
|
if (!entry.body) return "";
|
|
76
|
-
const { body
|
|
77
|
-
const command = attributes.command;
|
|
78
|
-
const channel = attributes.channel === 2 ? "stderr" : "stdout";
|
|
68
|
+
const { body } = entry;
|
|
79
69
|
const trailingNewline = body.endsWith("\n");
|
|
80
70
|
const lines = trailingNewline
|
|
81
71
|
? body.slice(0, -1).split("\n")
|
|
@@ -85,17 +75,11 @@ export function streamSummary(label, entry, MAX_LINES = 20) {
|
|
|
85
75
|
total <= MAX_LINES
|
|
86
76
|
? body
|
|
87
77
|
: lines.slice(-MAX_LINES).join("\n") + (trailingNewline ? "\n" : "");
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
? `# ${label} ${command} (${channel}, ${total}L)`
|
|
92
|
-
: `# ${label} ${command} (${channel}, lines ${total - MAX_LINES + 1} through ${total} of ${total}; <get line="1" limit="N"/> for head)`;
|
|
93
|
-
|
|
94
|
-
const out = `${header}\n${lineTail}`;
|
|
95
|
-
return out.length > SUMMARY_MAX_CHARS ? out.slice(0, SUMMARY_MAX_CHARS) : out;
|
|
78
|
+
return lineTail.length > SUMMARY_MAX_CHARS
|
|
79
|
+
? lineTail.slice(0, SUMMARY_MAX_CHARS)
|
|
80
|
+
: lineTail;
|
|
96
81
|
}
|
|
97
82
|
|
|
98
|
-
// Pattern-result log entry shared by get/set/store/rm.
|
|
99
83
|
export async function storePatternResult(
|
|
100
84
|
store,
|
|
101
85
|
runId,
|
|
@@ -17,7 +17,7 @@ run.
|
|
|
17
17
|
participants mutate a docsMap keyed by tool name). Render order
|
|
18
18
|
follows tool-registration order.
|
|
19
19
|
- **Filter**: `assembly.user` (priority 165) — renders
|
|
20
|
-
`instructions-user.md` as `<
|
|
20
|
+
`instructions-user.md` as `<system_requirements>` late in the user
|
|
21
21
|
message, between `<unknowns>` (150) and `<budget>` (175). The
|
|
22
22
|
user message is a sandwich: `<prompt>` (30) leads for cache
|
|
23
23
|
stability, dynamic state fills the middle, then rules and
|
|
@@ -39,7 +39,7 @@ The persona block is rendered by the persona plugin's own
|
|
|
39
39
|
Static within a run; only `[%TOOLS%]` substitutes at render. No
|
|
40
40
|
per-turn content here, ever.
|
|
41
41
|
- `instructions-user.md` — the per-turn imperative reminder
|
|
42
|
-
rendered as `<
|
|
42
|
+
rendered as `<system_requirements>` in the user message. Same bytes
|
|
43
43
|
every turn.
|
|
44
44
|
- `protocol.js` / `protocol.test.js` — pass-through stub on
|
|
45
45
|
`entry.recording` (priority 1) reserved for future
|
|
@@ -50,4 +50,4 @@ Example:
|
|
|
50
50
|
<update status="200">Paris</update>
|
|
51
51
|
|
|
52
52
|
YOU MUST NOT allow the `"tokens":N` sum of source entries, prompts, or log events to exceed `tokensFree="N"` budget.
|
|
53
|
-
YOU MUST terminate
|
|
53
|
+
YOU MUST terminate every turn with <update status="{102|200}">{ direct one-line answer or one-line summary }</update> (<= 80 chars)
|
|
@@ -28,13 +28,26 @@ export default class Instructions {
|
|
|
28
28
|
this.#core = core;
|
|
29
29
|
core.hooks.instructions.findLatestSummary =
|
|
30
30
|
this.findLatestSummary.bind(this);
|
|
31
|
-
// System message:
|
|
32
|
-
//
|
|
31
|
+
// System message: <system_commands> wraps the grammar + per-tool
|
|
32
|
+
// docs as a single semantic unit. Wrapper filters at 49 / 101
|
|
33
|
+
// sandwich the two content filters at 50 / 100. Other system
|
|
34
|
+
// participants (state blocks at 200/250/300/350) render after.
|
|
35
|
+
core.filter(
|
|
36
|
+
"assembly.system",
|
|
37
|
+
(content) => `${content}<system_commands>\n`,
|
|
38
|
+
49,
|
|
39
|
+
);
|
|
33
40
|
core.filter("assembly.system", this.assembleSystemBase.bind(this), 50);
|
|
34
41
|
core.filter("assembly.system", this.assembleSystemToolDocs.bind(this), 100);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
core.filter(
|
|
43
|
+
"assembly.system",
|
|
44
|
+
(content) => `${content}\n</system_commands>\n`,
|
|
45
|
+
101,
|
|
46
|
+
);
|
|
47
|
+
// User message: <system_requirements> at the action site —
|
|
48
|
+
// recency keeps protocol discipline (XML tag wrapping, terminal
|
|
49
|
+
// `<update>`) warm right before generation.
|
|
50
|
+
core.filter("assembly.user", this.assembleInstructions.bind(this), 165);
|
|
38
51
|
new Protocol(core);
|
|
39
52
|
}
|
|
40
53
|
|
|
@@ -76,7 +89,7 @@ export default class Instructions {
|
|
|
76
89
|
|
|
77
90
|
// assembly.user @ 165 — per-turn reminder, same body every turn.
|
|
78
91
|
assembleInstructions(content, _ctx) {
|
|
79
|
-
return `${content}<
|
|
92
|
+
return `${content}<system_requirements>\n${userInstructions}\n</system_requirements>\n`;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
// Latest terminal update (status=200) — used by cli.js to print the
|
|
@@ -16,8 +16,7 @@ export default class Known {
|
|
|
16
16
|
core.on("summarized", this.summary.bind(this));
|
|
17
17
|
core.filter("assembly.system", this.assembleSummarized.bind(this), 200);
|
|
18
18
|
core.filter("assembly.system", this.assembleVisible.bind(this), 250);
|
|
19
|
-
//
|
|
20
|
-
// The known:// scheme lifecycle is taught in instructions-user.md.
|
|
19
|
+
// Written via <set path="known://...">; lifecycle in instructions-user.md.
|
|
21
20
|
core.markHidden();
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -83,17 +82,11 @@ export default class Known {
|
|
|
83
82
|
return entry.body;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
// Summarized: first SUMMARY_MAX_CHARS of the body. The model already
|
|
87
|
-
// knows summarized data is approximate (taught in instructions), so
|
|
88
|
-
// we don't owe it a "[truncated]" marker that would push the body
|
|
89
|
-
// past the contract floor.
|
|
90
85
|
summary(entry) {
|
|
91
86
|
if (!entry.body) return "";
|
|
92
87
|
return entry.body.slice(0, SUMMARY_MAX_CHARS);
|
|
93
88
|
}
|
|
94
89
|
|
|
95
|
-
// Identity-keyed summary lines: every data entry the run is tracking
|
|
96
|
-
// at visibility=visible or visibility=summarized.
|
|
97
90
|
async assembleSummarized(content, ctx) {
|
|
98
91
|
const entries = ctx.rows.filter(
|
|
99
92
|
(r) =>
|
package/src/plugins/log/log.js
CHANGED
|
@@ -94,11 +94,19 @@ function renderLogTag(entry, rowsByPath) {
|
|
|
94
94
|
|
|
95
95
|
const meta = { action };
|
|
96
96
|
if (attrs?.path) meta.target = attrs.path;
|
|
97
|
-
// Suppress status on prompts; uniform 200 carries no signal.
|
|
98
97
|
if (statusValue != null && action !== "prompt") meta.status = statusValue;
|
|
99
98
|
if (entry.outcome) meta.outcome = entry.outcome;
|
|
100
99
|
if (typeof attrs?.query === "string") meta.query = attrs.query;
|
|
101
100
|
if (typeof attrs?.command === "string") meta.command = attrs.command;
|
|
101
|
+
if (typeof attrs?.from === "string") meta.from = attrs.from;
|
|
102
|
+
if (typeof attrs?.to === "string") meta.to = attrs.to;
|
|
103
|
+
if (typeof attrs?.question === "string") meta.question = attrs.question;
|
|
104
|
+
if (typeof attrs?.answer === "string") meta.answer = attrs.answer;
|
|
105
|
+
if (attrs?.line != null) meta.line = attrs.line;
|
|
106
|
+
if (attrs?.limit != null) meta.limit = attrs.limit;
|
|
107
|
+
if (attrs?.manifest !== undefined) meta.manifest = true;
|
|
108
|
+
if (attrs?.channel === 1) meta.channel = "stdout";
|
|
109
|
+
else if (attrs?.channel === 2) meta.channel = "stderr";
|
|
102
110
|
if (typeof attrs?.tags === "string") meta.tags = attrs.tags.slice(0, 80);
|
|
103
111
|
if (isSlice) {
|
|
104
112
|
meta.lines = `${attrs.lineStart}-${attrs.lineEnd}/${attrs.totalLines}`;
|
|
@@ -106,6 +114,12 @@ function renderLogTag(entry, rowsByPath) {
|
|
|
106
114
|
meta.lines = lineSource;
|
|
107
115
|
}
|
|
108
116
|
if (tokenSource != null) meta.tokens = tokenSource;
|
|
117
|
+
if (attrs?.beforeActionTokens != null) {
|
|
118
|
+
meta.beforeActionTokens = attrs.beforeActionTokens;
|
|
119
|
+
}
|
|
120
|
+
if (attrs?.afterActionTokens != null) {
|
|
121
|
+
meta.afterActionTokens = attrs.afterActionTokens;
|
|
122
|
+
}
|
|
109
123
|
|
|
110
124
|
return renderEntry(entry.path, meta, projectedBody(entry));
|
|
111
125
|
}
|
package/src/plugins/mv/mv.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import Entries from "../../agent/Entries.js";
|
|
2
|
-
import {
|
|
2
|
+
import { countTokens } from "../../agent/tokens.js";
|
|
3
|
+
import {
|
|
4
|
+
projectEmission,
|
|
5
|
+
storePatternResult,
|
|
6
|
+
summarizeEmission,
|
|
7
|
+
} from "../helpers.js";
|
|
3
8
|
import docs from "./mvDoc.js";
|
|
4
9
|
|
|
5
10
|
const LOG_ACTION_RE = /^log:\/\/turn_\d+\/(\w+)\//;
|
|
@@ -35,7 +40,6 @@ export default class Mv {
|
|
|
35
40
|
? entry.attributes.visibility
|
|
36
41
|
: undefined;
|
|
37
42
|
|
|
38
|
-
// Manifest: list what would be affected without performing the mv.
|
|
39
43
|
if (entry.attributes.manifest !== undefined) {
|
|
40
44
|
const matches = await store.getEntriesByPattern(runId, path);
|
|
41
45
|
await storePatternResult(store, runId, turn, "mv", path, null, matches, {
|
|
@@ -46,7 +50,7 @@ export default class Mv {
|
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
// Visibility-in-place: no destination, change visibility of
|
|
53
|
+
// Visibility-in-place: no destination, change visibility of matches.
|
|
50
54
|
if (visibility && !to) {
|
|
51
55
|
const matches = await store.getEntriesByPattern(runId, path);
|
|
52
56
|
for (const match of matches)
|
|
@@ -70,9 +74,7 @@ export default class Mv {
|
|
|
70
74
|
|
|
71
75
|
const source = await store.getBody(runId, path);
|
|
72
76
|
if (source === null) return;
|
|
73
|
-
// Tags
|
|
74
|
-
// destination inherits the source entry's tags. Same shape as
|
|
75
|
-
// visibility — explicit attr overrides, default inherits.
|
|
77
|
+
// Tags: explicit attr wins; otherwise destination inherits source's.
|
|
76
78
|
let destTags = null;
|
|
77
79
|
if (typeof entry.attributes.tags === "string") {
|
|
78
80
|
destTags = entry.attributes.tags;
|
|
@@ -90,19 +92,18 @@ export default class Mv {
|
|
|
90
92
|
? `Overwrote existing entry at ${to}`
|
|
91
93
|
: null;
|
|
92
94
|
|
|
93
|
-
const
|
|
95
|
+
const sourceTokens = countTokens(source);
|
|
96
|
+
const destOldTokens = existing !== null ? countTokens(existing) : 0;
|
|
97
|
+
const beforeTokens = sourceTokens + destOldTokens;
|
|
98
|
+
const afterTokens = sourceTokens;
|
|
99
|
+
|
|
94
100
|
if (destScheme === null) {
|
|
95
|
-
// Bare-file
|
|
96
|
-
// #materializeFile, gated on attrs.path + attrs.patched) the
|
|
97
|
-
// authoritative new body so it writes the source content to
|
|
98
|
-
// disk on accept. Without this the source rm fired but the
|
|
99
|
-
// destination was never created. Same shape as cp's bare-file
|
|
100
|
-
// branch.
|
|
101
|
+
// Bare-file: hand the shared set.js materializer attrs.patched.
|
|
101
102
|
await store.set({
|
|
102
103
|
runId,
|
|
103
104
|
turn,
|
|
104
105
|
path: entry.resultPath,
|
|
105
|
-
body,
|
|
106
|
+
body: "",
|
|
106
107
|
state: "proposed",
|
|
107
108
|
attributes: {
|
|
108
109
|
from: path,
|
|
@@ -112,6 +113,8 @@ export default class Mv {
|
|
|
112
113
|
path: to,
|
|
113
114
|
patched: source,
|
|
114
115
|
visibility,
|
|
116
|
+
beforeActionTokens: beforeTokens,
|
|
117
|
+
afterActionTokens: afterTokens,
|
|
115
118
|
},
|
|
116
119
|
loopId,
|
|
117
120
|
});
|
|
@@ -131,19 +134,26 @@ export default class Mv {
|
|
|
131
134
|
runId,
|
|
132
135
|
turn,
|
|
133
136
|
path: entry.resultPath,
|
|
134
|
-
body,
|
|
137
|
+
body: "",
|
|
135
138
|
state: "resolved",
|
|
136
|
-
attributes: {
|
|
139
|
+
attributes: {
|
|
140
|
+
from: path,
|
|
141
|
+
to,
|
|
142
|
+
isMove: true,
|
|
143
|
+
warning,
|
|
144
|
+
beforeActionTokens: beforeTokens,
|
|
145
|
+
afterActionTokens: afterTokens,
|
|
146
|
+
},
|
|
137
147
|
loopId,
|
|
138
148
|
});
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
|
|
142
152
|
full(entry) {
|
|
143
|
-
return
|
|
153
|
+
return projectEmission(entry.body);
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
summary() {
|
|
147
|
-
return
|
|
156
|
+
summary(entry) {
|
|
157
|
+
return summarizeEmission(entry.body);
|
|
148
158
|
}
|
|
149
159
|
}
|
|
@@ -3,13 +3,13 @@ export default class Persona {
|
|
|
3
3
|
core.registerScheme({ name: "persona", category: "data" });
|
|
4
4
|
core.hooks.tools.onView("persona", (entry) => entry.body, "visible");
|
|
5
5
|
core.hooks.tools.onView("persona", () => "", "summarized");
|
|
6
|
-
// assembly.
|
|
7
|
-
//
|
|
8
|
-
core.filter("assembly.
|
|
6
|
+
// assembly.user @ 10 — top of the user message. Sets voice/role
|
|
7
|
+
// freshly per turn, ahead of the prompt. Body from ctx.persona.
|
|
8
|
+
core.filter("assembly.user", this.assembleSystemPersona.bind(this), 10);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
assembleSystemPersona(content, ctx) {
|
|
12
12
|
if (!ctx.persona) return content;
|
|
13
|
-
return `${content}
|
|
13
|
+
return `${content}<system_instructions>\n${ctx.persona}\n</system_instructions>\n`;
|
|
14
14
|
}
|
|
15
15
|
}
|