@basein/runner 0.3.15 → 0.3.16
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 +119 -2
- package/dist/record/recorder.d.ts +39 -4
- package/dist/record/remote-recorder.d.ts +3 -4
- package/dist/record/remote-recorder.js +16 -0
- 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
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
import { parseQualifiedName } from "../control/correlation.js";
|
|
15
15
|
import { isHousekeepingCall } from "../record/housekeeping.js";
|
|
16
16
|
import { REDACT_SCAN_MAX_CHARS, redact, redactJsonText, redactText } from "../record/redact.js";
|
|
17
|
-
import { capText, REPORT_PAYLOAD_MAX_BYTES, serializeCapped } from "../record/truncate.js";
|
|
17
|
+
import { capText, DEFAULT_MAX_PAYLOAD_BYTES, REPORT_PAYLOAD_MAX_BYTES, serializeCapped, } from "../record/truncate.js";
|
|
18
18
|
import { logDetail, logLine, errText } from "../util/log.js";
|
|
19
19
|
import { serviceHealth, serviceTimeoutMs, timeoutSignal, } from "../util/service-health.js";
|
|
20
20
|
import { bashCommandFor } from "./bash-channel.js";
|
|
21
21
|
import { BuiltinNotRunnable, BuiltinTimeout, parseBashAllow, parseBuiltinTools, runBuiltin, } from "./builtins.js";
|
|
22
|
-
import { BASH_CHANNEL_MAX, MAX_REPLAY_REASON, truncate } from "./bundle.js";
|
|
22
|
+
import { BASH_CHANNEL_MAX, MAX_REPLAY_REASON, testWords, truncate } from "./bundle.js";
|
|
23
23
|
import { coverageOf, modeFor, reachOf } from "./coverage.js";
|
|
24
24
|
import { deriveParameters, } from "./derive.js";
|
|
25
25
|
import { buildHandoverNote, MAX_HANDOVER_NOTE, } from "./handover.js";
|
|
@@ -29,7 +29,7 @@ import { PREVIEW_BUDGET_MS, ScenarioReplayPlan, StepRunError, } from "./plan.js"
|
|
|
29
29
|
import { PRICING_VERSION } from "./pricing.js";
|
|
30
30
|
import { hasShellStep, sinkOf, sinkOfPlan, unsafeParameter } from "./safety.js";
|
|
31
31
|
import { SourceRunOutputs } from "./source-run.js";
|
|
32
|
-
import { clampToFrameStart, flattenChain, } from "./flatten.js";
|
|
32
|
+
import { clampToFrameStart, flattenChain, frameEdges, } from "./flatten.js";
|
|
33
33
|
import { blockingTargets } from "./targets.js";
|
|
34
34
|
import { isNoResult, toolResultError } from "./tool-error.js";
|
|
35
35
|
import { OUTCOME_RANK, hasTargets, isReadyScenario, } from "./types.js";
|
|
@@ -171,6 +171,12 @@ const HOST_RACE_MS = 200;
|
|
|
171
171
|
const MAX_STEP_RESULTS = 200;
|
|
172
172
|
/** Per-step error budget. The console shows these inline, under the step. */
|
|
173
173
|
const MAX_STEP_ERROR_CHARS = 500;
|
|
174
|
+
/**
|
|
175
|
+
* All of one plan's kept calls together, in characters. A plan of a few dozen
|
|
176
|
+
* calls keeps them whole; past this the rest keep a report's cut, so one turn
|
|
177
|
+
* cannot hold more than a few megabytes in the recorder.
|
|
178
|
+
*/
|
|
179
|
+
const TURN_CALLS_MAX_CHARS = 8 * 1024 * 1024;
|
|
174
180
|
export class ReplayController {
|
|
175
181
|
enabled;
|
|
176
182
|
work = new ProxyWorkQueue();
|
|
@@ -400,6 +406,13 @@ export class ReplayController {
|
|
|
400
406
|
if (match.withheld === "runner_update_required") {
|
|
401
407
|
return decline("the service hands this runner no plan until it is updated — run `bir update`", "runner_update_required");
|
|
402
408
|
}
|
|
409
|
+
// Held (the service's autocorrect.md AC4): the agent redid this plan's
|
|
410
|
+
// task after its last run, and the service hands the plan out again only
|
|
411
|
+
// when its owner lets it go or uses a correction. Its own code, so the
|
|
412
|
+
// turn is not read as "not ready", whose fix — calculate — is wrong here.
|
|
413
|
+
if (match.withheld === "held") {
|
|
414
|
+
return decline("the service holds this plan: the agent did its task again after its last run", "held");
|
|
415
|
+
}
|
|
403
416
|
// Gate 3 — a ready scenario with steps.
|
|
404
417
|
const scenario = match.scenario;
|
|
405
418
|
if (!match.scenarioId || !isReadyScenario(scenario)) {
|
|
@@ -460,6 +473,15 @@ export class ReplayController {
|
|
|
460
473
|
: "known_bad_first_step");
|
|
461
474
|
}
|
|
462
475
|
const planned = stopAt === undefined ? entries : entries.slice(0, stopAt);
|
|
476
|
+
// Another host's adapter steps through a plan call by call and knows no
|
|
477
|
+
// tests (AC5): asked for a step its test left out, it is refused, reads
|
|
478
|
+
// that as the plan's end, and the model's own calls after it are booked as
|
|
479
|
+
// a broken step — the step counts drift when two steps share a tool. Such
|
|
480
|
+
// a plan is declined for it here, before anything is spent, until the
|
|
481
|
+
// adapter reads `when`.
|
|
482
|
+
if (host && planned.some(underTest)) {
|
|
483
|
+
return decline(`the ${host.app} adapter cannot yet run a plan whose steps run only when a test says so`, "host_tests_unsupported");
|
|
484
|
+
}
|
|
463
485
|
// Gate 6 — tool coverage, over the steps that will actually run. A
|
|
464
486
|
// built-in on `BIR_BUILTIN_TOOLS` counts as reachable: the runner
|
|
465
487
|
// executes it itself (replay/builtins.ts), so a chain that starts with a
|
|
@@ -611,6 +633,10 @@ export class ReplayController {
|
|
|
611
633
|
}
|
|
612
634
|
state.mode = mode;
|
|
613
635
|
state.stepsPlanned = entries.length;
|
|
636
|
+
// The chain this plan was served at: what its own calls are marked with
|
|
637
|
+
// when they are recorded into a redo of the turn (AC2).
|
|
638
|
+
if (typeof scenario.chainRevision === "number")
|
|
639
|
+
state.chainRevision = scenario.chainRevision;
|
|
614
640
|
// Why the plan stops, when a call is what stops it: the note names the
|
|
615
641
|
// sub-task rather than a tool (R-CALL-36).
|
|
616
642
|
state.flatStop =
|
|
@@ -647,6 +673,8 @@ export class ReplayController {
|
|
|
647
673
|
stops: stopLines(entries),
|
|
648
674
|
coverage: coverageOf(entries.map((e) => e.step), state.wrapped, allowServers, builtinTools).join(","),
|
|
649
675
|
tools: entries.map((e) => e.step.toolName ?? "").join(","),
|
|
676
|
+
// Which steps run only under a test, by flat position (AC5).
|
|
677
|
+
tests: testLines(entries),
|
|
650
678
|
});
|
|
651
679
|
return state;
|
|
652
680
|
}
|
|
@@ -963,6 +991,41 @@ export class ReplayController {
|
|
|
963
991
|
return this.concurrentCall(state, plan, toolName);
|
|
964
992
|
}
|
|
965
993
|
}
|
|
994
|
+
// The steps this request does not need are left out before the call is
|
|
995
|
+
// judged, and a test that says "the other half" stops the plan here
|
|
996
|
+
// (AC5). Only a steer plan's cursor moves with the model's calls.
|
|
997
|
+
if (state.mode === "steer") {
|
|
998
|
+
if (this.settleCursor(state, plan, "above") === "stopped") {
|
|
999
|
+
const note = this.takeNote(state);
|
|
1000
|
+
return note ? this.deliverInstead(note, toolName, otherHost) : { kind: "abort" };
|
|
1001
|
+
}
|
|
1002
|
+
if (plan.isDone()) {
|
|
1003
|
+
// What was left of the plan was not needed: it is complete, and this
|
|
1004
|
+
// call is the model's own — unless a parked step is next, whose
|
|
1005
|
+
// hand-over is told in place of this call.
|
|
1006
|
+
this.finishSteer(state, plan);
|
|
1007
|
+
const note = this.takeNote(state);
|
|
1008
|
+
return note ? this.deliverInstead(note, toolName, otherHost) : { kind: "passthrough" };
|
|
1009
|
+
}
|
|
1010
|
+
// A call to a step that was just left out is told so in its place —
|
|
1011
|
+
// even when the note already went out, which a model may not have read:
|
|
1012
|
+
// read as a divergence, it would end the plan. Twice at most, so a model
|
|
1013
|
+
// that means the call for itself is not refused for ever.
|
|
1014
|
+
const skip = state.skipNote;
|
|
1015
|
+
if (skip && toolName !== plan.expectedTool() && skip.tools.has(toolName) && (skip.refused ?? 0) < 2) {
|
|
1016
|
+
skip.delivered = true;
|
|
1017
|
+
skip.refused = (skip.refused ?? 0) + 1;
|
|
1018
|
+
return this.deliverInstead(skip.text, toolName, otherHost);
|
|
1019
|
+
}
|
|
1020
|
+
// A call to the next step's tool: a note the model has read is done
|
|
1021
|
+
// with. One it has not read yet waits for the pin guard (the control
|
|
1022
|
+
// server's `pinTaken` / `skipNoteFor`): when the left-out step and the
|
|
1023
|
+
// next one share a tool, the call may be the left-out one, made because
|
|
1024
|
+
// the note was late — its arguments then fail the guard, and the note
|
|
1025
|
+
// goes in its place rather than the call running as written.
|
|
1026
|
+
if (skip && toolName === plan.expectedTool() && skip.delivered)
|
|
1027
|
+
state.skipNote = undefined;
|
|
1028
|
+
}
|
|
966
1029
|
// A direct plan does not steer individual calls: the model was asked for one
|
|
967
1030
|
// tool call and made a different one. That is divergence.
|
|
968
1031
|
const expected = plan.expectedTool();
|
|
@@ -1353,6 +1416,11 @@ export class ReplayController {
|
|
|
1353
1416
|
ms: Date.now() - pin.pinnedAt,
|
|
1354
1417
|
});
|
|
1355
1418
|
}
|
|
1419
|
+
// With this output in, the next steps' tests can be decided: the ones this
|
|
1420
|
+
// request does not need are left out, and one that says "the other half"
|
|
1421
|
+
// hands over here, its note on this very PostToolUse (AC5).
|
|
1422
|
+
if (this.settleCursor(state, plan, "above") === "stopped")
|
|
1423
|
+
return false;
|
|
1356
1424
|
const done = plan.isDone();
|
|
1357
1425
|
logDetail("replay.thread", {
|
|
1358
1426
|
n: pin.stepIndex,
|
|
@@ -1360,7 +1428,17 @@ export class ReplayController {
|
|
|
1360
1428
|
emitted: derivedKeys.join(",") || undefined,
|
|
1361
1429
|
done,
|
|
1362
1430
|
});
|
|
1363
|
-
if (done
|
|
1431
|
+
if (done)
|
|
1432
|
+
this.finishSteer(state, plan);
|
|
1433
|
+
return done;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* A steer plan whose cursor reached its end: in front of a parked step (or a
|
|
1437
|
+
* call the service could not answer), which hands over; otherwise it ran to
|
|
1438
|
+
* its end — and from here every call is the model's own.
|
|
1439
|
+
*/
|
|
1440
|
+
finishSteer(state, plan) {
|
|
1441
|
+
if (plan.atStop()) {
|
|
1364
1442
|
// Every planned step ran, and the next one is parked (fallbk.md D3) — or
|
|
1365
1443
|
// is a call the service could not answer (segmented.md R-CALL-29).
|
|
1366
1444
|
const parked = plan.stopStep();
|
|
@@ -1371,24 +1449,155 @@ export class ReplayController {
|
|
|
1371
1449
|
position: plan.stepCount,
|
|
1372
1450
|
delivery: "above",
|
|
1373
1451
|
});
|
|
1374
|
-
return
|
|
1452
|
+
return;
|
|
1375
1453
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1454
|
+
this.upgrade(state, "steered_full");
|
|
1455
|
+
state.ranToEnd = true;
|
|
1456
|
+
// A completing retire: the last step threaded and the plan's work is over
|
|
1457
|
+
// (segmented.md R-MONEY-3). Everything the agent spends after this is the
|
|
1458
|
+
// agent's own, not this plan's.
|
|
1459
|
+
this.retire(state, undefined, true);
|
|
1460
|
+
// The steps that ran: a step its test left out is no step done (AC5).
|
|
1461
|
+
const before = this.ranBefore(plan, plan.stepCount);
|
|
1462
|
+
logLine("replay.done", {
|
|
1463
|
+
scenario: state.scenarioId ?? undefined,
|
|
1464
|
+
mode: state.mode,
|
|
1465
|
+
steps: `${before.left.length > 0 ? before.ran.length : plan.stepCount}/${plan.stepCount}`,
|
|
1466
|
+
notTaken: before.left.length || undefined,
|
|
1467
|
+
outcome: state.outcome,
|
|
1468
|
+
ms: Date.now() - state.armedAt,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Move a plan's own cursor past the steps this request does not need, and
|
|
1473
|
+
* act on what its tests decide (the service's autocorrect.md AC5). Every
|
|
1474
|
+
* verdict is recorded as it is made. `stopped` means the plan handed over —
|
|
1475
|
+
* the other half, a test that could not answer, or no step needed at all —
|
|
1476
|
+
* and its note waits in {@link takeNote}. Otherwise the steps left out are
|
|
1477
|
+
* named in a note for the model ({@link takeSkipNote}), and the caller goes
|
|
1478
|
+
* on: a step is due, or the plan is done.
|
|
1479
|
+
*
|
|
1480
|
+
* Nothing at all for a plan without tests, which behaves exactly as before.
|
|
1481
|
+
*/
|
|
1482
|
+
settleCursor(state, plan, delivery) {
|
|
1483
|
+
if (!plan.hasTests())
|
|
1484
|
+
return "ready";
|
|
1485
|
+
// Never throws: a sub-task whose results cannot go back after its last
|
|
1486
|
+
// step was left out is a stop like any other (`result_map`).
|
|
1487
|
+
const settled = plan.settleTests(this.observeStep(state));
|
|
1488
|
+
// The recorded outputs of the steps left out are passed over, so a
|
|
1489
|
+
// divergence's bundle later stands each step's own in (source-run.ts).
|
|
1490
|
+
const source = state.sourceRun;
|
|
1491
|
+
if (source) {
|
|
1492
|
+
for (const l of settled.left) {
|
|
1493
|
+
for (let i = l.flatIndex; i < l.until; i += 1) {
|
|
1494
|
+
const entry = plan.allEntries()[i];
|
|
1495
|
+
if (entry)
|
|
1496
|
+
source.leftOut(entry);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (settled.stop) {
|
|
1501
|
+
const stop = settled.stop;
|
|
1502
|
+
const position = stop.flatIndex ?? plan.currentStepIndex;
|
|
1503
|
+
this.handOver(state, {
|
|
1504
|
+
kind: stop.kind,
|
|
1505
|
+
step: plan.allSteps()[position],
|
|
1506
|
+
entry: plan.allEntries()[position],
|
|
1507
|
+
position,
|
|
1508
|
+
error: stop.error,
|
|
1509
|
+
delivery,
|
|
1510
|
+
stopReason: stop.reason,
|
|
1511
|
+
whenText: stop.whenText,
|
|
1512
|
+
subTask: stop.subTask,
|
|
1389
1513
|
});
|
|
1514
|
+
return "stopped";
|
|
1515
|
+
}
|
|
1516
|
+
if (settled.left.length === 0)
|
|
1517
|
+
return "ready";
|
|
1518
|
+
if (plan.isDone() && !plan.atStop() && state.stepsPinned === 0) {
|
|
1519
|
+
// Every step was left out: none of the plan's work is this request's,
|
|
1520
|
+
// and the model does the task (AC5). Reported like any hand-over before
|
|
1521
|
+
// a step ran.
|
|
1522
|
+
this.handOver(state, {
|
|
1523
|
+
kind: "other_branch",
|
|
1524
|
+
step: plan.allSteps()[0],
|
|
1525
|
+
entry: plan.allEntries()[0],
|
|
1526
|
+
position: 0,
|
|
1527
|
+
delivery,
|
|
1528
|
+
noneNeeded: true,
|
|
1529
|
+
});
|
|
1530
|
+
return "stopped";
|
|
1390
1531
|
}
|
|
1391
|
-
|
|
1532
|
+
state.skipNote = {
|
|
1533
|
+
text: this.skipNoteText(state, plan, settled.left),
|
|
1534
|
+
// Every tool of a sub-task left out whole, not its call's intent: a call
|
|
1535
|
+
// the model makes to any of them is one to a step left out.
|
|
1536
|
+
tools: new Set(settled.left.flatMap((l) => (l.tools.length > 0 ? l.tools : [l.toolName]))),
|
|
1537
|
+
delivered: false,
|
|
1538
|
+
complete: plan.isDone() && !plan.atStop(),
|
|
1539
|
+
};
|
|
1540
|
+
return "ready";
|
|
1541
|
+
}
|
|
1542
|
+
/** What the model is told about steps a test left out (AC5). */
|
|
1543
|
+
skipNoteText(state, plan, left) {
|
|
1544
|
+
const named = left.map((l) => `step ${l.flatIndex + 1} (${l.toolName})`);
|
|
1545
|
+
const texts = [...new Set(left.map((l) => l.text).filter((t) => !!t))];
|
|
1546
|
+
const why = texts.length === 1 ? ` — only for when ${texts[0]}` : "";
|
|
1547
|
+
const what = left.length === 1 ? `Not needed for this request: ${named[0]}${why}.` : `Not needed for this request: ${named.join(", ")}${why}.`;
|
|
1548
|
+
let next;
|
|
1549
|
+
if (plan.isDone() && !plan.atStop()) {
|
|
1550
|
+
next =
|
|
1551
|
+
state.armedBy === "intent"
|
|
1552
|
+
? "The sequence is complete: continue your task, calling tools as needed."
|
|
1553
|
+
: "The sequence is complete: answer the user's request from the tool results.";
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
const entry = plan.currentEntry();
|
|
1557
|
+
next = `Leave ${left.length === 1 ? "it" : "them"} out, and call step ${plan.currentStepIndex + 1}${entry?.step.toolName ? ` (${entry.step.toolName})` : ""} next.`;
|
|
1558
|
+
}
|
|
1559
|
+
return `[BaseInstRunner calculated replay] ${what} ${next}`;
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* The note naming the steps a test just left out, claimed for delivery on a
|
|
1563
|
+
* `PostToolUse` (AC5). Returned once; undefined when there is none.
|
|
1564
|
+
*/
|
|
1565
|
+
takeSkipNote(state) {
|
|
1566
|
+
const skip = state?.skipNote;
|
|
1567
|
+
if (!skip || skip.delivered)
|
|
1568
|
+
return undefined;
|
|
1569
|
+
skip.delivered = true;
|
|
1570
|
+
return skip.text;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* The same note, for a call the model is about to make to one of the steps
|
|
1574
|
+
* it names — when the note has not reached the model yet. The control server
|
|
1575
|
+
* gives it in place of that call, whether the plan is still steering or has
|
|
1576
|
+
* just finished: a step left out must never run because the note was late.
|
|
1577
|
+
*/
|
|
1578
|
+
skipNoteFor(state, toolName) {
|
|
1579
|
+
const skip = state?.skipNote;
|
|
1580
|
+
if (!skip || skip.delivered || !skip.tools.has(toolName))
|
|
1581
|
+
return undefined;
|
|
1582
|
+
skip.delivered = true;
|
|
1583
|
+
return skip.text;
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* The pin guard took a call to the next step as that step: a note still
|
|
1587
|
+
* waiting to say that a step before it was left out has nothing left to
|
|
1588
|
+
* stop, and goes — as it went at the call before the guard existed.
|
|
1589
|
+
*/
|
|
1590
|
+
pinTaken(state) {
|
|
1591
|
+
if (state?.skipNote && !state.skipNote.complete)
|
|
1592
|
+
state.skipNote = undefined;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* The plan's own calls of the turn, whole, in the order it made them — for
|
|
1596
|
+
* the turn's newest run when the agent redoes the task (AC2). Empty unless
|
|
1597
|
+
* the control server asked for them to be kept ({@link ReplayState.keepCalls}).
|
|
1598
|
+
*/
|
|
1599
|
+
turnCallsOf(state) {
|
|
1600
|
+
return state?.turnCalls ? [...state.turnCalls.values()] : [];
|
|
1392
1601
|
}
|
|
1393
1602
|
/**
|
|
1394
1603
|
* The result of a call pinned under a plan that has since handed over
|
|
@@ -1619,7 +1828,12 @@ export class ReplayController {
|
|
|
1619
1828
|
ran: live ? undefined : 0,
|
|
1620
1829
|
// An output-logic failure stops *after* its step's tool ran: the step
|
|
1621
1830
|
// is done, its result is in the bundle, and it must not be run again.
|
|
1622
|
-
|
|
1831
|
+
// Not a sub-task's result mapping after its last step was left out
|
|
1832
|
+
// (`result_map`): that step did not run (AC5).
|
|
1833
|
+
ranThisStep: stop.stage === "tool_output_logic" && stop.reason !== "result_map",
|
|
1834
|
+
// The other half (AC5): what the steps from here are for.
|
|
1835
|
+
whenText: stop.whenText,
|
|
1836
|
+
subTask: stop.subTask,
|
|
1623
1837
|
});
|
|
1624
1838
|
if (stop.reason === "cancelled") {
|
|
1625
1839
|
// The caller is gone and reads nothing. The note stays undelivered: a
|
|
@@ -1650,6 +1864,20 @@ export class ReplayController {
|
|
|
1650
1864
|
partial: result.partial,
|
|
1651
1865
|
};
|
|
1652
1866
|
}
|
|
1867
|
+
if (result.executed === 0 && result.recorded === 0 && result.notTaken > 0) {
|
|
1868
|
+
// Every step was left out by its test: none of the plan's work is this
|
|
1869
|
+
// request's (AC5). The model does the task, told so.
|
|
1870
|
+
this.handOver(state, {
|
|
1871
|
+
kind: "other_branch",
|
|
1872
|
+
step: plan.allSteps()[0],
|
|
1873
|
+
entry: plan.allEntries()[0],
|
|
1874
|
+
position: 0,
|
|
1875
|
+
delivery: "none",
|
|
1876
|
+
ran: 0,
|
|
1877
|
+
noneNeeded: true,
|
|
1878
|
+
});
|
|
1879
|
+
return handedOver(this.takeNote(state) ?? state.handover?.note) ?? { ok: false, why: "" };
|
|
1880
|
+
}
|
|
1653
1881
|
if (result.executed === 0) {
|
|
1654
1882
|
// Every step stood on its recorded output (BIR_REPLAY_RECORDED_OUTPUTS):
|
|
1655
1883
|
// nothing happened here, and an answer built on last week's outputs is
|
|
@@ -1725,12 +1953,15 @@ export class ReplayController {
|
|
|
1725
1953
|
const steps = this.stepResultsOf(state);
|
|
1726
1954
|
const isBaseline = state.outcome === "not_steered" || state.outcome === "failed";
|
|
1727
1955
|
// A costless decline is noise on both sides — *unless* it says something the
|
|
1728
|
-
// recording page cannot learn any other way: a step that actually broke,
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
1956
|
+
// recording page cannot learn any other way: a step that actually broke, the
|
|
1957
|
+
// gate that declined, or a plan that stood in front of the other half of
|
|
1958
|
+
// its task (AC5), which the service takes the agent's work after as its
|
|
1959
|
+
// evidence of that half. The service accepts a costless report that carries
|
|
1960
|
+
// any of them (its errorshandling.md).
|
|
1731
1961
|
if (isBaseline &&
|
|
1732
1962
|
total <= 0 &&
|
|
1733
1963
|
!state.declined &&
|
|
1964
|
+
state.handover?.kind !== "other_branch" &&
|
|
1734
1965
|
!steps?.some((s) => s.status === "failed")) {
|
|
1735
1966
|
return undefined;
|
|
1736
1967
|
}
|
|
@@ -1771,9 +2002,18 @@ export class ReplayController {
|
|
|
1771
2002
|
declined: state.declined,
|
|
1772
2003
|
// Where the plan handed the task to the model (fallbk.md). Only on
|
|
1773
2004
|
// `fell_back`: a hand-over before any step ran is `failed`, and a parked
|
|
1774
|
-
// step found after a divergence is still a divergence.
|
|
1775
|
-
|
|
1776
|
-
|
|
2005
|
+
// step found after a divergence is still a divergence. Except the other
|
|
2006
|
+
// half of an "if … otherwise" (AC5), named whatever the outcome: the
|
|
2007
|
+
// agent's work after it is the service's evidence of that half, whether
|
|
2008
|
+
// or not a step ran first.
|
|
2009
|
+
fallbackStepIndex: state.outcome === "fell_back" || state.handover?.kind === "other_branch"
|
|
2010
|
+
? state.handover?.stepIndex
|
|
2011
|
+
: undefined,
|
|
2012
|
+
fallbackKind: state.outcome === "fell_back" || state.handover?.kind === "other_branch"
|
|
2013
|
+
? state.handover?.kind
|
|
2014
|
+
: undefined,
|
|
2015
|
+
// The agent did not take the plan's result and redid the task (AC1).
|
|
2016
|
+
...(d.redoneBy ? { redoneBy: d.redoneBy } : {}),
|
|
1777
2017
|
// Segments of this plan's recording that armed later in the turn
|
|
1778
2018
|
// (segmented.md R-MONEY-4): their share comes off this plan's baseline.
|
|
1779
2019
|
sharedWith: state.sharedWith.length > 0 ? [...state.sharedWith] : undefined,
|
|
@@ -2113,9 +2353,11 @@ export class ReplayController {
|
|
|
2113
2353
|
// Same rule as `runArmed`: a recovered chain whose tools errored is
|
|
2114
2354
|
// `failed`, and so is one that stopped at a step that broke — its logic
|
|
2115
2355
|
// threw, or it was sent and failed or never answered. A step that simply
|
|
2116
|
-
// could not be sent from here, or the hook's clock, is still a divergence
|
|
2356
|
+
// could not be sent from here, or the hook's clock, is still a divergence;
|
|
2357
|
+
// so is a stop in front of the other half (AC5), which broke nothing.
|
|
2117
2358
|
const broke = composed.errored > 0 ||
|
|
2118
2359
|
(stop !== undefined &&
|
|
2360
|
+
stop.kind !== "other_branch" &&
|
|
2119
2361
|
stop.reason !== "not_run" &&
|
|
2120
2362
|
stop.reason !== "budget" &&
|
|
2121
2363
|
stop.reason !== "cancelled");
|
|
@@ -2125,7 +2367,12 @@ export class ReplayController {
|
|
|
2125
2367
|
// delivered nothing: no saving, a baseline sample, as `handOver` books it.
|
|
2126
2368
|
// Recorded stand-ins are no work here (BIR_REPLAY_RECORDED_OUTPUTS).
|
|
2127
2369
|
const didWork = composed.executed > 0 || state.stepsPinned > 0;
|
|
2128
|
-
|
|
2370
|
+
// A bundle that stops in front of the other half (AC5) is a hand-over,
|
|
2371
|
+
// however the plan got there: booked as the contract books one — `fell_back`
|
|
2372
|
+
// when a step did its work — so the service takes the agent's work after
|
|
2373
|
+
// it as that half's evidence. Booked `diverged`, it never did.
|
|
2374
|
+
const otherHalf = stop?.kind === "other_branch";
|
|
2375
|
+
this.upgrade(state, broke || !didWork ? "failed" : otherHalf ? "fell_back" : "diverged");
|
|
2129
2376
|
state.stepsPinned += composed.executed;
|
|
2130
2377
|
// The headline number of this whole design: recovering from a divergence
|
|
2131
2378
|
// costs nothing, because the proxies were already connected.
|
|
@@ -2153,20 +2400,34 @@ export class ReplayController {
|
|
|
2153
2400
|
// ahead — unless a step was *sent* and never answered. That one may have
|
|
2154
2401
|
// done its work, and the model must know before it does the task itself.
|
|
2155
2402
|
if (stop?.reason === "uncertain" && stop.flatIndex !== undefined) {
|
|
2156
|
-
const note = this.stopNote(state, plan, stop, "none");
|
|
2403
|
+
const note = this.stopNote(state, plan, stop, "none", composed.left);
|
|
2157
2404
|
return this.deliverInstead(note, toolName, state.host !== undefined);
|
|
2158
2405
|
}
|
|
2406
|
+
// The rest is the other half (AC5): the model is told so, in place of
|
|
2407
|
+
// its call, before it does any of the plan's remaining steps.
|
|
2408
|
+
if (stop?.kind === "other_branch" && stop.flatIndex !== undefined) {
|
|
2409
|
+
const note = this.stopNote(state, plan, stop, "none", composed.left);
|
|
2410
|
+
return this.deliverInstead(note, toolName, state.host !== undefined);
|
|
2411
|
+
}
|
|
2412
|
+
// Every remaining step was left out by its test: the steps the session
|
|
2413
|
+
// ran were the whole of the plan this request needs, and it ran to its
|
|
2414
|
+
// end (newestRun.md N7 reads that).
|
|
2415
|
+
const ranOut = !stop && composed.endedAt >= plan.stepCount;
|
|
2416
|
+
const parkedNext = plan.stopsEarly() && composed.endedAt === plan.stepCount;
|
|
2417
|
+
if (ranOut && !parkedNext && composed.notTaken > 0 && state.stepsPinned > 0)
|
|
2418
|
+
state.ranToEnd = true;
|
|
2159
2419
|
return { kind: "abort" };
|
|
2160
2420
|
}
|
|
2161
2421
|
// The bundle stops short of the task: say so ahead of it — which step, and
|
|
2162
2422
|
// what may be assumed about it — and the model carries on from there. The
|
|
2163
|
-
// outcome stays as judged above, so the report carries no fallback fields
|
|
2423
|
+
// outcome stays as judged above, so the report carries no fallback fields
|
|
2424
|
+
// (the other half of an "if … otherwise" excepted: see `buildReport`).
|
|
2164
2425
|
let text = composed.text;
|
|
2165
|
-
if (stop && stop.kind === "step_failed" && stop.flatIndex !== undefined) {
|
|
2166
|
-
const note = this.stopNote(state, plan, stop, "below");
|
|
2426
|
+
if (stop && (stop.kind === "step_failed" || stop.kind === "other_branch") && stop.flatIndex !== undefined) {
|
|
2427
|
+
const note = this.stopNote(state, plan, stop, "below", composed.left);
|
|
2167
2428
|
text = `${note}\n\n${composed.text}`;
|
|
2168
2429
|
}
|
|
2169
|
-
else if (plan.stopsEarly()) {
|
|
2430
|
+
else if (plan.stopsEarly() && composed.endedAt === plan.stepCount) {
|
|
2170
2431
|
const parked = plan.stopStep();
|
|
2171
2432
|
const stopKind = this.stopKindOf(state, plan.stopEntry());
|
|
2172
2433
|
state.handover = {
|
|
@@ -2180,6 +2441,8 @@ export class ReplayController {
|
|
|
2180
2441
|
position: plan.stepCount,
|
|
2181
2442
|
delivery: "below",
|
|
2182
2443
|
executedAbove: plan.currentStepIndex,
|
|
2444
|
+
// The steps the bundle left out ran no more than the cursor's (AC5).
|
|
2445
|
+
leftOut: composed.left,
|
|
2183
2446
|
}),
|
|
2184
2447
|
delivered: true,
|
|
2185
2448
|
};
|
|
@@ -2202,7 +2465,9 @@ export class ReplayController {
|
|
|
2202
2465
|
* plan's hand-over (already delivered — it goes out with the bundle), so the
|
|
2203
2466
|
* model's own work from here is recorded as a fragment.
|
|
2204
2467
|
*/
|
|
2205
|
-
stopNote(state, plan, stop, delivery
|
|
2468
|
+
stopNote(state, plan, stop, delivery,
|
|
2469
|
+
/** The steps the bundle left out (AC5): no step that ran. */
|
|
2470
|
+
left = []) {
|
|
2206
2471
|
const position = stop.flatIndex ?? plan.currentStepIndex;
|
|
2207
2472
|
const step = plan.allSteps()[position];
|
|
2208
2473
|
const entry = plan.allEntries()[position];
|
|
@@ -2219,8 +2484,12 @@ export class ReplayController {
|
|
|
2219
2484
|
// steps that ran; "no step ran" would invite the model to run them again.
|
|
2220
2485
|
ran: delivery === "none" ? plan.currentStepIndex : undefined,
|
|
2221
2486
|
executedAbove: plan.currentStepIndex,
|
|
2222
|
-
// An output-logic failure stops after its step's tool ran (plan.ts)
|
|
2223
|
-
|
|
2487
|
+
// An output-logic failure stops after its step's tool ran (plan.ts) —
|
|
2488
|
+
// not a sub-task's result mapping after its last step was left out.
|
|
2489
|
+
ranThisStep: stop.stage === "tool_output_logic" && stop.reason !== "result_map",
|
|
2490
|
+
whenText: stop.whenText,
|
|
2491
|
+
subTask: stop.subTask,
|
|
2492
|
+
leftOut: left,
|
|
2224
2493
|
});
|
|
2225
2494
|
state.handover ??= {
|
|
2226
2495
|
kind: stop.kind,
|
|
@@ -2267,21 +2536,25 @@ export class ReplayController {
|
|
|
2267
2536
|
segment: state.flatStop?.segmentId ?? undefined,
|
|
2268
2537
|
ran,
|
|
2269
2538
|
outcome: state.outcome,
|
|
2270
|
-
why: h.kind === "
|
|
2271
|
-
?
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2539
|
+
why: h.kind === "other_branch"
|
|
2540
|
+
? h.noneNeeded
|
|
2541
|
+
? "no step of the plan was needed for this request — the agent does the task"
|
|
2542
|
+
: `this request is the other half: the steps from here are only for when ${h.whenText ?? "their test says so"} — the agent continues`
|
|
2543
|
+
: h.kind === "unusable_call"
|
|
2544
|
+
? "a sub-task of this chain cannot run — the agent does that part itself"
|
|
2545
|
+
: h.kind === "known_bad_step"
|
|
2546
|
+
? "the next step has failed too often — the agent continues from here"
|
|
2547
|
+
: h.kind === "nondeterministic_step"
|
|
2548
|
+
? "the next step needs a judgement the plan cannot compute — the agent makes it"
|
|
2549
|
+
: h.stopReason === "concurrent"
|
|
2550
|
+
? "a second call arrived while the pinned step was still running — the agent continues from here"
|
|
2551
|
+
: h.stopReason === "budget" || h.stopReason === "cancelled"
|
|
2552
|
+
? "the run was cut short before this step — the agent continues from here"
|
|
2553
|
+
: h.stopReason === "host_rejected" || h.stopReason === "host_edited"
|
|
2554
|
+
? "the host stopped this step itself — no fault of the plan; the agent continues from here"
|
|
2555
|
+
: h.stopReason === "not_run" || h.stopReason === "uncertain"
|
|
2556
|
+
? "a step produced no result here — the agent continues from here"
|
|
2557
|
+
: "a step broke — the agent continues from here",
|
|
2285
2558
|
});
|
|
2286
2559
|
}
|
|
2287
2560
|
/**
|
|
@@ -2295,12 +2568,46 @@ export class ReplayController {
|
|
|
2295
2568
|
return "nondeterministic_step";
|
|
2296
2569
|
return state.flatStop ? "unusable_call" : "known_bad_step";
|
|
2297
2570
|
}
|
|
2571
|
+
/**
|
|
2572
|
+
* The steps before flat position `bound` that ran, 1-based, and the ones
|
|
2573
|
+
* their tests left out (AC5) — the plan cursor's own, and a divergence
|
|
2574
|
+
* bundle's (`extra`). `left` is empty when no step was left out; `ran` is
|
|
2575
|
+
* then not filled, since every step before `bound` ran.
|
|
2576
|
+
*/
|
|
2577
|
+
ranBefore(plan, bound, extra = []) {
|
|
2578
|
+
const left = [...(plan?.leftOut() ?? []), ...extra].filter((l) => l.flatIndex < bound);
|
|
2579
|
+
if (left.length === 0)
|
|
2580
|
+
return { ran: [], left };
|
|
2581
|
+
const out = new Set();
|
|
2582
|
+
for (const l of left)
|
|
2583
|
+
for (let i = l.flatIndex; i < Math.min(l.until, bound); i += 1)
|
|
2584
|
+
out.add(i);
|
|
2585
|
+
const ran = [];
|
|
2586
|
+
for (let i = 0; i < bound; i += 1)
|
|
2587
|
+
if (!out.has(i))
|
|
2588
|
+
ran.push(i + 1);
|
|
2589
|
+
return { ran, left };
|
|
2590
|
+
}
|
|
2298
2591
|
noteFor(state, h) {
|
|
2299
2592
|
const plan = state.plan;
|
|
2300
|
-
|
|
2593
|
+
// The steps before the stop — or the bound a caller gives — less the ones
|
|
2594
|
+
// their tests left out (AC5): a note must never say a step ran that did not.
|
|
2595
|
+
// A `result_map` stop rests on the step left out, which is then named so.
|
|
2596
|
+
const bound = h.ran ?? (h.ranThisStep || h.stopReason === "result_map" ? h.position + 1 : h.position);
|
|
2597
|
+
const before = this.ranBefore(plan, bound, h.leftOut);
|
|
2598
|
+
const skipped = before.left.length > 0;
|
|
2599
|
+
const executed = skipped ? before.ran.length : bound;
|
|
2600
|
+
const executedAbove = h.executedAbove === undefined || !skipped
|
|
2601
|
+
? h.executedAbove
|
|
2602
|
+
: before.ran.filter((n) => n <= h.executedAbove).length;
|
|
2301
2603
|
// A step its host stopped was dealt with there — refused, answered, or
|
|
2302
2604
|
// run with a person's arguments — so it is not among what is left to do.
|
|
2303
|
-
|
|
2605
|
+
// Nor is the step a test left out, after which a sub-task's results could
|
|
2606
|
+
// not go back (`result_map`, AC5).
|
|
2607
|
+
const handled = h.ranThisStep === true ||
|
|
2608
|
+
h.stopReason === "host_rejected" ||
|
|
2609
|
+
h.stopReason === "host_edited" ||
|
|
2610
|
+
h.stopReason === "result_map";
|
|
2304
2611
|
let params;
|
|
2305
2612
|
try {
|
|
2306
2613
|
params = plan?.parameters;
|
|
@@ -2327,13 +2634,30 @@ export class ReplayController {
|
|
|
2327
2634
|
ownerScenarioId: h.entry && h.entry.depth > 0 ? h.entry.scenarioId : undefined,
|
|
2328
2635
|
inSubTask: h.entry && h.entry.depth > 0 ? (h.entry.frame.callIntent ?? null) : null,
|
|
2329
2636
|
executed: Math.max(0, Math.min(executed, plan?.totalSteps ?? executed)),
|
|
2330
|
-
|
|
2637
|
+
...(skipped
|
|
2638
|
+
? {
|
|
2639
|
+
ranSteps: before.ran,
|
|
2640
|
+
leftOut: before.left.map((l) => ({
|
|
2641
|
+
from: l.flatIndex + 1,
|
|
2642
|
+
to: Math.min(l.until, bound),
|
|
2643
|
+
toolName: l.toolName,
|
|
2644
|
+
text: l.text,
|
|
2645
|
+
})),
|
|
2646
|
+
}
|
|
2647
|
+
: {}),
|
|
2648
|
+
executedAbove,
|
|
2331
2649
|
inFlight: h.inFlight,
|
|
2332
2650
|
ranThisStep: h.ranThisStep,
|
|
2333
2651
|
totalSteps: plan?.totalSteps ?? state.stepsPlanned,
|
|
2334
|
-
|
|
2652
|
+
// A step that runs only under a test is marked so: the list must not
|
|
2653
|
+
// read as steps this request needs (AC5).
|
|
2654
|
+
remainingTools: plan ? plan.remainingFrom(handled ? h.position + 1 : h.position) : [],
|
|
2335
2655
|
params,
|
|
2336
2656
|
delivery: h.delivery,
|
|
2657
|
+
whenText: h.whenText ?? null,
|
|
2658
|
+
subTask: h.subTask ?? null,
|
|
2659
|
+
// "None was needed" is the note's own wording for the end of the plan.
|
|
2660
|
+
...(h.noneNeeded ? { stepIndex: plan?.totalSteps ?? state.stepsPlanned } : {}),
|
|
2337
2661
|
});
|
|
2338
2662
|
}
|
|
2339
2663
|
/**
|
|
@@ -2449,7 +2773,13 @@ export class ReplayController {
|
|
|
2449
2773
|
if (!force && !this.recordedFallback)
|
|
2450
2774
|
return undefined;
|
|
2451
2775
|
const source = state.sourceRun;
|
|
2452
|
-
|
|
2776
|
+
if (!source)
|
|
2777
|
+
return undefined;
|
|
2778
|
+
// A step a test left out passes over its recorded output, unless a
|
|
2779
|
+
// correction added it (AC5, source-run.ts).
|
|
2780
|
+
return Object.assign((entry) => source.outputFor(entry), {
|
|
2781
|
+
leftOut: (entry) => source.leftOut(entry),
|
|
2782
|
+
});
|
|
2453
2783
|
}
|
|
2454
2784
|
/**
|
|
2455
2785
|
* Wait for a plan's parameters within `budgetMs`, then judge whether the plan
|
|
@@ -2631,9 +2961,23 @@ export class ReplayController {
|
|
|
2631
2961
|
// step of the same number — the very mix-up the payloads are there to end.
|
|
2632
2962
|
const own = !info.entry || info.entry.depth === 0;
|
|
2633
2963
|
// A step whose input logic threw never had an input: the `{}` the plan
|
|
2634
|
-
// reports there is a placeholder, not a call with no arguments.
|
|
2635
|
-
|
|
2636
|
-
const
|
|
2964
|
+
// reports there is a placeholder, not a call with no arguments. Nor had a
|
|
2965
|
+
// step its test left out or stopped in front of (AC5).
|
|
2966
|
+
const noCall = info.stage === "tool_input_logic" || info.stage === "when_logic" || status === "not_taken";
|
|
2967
|
+
const sent = own && !noCall ? info.input : undefined;
|
|
2968
|
+
const received = own && !noCall ? info.output : undefined;
|
|
2969
|
+
// The call, kept whole for the turn's newest run (AC2): only one that
|
|
2970
|
+
// really went out — it answered, or it answered with an error.
|
|
2971
|
+
if (state.keepCalls &&
|
|
2972
|
+
!noCall &&
|
|
2973
|
+
info.input !== undefined &&
|
|
2974
|
+
(status === "ok" || (status === "failed" && info.stage === "tool_call"))) {
|
|
2975
|
+
// By frame, not by scenario: a chain that calls the same sub-task twice
|
|
2976
|
+
// makes two calls of each of its steps, and both are the turn's (AC5
|
|
2977
|
+
// review). A frame is one call of one sub-task.
|
|
2978
|
+
const callKey = info.entry ? `${info.entry.frame.id}#${info.entry.stepIndex}` : key;
|
|
2979
|
+
this.keepCall(state, callKey, info, status === "failed");
|
|
2980
|
+
}
|
|
2637
2981
|
state.stepResults.set(key, {
|
|
2638
2982
|
stepIndex: info.step.stepIndex,
|
|
2639
2983
|
toolName: info.step.toolName ?? "",
|
|
@@ -2654,6 +2998,42 @@ export class ReplayController {
|
|
|
2654
2998
|
chainRevision: info.entry && info.entry.depth > 0 ? info.entry.frame.chainRevision : undefined,
|
|
2655
2999
|
});
|
|
2656
3000
|
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Keep one call of the plan whole (AC2): its arguments and its answer,
|
|
3003
|
+
* redacted and capped as a recorded call's are. Later news about the same
|
|
3004
|
+
* step replaces earlier, in place, so the order is the order the calls went
|
|
3005
|
+
* out. Past {@link TURN_CALLS_MAX_CHARS} a call keeps only a report's cut.
|
|
3006
|
+
*/
|
|
3007
|
+
keepCall(state, key, info, failed) {
|
|
3008
|
+
const calls = (state.turnCalls ??= new Map());
|
|
3009
|
+
if (calls.size >= MAX_STEP_RESULTS && !calls.has(key))
|
|
3010
|
+
return;
|
|
3011
|
+
const spent = state.turnCallChars ?? 0;
|
|
3012
|
+
const room = spent < TURN_CALLS_MAX_CHARS;
|
|
3013
|
+
const input = room
|
|
3014
|
+
? serializeCapped(redact(info.input ?? {}))
|
|
3015
|
+
: reportPayload(serializeCapped(redact(info.input ?? {})), true);
|
|
3016
|
+
const answer = info.output === undefined ? undefined : room ? heldPayload(info.output) : reportPayload(info.output);
|
|
3017
|
+
const inSegment = info.entry && info.entry.depth > 0;
|
|
3018
|
+
const call = {
|
|
3019
|
+
scenarioId: inSegment ? info.entry.scenarioId : (state.scenarioId ?? info.entry?.scenarioId ?? ""),
|
|
3020
|
+
stepIndex: info.step.stepIndex,
|
|
3021
|
+
chainRevision: inSegment
|
|
3022
|
+
? (info.entry.frame.chainRevision ?? null)
|
|
3023
|
+
: (state.chainRevision ?? null),
|
|
3024
|
+
toolName: info.step.toolName ?? "",
|
|
3025
|
+
input,
|
|
3026
|
+
...(failed
|
|
3027
|
+
? { error: info.error ? capText(redactText(info.error), DEFAULT_MAX_PAYLOAD_BYTES) : answer }
|
|
3028
|
+
: answer !== undefined
|
|
3029
|
+
? { output: answer }
|
|
3030
|
+
: {}),
|
|
3031
|
+
};
|
|
3032
|
+
const before = calls.get(key);
|
|
3033
|
+
const size = (c) => c ? c.input.length + (c.output?.length ?? 0) + (c.error?.length ?? 0) : 0;
|
|
3034
|
+
state.turnCallChars = spent - size(before) + size(call);
|
|
3035
|
+
calls.set(key, call);
|
|
3036
|
+
}
|
|
2657
3037
|
/** The step verdicts, in chain order, for the execution report. */
|
|
2658
3038
|
stepResultsOf(state) {
|
|
2659
3039
|
if (state.stepResults.size === 0)
|
|
@@ -2694,6 +3074,16 @@ export class ReplayController {
|
|
|
2694
3074
|
});
|
|
2695
3075
|
return;
|
|
2696
3076
|
}
|
|
3077
|
+
if (info.outcome === "not_taken") {
|
|
3078
|
+
const text = testWords(info.step.when?.text);
|
|
3079
|
+
logLine("replay.step_not_taken", {
|
|
3080
|
+
n: info.step.stepIndex,
|
|
3081
|
+
tool: info.step.toolName ?? info.step.callIntent ?? undefined,
|
|
3082
|
+
scenario: info.entry && info.entry.depth > 0 ? info.entry.scenarioId : undefined,
|
|
3083
|
+
why: `its test said this request does not need it${text ? ` — only for when ${text}` : ""}; left out, and the plan goes on`,
|
|
3084
|
+
});
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
2697
3087
|
logLine("replay.step", {
|
|
2698
3088
|
n: info.step.stepIndex,
|
|
2699
3089
|
tool: mcp?.toolName ?? info.step.toolName,
|
|
@@ -2726,6 +3116,12 @@ export class ReplayController {
|
|
|
2726
3116
|
}
|
|
2727
3117
|
state.retired = true;
|
|
2728
3118
|
state.pinned.clear();
|
|
3119
|
+
// A note naming a step to call next is void once the plan is: after a
|
|
3120
|
+
// divergence its bundle may have run that step already, and after a
|
|
3121
|
+
// hand-over it contradicts the hand-over's note. Only "the sequence is
|
|
3122
|
+
// complete", from a plan that finished, still holds (AC5 review).
|
|
3123
|
+
if (state.skipNote && !(completing && state.skipNote.complete))
|
|
3124
|
+
state.skipNote = undefined;
|
|
2729
3125
|
// A call waiting for a pin to thread has nothing left to wait for.
|
|
2730
3126
|
this.notifyPins(state);
|
|
2731
3127
|
}
|
|
@@ -2849,6 +3245,47 @@ function reportPayload(json, clean = false) {
|
|
|
2849
3245
|
}
|
|
2850
3246
|
return capText(redactJsonText(json), REPORT_PAYLOAD_MAX_BYTES);
|
|
2851
3247
|
}
|
|
3248
|
+
/**
|
|
3249
|
+
* A plan call's answer as a recorded call keeps it (autocorrect.md AC2): the
|
|
3250
|
+
* JSON redacted by its keys and capped at 64 KiB the way the control server
|
|
3251
|
+
* records every call — whole, not cut to what a report carries. Text that is
|
|
3252
|
+
* not JSON, or too long to scan, is redacted as text.
|
|
3253
|
+
*/
|
|
3254
|
+
function heldPayload(text) {
|
|
3255
|
+
if (text.length <= REDACT_SCAN_MAX_CHARS) {
|
|
3256
|
+
try {
|
|
3257
|
+
return serializeCapped(redact(JSON.parse(text)));
|
|
3258
|
+
}
|
|
3259
|
+
catch {
|
|
3260
|
+
/* not JSON: kept as text */
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
return capText(text, DEFAULT_MAX_PAYLOAD_BYTES, redactText);
|
|
3264
|
+
}
|
|
3265
|
+
/** Whether an entry runs only under a test (AC5): its own, or a sub-task's it is in. */
|
|
3266
|
+
function underTest(entry) {
|
|
3267
|
+
if (entry.step.when)
|
|
3268
|
+
return true;
|
|
3269
|
+
for (let f = entry.frame; f; f = f.parent)
|
|
3270
|
+
if (f.when)
|
|
3271
|
+
return true;
|
|
3272
|
+
return false;
|
|
3273
|
+
}
|
|
3274
|
+
/** The tests of a flat chain, by flat position, for the `plan.armed` line (AC5). */
|
|
3275
|
+
function testLines(entries) {
|
|
3276
|
+
// A sub-task's test sits where the sub-task begins — at its callee's first
|
|
3277
|
+
// step when its chain begins with a call.
|
|
3278
|
+
const { startsAt } = frameEdges(entries);
|
|
3279
|
+
const lines = entries
|
|
3280
|
+
.map((e, i) => {
|
|
3281
|
+
const own = e.step.when;
|
|
3282
|
+
const call = (startsAt.get(i) ?? []).find((f) => f.when)?.when;
|
|
3283
|
+
const t = own ?? call;
|
|
3284
|
+
return t ? `${i}:${call && !own ? "call/" : ""}${t.otherwise}` : undefined;
|
|
3285
|
+
})
|
|
3286
|
+
.filter((s) => s !== undefined);
|
|
3287
|
+
return lines.length > 0 ? lines.join(",") : undefined;
|
|
3288
|
+
}
|
|
2852
3289
|
/**
|
|
2853
3290
|
* The index a hand-over reports, in the *caller's* chain (R-CALL-32).
|
|
2854
3291
|
*
|