@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/agent/Entries.js
CHANGED
|
@@ -1,18 +1,85 @@
|
|
|
1
1
|
import slugify from "../sql/functions/slugify.js";
|
|
2
|
-
import { PermissionError } from "./errors.js";
|
|
2
|
+
import { EntryOverflowError, PermissionError } from "./errors.js";
|
|
3
3
|
import encodeSegment from "./pathEncode.js";
|
|
4
4
|
|
|
5
|
+
// Update entry bodies are promised ≤ 80 chars to clients (run summary
|
|
6
|
+
// payload, model-facing <log> rendering). Mirror of SUMMARY_MAX_CHARS:
|
|
7
|
+
// the boundary chops + emits a soft error so the violation is visible
|
|
8
|
+
// without crashing the run. Lives here because Entries.update is the
|
|
9
|
+
// canonical persistence boundary all callers fund-route through.
|
|
10
|
+
const UPDATE_BODY_MAX = 80;
|
|
11
|
+
|
|
12
|
+
// SQLite surfaces the CHECK as either err.code === "SQLITE_CONSTRAINT_CHECK"
|
|
13
|
+
// or an Error whose message names the failing column. Both forms appear in
|
|
14
|
+
// the wild depending on the driver build, so we match defensively.
|
|
15
|
+
// Caller-side contract: only invoked from a SQL try/catch, so err is always
|
|
16
|
+
// an Error instance — err.message is a string (possibly empty), not undefined.
|
|
17
|
+
function isBodyOverflow(err) {
|
|
18
|
+
if (!err) return false;
|
|
19
|
+
if (err.code === "SQLITE_CONSTRAINT_CHECK") return true;
|
|
20
|
+
return err.message.includes("CHECK") && err.message.includes("length(body)");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function translateBodyOverflow(err, path, body) {
|
|
24
|
+
if (!isBodyOverflow(err)) return err;
|
|
25
|
+
const size = body == null ? 0 : body.length;
|
|
26
|
+
return new EntryOverflowError(path, size);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Already-an-error path: log://turn_N/error/<slug>. The auto-failure
|
|
30
|
+
// hook below skips these to break the recursion (error.log.emit's
|
|
31
|
+
// handler ALSO writes state=failed when materializing its own entry).
|
|
32
|
+
const ERROR_PATH_RE = /^log:\/\/turn_\d+\/error\//;
|
|
33
|
+
|
|
34
|
+
// Streaming data channels for env/sh actions (env://turn_N/cmd_K,
|
|
35
|
+
// sh://turn_N/cmd_K). Their failure is already captured by the parent
|
|
36
|
+
// log://turn_N/<scheme>/<slug> action entry's auto-emit; emitting again
|
|
37
|
+
// for each channel produces redundant duplicates with empty-body
|
|
38
|
+
// fallback messages.
|
|
39
|
+
const CHANNEL_PATH_RE = /^(env|sh):\/\/turn_\d+\//;
|
|
40
|
+
|
|
5
41
|
export default class Entries {
|
|
6
42
|
#db;
|
|
7
43
|
#onChanged;
|
|
44
|
+
#onError;
|
|
45
|
+
#onFailed;
|
|
46
|
+
#onSoftError;
|
|
8
47
|
#schemes = new Map();
|
|
9
48
|
#schemesLoaded = null;
|
|
10
49
|
#seq = 0;
|
|
11
50
|
#pendingResolutions = new Map();
|
|
12
51
|
|
|
13
|
-
|
|
52
|
+
// onError is the centralized site for storage-layer rejections that
|
|
53
|
+
// should surface to the model as strikes rather than crash the run.
|
|
54
|
+
// Today: EntryOverflowError (RUMMY_ENTRY_SIZE_MAX CHECK violations).
|
|
55
|
+
// When onError is supplied, set() catches the typed error, dispatches
|
|
56
|
+
// it to the callback (which emits hooks.error.log → 413 strike), and
|
|
57
|
+
// returns silently — callers don't need to handle storage-layer
|
|
58
|
+
// rejections at every write site. When onError is null (e.g. unit
|
|
59
|
+
// tests with a bare Entries), the error propagates as before.
|
|
60
|
+
//
|
|
61
|
+
// onFailed is the universal failure-rendering enforcer: every
|
|
62
|
+
// transition to state="failed" on a non-error path fires this
|
|
63
|
+
// callback so a SEPARATE log://turn_N/error/<slug> entry is created
|
|
64
|
+
// alongside the action entry. Without this, plugins that record
|
|
65
|
+
// failure via entries.set({state: "failed", ...}) leave nothing for
|
|
66
|
+
// the model to recognize as an error — failure encodes only as tiny
|
|
67
|
+
// JSON metadata indistinguishable from a successful entry. The
|
|
68
|
+
// callback wires to hooks.error.log.emit (see ProjectAgent).
|
|
69
|
+
constructor(
|
|
70
|
+
db,
|
|
71
|
+
{
|
|
72
|
+
onChanged = null,
|
|
73
|
+
onError = null,
|
|
74
|
+
onFailed = null,
|
|
75
|
+
onSoftError = null,
|
|
76
|
+
} = {},
|
|
77
|
+
) {
|
|
14
78
|
this.#db = db;
|
|
15
79
|
this.#onChanged = onChanged;
|
|
80
|
+
this.#onError = onError;
|
|
81
|
+
this.#onFailed = onFailed;
|
|
82
|
+
this.#onSoftError = onSoftError;
|
|
16
83
|
}
|
|
17
84
|
|
|
18
85
|
// Populate the scheme cache; idempotent, lazy on first need.
|
|
@@ -42,7 +109,16 @@ export default class Entries {
|
|
|
42
109
|
}
|
|
43
110
|
|
|
44
111
|
static normalizePath(path) {
|
|
45
|
-
if (!path
|
|
112
|
+
if (!path) return path;
|
|
113
|
+
if (!path.includes("://")) {
|
|
114
|
+
// Bare file path: strip a single leading `./` for canonical
|
|
115
|
+
// form. `./main.go` and `main.go` must resolve to the same
|
|
116
|
+
// entry — otherwise SEARCH/REPLACE edits on `./main.go`
|
|
117
|
+
// land in a phantom entry while reads of `main.go` see the
|
|
118
|
+
// original, and the model can't reconcile.
|
|
119
|
+
if (path.startsWith("./")) return path.slice(2);
|
|
120
|
+
return path;
|
|
121
|
+
}
|
|
46
122
|
const sep = path.indexOf("://");
|
|
47
123
|
const scheme = path.slice(0, sep).toLowerCase();
|
|
48
124
|
const rest = path.slice(sep + 3);
|
|
@@ -72,28 +148,29 @@ export default class Entries {
|
|
|
72
148
|
return `${candidate}_${++this.#seq}`;
|
|
73
149
|
}
|
|
74
150
|
|
|
75
|
-
// Single namespace log://turn_N/action/slug
|
|
151
|
+
// Single namespace log://turn_N/action/slug. slug is built via slugify
|
|
152
|
+
// (80-char cap + integer tie-breaker on collision) — same contract as
|
|
153
|
+
// slugPath. Plugins (including externals) can trust that any target
|
|
154
|
+
// they pass will produce a bounded, unique log path, regardless of
|
|
155
|
+
// the target's length or character composition. Full payload always
|
|
156
|
+
// belongs in the entry body, not the slug.
|
|
76
157
|
async logPath(runId, turn, action, target) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// worst-case expansion. The full message belongs in body, not path.
|
|
82
|
-
const safeTarget = String(target).slice(0, 150);
|
|
83
|
-
const encodedTarget = encodeSegment(safeTarget);
|
|
84
|
-
const candidate = `log://turn_${turn}/${action}/${encodedTarget}`;
|
|
158
|
+
const slug = target == null ? "" : slugify(String(target));
|
|
159
|
+
const base = slug
|
|
160
|
+
? `log://turn_${turn}/${action}/${slug}`
|
|
161
|
+
: `log://turn_${turn}/${action}/_`;
|
|
85
162
|
const existing = await this.#db.get_entry_body.get({
|
|
86
163
|
run_id: runId,
|
|
87
|
-
path:
|
|
164
|
+
path: base,
|
|
88
165
|
});
|
|
89
|
-
if (!existing) return
|
|
90
|
-
return `${
|
|
166
|
+
if (!existing) return base;
|
|
167
|
+
return `${base}_${++this.#seq}`;
|
|
91
168
|
}
|
|
92
169
|
|
|
93
|
-
async slugPath(runId, scheme, content,
|
|
94
|
-
//
|
|
170
|
+
async slugPath(runId, scheme, content, tags) {
|
|
171
|
+
// tags > content > empty; slugify("") yields "" and we sequence-only.
|
|
95
172
|
let source = "";
|
|
96
|
-
if (
|
|
173
|
+
if (tags) source = tags;
|
|
97
174
|
else if (content) source = content;
|
|
98
175
|
const base = slugify(source);
|
|
99
176
|
const prefix = `${scheme}://`;
|
|
@@ -149,7 +226,35 @@ export default class Entries {
|
|
|
149
226
|
}
|
|
150
227
|
|
|
151
228
|
// set — create or update an entry; see PLUGINS.md primitives.
|
|
152
|
-
async set({
|
|
229
|
+
async set(args) {
|
|
230
|
+
if (!args.runId) throw new Error("set: runId is required");
|
|
231
|
+
if (!args.path) throw new Error("set: path is required");
|
|
232
|
+
try {
|
|
233
|
+
return await this.#setImpl(args);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
// EntryOverflowError: storage-layer CHECK fired. When the host
|
|
236
|
+
// supplies onError (the production wiring), route the strike
|
|
237
|
+
// to error.log and return silently — every set() caller in
|
|
238
|
+
// the codebase becomes overflow-safe without per-site catches.
|
|
239
|
+
// Without onError (raw unit tests), propagate as before.
|
|
240
|
+
if (err instanceof EntryOverflowError && this.#onError) {
|
|
241
|
+
// Destructure with the same defaults as #setImpl so the
|
|
242
|
+
// callback sees the same loopId/turn shape callers wrote
|
|
243
|
+
// against — no `??` fallback shim, just contract alignment.
|
|
244
|
+
const { runId, loopId = null, turn = 0 } = args;
|
|
245
|
+
await this.#onError({
|
|
246
|
+
runId,
|
|
247
|
+
loopId,
|
|
248
|
+
turn,
|
|
249
|
+
error: err,
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async #setImpl({
|
|
153
258
|
runId,
|
|
154
259
|
projectId = null,
|
|
155
260
|
turn = 0,
|
|
@@ -166,20 +271,21 @@ export default class Entries {
|
|
|
166
271
|
loopId = null,
|
|
167
272
|
writer = "plugin",
|
|
168
273
|
}) {
|
|
169
|
-
if (!runId) throw new Error("set: runId is required");
|
|
170
|
-
if (!path) throw new Error("set: path is required");
|
|
171
|
-
|
|
172
274
|
// Pattern mode is explicit; never inferred from `*` in path.
|
|
173
275
|
const isPattern = pattern === true || bodyFilter !== null;
|
|
174
276
|
|
|
175
277
|
if (isPattern) {
|
|
176
278
|
if (body != null && !append) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
279
|
+
try {
|
|
280
|
+
await this.#db.update_body_by_pattern.run({
|
|
281
|
+
run_id: runId,
|
|
282
|
+
path,
|
|
283
|
+
body: bodyFilter,
|
|
284
|
+
new_body: body,
|
|
285
|
+
});
|
|
286
|
+
} catch (err) {
|
|
287
|
+
throw translateBodyOverflow(err, path, body);
|
|
288
|
+
}
|
|
183
289
|
await this.#db.bump_write_count_by_pattern.run({
|
|
184
290
|
run_id: runId,
|
|
185
291
|
path,
|
|
@@ -212,11 +318,15 @@ export default class Entries {
|
|
|
212
318
|
// Append mode: streaming body growth on an existing entry.
|
|
213
319
|
if (append) {
|
|
214
320
|
if (body == null) throw new Error("set: append requires body");
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
321
|
+
try {
|
|
322
|
+
await this.#db.append_entry_body.run({
|
|
323
|
+
run_id: runId,
|
|
324
|
+
path: normalized,
|
|
325
|
+
chunk: body,
|
|
326
|
+
});
|
|
327
|
+
} catch (err) {
|
|
328
|
+
throw translateBodyOverflow(err, normalized, body);
|
|
329
|
+
}
|
|
220
330
|
this.#emitChanged(runId, normalized, "append");
|
|
221
331
|
return;
|
|
222
332
|
}
|
|
@@ -232,6 +342,15 @@ export default class Entries {
|
|
|
232
342
|
});
|
|
233
343
|
this.#emitChanged(runId, normalized, "resolve");
|
|
234
344
|
this.#drainPendingResolution(runId, normalized);
|
|
345
|
+
if (state === "failed") {
|
|
346
|
+
await this.#fireFailed({
|
|
347
|
+
runId,
|
|
348
|
+
turn,
|
|
349
|
+
loopId,
|
|
350
|
+
path: normalized,
|
|
351
|
+
outcome,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
235
354
|
}
|
|
236
355
|
if (visibility != null) {
|
|
237
356
|
await this.#db.set_visibility.run({
|
|
@@ -264,20 +383,37 @@ export default class Entries {
|
|
|
264
383
|
const m = normalized.match(/^log:\/\/turn_\d+\/([^/]+)\//);
|
|
265
384
|
if (m) effectiveAttributes.action = m[1];
|
|
266
385
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
:
|
|
274
|
-
|
|
275
|
-
|
|
386
|
+
let entry;
|
|
387
|
+
try {
|
|
388
|
+
entry = await this.#db.upsert_entry.get({
|
|
389
|
+
scope,
|
|
390
|
+
path: normalized,
|
|
391
|
+
body,
|
|
392
|
+
attributes: effectiveAttributes
|
|
393
|
+
? JSON.stringify(effectiveAttributes)
|
|
394
|
+
: null,
|
|
395
|
+
hash,
|
|
396
|
+
});
|
|
397
|
+
} catch (err) {
|
|
398
|
+
throw translateBodyOverflow(err, normalized, body);
|
|
399
|
+
}
|
|
276
400
|
const effectiveState = state === undefined ? "resolved" : state;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
401
|
+
// Visibility resolution: explicit > preserve-existing > scheme-default.
|
|
402
|
+
// A body update without visibility= must NOT silently reset visibility
|
|
403
|
+
// to the scheme default — that would hide content the model just
|
|
404
|
+
// promoted (e.g. a model <get>'d file then <set> SEARCH/REPLACE
|
|
405
|
+
// would lose its visible status). Preserve what's there.
|
|
406
|
+
let effectiveVisibility;
|
|
407
|
+
if (visibility !== undefined) {
|
|
408
|
+
effectiveVisibility = visibility;
|
|
409
|
+
} else {
|
|
410
|
+
const existing = await this.getState(runId, normalized);
|
|
411
|
+
if (existing?.visibility) {
|
|
412
|
+
effectiveVisibility = existing.visibility;
|
|
413
|
+
} else {
|
|
414
|
+
effectiveVisibility = this.#defaultVisibility(scheme, category);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
281
417
|
await this.#db.upsert_run_view.run({
|
|
282
418
|
run_id: runId,
|
|
283
419
|
entry_id: entry.id,
|
|
@@ -291,6 +427,42 @@ export default class Entries {
|
|
|
291
427
|
if (effectiveState !== "proposed") {
|
|
292
428
|
this.#drainPendingResolution(runId, normalized);
|
|
293
429
|
}
|
|
430
|
+
if (effectiveState === "failed") {
|
|
431
|
+
await this.#fireFailed({
|
|
432
|
+
runId,
|
|
433
|
+
turn,
|
|
434
|
+
loopId,
|
|
435
|
+
path: normalized,
|
|
436
|
+
body,
|
|
437
|
+
outcome,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Fire onFailed for any state→failed transition on a non-error path.
|
|
443
|
+
// The auto-emit creates a sibling log://turn_N/error/<slug> entry so
|
|
444
|
+
// the failure appears in the model's <log> as a category-distinct
|
|
445
|
+
// item, not just metadata buried in the action's own log entry.
|
|
446
|
+
async #fireFailed({ runId, turn, loopId, path, body, outcome }) {
|
|
447
|
+
if (!this.#onFailed) return;
|
|
448
|
+
if (ERROR_PATH_RE.test(path)) return;
|
|
449
|
+
if (CHANNEL_PATH_RE.test(path)) return;
|
|
450
|
+
// Body-less state changes don't carry a message; fall back to the
|
|
451
|
+
// outcome string (or the path itself) so the error entry has a
|
|
452
|
+
// recognizable slug instead of an empty one.
|
|
453
|
+
let message = body;
|
|
454
|
+
if (!message) {
|
|
455
|
+
if (outcome) message = `failed: ${outcome}`;
|
|
456
|
+
else message = `failed: ${path}`;
|
|
457
|
+
}
|
|
458
|
+
await this.#onFailed({
|
|
459
|
+
runId,
|
|
460
|
+
turn,
|
|
461
|
+
loopId,
|
|
462
|
+
sourcePath: path,
|
|
463
|
+
body: message,
|
|
464
|
+
outcome,
|
|
465
|
+
});
|
|
294
466
|
}
|
|
295
467
|
|
|
296
468
|
// get — promote entry(ies); see PLUGINS.md primitives.
|
|
@@ -399,6 +571,9 @@ export default class Entries {
|
|
|
399
571
|
}
|
|
400
572
|
|
|
401
573
|
// update — once-per-turn lifecycle signal; see PLUGINS.md.
|
|
574
|
+
// Body chopped to UPDATE_BODY_MAX with a soft error fire so clients
|
|
575
|
+
// always receive ≤ 80 chars and the violation is visible to the model
|
|
576
|
+
// next turn. Applies to ALL callers — system, plugin, model.
|
|
402
577
|
async update({
|
|
403
578
|
runId,
|
|
404
579
|
turn = 0,
|
|
@@ -410,12 +585,24 @@ export default class Entries {
|
|
|
410
585
|
}) {
|
|
411
586
|
if (!runId) throw new Error("update: runId is required");
|
|
412
587
|
if (body == null) throw new Error("update: body is required");
|
|
413
|
-
|
|
588
|
+
let storedBody = body;
|
|
589
|
+
if (body.length > UPDATE_BODY_MAX) {
|
|
590
|
+
storedBody = body.slice(0, UPDATE_BODY_MAX);
|
|
591
|
+
if (this.#onSoftError) {
|
|
592
|
+
await this.#onSoftError({
|
|
593
|
+
runId,
|
|
594
|
+
turn,
|
|
595
|
+
loopId,
|
|
596
|
+
message: "error: YOU MUST keep the update body to <= 80 characters",
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const path = await this.logPath(runId, turn, "update", storedBody);
|
|
414
601
|
await this.set({
|
|
415
602
|
runId,
|
|
416
603
|
turn,
|
|
417
604
|
path,
|
|
418
|
-
body,
|
|
605
|
+
body: storedBody,
|
|
419
606
|
state: "resolved",
|
|
420
607
|
loopId,
|
|
421
608
|
writer,
|
|
@@ -527,10 +714,10 @@ export default class Entries {
|
|
|
527
714
|
});
|
|
528
715
|
}
|
|
529
716
|
|
|
530
|
-
async
|
|
531
|
-
await this.#db.
|
|
717
|
+
async setNextTurn(runId, nextTurn) {
|
|
718
|
+
await this.#db.set_next_turn.run({
|
|
532
719
|
run_id: runId,
|
|
533
|
-
|
|
720
|
+
next_turn: nextTurn,
|
|
534
721
|
});
|
|
535
722
|
}
|
|
536
723
|
|
|
@@ -544,15 +731,6 @@ export default class Entries {
|
|
|
544
731
|
return targets;
|
|
545
732
|
}
|
|
546
733
|
|
|
547
|
-
// Budget postDispatch fallback: demote every visible entry in the run.
|
|
548
|
-
async demoteRunVisibleEntries(runId) {
|
|
549
|
-
const targets = await this.#db.get_run_visible_targets.all({
|
|
550
|
-
run_id: runId,
|
|
551
|
-
});
|
|
552
|
-
await this.#db.demote_run_visible.run({ run_id: runId });
|
|
553
|
-
return targets;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
734
|
// Plugin-facing run lookup; avoids reaching into core.db.
|
|
557
735
|
async getRun(runId) {
|
|
558
736
|
return this.#db.get_run_by_id.get({ id: runId });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import LlmProvider from "../llm/LlmProvider.js";
|
|
2
2
|
import AgentLoop from "./AgentLoop.js";
|
|
3
3
|
import Entries from "./Entries.js";
|
|
4
|
+
import { SOFT_FAILURE_OUTCOMES } from "./errors.js";
|
|
4
5
|
import TurnExecutor from "./TurnExecutor.js";
|
|
5
6
|
|
|
6
7
|
export default class ProjectAgent {
|
|
@@ -16,6 +17,49 @@ export default class ProjectAgent {
|
|
|
16
17
|
this.#llm = new LlmProvider(db, hooks);
|
|
17
18
|
this.#entries = new Entries(db, {
|
|
18
19
|
onChanged: (event) => hooks.entry.changed.emit(event),
|
|
20
|
+
onError: ({ runId, loopId, turn, error }) =>
|
|
21
|
+
hooks.error.log.emit({
|
|
22
|
+
store: this.#entries,
|
|
23
|
+
runId,
|
|
24
|
+
turn,
|
|
25
|
+
loopId,
|
|
26
|
+
message: error.message,
|
|
27
|
+
status: 413,
|
|
28
|
+
attributes: { path: error.path, size: error.size },
|
|
29
|
+
}),
|
|
30
|
+
// Universal failure-rendering: every state→failed transition on
|
|
31
|
+
// a non-error path fires error.log.emit so a sibling
|
|
32
|
+
// log://turn_N/error/<slug> entry is created. The error plugin's
|
|
33
|
+
// own #onErrorLog handler also writes state=failed on the error
|
|
34
|
+
// entry; Entries.#fireFailed skips when path matches
|
|
35
|
+
// log://turn_*/error/* so no recursion.
|
|
36
|
+
//
|
|
37
|
+
// soft=true when the outcome is in SOFT_FAILURE_OUTCOMES
|
|
38
|
+
// (not_found, conflict): the error entry still renders so the
|
|
39
|
+
// model can read the finding, but error.log skips turnErrors++
|
|
40
|
+
// so the strike accumulator doesn't penalize legitimate
|
|
41
|
+
// state-discovery via the auto-emit path. Without this, soft
|
|
42
|
+
// outcomes count as strikes on the turnErrors path even though
|
|
43
|
+
// recordedFailed correctly excludes them.
|
|
44
|
+
onFailed: ({ runId, loopId, turn, sourcePath, body, outcome }) =>
|
|
45
|
+
hooks.error.log.emit({
|
|
46
|
+
store: this.#entries,
|
|
47
|
+
runId,
|
|
48
|
+
turn,
|
|
49
|
+
loopId,
|
|
50
|
+
message: body,
|
|
51
|
+
attributes: { sourcePath, outcome },
|
|
52
|
+
soft: SOFT_FAILURE_OUTCOMES.has(outcome),
|
|
53
|
+
}),
|
|
54
|
+
onSoftError: ({ runId, loopId, turn, message }) =>
|
|
55
|
+
hooks.error.log.emit({
|
|
56
|
+
store: this.#entries,
|
|
57
|
+
runId,
|
|
58
|
+
turn,
|
|
59
|
+
loopId,
|
|
60
|
+
message,
|
|
61
|
+
soft: true,
|
|
62
|
+
}),
|
|
19
63
|
});
|
|
20
64
|
this.#entries.loadSchemes(db);
|
|
21
65
|
|