@possumtech/rummy 2.0.1 → 2.1.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 +12 -7
- package/BENCH_ENVIRONMENT.md +230 -0
- package/CLIENT_INTERFACE.md +396 -0
- package/PLUGINS.md +93 -1
- package/SPEC.md +305 -28
- package/bin/postinstall.js +2 -2
- package/bin/rummy.js +2 -2
- package/last_run.txt +5617 -0
- package/migrations/001_initial_schema.sql +2 -1
- package/package.json +6 -2
- package/scriptify/cache_probe.js +66 -0
- package/scriptify/cache_probe_grok.js +74 -0
- package/service.js +22 -11
- package/src/agent/AgentLoop.js +33 -139
- package/src/agent/ContextAssembler.js +2 -9
- package/src/agent/Entries.js +36 -101
- package/src/agent/ProjectAgent.js +2 -9
- package/src/agent/TurnExecutor.js +45 -83
- package/src/agent/XmlParser.js +247 -273
- package/src/agent/budget.js +5 -28
- package/src/agent/config.js +38 -0
- package/src/agent/errors.js +7 -13
- package/src/agent/httpStatus.js +1 -19
- package/src/agent/known_store.sql +7 -2
- package/src/agent/materializeContext.js +12 -17
- package/src/agent/pathEncode.js +5 -0
- package/src/agent/rummyHome.js +9 -0
- package/src/agent/runs.sql +18 -0
- package/src/agent/tokens.js +2 -8
- package/src/hooks/HookRegistry.js +1 -16
- package/src/hooks/Hooks.js +8 -33
- package/src/hooks/PluginContext.js +3 -21
- package/src/hooks/RpcRegistry.js +1 -4
- package/src/hooks/RummyContext.js +2 -16
- package/src/hooks/ToolRegistry.js +5 -15
- package/src/llm/LlmProvider.js +28 -23
- package/src/llm/errors.js +41 -4
- package/src/llm/openaiStream.js +125 -0
- package/src/llm/retry.js +61 -15
- package/src/plugins/budget/budget.js +14 -81
- package/src/plugins/cli/README.md +87 -0
- package/src/plugins/cli/bin.js +61 -0
- package/src/plugins/cli/cli.js +120 -0
- package/src/plugins/env/README.md +2 -1
- package/src/plugins/env/env.js +4 -6
- package/src/plugins/env/envDoc.md +2 -2
- package/src/plugins/error/error.js +23 -23
- package/src/plugins/file/file.js +2 -22
- package/src/plugins/get/get.js +12 -34
- package/src/plugins/get/getDoc.md +5 -3
- package/src/plugins/hedberg/edits.js +1 -11
- package/src/plugins/hedberg/hedberg.js +3 -26
- package/src/plugins/hedberg/normalize.js +1 -5
- package/src/plugins/hedberg/patterns.js +4 -15
- package/src/plugins/hedberg/sed.js +1 -7
- package/src/plugins/helpers.js +28 -20
- package/src/plugins/index.js +25 -41
- package/src/plugins/instructions/README.md +18 -0
- package/src/plugins/instructions/instructions.js +13 -76
- package/src/plugins/instructions/instructions.md +19 -18
- package/src/plugins/instructions/instructions_104.md +5 -4
- package/src/plugins/instructions/instructions_105.md +16 -15
- package/src/plugins/instructions/instructions_106.md +15 -14
- package/src/plugins/instructions/instructions_107.md +13 -6
- package/src/plugins/known/README.md +26 -6
- package/src/plugins/known/known.js +36 -34
- package/src/plugins/log/README.md +2 -2
- package/src/plugins/log/log.js +6 -33
- package/src/plugins/ollama/ollama.js +50 -66
- package/src/plugins/openai/openai.js +26 -44
- package/src/plugins/openrouter/openrouter.js +28 -52
- package/src/plugins/policy/README.md +8 -2
- package/src/plugins/policy/policy.js +8 -21
- package/src/plugins/prompt/README.md +22 -0
- package/src/plugins/prompt/prompt.js +8 -16
- package/src/plugins/rm/rm.js +5 -2
- package/src/plugins/rm/rmDoc.md +4 -4
- package/src/plugins/rpc/README.md +2 -1
- package/src/plugins/rpc/rpc.js +51 -47
- package/src/plugins/set/README.md +5 -1
- package/src/plugins/set/set.js +23 -33
- package/src/plugins/set/setDoc.md +1 -1
- package/src/plugins/sh/README.md +2 -1
- package/src/plugins/sh/sh.js +5 -11
- package/src/plugins/sh/shDoc.md +2 -2
- package/src/plugins/stream/README.md +6 -5
- package/src/plugins/stream/stream.js +6 -35
- package/src/plugins/telemetry/telemetry.js +26 -19
- package/src/plugins/think/think.js +4 -7
- package/src/plugins/unknown/unknown.js +8 -13
- package/src/plugins/update/update.js +36 -35
- package/src/plugins/update/updateDoc.md +3 -3
- package/src/plugins/xai/xai.js +30 -20
- package/src/plugins/yolo/yolo.js +8 -41
- package/src/server/ClientConnection.js +17 -47
- package/src/server/SocketServer.js +14 -14
- package/src/server/protocol.js +1 -10
- package/src/sql/functions/slugify.js +5 -7
- package/src/sql/v_model_context.sql +4 -11
- package/turns/cli_1777462658211/turn_001.txt +772 -0
- package/turns/cli_1777462658211/turn_002.txt +606 -0
- package/turns/cli_1777462658211/turn_003.txt +667 -0
- package/turns/cli_1777462658211/turn_004.txt +297 -0
- package/turns/cli_1777462658211/turn_005.txt +301 -0
- package/turns/cli_1777462658211/turn_006.txt +262 -0
- package/turns/cli_1777465095132/turn_001.txt +715 -0
- package/turns/cli_1777465095132/turn_002.txt +236 -0
- package/turns/cli_1777465095132/turn_003.txt +287 -0
- package/turns/cli_1777465095132/turn_004.txt +694 -0
- package/turns/cli_1777465095132/turn_005.txt +422 -0
- package/turns/cli_1777465095132/turn_006.txt +365 -0
- package/turns/cli_1777465095132/turn_007.txt +885 -0
- package/turns/cli_1777465095132/turn_008.txt +1277 -0
- package/turns/cli_1777465095132/turn_009.txt +736 -0
package/src/agent/Entries.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import slugify from "../sql/functions/slugify.js";
|
|
2
2
|
import { PermissionError } from "./errors.js";
|
|
3
|
+
import encodeSegment from "./pathEncode.js";
|
|
3
4
|
|
|
4
5
|
export default class Entries {
|
|
5
6
|
#db;
|
|
@@ -14,10 +15,7 @@ export default class Entries {
|
|
|
14
15
|
this.#onChanged = onChanged;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
* Populate the scheme cache. Can be called explicitly (e.g. at boot
|
|
19
|
-
* after initPlugins finishes) or runs lazily on first need. Idempotent.
|
|
20
|
-
*/
|
|
18
|
+
// Populate the scheme cache; idempotent, lazy on first need.
|
|
21
19
|
async loadSchemes(db) {
|
|
22
20
|
const rows = await (db || this.#db).get_all_schemes.all();
|
|
23
21
|
this.#schemes.clear();
|
|
@@ -51,9 +49,9 @@ export default class Entries {
|
|
|
51
49
|
try {
|
|
52
50
|
// Decode first (idempotent), then encode — but preserve slashes
|
|
53
51
|
const decoded = decodeURIComponent(rest);
|
|
54
|
-
return `${scheme}://${decoded.split("/").map(
|
|
52
|
+
return `${scheme}://${decoded.split("/").map(encodeSegment).join("/")}`;
|
|
55
53
|
} catch {
|
|
56
|
-
return `${scheme}://${rest.split("/").map(
|
|
54
|
+
return `${scheme}://${rest.split("/").map(encodeSegment).join("/")}`;
|
|
57
55
|
}
|
|
58
56
|
}
|
|
59
57
|
|
|
@@ -63,7 +61,7 @@ export default class Entries {
|
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
async dedup(runId, scheme, target, turn) {
|
|
66
|
-
const encodedTarget =
|
|
64
|
+
const encodedTarget = encodeSegment(target);
|
|
67
65
|
const turnPrefix = turn ? `turn_${turn}/` : "";
|
|
68
66
|
const candidate = `${scheme}://${turnPrefix}${encodedTarget}`;
|
|
69
67
|
const existing = await this.#db.get_entry_body.get({
|
|
@@ -74,12 +72,15 @@ export default class Entries {
|
|
|
74
72
|
return `${candidate}_${++this.#seq}`;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
//
|
|
78
|
-
// The action segment is the tool/plugin name (set, get, search, update,
|
|
79
|
-
// error, etc.). Target is URL-encoded so slashes and scheme separators
|
|
80
|
-
// survive round-trips.
|
|
75
|
+
// Single namespace log://turn_N/action/slug; target URL-encoded for round-trip safety.
|
|
81
76
|
async logPath(runId, turn, action, target) {
|
|
82
|
-
|
|
77
|
+
// Cap target before encoding: the schema's CHECK(length(path) <= 2048)
|
|
78
|
+
// otherwise blows up when callers pass long error messages or other
|
|
79
|
+
// arbitrary text. encodeURIComponent expands ~3x for ASCII, more for
|
|
80
|
+
// Unicode; 150 raw chars stays comfortably under 2048 even after
|
|
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);
|
|
83
84
|
const candidate = `log://turn_${turn}/${action}/${encodedTarget}`;
|
|
84
85
|
const existing = await this.#db.get_entry_body.get({
|
|
85
86
|
run_id: runId,
|
|
@@ -90,9 +91,7 @@ export default class Entries {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
async slugPath(runId, scheme, content, summary) {
|
|
93
|
-
//
|
|
94
|
-
// handles empty explicitly by returning "" and the caller generates
|
|
95
|
-
// a sequence-only path.
|
|
94
|
+
// summary > content > empty; slugify("") yields "" and we sequence-only.
|
|
96
95
|
let source = "";
|
|
97
96
|
if (summary) source = summary;
|
|
98
97
|
else if (content) source = content;
|
|
@@ -111,12 +110,7 @@ export default class Entries {
|
|
|
111
110
|
return `${prefix}${base}_${++this.#seq}`;
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
* Resolve a scheme's declared scope kind + writer list + category.
|
|
116
|
-
* Unregistered or declaration-less schemes default to run-level +
|
|
117
|
-
* model/plugin writers so ad-hoc paths (e.g. bare filenames) still
|
|
118
|
-
* work.
|
|
119
|
-
*/
|
|
113
|
+
// Scheme's scope/writers/category; bare paths default to run + model/plugin.
|
|
120
114
|
async #schemeRules(scheme) {
|
|
121
115
|
await this.#ensureSchemes();
|
|
122
116
|
const row = scheme ? this.#schemes.get(scheme) : null;
|
|
@@ -154,17 +148,7 @@ export default class Entries {
|
|
|
154
148
|
return `run:${runId}`;
|
|
155
149
|
}
|
|
156
150
|
|
|
157
|
-
|
|
158
|
-
* set — create or update an entry. The semantically wide primitive.
|
|
159
|
-
*
|
|
160
|
-
* Modes (selected by which options are present):
|
|
161
|
-
* — write content: body given, state ∈ {proposed,streaming,resolved,failed,cancelled}
|
|
162
|
-
* — change visibility only: visibility given, body omitted
|
|
163
|
-
* — change state only: state given, body omitted (resolve a proposal)
|
|
164
|
-
* — merge attributes: attributes given, body omitted
|
|
165
|
-
* — append to body: append:true (streaming)
|
|
166
|
-
* — pattern match: path contains wildcards or bodyFilter set
|
|
167
|
-
*/
|
|
151
|
+
// set — create or update an entry; see PLUGINS.md primitives.
|
|
168
152
|
async set({
|
|
169
153
|
runId,
|
|
170
154
|
projectId = null,
|
|
@@ -185,14 +169,9 @@ export default class Entries {
|
|
|
185
169
|
if (!runId) throw new Error("set: runId is required");
|
|
186
170
|
if (!path) throw new Error("set: path is required");
|
|
187
171
|
|
|
188
|
-
// Pattern mode is explicit
|
|
189
|
-
// body filter is supplied. The literal `*` character can appear
|
|
190
|
-
// inside legitimate exact paths (e.g. rm://foo%2F* as a result
|
|
191
|
-
// path for an rm against a pattern); we don't infer pattern mode
|
|
192
|
-
// from the path alone.
|
|
172
|
+
// Pattern mode is explicit; never inferred from `*` in path.
|
|
193
173
|
const isPattern = pattern === true || bodyFilter !== null;
|
|
194
174
|
|
|
195
|
-
// Pattern mode: update matching entries (visibility / body / both).
|
|
196
175
|
if (isPattern) {
|
|
197
176
|
if (body != null && !append) {
|
|
198
177
|
await this.#db.update_body_by_pattern.run({
|
|
@@ -279,14 +258,7 @@ export default class Entries {
|
|
|
279
258
|
throw new PermissionError(scheme, writer, writers);
|
|
280
259
|
}
|
|
281
260
|
const scope = this.#resolveScope(kind, runId, projectId);
|
|
282
|
-
//
|
|
283
|
-
// client UIs, tests) can read the action without parsing the
|
|
284
|
-
// path. Only inject `action` when the caller passes attributes
|
|
285
|
-
// — a null `attributes` means "don't touch existing" and the
|
|
286
|
-
// SQL's COALESCE handles preservation on UPDATE. If we generated
|
|
287
|
-
// `{action: m[1]}` for every null-attributes log write, every
|
|
288
|
-
// body-only update to a log entry would clobber existing attrs
|
|
289
|
-
// (command, summary, demotedCount, ...).
|
|
261
|
+
// Inject `action` only when caller passes attributes; null means COALESCE preserves existing.
|
|
290
262
|
const effectiveAttributes = attributes ? { ...attributes } : null;
|
|
291
263
|
if (scheme === "log" && effectiveAttributes) {
|
|
292
264
|
const m = normalized.match(/^log:\/\/turn_\d+\/([^/]+)\//);
|
|
@@ -321,11 +293,7 @@ export default class Entries {
|
|
|
321
293
|
}
|
|
322
294
|
}
|
|
323
295
|
|
|
324
|
-
|
|
325
|
-
* get — promote entry(ies) to visible visibility. Default visibility is
|
|
326
|
-
* "visible"; pass visibility explicitly for a read-with-side-effect at
|
|
327
|
-
* a different visibility (rare).
|
|
328
|
-
*/
|
|
296
|
+
// get — promote entry(ies); see PLUGINS.md primitives.
|
|
329
297
|
async get({
|
|
330
298
|
runId,
|
|
331
299
|
turn = 0,
|
|
@@ -352,11 +320,7 @@ export default class Entries {
|
|
|
352
320
|
this.#emitChanged(runId, path, "promote");
|
|
353
321
|
}
|
|
354
322
|
|
|
355
|
-
|
|
356
|
-
* rm — remove entry view(s). Matches single path or pattern; optional
|
|
357
|
-
* bodyFilter narrows pattern matches. `filesOnly` restricts to bare
|
|
358
|
-
* file-scheme entries (scheme IS NULL).
|
|
359
|
-
*/
|
|
323
|
+
// rm — remove entry view(s); see PLUGINS.md primitives.
|
|
360
324
|
async rm({ runId, path, bodyFilter = null, filesOnly = false }) {
|
|
361
325
|
if (!runId) throw new Error("rm: runId is required");
|
|
362
326
|
if (!path) throw new Error("rm: path is required");
|
|
@@ -381,10 +345,7 @@ export default class Entries {
|
|
|
381
345
|
this.#emitChanged(runId, path, "remove");
|
|
382
346
|
}
|
|
383
347
|
|
|
384
|
-
|
|
385
|
-
* cp — copy an entry to a new path. Source body becomes new body;
|
|
386
|
-
* source view unchanged.
|
|
387
|
-
*/
|
|
348
|
+
// cp — copy an entry to a new path; see PLUGINS.md primitives.
|
|
388
349
|
async cp({
|
|
389
350
|
runId,
|
|
390
351
|
turn = 0,
|
|
@@ -411,9 +372,7 @@ export default class Entries {
|
|
|
411
372
|
});
|
|
412
373
|
}
|
|
413
374
|
|
|
414
|
-
|
|
415
|
-
* mv — rename an entry. Equivalent to cp + rm on source.
|
|
416
|
-
*/
|
|
375
|
+
// mv — rename (cp + rm).
|
|
417
376
|
async mv({
|
|
418
377
|
runId,
|
|
419
378
|
turn = 0,
|
|
@@ -439,13 +398,7 @@ export default class Entries {
|
|
|
439
398
|
await this.rm({ runId, path: from });
|
|
440
399
|
}
|
|
441
400
|
|
|
442
|
-
|
|
443
|
-
* update — once-per-turn lifecycle signal from the model (or plugin
|
|
444
|
-
* speaking on its behalf). Writes to update://<slug> with body as the
|
|
445
|
-
* content and attributes.status carrying the model's continuation code
|
|
446
|
-
* (102 continue, 200/204 terminal, 422 can't-answer). Returns the
|
|
447
|
-
* slug path.
|
|
448
|
-
*/
|
|
401
|
+
// update — once-per-turn lifecycle signal; see PLUGINS.md.
|
|
449
402
|
async update({
|
|
450
403
|
runId,
|
|
451
404
|
turn = 0,
|
|
@@ -475,7 +428,12 @@ export default class Entries {
|
|
|
475
428
|
runId,
|
|
476
429
|
path,
|
|
477
430
|
body = null,
|
|
478
|
-
{
|
|
431
|
+
{
|
|
432
|
+
limit = null,
|
|
433
|
+
offset = null,
|
|
434
|
+
since = null,
|
|
435
|
+
includeAuditSchemes = false,
|
|
436
|
+
} = {},
|
|
479
437
|
) {
|
|
480
438
|
return this.#db.get_entries_by_pattern.all({
|
|
481
439
|
run_id: runId,
|
|
@@ -483,6 +441,7 @@ export default class Entries {
|
|
|
483
441
|
body: body ? body : null,
|
|
484
442
|
limit,
|
|
485
443
|
offset,
|
|
444
|
+
since,
|
|
486
445
|
include_audit_schemes: includeAuditSchemes ? 1 : null,
|
|
487
446
|
});
|
|
488
447
|
}
|
|
@@ -497,10 +456,7 @@ export default class Entries {
|
|
|
497
456
|
}
|
|
498
457
|
|
|
499
458
|
async waitForResolution(runId, path) {
|
|
500
|
-
//
|
|
501
|
-
// (yolo) flipped the entry to terminal during proposal.pending,
|
|
502
|
-
// the state change has already happened and no future drain will
|
|
503
|
-
// fire. Without this guard, in-process resolvers would deadlock.
|
|
459
|
+
// Pre-check: yolo's synchronous resolver may have already flipped state, no drain will fire.
|
|
504
460
|
const current = await this.getState(runId, path);
|
|
505
461
|
if (
|
|
506
462
|
current &&
|
|
@@ -559,9 +515,7 @@ export default class Entries {
|
|
|
559
515
|
return new Set(rows.map((r) => r.body));
|
|
560
516
|
}
|
|
561
517
|
|
|
562
|
-
|
|
563
|
-
* Unknown entries for a run, in DB order. Rows include path + body.
|
|
564
|
-
*/
|
|
518
|
+
// Unknown entries in DB order; rows include path + body.
|
|
565
519
|
async getUnknowns(runId) {
|
|
566
520
|
return this.#db.get_unknowns.all({ run_id: runId });
|
|
567
521
|
}
|
|
@@ -580,14 +534,7 @@ export default class Entries {
|
|
|
580
534
|
});
|
|
581
535
|
}
|
|
582
536
|
|
|
583
|
-
|
|
584
|
-
* Demote all promoted entries for a run on a given turn. Returns the
|
|
585
|
-
* affected rows (path, tokens) so callers can summarize.
|
|
586
|
-
*
|
|
587
|
-
* Implemented as SELECT-then-UPDATE because SQLite's RETURNING doesn't
|
|
588
|
-
* support the cross-table lookup needed to report content paths/tokens
|
|
589
|
-
* from the view-layer update.
|
|
590
|
-
*/
|
|
537
|
+
// SELECT-then-UPDATE: SQLite RETURNING can't cross to the view layer.
|
|
591
538
|
async demoteTurnEntries(runId, turn) {
|
|
592
539
|
const targets = await this.#db.get_turn_demotion_targets.all({
|
|
593
540
|
run_id: runId,
|
|
@@ -597,14 +544,7 @@ export default class Entries {
|
|
|
597
544
|
return targets;
|
|
598
545
|
}
|
|
599
546
|
|
|
600
|
-
|
|
601
|
-
* Demote every currently-visible entry in a run. Used by budget
|
|
602
|
-
* postDispatch as the fallback when this-turn demotion finds nothing
|
|
603
|
-
* and the packet still overflows — left-over promotions from prior
|
|
604
|
-
* turns the model didn't demote themselves. Returns the affected
|
|
605
|
-
* rows (path, tokens, turn) ordered oldest promotion first so the
|
|
606
|
-
* error body can name them.
|
|
607
|
-
*/
|
|
547
|
+
// Budget postDispatch fallback: demote every visible entry in the run.
|
|
608
548
|
async demoteRunVisibleEntries(runId) {
|
|
609
549
|
const targets = await this.#db.get_run_visible_targets.all({
|
|
610
550
|
run_id: runId,
|
|
@@ -613,17 +553,12 @@ export default class Entries {
|
|
|
613
553
|
return targets;
|
|
614
554
|
}
|
|
615
555
|
|
|
616
|
-
|
|
617
|
-
* Run metadata lookup. Exposed here so plugins don't reach into
|
|
618
|
-
* core.db for run-scoped lookups.
|
|
619
|
-
*/
|
|
556
|
+
// Plugin-facing run lookup; avoids reaching into core.db.
|
|
620
557
|
async getRun(runId) {
|
|
621
558
|
return this.#db.get_run_by_id.get({ id: runId });
|
|
622
559
|
}
|
|
623
560
|
|
|
624
|
-
|
|
625
|
-
* Turn-level usage stats write (telemetry). Same rationale as getRun.
|
|
626
|
-
*/
|
|
561
|
+
// Plugin-facing turn-stats write.
|
|
627
562
|
async updateTurnStats(stats) {
|
|
628
563
|
return this.#db.update_turn_stats.run(stats);
|
|
629
564
|
}
|
|
@@ -87,10 +87,7 @@ export default class ProjectAgent {
|
|
|
87
87
|
return this.#agentLoop.inject(run, message, mode, options);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
//
|
|
91
|
-
// Caller is expected to follow up with a kickoff (ask/act) that
|
|
92
|
-
// operates on the returned alias. Lets RPC respond with the real
|
|
93
|
-
// alias before the long-running loop starts.
|
|
90
|
+
// Create/fork the run row synchronously; caller follows up with ask/act.
|
|
94
91
|
async ensureRun(projectId, model, run, prompt, options = {}) {
|
|
95
92
|
return this.#agentLoop.ensureRun(projectId, model, run, prompt, options);
|
|
96
93
|
}
|
|
@@ -103,11 +100,7 @@ export default class ProjectAgent {
|
|
|
103
100
|
this.#agentLoop.abort(runId);
|
|
104
101
|
}
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
* Abort every in-flight run and wait for them to settle. Called
|
|
108
|
-
* from the server's close path so the Node event loop isn't held
|
|
109
|
-
* open by detached kickoff Promises after shutdown.
|
|
110
|
-
*/
|
|
103
|
+
// Abort all in-flight runs and drain so the event loop can exit.
|
|
111
104
|
async shutdown() {
|
|
112
105
|
await this.#agentLoop.abortAll();
|
|
113
106
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import RummyContext from "../hooks/RummyContext.js";
|
|
2
2
|
import { ContextExceededError } from "../llm/errors.js";
|
|
3
|
+
import { PermissionError } from "./errors.js";
|
|
3
4
|
import materializeContext from "./materializeContext.js";
|
|
4
5
|
import XmlParser from "./XmlParser.js";
|
|
5
6
|
|
|
@@ -44,7 +45,6 @@ export default class TurnExecutor {
|
|
|
44
45
|
sequence: turn,
|
|
45
46
|
});
|
|
46
47
|
|
|
47
|
-
// Build RummyContext before turn.started so plugins can write entries
|
|
48
48
|
const rummy = new RummyContext(
|
|
49
49
|
{
|
|
50
50
|
tag: "turn",
|
|
@@ -78,7 +78,6 @@ export default class TurnExecutor {
|
|
|
78
78
|
loopPrompt,
|
|
79
79
|
},
|
|
80
80
|
);
|
|
81
|
-
// Plugins write prompt/instructions entries
|
|
82
81
|
await this.#hooks.turn.started.emit({
|
|
83
82
|
rummy,
|
|
84
83
|
mode,
|
|
@@ -89,12 +88,9 @@ export default class TurnExecutor {
|
|
|
89
88
|
|
|
90
89
|
await this.#hooks.processTurn(rummy);
|
|
91
90
|
|
|
92
|
-
// Project instructions://system through the instructions tool's projection
|
|
93
91
|
const systemPrompt =
|
|
94
92
|
await this.#hooks.instructions.resolveSystemPrompt(rummy);
|
|
95
93
|
|
|
96
|
-
// Materialize turn_context: VIEW rows projected through tools
|
|
97
|
-
const demoted = [];
|
|
98
94
|
const budgetCtx = {
|
|
99
95
|
runId: currentRunId,
|
|
100
96
|
loopId: currentLoopId,
|
|
@@ -102,7 +98,6 @@ export default class TurnExecutor {
|
|
|
102
98
|
systemPrompt,
|
|
103
99
|
mode,
|
|
104
100
|
toolSet,
|
|
105
|
-
demoted,
|
|
106
101
|
loopIteration,
|
|
107
102
|
};
|
|
108
103
|
const initial = await materializeContext({
|
|
@@ -118,13 +113,6 @@ export default class TurnExecutor {
|
|
|
118
113
|
rowCount: initial.rows.length,
|
|
119
114
|
});
|
|
120
115
|
|
|
121
|
-
await this.#hooks.run.progress.emit({
|
|
122
|
-
projectId,
|
|
123
|
-
run: currentAlias,
|
|
124
|
-
turn,
|
|
125
|
-
status: "thinking",
|
|
126
|
-
});
|
|
127
|
-
|
|
128
116
|
const budgetResult = await this.#hooks.budget.enforce({
|
|
129
117
|
contextSize,
|
|
130
118
|
messages: initial.messages,
|
|
@@ -158,15 +146,19 @@ export default class TurnExecutor {
|
|
|
158
146
|
turn,
|
|
159
147
|
});
|
|
160
148
|
|
|
161
|
-
// Call LLM. Transient-error retry + context-exceeded detection live
|
|
162
|
-
// in LlmProvider; context-exceeded surfaces as ContextExceededError.
|
|
163
149
|
await this.#hooks.llm.request.started.emit({ model: requestedModel, turn });
|
|
164
150
|
let rawResult;
|
|
165
151
|
try {
|
|
166
152
|
rawResult = await this.#llmProvider.completion(
|
|
167
153
|
filteredMessages,
|
|
168
154
|
requestedModel,
|
|
169
|
-
{
|
|
155
|
+
{
|
|
156
|
+
temperature: options?.temperature,
|
|
157
|
+
signal,
|
|
158
|
+
// Per-run stable identifier for provider-side prompt caching
|
|
159
|
+
// (xAI prompt_cache_key, OpenAI prompt_cache_key, etc.).
|
|
160
|
+
runAlias: runRow?.alias || `run_${currentRunId}`,
|
|
161
|
+
},
|
|
170
162
|
);
|
|
171
163
|
} catch (err) {
|
|
172
164
|
if (err instanceof ContextExceededError) {
|
|
@@ -201,19 +193,8 @@ export default class TurnExecutor {
|
|
|
201
193
|
usage: result.usage,
|
|
202
194
|
});
|
|
203
195
|
const responseMessage = result.choices?.[0]?.message;
|
|
204
|
-
// A valid completion response always carries content (possibly
|
|
205
|
-
// empty) on the message; protect against that specific case so
|
|
206
|
-
// downstream parsers see a string.
|
|
207
196
|
const content = responseMessage?.content ? responseMessage.content : "";
|
|
208
197
|
|
|
209
|
-
await this.#hooks.run.progress.emit({
|
|
210
|
-
projectId,
|
|
211
|
-
run: currentAlias,
|
|
212
|
-
turn,
|
|
213
|
-
status: "processing",
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Parse and emit — plugins handle audit storage
|
|
217
198
|
const { commands, warnings, unparsed } = XmlParser.parse(content);
|
|
218
199
|
for (const w of warnings) {
|
|
219
200
|
await this.#hooks.error.log.emit({
|
|
@@ -225,7 +206,7 @@ export default class TurnExecutor {
|
|
|
225
206
|
status: 422,
|
|
226
207
|
});
|
|
227
208
|
}
|
|
228
|
-
if (commands.length === 0 &&
|
|
209
|
+
if (commands.length === 0 && unparsed?.trim() && warnings.length === 0) {
|
|
229
210
|
await this.#hooks.error.log.emit({
|
|
230
211
|
store: this.#entries,
|
|
231
212
|
runId: currentRunId,
|
|
@@ -236,10 +217,7 @@ export default class TurnExecutor {
|
|
|
236
217
|
});
|
|
237
218
|
}
|
|
238
219
|
|
|
239
|
-
//
|
|
240
|
-
// <think> tag, other plugin reasoning sources). Filter starts with
|
|
241
|
-
// the API-provided reasoning_content and layers on each plugin's
|
|
242
|
-
// contribution.
|
|
220
|
+
// Layer plugin reasoning contributions onto the API-provided seed.
|
|
243
221
|
if (responseMessage) {
|
|
244
222
|
const seed = responseMessage.reasoning_content
|
|
245
223
|
? responseMessage.reasoning_content
|
|
@@ -264,7 +242,7 @@ export default class TurnExecutor {
|
|
|
264
242
|
userMsg: userMsg?.content,
|
|
265
243
|
});
|
|
266
244
|
|
|
267
|
-
//
|
|
245
|
+
// PHASE 1: RECORD
|
|
268
246
|
const recorded = [];
|
|
269
247
|
for (const cmd of commands) {
|
|
270
248
|
const entry = await this.#record(
|
|
@@ -277,14 +255,7 @@ export default class TurnExecutor {
|
|
|
277
255
|
if (entry) recorded.push(entry);
|
|
278
256
|
}
|
|
279
257
|
|
|
280
|
-
//
|
|
281
|
-
// Sequential queue. Each tool completes before the next starts.
|
|
282
|
-
// On failure: abort remaining. On proposal: notify client, await
|
|
283
|
-
// resolution, continue.
|
|
284
|
-
// Narration text outside tags is fine when the turn also emitted
|
|
285
|
-
// at least one command — "OK", "Let me check:", reasoning prefixes
|
|
286
|
-
// are natural. Parse warnings and no-tags responses already emitted
|
|
287
|
-
// errors above; dispatch crashes and failed entries emit below.
|
|
258
|
+
// PHASE 2: DISPATCH — sequential; abort-after-failure; proposals notify-and-await.
|
|
288
259
|
let abortAfter = null;
|
|
289
260
|
|
|
290
261
|
for (const entry of recorded) {
|
|
@@ -309,6 +280,21 @@ export default class TurnExecutor {
|
|
|
309
280
|
try {
|
|
310
281
|
await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
|
|
311
282
|
} catch (dispatchErr) {
|
|
283
|
+
// PermissionError is the model attempting a documented-forbidden
|
|
284
|
+
// write (e.g. <set path="prompt://1"> with body). Surface as a
|
|
285
|
+
// soft 403 so the model can adjust on the next turn; do not
|
|
286
|
+
// abort sibling entries — the rest of the turn was valid.
|
|
287
|
+
if (dispatchErr instanceof PermissionError) {
|
|
288
|
+
await this.#hooks.error.log.emit({
|
|
289
|
+
store: this.#entries,
|
|
290
|
+
runId: currentRunId,
|
|
291
|
+
turn,
|
|
292
|
+
loopId: currentLoopId,
|
|
293
|
+
message: dispatchErr.message,
|
|
294
|
+
status: 403,
|
|
295
|
+
});
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
312
298
|
await this.#hooks.error.log.emit({
|
|
313
299
|
store: this.#entries,
|
|
314
300
|
runId: currentRunId,
|
|
@@ -323,11 +309,9 @@ export default class TurnExecutor {
|
|
|
323
309
|
await this.#hooks.tool.after.emit({ entry, rummy });
|
|
324
310
|
await this.#hooks.entry.created.emit(entry);
|
|
325
311
|
|
|
326
|
-
// Plugins (e.g. set
|
|
327
|
-
// recorded entry — e.g. search/replace revisions → set:// 202.
|
|
312
|
+
// Plugins materialize pending proposals (e.g. set search/replace → 202).
|
|
328
313
|
await this.#hooks.proposal.prepare.emit({ rummy, recorded: [entry] });
|
|
329
314
|
|
|
330
|
-
// Check for any proposals created by this entry's dispatch
|
|
331
315
|
const proposed = await this.#entries.getUnresolved(currentRunId);
|
|
332
316
|
for (const p of proposed) {
|
|
333
317
|
await this.#hooks.proposal.pending.emit({
|
|
@@ -338,39 +322,18 @@ export default class TurnExecutor {
|
|
|
338
322
|
});
|
|
339
323
|
await this.#entries.waitForResolution(currentRunId, p.path);
|
|
340
324
|
const resolved = await this.#entries.getState(currentRunId, p.path);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
runId: currentRunId,
|
|
345
|
-
turn,
|
|
346
|
-
loopId: currentLoopId,
|
|
347
|
-
message: `Proposal ${p.path} rejected: status ${resolved.status}.`,
|
|
348
|
-
status: resolved.status,
|
|
349
|
-
});
|
|
350
|
-
abortAfter = entry.scheme;
|
|
351
|
-
}
|
|
325
|
+
// Failure surfaces in the proposal entry itself; abort cascade
|
|
326
|
+
// triggers the trailing-action "Aborted — preceding <X>" body.
|
|
327
|
+
if (resolved?.status >= 400) abortAfter = entry.scheme;
|
|
352
328
|
}
|
|
353
329
|
|
|
354
330
|
if (!abortAfter) {
|
|
355
331
|
const entryPath = entry.resultPath || entry.path;
|
|
356
332
|
const row = await this.#entries.getState(currentRunId, entryPath);
|
|
357
|
-
if (row?.status >= 400)
|
|
358
|
-
await this.#hooks.error.log.emit({
|
|
359
|
-
store: this.#entries,
|
|
360
|
-
runId: currentRunId,
|
|
361
|
-
turn,
|
|
362
|
-
loopId: currentLoopId,
|
|
363
|
-
message: `Entry ${entryPath} failed: status ${row.status}.`,
|
|
364
|
-
status: row.status,
|
|
365
|
-
});
|
|
366
|
-
abortAfter = entry.scheme;
|
|
367
|
-
}
|
|
333
|
+
if (row?.status >= 400) abortAfter = entry.scheme;
|
|
368
334
|
}
|
|
369
335
|
}
|
|
370
336
|
|
|
371
|
-
// Turn Demotion: budget plugin re-materializes end-of-turn context
|
|
372
|
-
// and demotes this turn's promoted entries on overflow. Overflow
|
|
373
|
-
// emits an error (status 413) via the unified error channel.
|
|
374
337
|
await this.#hooks.budget.postDispatch({
|
|
375
338
|
contextSize,
|
|
376
339
|
ctx: budgetCtx,
|
|
@@ -409,21 +372,14 @@ export default class TurnExecutor {
|
|
|
409
372
|
return turnResult;
|
|
410
373
|
}
|
|
411
374
|
|
|
412
|
-
|
|
413
|
-
* Record a parsed command as a known_entries row.
|
|
414
|
-
* Returns the recorded entry descriptor, or null if rejected/skipped.
|
|
415
|
-
*/
|
|
375
|
+
// Record a parsed command; returns the entry descriptor or rejects on bad shapes.
|
|
416
376
|
async #record(runId, loopId, turn, mode, cmd) {
|
|
417
377
|
const scheme = cmd.name;
|
|
418
|
-
// Each tool's XmlParser shape surfaces exactly one of these
|
|
419
|
-
// three fields as its addressable target. Treat absent as empty
|
|
420
|
-
// so the length/control-char validation below catches bad shapes
|
|
421
|
-
// rather than letting an undefined slip through.
|
|
422
378
|
let rawTarget = "";
|
|
423
379
|
if (cmd.path) rawTarget = cmd.path;
|
|
424
380
|
else if (cmd.command) rawTarget = cmd.command;
|
|
425
381
|
else if (cmd.question) rawTarget = cmd.question;
|
|
426
|
-
// Reject
|
|
382
|
+
// Reject likely reasoning bleed: oversize or control chars in target.
|
|
427
383
|
if (rawTarget.length > 512 || /\p{Cc}/u.test(rawTarget)) {
|
|
428
384
|
const rejectPath = await this.#entries.logPath(
|
|
429
385
|
runId,
|
|
@@ -454,18 +410,14 @@ export default class TurnExecutor {
|
|
|
454
410
|
const target = rawTarget;
|
|
455
411
|
const resultPath = await this.#entries.logPath(runId, turn, scheme, target);
|
|
456
412
|
|
|
457
|
-
// Pass parsed command fields through as attributes
|
|
458
413
|
const { name: _, ...attributes } = cmd;
|
|
459
414
|
if (cmd.path) attributes.path = target;
|
|
460
415
|
|
|
461
|
-
// Same per-shape resolution as rawTarget; the three sources are
|
|
462
|
-
// mutually exclusive per tool. Empty string when none set.
|
|
463
416
|
let body = "";
|
|
464
417
|
if (cmd.body) body = cmd.body;
|
|
465
418
|
else if (cmd.command) body = cmd.command;
|
|
466
419
|
else if (cmd.question) body = cmd.question;
|
|
467
420
|
|
|
468
|
-
// Filter: plugins can validate/transform before recording
|
|
469
421
|
const filtered = await this.#hooks.entry.recording.filter(
|
|
470
422
|
{
|
|
471
423
|
scheme,
|
|
@@ -478,7 +430,17 @@ export default class TurnExecutor {
|
|
|
478
430
|
{ store: this.#entries, runId, turn, loopId, mode },
|
|
479
431
|
);
|
|
480
432
|
if (filtered.state === "failed" || filtered.state === "cancelled") {
|
|
481
|
-
|
|
433
|
+
await this.#entries.set({
|
|
434
|
+
runId,
|
|
435
|
+
turn,
|
|
436
|
+
loopId,
|
|
437
|
+
path: filtered.path,
|
|
438
|
+
body: filtered.body,
|
|
439
|
+
state: filtered.state,
|
|
440
|
+
outcome: filtered.outcome,
|
|
441
|
+
attributes: filtered.attributes,
|
|
442
|
+
});
|
|
443
|
+
return { ...filtered, resultPath: filtered.path };
|
|
482
444
|
}
|
|
483
445
|
|
|
484
446
|
return {
|