@basein/runner 0.3.15 → 0.3.17
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/README.md +7 -0
- package/dist/bin/bir-scenario.js +8 -5
- package/dist/bin/bir.js +2 -1
- package/dist/bin/investigate.d.ts +71 -0
- package/dist/bin/investigate.js +145 -13
- package/dist/bin/scenario-edit.d.ts +45 -1
- package/dist/bin/scenario-edit.js +134 -14
- package/dist/control/server.d.ts +16 -0
- package/dist/control/server.js +156 -7
- package/dist/control/transcript.d.ts +18 -0
- package/dist/control/transcript.js +55 -0
- package/dist/record/recorder.d.ts +47 -4
- package/dist/record/remote-recorder.d.ts +5 -4
- package/dist/record/remote-recorder.js +23 -1
- package/dist/record/switchable-recorder.d.ts +1 -5
- package/dist/replay/bundle.d.ts +10 -0
- package/dist/replay/bundle.js +23 -2
- package/dist/replay/controller.d.ts +133 -0
- package/dist/replay/controller.js +496 -59
- package/dist/replay/flatten.d.ts +39 -3
- package/dist/replay/flatten.js +42 -2
- package/dist/replay/handover.d.ts +39 -1
- package/dist/replay/handover.js +81 -2
- package/dist/replay/logic.d.ts +8 -0
- package/dist/replay/logic.js +19 -0
- package/dist/replay/plan.d.ts +165 -4
- package/dist/replay/plan.js +488 -25
- package/dist/replay/source-run.d.ts +14 -1
- package/dist/replay/source-run.js +25 -2
- package/dist/replay/targets.js +8 -1
- package/dist/replay/types.d.ts +52 -3
- package/dist/replay/types.js +14 -0
- package/dist/util/service-headers.d.ts +7 -1
- package/dist/util/service-headers.js +7 -1
- package/docs/BaseInstRunner.md +3 -1
- package/docs/calculatedReplay.md +117 -6
- package/docs/calculatedReplayGuide.md +41 -3
- package/package.json +1 -1
package/dist/replay/plan.js
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* step to know its reach, so it can choose between the proxy, a recorded
|
|
21
21
|
* output, and skipping.
|
|
22
22
|
*/
|
|
23
|
-
import { evalParamMapLogic, evalParamsLogic, evalResponseParamsLogic, evalResultMapLogic, evalToolInputLogic, evalToolOutputLogic, LogicTimeoutError, } from "./logic.js";
|
|
24
|
-
import { flatEntriesOf } from "./flatten.js";
|
|
25
|
-
import { assembleBundle, bundleInput, MAX_REPLAY_REASON } from "./bundle.js";
|
|
23
|
+
import { evalParamMapLogic, evalParamsLogic, evalResponseParamsLogic, evalResultMapLogic, evalToolInputLogic, evalToolOutputLogic, evalWhenLogic, LogicTimeoutError, } from "./logic.js";
|
|
24
|
+
import { flatEntriesOf, frameEdges } from "./flatten.js";
|
|
25
|
+
import { assembleBundle, bundleInput, MAX_REPLAY_REASON, testWords } from "./bundle.js";
|
|
26
26
|
import { redact, redactText } from "../record/redact.js";
|
|
27
27
|
import { sinkOfPlan, unsafeParameter } from "./safety.js";
|
|
28
28
|
import { errText } from "../util/log.js";
|
|
@@ -259,6 +259,18 @@ export class ScenarioReplayPlan {
|
|
|
259
259
|
stopAt;
|
|
260
260
|
/** This plan is a sub-task inside the agent's own task (segmented.md R-OUT-13). */
|
|
261
261
|
subTask;
|
|
262
|
+
/**
|
|
263
|
+
* Where each called frame begins and ends, its callees' entries included:
|
|
264
|
+
* where it is entered, its test decided and it is left, and where a
|
|
265
|
+
* sub-task its test leaves out ends (AC5).
|
|
266
|
+
*/
|
|
267
|
+
edges;
|
|
268
|
+
/** Each entry's flat position. */
|
|
269
|
+
positions = new Map();
|
|
270
|
+
/** Every frame of the flat list, by id. */
|
|
271
|
+
frames = new Map();
|
|
272
|
+
/** The plan's own walk: what its cursor has entered and decided (AC5). */
|
|
273
|
+
walk = { entered: new Set(), tested: new Set(), passed: new Set(), left: [] };
|
|
262
274
|
constructor(opts) {
|
|
263
275
|
this.scenario = opts.scenario;
|
|
264
276
|
this.scenarioId = opts.scenario.id;
|
|
@@ -279,6 +291,19 @@ export class ScenarioReplayPlan {
|
|
|
279
291
|
typeof stop === "number" && stop >= 0 && stop < this.entries.length
|
|
280
292
|
? stop
|
|
281
293
|
: this.entries.length;
|
|
294
|
+
this.edges = frameEdges(this.entries);
|
|
295
|
+
this.entries.forEach((entry, index) => {
|
|
296
|
+
this.positions.set(entry, index);
|
|
297
|
+
for (let f = entry.frame; f; f = f.parent)
|
|
298
|
+
this.frames.set(f.id, f);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
frameById(id) {
|
|
302
|
+
return this.frames.get(id);
|
|
303
|
+
}
|
|
304
|
+
/** The called frames that begin at flat position `flatIndex`, outermost first. */
|
|
305
|
+
framesStartingAt(flatIndex) {
|
|
306
|
+
return this.edges.startsAt.get(flatIndex) ?? [];
|
|
282
307
|
}
|
|
283
308
|
/** The caller's own frame — the one at depth 0. */
|
|
284
309
|
rootFrame() {
|
|
@@ -304,10 +329,22 @@ export class ScenarioReplayPlan {
|
|
|
304
329
|
* out. A mapping that omits a setting therefore cannot pass the service's
|
|
305
330
|
* check and then run `undefined` here.
|
|
306
331
|
*/
|
|
307
|
-
|
|
308
|
-
|
|
332
|
+
enterFramesAt(flatIndex, walk) {
|
|
333
|
+
// Outermost first: a sub-task whose chain begins with a call begins where
|
|
334
|
+
// its callee does, and the callee's mapping reads the sub-task's
|
|
335
|
+
// parameters — built here, before it (AC5 review).
|
|
336
|
+
for (const frame of this.framesStartingAt(flatIndex))
|
|
337
|
+
this.enterFrame(frame, walk);
|
|
338
|
+
}
|
|
339
|
+
enterFrame(frame, walk) {
|
|
340
|
+
if (frame.depth === 0)
|
|
309
341
|
return;
|
|
310
|
-
|
|
342
|
+
// Once per walk: a step's test may need the frame's parameters before the
|
|
343
|
+
// step's input is computed (AC5), and a steer cursor computes one step's
|
|
344
|
+
// input again when a pin was declined.
|
|
345
|
+
if (walk.entered.has(frame.id))
|
|
346
|
+
return;
|
|
347
|
+
const resp = walk.resp;
|
|
311
348
|
const parent = frame.parent;
|
|
312
349
|
const parentParams = parent
|
|
313
350
|
? parent.depth === 0
|
|
@@ -329,15 +366,91 @@ export class ScenarioReplayPlan {
|
|
|
329
366
|
resp.set(frame.id, {});
|
|
330
367
|
else
|
|
331
368
|
frame.respParams = {};
|
|
369
|
+
walk.entered.add(frame.id);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Leave every called frame that ends at `flatIndex` and that `walk`
|
|
373
|
+
* entered, innermost first: a sub-task whose chain ends with a call is left
|
|
374
|
+
* after its callee has handed it its results.
|
|
375
|
+
*/
|
|
376
|
+
leaveFramesAt(flatIndex, walk) {
|
|
377
|
+
for (const frame of this.edges.endsAt.get(flatIndex) ?? []) {
|
|
378
|
+
if (walk.entered.has(frame.id))
|
|
379
|
+
this.leaveFrame(frame, walk.resp);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* After a skip that ends at flat position `last`: leave every entered
|
|
384
|
+
* sub-task that ends there — its results still go back to its caller. A
|
|
385
|
+
* result mapping that throws ends the walk: its verdict is filed on the
|
|
386
|
+
* call row, in the caller's chain, and the stop says the step at `last` was
|
|
387
|
+
* left out, not run (`result_map`). Rethrown instead when `rethrow`.
|
|
388
|
+
*/
|
|
389
|
+
leaveAfterSkip(last, walk, onStep, rethrow) {
|
|
390
|
+
for (const frame of this.edges.endsAt.get(last) ?? []) {
|
|
391
|
+
if (!walk.entered.has(frame.id))
|
|
392
|
+
continue;
|
|
393
|
+
try {
|
|
394
|
+
this.leaveFrame(frame, walk.resp);
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
if (rethrow)
|
|
398
|
+
throw err;
|
|
399
|
+
const error = errText(err);
|
|
400
|
+
const { step, entry } = this.callRowOf(frame, this.entries[last]);
|
|
401
|
+
onStep?.({ step, entry, input: {}, outcome: "failed", stage: "tool_output_logic", error, ms: 0 });
|
|
402
|
+
return {
|
|
403
|
+
kind: "step_failed",
|
|
404
|
+
stepIndex: step.stepIndex,
|
|
405
|
+
// At the step left out, which did not run: what ran before it did
|
|
406
|
+
// its work, and what comes after it is left to do.
|
|
407
|
+
flatIndex: last,
|
|
408
|
+
stage: "tool_output_logic",
|
|
409
|
+
error,
|
|
410
|
+
reason: "result_map",
|
|
411
|
+
subTask: frame.callIntent ?? null,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The call row that opened `frame`, as a step of its caller's chain: where
|
|
419
|
+
* a sub-task's test and its result mapping are filed (R-CALL-13).
|
|
420
|
+
*/
|
|
421
|
+
callRowOf(frame, at) {
|
|
422
|
+
const step = frame.callStep ?? {
|
|
423
|
+
stepIndex: frame.callerStepIndex ?? at.stepIndex,
|
|
424
|
+
kind: "segment",
|
|
425
|
+
toolName: null,
|
|
426
|
+
toolInputLogic: null,
|
|
427
|
+
toolOutputLogic: null,
|
|
428
|
+
reasoning: null,
|
|
429
|
+
};
|
|
430
|
+
const owner = frame.parent ?? frame;
|
|
431
|
+
return {
|
|
432
|
+
step,
|
|
433
|
+
entry: {
|
|
434
|
+
scenarioId: owner.scenarioId,
|
|
435
|
+
runId: owner.runId,
|
|
436
|
+
stepFrom: owner.stepFrom,
|
|
437
|
+
stepTo: owner.stepTo,
|
|
438
|
+
stepIndex: step.stepIndex,
|
|
439
|
+
step,
|
|
440
|
+
frame: owner,
|
|
441
|
+
depth: owner.depth,
|
|
442
|
+
first: false,
|
|
443
|
+
last: false,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
332
446
|
}
|
|
333
447
|
/**
|
|
334
448
|
* Leaving a called frame: hand what the segment emitted back to its caller
|
|
335
449
|
* (R-CALL-29). `out` is the frame's own accumulated `respParams`.
|
|
336
450
|
*/
|
|
337
|
-
leaveFrame(
|
|
338
|
-
if (
|
|
451
|
+
leaveFrame(frame, resp) {
|
|
452
|
+
if (frame.depth === 0)
|
|
339
453
|
return;
|
|
340
|
-
const frame = entry.frame;
|
|
341
454
|
const parent = frame.parent;
|
|
342
455
|
if (!frame.resultMapLogic || !parent)
|
|
343
456
|
return;
|
|
@@ -448,10 +561,6 @@ export class ScenarioReplayPlan {
|
|
|
448
561
|
stopEntry() {
|
|
449
562
|
return this.stopsEarly() ? this.entries[this.stopAt] : undefined;
|
|
450
563
|
}
|
|
451
|
-
/** Tool names from position `from` to the end of the flat chain. */
|
|
452
|
-
toolsFrom(from) {
|
|
453
|
-
return this.entries.slice(Math.max(0, from)).map((e) => e.step.toolName ?? "");
|
|
454
|
-
}
|
|
455
564
|
/** Every entry in order — for the report, and for the owning scenario ids. */
|
|
456
565
|
allEntries() {
|
|
457
566
|
return this.entries;
|
|
@@ -514,10 +623,265 @@ export class ScenarioReplayPlan {
|
|
|
514
623
|
if (!entry)
|
|
515
624
|
throw new Error("no current step to compute input for");
|
|
516
625
|
// Entering a called frame happens here, on its first step, because this is
|
|
517
|
-
// the first moment the caller's own accumulation is final (R-CALL-29)
|
|
518
|
-
|
|
626
|
+
// the first moment the caller's own accumulation is final (R-CALL-29) —
|
|
627
|
+
// unless the step's test needed the frame first (AC5).
|
|
628
|
+
this.enterFramesAt(this.stepIndex, this.walk);
|
|
519
629
|
return evalToolInputLogic(entry.step.toolInputLogic ?? "return {};", this.paramsFor(entry), entry.frame.intent, this.respFor(entry));
|
|
520
630
|
}
|
|
631
|
+
// ── tests (the service's autocorrect.md AC5) ──────────────────────────────
|
|
632
|
+
//
|
|
633
|
+
// A step may run only when its test says so: a JavaScript body over the
|
|
634
|
+
// parameters, the intent and what the earlier steps accumulated, decided
|
|
635
|
+
// when the step comes due — never sooner, because the accumulation it reads
|
|
636
|
+
// is final only then. A call row's test decides its whole sub-task, in the
|
|
637
|
+
// caller's scope, before the sub-task's frame is entered. `skip` leaves the
|
|
638
|
+
// step out; `hand_over` stops the plan in front of it (the other half of an
|
|
639
|
+
// "if … otherwise", which the agent does); a test that throws or answers
|
|
640
|
+
// anything but true or false stops it too. Nothing is ever guessed.
|
|
641
|
+
/** Whether the plan's cursor rests in front of the step it is parked before. */
|
|
642
|
+
atStop() {
|
|
643
|
+
return this.stopsEarly() && this.stepIndex === this.stopAt;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* The test an entry runs under, in words, when it has one: its own, or that
|
|
647
|
+
* of a sub-task beginning at it — one whose chain begins with a call begins
|
|
648
|
+
* at its callee's first step.
|
|
649
|
+
*/
|
|
650
|
+
onlyWhen(entry) {
|
|
651
|
+
const own = entry.step.when;
|
|
652
|
+
if (own)
|
|
653
|
+
return { text: testWords(own.text) };
|
|
654
|
+
const at = this.positions.get(entry);
|
|
655
|
+
const call = at === undefined ? undefined : this.framesStartingAt(at).find((f) => f.when)?.when;
|
|
656
|
+
if (call)
|
|
657
|
+
return { text: testWords(call.text) };
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* The tool names of the steps from flat position `from` to the end, each
|
|
662
|
+
* that runs only under a test marked so — for a hand-over note's "remaining
|
|
663
|
+
* planned steps", which must not read as steps this request needs.
|
|
664
|
+
*/
|
|
665
|
+
remainingFrom(from) {
|
|
666
|
+
return this.entries.slice(Math.max(0, from)).map((e) => {
|
|
667
|
+
const name = e.step.toolName ?? "";
|
|
668
|
+
const t = this.onlyWhen(e);
|
|
669
|
+
return t ? `${name} (only when ${t.text ?? "a request needs it"})` : name;
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
/** The steps the plan's own cursor has left out so far, in order (AC5). */
|
|
673
|
+
leftOut() {
|
|
674
|
+
return this.walk.left;
|
|
675
|
+
}
|
|
676
|
+
/** Whether any planned step runs only under a test. */
|
|
677
|
+
hasTests() {
|
|
678
|
+
return this.entries.slice(0, this.stopAt).some((e) => this.onlyWhen(e) !== undefined);
|
|
679
|
+
}
|
|
680
|
+
/** One test, answered — or the reason it could not be. Never throws. */
|
|
681
|
+
answer(test, params, intent, resp) {
|
|
682
|
+
try {
|
|
683
|
+
return { yes: evalWhenLogic(test.logic, params, intent, resp) };
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
return { error: errText(err) };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* What the tests decide at flat position `flatIndex` for `walk`. Enters the
|
|
691
|
+
* position's frame when its own test needs the frame's parameters, and
|
|
692
|
+
* nothing else changes: the caller moves its cursor.
|
|
693
|
+
*/
|
|
694
|
+
gate(flatIndex, walk) {
|
|
695
|
+
const entry = this.entries[flatIndex];
|
|
696
|
+
if (!entry)
|
|
697
|
+
return { kind: "run" };
|
|
698
|
+
const frame = entry.frame;
|
|
699
|
+
const resp = walk.resp;
|
|
700
|
+
// 1. The sub-tasks that begin here, outermost first — one whose chain
|
|
701
|
+
// begins with a call begins where its callee's steps do, so nothing of
|
|
702
|
+
// it runs before its test says it is needed. Each test is decided
|
|
703
|
+
// once, in its caller's scope, before its frame is entered; a caller
|
|
704
|
+
// that begins here too is entered first, for its parameters.
|
|
705
|
+
const starting = this.framesStartingAt(flatIndex);
|
|
706
|
+
for (let k = 0; k < starting.length; k += 1) {
|
|
707
|
+
const sub = starting[k];
|
|
708
|
+
if (!sub.when || walk.tested.has(sub.id))
|
|
709
|
+
continue;
|
|
710
|
+
try {
|
|
711
|
+
for (const outer of starting.slice(0, k))
|
|
712
|
+
this.enterFrame(outer, walk);
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
return {
|
|
716
|
+
kind: "stop",
|
|
717
|
+
stop: { kind: "step_failed", stepIndex: entry.stepIndex, flatIndex, stage: "tool_input_logic", error: errText(err) },
|
|
718
|
+
info: { step: entry.step, entry, input: {}, outcome: "failed", stage: "tool_input_logic", error: errText(err), ms: 0 },
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const decided = this.decideCall(sub, entry, flatIndex, walk);
|
|
722
|
+
if (decided)
|
|
723
|
+
return decided;
|
|
724
|
+
walk.tested.add(sub.id);
|
|
725
|
+
}
|
|
726
|
+
// 2. The step's own test, in its own frame's scope.
|
|
727
|
+
const test = entry.step.when;
|
|
728
|
+
if (!test || walk.passed.has(flatIndex))
|
|
729
|
+
return { kind: "run" };
|
|
730
|
+
try {
|
|
731
|
+
// Its frame's parameters, which a test inside a sub-task reads.
|
|
732
|
+
this.enterFramesAt(flatIndex, walk);
|
|
733
|
+
}
|
|
734
|
+
catch (err) {
|
|
735
|
+
return {
|
|
736
|
+
kind: "stop",
|
|
737
|
+
stop: { kind: "step_failed", stepIndex: entry.stepIndex, flatIndex, stage: "tool_input_logic", error: errText(err) },
|
|
738
|
+
info: { step: entry.step, entry, input: {}, outcome: "failed", stage: "tool_input_logic", error: errText(err), ms: 0 },
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
const own = resp ? (resp.get(frame.id) ?? {}) : this.respFor(entry);
|
|
742
|
+
const said = this.answer(test, this.paramsFor(entry), frame.intent, own);
|
|
743
|
+
const text = testWords(test.text);
|
|
744
|
+
if ("error" in said) {
|
|
745
|
+
return {
|
|
746
|
+
kind: "stop",
|
|
747
|
+
stop: {
|
|
748
|
+
kind: "step_failed",
|
|
749
|
+
stepIndex: entry.stepIndex,
|
|
750
|
+
flatIndex,
|
|
751
|
+
stage: "when_logic",
|
|
752
|
+
error: said.error,
|
|
753
|
+
reason: "test_failed",
|
|
754
|
+
},
|
|
755
|
+
info: { step: entry.step, entry, input: {}, outcome: "failed", stage: "when_logic", error: said.error, ms: 0 },
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (said.yes) {
|
|
759
|
+
walk.passed.add(flatIndex);
|
|
760
|
+
return { kind: "run" };
|
|
761
|
+
}
|
|
762
|
+
if (test.otherwise === "skip") {
|
|
763
|
+
return {
|
|
764
|
+
kind: "skip",
|
|
765
|
+
until: flatIndex + 1,
|
|
766
|
+
info: { step: entry.step, entry, input: {}, outcome: "not_taken", ms: 0 },
|
|
767
|
+
entry,
|
|
768
|
+
text,
|
|
769
|
+
tools: [entry.step.toolName ?? ""],
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
return { kind: "stop", stop: { kind: "other_branch", stepIndex: entry.stepIndex, flatIndex, whenText: text } };
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* A sub-task's test, decided in its caller's scope at flat position
|
|
776
|
+
* `flatIndex`, where the sub-task begins. Undefined when it says yes.
|
|
777
|
+
*/
|
|
778
|
+
decideCall(sub, entry, flatIndex, walk) {
|
|
779
|
+
const resp = walk.resp;
|
|
780
|
+
const parent = sub.parent;
|
|
781
|
+
const params = !parent || parent.depth === 0 ? this.params : (parent.parameters ?? {});
|
|
782
|
+
const parentResp = parent
|
|
783
|
+
? resp
|
|
784
|
+
? (resp.get(parent.id) ?? (parent.depth === 0 ? this.respParams : parent.respParams))
|
|
785
|
+
: parent.depth === 0
|
|
786
|
+
? this.respParams
|
|
787
|
+
: parent.respParams
|
|
788
|
+
: this.respParams;
|
|
789
|
+
// The call row, as a step of its caller's chain: its verdict is filed
|
|
790
|
+
// there, under the call's own index.
|
|
791
|
+
const call = this.callRowOf(sub, entry);
|
|
792
|
+
const owner = parent ?? sub;
|
|
793
|
+
const said = this.answer(sub.when, params, owner.intent ?? this.intent, parentResp);
|
|
794
|
+
const text = testWords(sub.when.text);
|
|
795
|
+
if ("error" in said) {
|
|
796
|
+
return {
|
|
797
|
+
kind: "stop",
|
|
798
|
+
stop: {
|
|
799
|
+
kind: "step_failed",
|
|
800
|
+
stepIndex: call.step.stepIndex,
|
|
801
|
+
flatIndex,
|
|
802
|
+
stage: "when_logic",
|
|
803
|
+
error: said.error,
|
|
804
|
+
reason: "test_failed",
|
|
805
|
+
},
|
|
806
|
+
info: { step: call.step, entry: call.entry, input: {}, outcome: "failed", stage: "when_logic", error: said.error, ms: 0 },
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
if (said.yes)
|
|
810
|
+
return undefined;
|
|
811
|
+
if (sub.when.otherwise === "skip") {
|
|
812
|
+
const until = (this.edges.end.get(sub.id) ?? flatIndex) + 1;
|
|
813
|
+
return {
|
|
814
|
+
kind: "skip",
|
|
815
|
+
until,
|
|
816
|
+
info: { step: call.step, entry: call.entry, input: {}, outcome: "not_taken", ms: 0 },
|
|
817
|
+
entry: call.entry,
|
|
818
|
+
text,
|
|
819
|
+
// Every tool of the sub-task: a call the model makes to any of them is
|
|
820
|
+
// one to a step left out.
|
|
821
|
+
tools: [...new Set(this.entries.slice(flatIndex, until).map((e) => e.step.toolName ?? ""))],
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
kind: "stop",
|
|
826
|
+
stop: { kind: "other_branch", stepIndex: call.step.stepIndex, flatIndex, whenText: text },
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/** What a skip the gate decided leaves out, as the notes name it. */
|
|
830
|
+
leftOutOf(g, flatIndex) {
|
|
831
|
+
return {
|
|
832
|
+
stepIndex: g.entry.stepIndex,
|
|
833
|
+
flatIndex,
|
|
834
|
+
until: g.until,
|
|
835
|
+
toolName: g.entry.step.toolName ?? g.entry.step.callIntent ?? "a sub-task",
|
|
836
|
+
tools: g.tools.filter((t) => t !== ""),
|
|
837
|
+
text: g.text,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Tell a recorded-output source of the steps one skip left out — every step
|
|
842
|
+
* of a sub-task left out whole — so its queues pass over the outputs of the
|
|
843
|
+
* ones that were in the recording ({@link RecordedOutputFor}).
|
|
844
|
+
*/
|
|
845
|
+
passOver(l, recordedOutputFor) {
|
|
846
|
+
const tell = recordedOutputFor?.leftOut;
|
|
847
|
+
if (!tell)
|
|
848
|
+
return;
|
|
849
|
+
for (let i = l.flatIndex; i < l.until; i += 1) {
|
|
850
|
+
const entry = this.entries[i];
|
|
851
|
+
if (entry)
|
|
852
|
+
tell(entry);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Move the plan's own cursor past every step this request does not need,
|
|
857
|
+
* deciding each test as its step comes due (AC5). Stops at a step that will
|
|
858
|
+
* run, at the end, or at a test that stops the plan — which it returns, with
|
|
859
|
+
* the steps it left out. The observer is told each verdict as it is made.
|
|
860
|
+
* A sub-task whose results cannot go back after its last step was left out
|
|
861
|
+
* stops the plan too (`result_map`), its verdict filed on the call row.
|
|
862
|
+
*/
|
|
863
|
+
settleTests(onStep, opts = {}) {
|
|
864
|
+
const left = [];
|
|
865
|
+
while (this.stepIndex < this.stopAt) {
|
|
866
|
+
const g = this.gate(this.stepIndex, this.walk);
|
|
867
|
+
if (g.kind === "run")
|
|
868
|
+
break;
|
|
869
|
+
if (g.kind === "stop") {
|
|
870
|
+
if (g.info)
|
|
871
|
+
onStep?.(g.info);
|
|
872
|
+
return { stop: g.stop, left };
|
|
873
|
+
}
|
|
874
|
+
onStep?.(g.info);
|
|
875
|
+
const l = this.leftOutOf(g, this.stepIndex);
|
|
876
|
+
left.push(l);
|
|
877
|
+
this.walk.left.push(l);
|
|
878
|
+
const failed = this.leaveAfterSkip(g.until - 1, this.walk, onStep, opts.rethrow === true);
|
|
879
|
+
if (failed)
|
|
880
|
+
return { stop: failed, left };
|
|
881
|
+
this.stepIndex = g.until;
|
|
882
|
+
}
|
|
883
|
+
return { left };
|
|
884
|
+
}
|
|
521
885
|
/**
|
|
522
886
|
* Thread a real tool output into `respParams` and advance.
|
|
523
887
|
*
|
|
@@ -540,7 +904,7 @@ export class ScenarioReplayPlan {
|
|
|
540
904
|
}
|
|
541
905
|
// Leaving a called frame hands its results back to the caller before the
|
|
542
906
|
// caller's next step reads them (R-CALL-29).
|
|
543
|
-
this.
|
|
907
|
+
this.leaveFramesAt(this.stepIndex, this.walk);
|
|
544
908
|
this.stepIndex += 1;
|
|
545
909
|
return derivedKeys;
|
|
546
910
|
}
|
|
@@ -661,9 +1025,19 @@ export class ScenarioReplayPlan {
|
|
|
661
1025
|
// because in steer mode the list *is* the instruction.
|
|
662
1026
|
const planned = this.entries.slice(0, this.stopAt);
|
|
663
1027
|
const args = this.previewArguments(planned, opts?.previewBudgetMs, opts?.previewBodyMs);
|
|
1028
|
+
// A step that runs only under a test says so on its line (AC5): which of
|
|
1029
|
+
// them this request needs is known only as the steps run.
|
|
1030
|
+
const tests = planned.map((e) => this.onlyWhen(e));
|
|
1031
|
+
const anyTest = tests.some((t) => t !== undefined);
|
|
1032
|
+
const onlyWhen = (i) => {
|
|
1033
|
+
const t = tests[i];
|
|
1034
|
+
if (!t)
|
|
1035
|
+
return "";
|
|
1036
|
+
return ` (only when ${t.text ?? "this request needs it"})`;
|
|
1037
|
+
};
|
|
664
1038
|
const listing = (withWhy, argsBefore) => planned.map((e, i) => {
|
|
665
1039
|
const why = withWhy ? clipReasoning(e.step.reasoning) : "";
|
|
666
|
-
const head = ` ${i + 1}. ${e.step.toolName ?? ""}${why ? ` — ${why}` : ""}`;
|
|
1040
|
+
const head = ` ${i + 1}. ${e.step.toolName ?? ""}${onlyWhen(i)}${why ? ` — ${why}` : ""}`;
|
|
667
1041
|
const shown = i < argsBefore ? args[i] : undefined;
|
|
668
1042
|
return shown ? `${head}\n ${shown}` : head;
|
|
669
1043
|
});
|
|
@@ -701,7 +1075,9 @@ export class ScenarioReplayPlan {
|
|
|
701
1075
|
"[BaseInstRunner calculated replay] A known-good tool sequence was recorded",
|
|
702
1076
|
`for this request${intent ? ` (intent: ${intent})` : ""}.`,
|
|
703
1077
|
"",
|
|
704
|
-
|
|
1078
|
+
anyTest
|
|
1079
|
+
? 'It runs these tools, in order, and returns their results. A step marked "only when" runs only when this request needs it, and the results say which ones did not:'
|
|
1080
|
+
: "It runs these tools, in order, and returns their results:",
|
|
705
1081
|
"",
|
|
706
1082
|
...lines,
|
|
707
1083
|
"",
|
|
@@ -720,6 +1096,14 @@ export class ScenarioReplayPlan {
|
|
|
720
1096
|
"",
|
|
721
1097
|
...lines,
|
|
722
1098
|
...(intent ? ["", `Context: ${intent}`] : []),
|
|
1099
|
+
...(anyTest
|
|
1100
|
+
? [
|
|
1101
|
+
"",
|
|
1102
|
+
'A step marked "only when" may not be needed for this request. After each call the',
|
|
1103
|
+
"system says when the next one is not needed: leave that one out and go on with the",
|
|
1104
|
+
"one it names. It supplies arguments only for the steps this request needs.",
|
|
1105
|
+
]
|
|
1106
|
+
: []),
|
|
723
1107
|
"",
|
|
724
1108
|
...(anyArgs
|
|
725
1109
|
? [
|
|
@@ -764,16 +1148,67 @@ export class ScenarioReplayPlan {
|
|
|
764
1148
|
const resp = new Map();
|
|
765
1149
|
if (this.entries[0])
|
|
766
1150
|
resp.set(this.rootFrame().id, { ...this.respParams });
|
|
1151
|
+
// A sub-task the plan's cursor is inside goes on from what it has made so
|
|
1152
|
+
// far: its copy, and the frames and tests the cursor already settled. A
|
|
1153
|
+
// bundle that started such a frame afresh read `{}` for it — its steps'
|
|
1154
|
+
// outputs were lost, and a test there read nothing (AC5 review).
|
|
1155
|
+
for (const id of this.walk.entered) {
|
|
1156
|
+
const frame = this.frameById(id);
|
|
1157
|
+
if (frame)
|
|
1158
|
+
resp.set(id, { ...frame.respParams });
|
|
1159
|
+
}
|
|
1160
|
+
// Its own walk, on those copies: what it enters and decides is not the
|
|
1161
|
+
// live plan's either (AC5).
|
|
1162
|
+
const walk = {
|
|
1163
|
+
resp,
|
|
1164
|
+
entered: new Set(this.walk.entered),
|
|
1165
|
+
tested: new Set(this.walk.tested),
|
|
1166
|
+
passed: new Set(this.walk.passed),
|
|
1167
|
+
left: [],
|
|
1168
|
+
};
|
|
767
1169
|
const entries = [];
|
|
768
1170
|
let executed = 0;
|
|
769
1171
|
let recordedCount = 0;
|
|
770
1172
|
let skipped = 0;
|
|
771
1173
|
let errored = 0;
|
|
1174
|
+
let notTaken = 0;
|
|
772
1175
|
let stopped;
|
|
773
1176
|
const stopOnFailure = opts.stopOnFailure === true;
|
|
774
|
-
|
|
1177
|
+
let flatIndex = this.stepIndex;
|
|
1178
|
+
for (; flatIndex < this.stopAt; flatIndex += 1) {
|
|
775
1179
|
const entry = this.entries[flatIndex];
|
|
776
1180
|
const step = entry.step;
|
|
1181
|
+
// A step this request does not need is left out, and said so in the
|
|
1182
|
+
// bundle; a test that says "the other half" stops the bundle here (AC5).
|
|
1183
|
+
const g = this.gate(flatIndex, walk);
|
|
1184
|
+
if (g.kind === "stop") {
|
|
1185
|
+
if (g.info)
|
|
1186
|
+
onStep?.(g.info);
|
|
1187
|
+
stopped = g.stop;
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
if (g.kind === "skip") {
|
|
1191
|
+
onStep?.(g.info);
|
|
1192
|
+
notTaken += 1;
|
|
1193
|
+
const l = this.leftOutOf(g, flatIndex);
|
|
1194
|
+
walk.left.push(l);
|
|
1195
|
+
this.passOver(l, recordedOutputFor);
|
|
1196
|
+
entries.push({
|
|
1197
|
+
toolName: g.entry.step.toolName ?? g.entry.step.callIntent ?? "a sub-task",
|
|
1198
|
+
input: "",
|
|
1199
|
+
response: "",
|
|
1200
|
+
notNeeded: { text: g.text },
|
|
1201
|
+
});
|
|
1202
|
+
// The sub-tasks that end with the step left out still hand back what
|
|
1203
|
+
// they made; one that cannot stops the bundle (`result_map`).
|
|
1204
|
+
const failed = this.leaveAfterSkip(g.until - 1, walk, onStep, !stopOnFailure);
|
|
1205
|
+
if (failed) {
|
|
1206
|
+
stopped = failed;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
flatIndex = g.until - 1;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
777
1212
|
const before = this.stopBefore(entry, flatIndex, opts.signal, opts.deadline);
|
|
778
1213
|
if (before) {
|
|
779
1214
|
stopped = before;
|
|
@@ -783,7 +1218,7 @@ export class ScenarioReplayPlan {
|
|
|
783
1218
|
let computed;
|
|
784
1219
|
try {
|
|
785
1220
|
computed = reportingStage(onStep, entry, "tool_input_logic", {}, startedAt, () => {
|
|
786
|
-
this.
|
|
1221
|
+
this.enterFramesAt(flatIndex, walk);
|
|
787
1222
|
const respParams = resp.get(entry.frame.id) ?? {};
|
|
788
1223
|
return evalToolInputLogic(step.toolInputLogic ?? "return {};", this.paramsFor(entry), entry.frame.intent, respParams);
|
|
789
1224
|
});
|
|
@@ -849,7 +1284,7 @@ export class ScenarioReplayPlan {
|
|
|
849
1284
|
derivedKeys = Object.keys(derived);
|
|
850
1285
|
Object.assign(resp.get(entry.frame.id) ?? {}, derived);
|
|
851
1286
|
}
|
|
852
|
-
this.
|
|
1287
|
+
this.leaveFramesAt(flatIndex, walk);
|
|
853
1288
|
}, response);
|
|
854
1289
|
}
|
|
855
1290
|
catch (err) {
|
|
@@ -891,15 +1326,19 @@ export class ScenarioReplayPlan {
|
|
|
891
1326
|
return {
|
|
892
1327
|
text: assembleBundle(entries, maxChars, {
|
|
893
1328
|
// A bundle that stops short of the task — in front of a parked step, or
|
|
894
|
-
// at a step that broke — must never say "the work is done".
|
|
895
|
-
|
|
1329
|
+
// at a step that broke — must never say "the work is done". One that
|
|
1330
|
+
// left out a sub-task reaching past the parked step did not stop short.
|
|
1331
|
+
handover: (opts.handover === true && flatIndex <= this.stopAt) || stopped !== undefined,
|
|
896
1332
|
subTask: this.subTask,
|
|
897
1333
|
}),
|
|
898
1334
|
executed,
|
|
899
1335
|
recorded: recordedCount,
|
|
900
1336
|
skipped,
|
|
901
1337
|
errored,
|
|
1338
|
+
notTaken,
|
|
1339
|
+
left: walk.left,
|
|
902
1340
|
stopped,
|
|
1341
|
+
endedAt: flatIndex,
|
|
903
1342
|
};
|
|
904
1343
|
}
|
|
905
1344
|
/**
|
|
@@ -926,11 +1365,31 @@ export class ScenarioReplayPlan {
|
|
|
926
1365
|
let recordedCount = 0;
|
|
927
1366
|
let skipped = 0;
|
|
928
1367
|
let errored = 0;
|
|
1368
|
+
let notTaken = 0;
|
|
1369
|
+
const left = [];
|
|
929
1370
|
let partial = false;
|
|
930
1371
|
let aborted = false;
|
|
931
1372
|
let stopped;
|
|
932
1373
|
const stopOnFailure = opts.stopOnFailure === true;
|
|
933
1374
|
while (!this.isDone()) {
|
|
1375
|
+
// The steps this request does not need are left out, and said so in the
|
|
1376
|
+
// bundle; a test that says "the other half" stops the run here (AC5).
|
|
1377
|
+
// A sub-task whose results cannot go back after its last step was left
|
|
1378
|
+
// out stops the run (`result_map`) — or, without `stopOnFailure`, its
|
|
1379
|
+
// throw propagates, as any logic throw does.
|
|
1380
|
+
const settled = this.settleTests(onStep, { rethrow: !stopOnFailure });
|
|
1381
|
+
left.push(...settled.left);
|
|
1382
|
+
for (const l of settled.left) {
|
|
1383
|
+
this.passOver(l, recordedOutputFor);
|
|
1384
|
+
entries.push({ toolName: l.toolName, input: "", response: "", notNeeded: { text: l.text } });
|
|
1385
|
+
}
|
|
1386
|
+
notTaken += settled.left.length;
|
|
1387
|
+
if (settled.stop) {
|
|
1388
|
+
stopped = settled.stop;
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
if (this.isDone())
|
|
1392
|
+
break;
|
|
934
1393
|
const entry = this.entries[this.stepIndex];
|
|
935
1394
|
const step = entry.step;
|
|
936
1395
|
// Where this step sits in the flat list — what a stop reports. Taken now,
|
|
@@ -1042,8 +1501,9 @@ export class ScenarioReplayPlan {
|
|
|
1042
1501
|
}
|
|
1043
1502
|
entries.push({ toolName: step.toolName ?? "", input: bundleInput(computed), response, recorded });
|
|
1044
1503
|
}
|
|
1045
|
-
// Every planned step ran and the plan ends in front of a parked one
|
|
1046
|
-
|
|
1504
|
+
// Every planned step ran and the plan ends in front of a parked one — not
|
|
1505
|
+
// when a test left out a sub-task that reaches past it (AC5).
|
|
1506
|
+
if (!stopped && this.atStop()) {
|
|
1047
1507
|
// `known_bad_step` here means "in front of the plan's end": the
|
|
1048
1508
|
// controller reads the served reason and names the kind (D8).
|
|
1049
1509
|
stopped = {
|
|
@@ -1061,9 +1521,12 @@ export class ScenarioReplayPlan {
|
|
|
1061
1521
|
recorded: recordedCount,
|
|
1062
1522
|
skipped,
|
|
1063
1523
|
errored,
|
|
1524
|
+
notTaken,
|
|
1525
|
+
left,
|
|
1064
1526
|
partial,
|
|
1065
1527
|
stopped,
|
|
1066
1528
|
aborted: aborted || undefined,
|
|
1529
|
+
endedAt: this.stepIndex,
|
|
1067
1530
|
};
|
|
1068
1531
|
}
|
|
1069
1532
|
/**
|