@basein/runner 0.3.21 → 0.3.23
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/dist/bin/bir-hooks.d.ts +12 -16
- package/dist/bin/bir-hooks.js +12 -16
- package/dist/bin/bir-scenario.js +1 -1
- package/dist/bin/bir.js +1 -21
- package/dist/bin/investigate.d.ts +0 -9
- package/dist/bin/investigate.js +14 -42
- package/dist/bin/scenario-edit.js +13 -44
- package/dist/control/pin-guard.d.ts +16 -0
- package/dist/control/pin-guard.js +36 -0
- package/dist/control/recorder-main.d.ts +0 -15
- package/dist/control/recorder-main.js +7 -39
- package/dist/control/server.d.ts +81 -131
- package/dist/control/server.js +460 -968
- package/dist/record/recorder.d.ts +47 -138
- package/dist/record/recorder.js +0 -3
- package/dist/record/remote-recorder.d.ts +1 -9
- package/dist/record/remote-recorder.js +11 -49
- package/dist/record/switchable-recorder.js +0 -2
- package/dist/replay/bundle.d.ts +2 -2
- package/dist/replay/bundle.js +3 -3
- package/dist/replay/controller.d.ts +75 -124
- package/dist/replay/controller.js +207 -529
- package/dist/replay/derive.d.ts +0 -10
- package/dist/replay/derive.js +0 -3
- package/dist/replay/handover.d.ts +2 -37
- package/dist/replay/handover.js +38 -106
- package/dist/replay/logic.d.ts +0 -11
- package/dist/replay/logic.js +0 -24
- package/dist/replay/plan.d.ts +43 -163
- package/dist/replay/plan.js +144 -556
- package/dist/replay/safety.d.ts +4 -5
- package/dist/replay/safety.js +7 -7
- package/dist/replay/source-run.d.ts +15 -24
- package/dist/replay/source-run.js +25 -58
- package/dist/replay/targets.d.ts +11 -21
- package/dist/replay/targets.js +30 -69
- package/dist/replay/types.d.ts +9 -96
- package/dist/replay/types.js +3 -16
- package/dist/util/service-headers.d.ts +1 -2
- package/dist/util/service-headers.js +1 -2
- package/docs/calculatedReplay.md +17 -74
- package/docs/calculatedReplayGuide.md +10 -30
- package/package.json +1 -1
- package/production.md +5 -35
- package/dist/replay/condition-words.d.ts +0 -28
- package/dist/replay/condition-words.js +0 -59
- package/dist/replay/flatten.d.ts +0 -161
- package/dist/replay/flatten.js +0 -222
- package/dist/replay/reads-only.d.ts +0 -29
- package/dist/replay/reads-only.js +0 -261
|
@@ -29,7 +29,6 @@ import { PREVIEW_BUDGET_MS, RecordedValueError, ScenarioReplayPlan, StepRunError
|
|
|
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, frameEdges, } from "./flatten.js";
|
|
33
32
|
import { blockingTargets, testedOnlyTargets } from "./targets.js";
|
|
34
33
|
import { isNoResult, toolResultError } from "./tool-error.js";
|
|
35
34
|
import { OUTCOME_RANK, hasTargets, isReadyScenario, } from "./types.js";
|
|
@@ -168,7 +167,7 @@ function handedOver(note) {
|
|
|
168
167
|
}
|
|
169
168
|
export const DEFAULT_INTENT_MATCH = {
|
|
170
169
|
enabled: true,
|
|
171
|
-
budgetMs:
|
|
170
|
+
budgetMs: 1_500,
|
|
172
171
|
maxPerTurn: 3,
|
|
173
172
|
maxRequestsPerTurn: 40,
|
|
174
173
|
};
|
|
@@ -197,6 +196,15 @@ const MAX_STEP_ERROR_CHARS = 500;
|
|
|
197
196
|
* cannot hold more than a few megabytes in the recorder.
|
|
198
197
|
*/
|
|
199
198
|
const TURN_CALLS_MAX_CHARS = 8 * 1024 * 1024;
|
|
199
|
+
/**
|
|
200
|
+
* Whether a plan is a part of the task the agent is already doing — armed
|
|
201
|
+
* mid-turn by its intent — so its words say "continue" and never "answer the
|
|
202
|
+
* user". A continuation is armed mid-turn too, and it is the rest of the
|
|
203
|
+
* user's request: it ends with the answer.
|
|
204
|
+
*/
|
|
205
|
+
export function isSubTask(state) {
|
|
206
|
+
return state.armedBy === "intent" && state.continuation === undefined;
|
|
207
|
+
}
|
|
200
208
|
export class ReplayController {
|
|
201
209
|
enabled;
|
|
202
210
|
work = new ProxyWorkQueue();
|
|
@@ -291,18 +299,6 @@ export class ReplayController {
|
|
|
291
299
|
? builtins
|
|
292
300
|
: new Set([...builtins].filter(reachable));
|
|
293
301
|
}
|
|
294
|
-
/**
|
|
295
|
-
* Whether a handed-out segment may actually arm (segmented.md R-OUT-10).
|
|
296
|
-
*
|
|
297
|
-
* Only when the option says so. The recorder passes it on unless
|
|
298
|
-
* `BIR_SEGMENT_ARM=0` (from 0.3.19; the service's segmentsbreakdown.md
|
|
299
|
-
* SB-21); a controller built without it stays observe-only, asks for no
|
|
300
|
-
* segment and arms none. The server enforces the same thing from its side
|
|
301
|
-
* (R-OUT-9).
|
|
302
|
-
*/
|
|
303
|
-
get segmentArm() {
|
|
304
|
-
return this.opts.segmentArm === true;
|
|
305
|
-
}
|
|
306
302
|
/** The plan's own delivery vehicle is never a scenario step. */
|
|
307
303
|
isDirectTool(toolName) {
|
|
308
304
|
return toolName === DIRECT_TOOL_NAME;
|
|
@@ -362,13 +358,6 @@ export class ReplayController {
|
|
|
362
358
|
reported: false,
|
|
363
359
|
retired: false,
|
|
364
360
|
armedBy,
|
|
365
|
-
// An older server sends no `kind`, and a whole-run scenario is the safe
|
|
366
|
-
// reading: it is what every hand-out was before segments existed.
|
|
367
|
-
kind: match.kind ?? "scenario",
|
|
368
|
-
segment: match.segment,
|
|
369
|
-
key: match.key,
|
|
370
|
-
verified: match.verified,
|
|
371
|
-
sharedWith: [],
|
|
372
361
|
};
|
|
373
362
|
}
|
|
374
363
|
/**
|
|
@@ -385,6 +374,30 @@ export class ReplayController {
|
|
|
385
374
|
state.retired = true;
|
|
386
375
|
return state;
|
|
387
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* The state of a continuation handed out and not armed (the service's
|
|
379
|
+
* fallbk.md Part 3): declined before any gate ran — its first step is not
|
|
380
|
+
* the model's call (`not_first_step`), or the service's answer came too
|
|
381
|
+
* late (`match_late`). Like {@link lateMatch}, it exists only for its
|
|
382
|
+
* ticket's report, with the reason.
|
|
383
|
+
*/
|
|
384
|
+
declinedContinuation(match, code, continuation) {
|
|
385
|
+
const state = this.blankState(match, new Set(), "intent");
|
|
386
|
+
state.continuation = continuation;
|
|
387
|
+
state.declined = code;
|
|
388
|
+
state.retired = true;
|
|
389
|
+
logLine("replay.decision", {
|
|
390
|
+
verdict: "no-steer",
|
|
391
|
+
code,
|
|
392
|
+
run: match.runId,
|
|
393
|
+
scenario: match.scenarioId ?? undefined,
|
|
394
|
+
continues: `${continuation.ofScenarioId}@${continuation.stepIndex}`,
|
|
395
|
+
why: code === "not_first_step"
|
|
396
|
+
? "a continuation starts only on its first step, and the model's call after the hand-over is another one"
|
|
397
|
+
: "the continuation came after the call it could have replaced had gone ahead",
|
|
398
|
+
});
|
|
399
|
+
return state;
|
|
400
|
+
}
|
|
388
401
|
/**
|
|
389
402
|
* Run the gate ladder and, if every gate passes, arm a plan.
|
|
390
403
|
*
|
|
@@ -393,15 +406,14 @@ export class ReplayController {
|
|
|
393
406
|
* up with a denominator nobody measured.
|
|
394
407
|
*
|
|
395
408
|
* `host` is given for a session whose host is not Claude Code, and is kept
|
|
396
|
-
* on the state (see {@link HostContext}).
|
|
409
|
+
* on the state (see {@link HostContext}). `extra.continuation` arms a
|
|
410
|
+
* continuation (the service's fallbk.md Part 3): see
|
|
411
|
+
* {@link ReplayState.continuation}.
|
|
397
412
|
*/
|
|
398
|
-
async arm(match, prompt, wrapped, armedBy = "prompt", ctx = {}, host) {
|
|
413
|
+
async arm(match, prompt, wrapped, armedBy = "prompt", ctx = {}, host, extra = {}) {
|
|
399
414
|
const state = this.blankState(match, wrapped, armedBy, host);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (armedBy === "prompt")
|
|
403
|
-
state.request = prompt;
|
|
404
|
-
const isSegment = state.kind === "segment";
|
|
415
|
+
if (extra.continuation)
|
|
416
|
+
state.continuation = extra.continuation;
|
|
405
417
|
const decline = (why, code) => {
|
|
406
418
|
state.declined = code;
|
|
407
419
|
logLine("replay.decision", {
|
|
@@ -410,19 +422,10 @@ export class ReplayController {
|
|
|
410
422
|
run: match.runId,
|
|
411
423
|
scenario: match.scenarioId ?? undefined,
|
|
412
424
|
similarity: match.similarity.toFixed(3),
|
|
413
|
-
kind: state.kind,
|
|
414
|
-
key: state.key,
|
|
415
|
-
verified: state.verified?.verdict,
|
|
416
425
|
why,
|
|
417
426
|
});
|
|
418
427
|
return state;
|
|
419
428
|
};
|
|
420
|
-
// The server never sends a segment to a runner that did not ask for one
|
|
421
|
-
// (R-OUT-9). One arriving anyway is declined here rather than run: an
|
|
422
|
-
// observe-only period that quietly armed something would be worthless.
|
|
423
|
-
if (isSegment && !this.segmentArm) {
|
|
424
|
-
return decline("segment hand-out while BIR_SEGMENT_ARM is off", "replay_disabled");
|
|
425
|
-
}
|
|
426
429
|
// Gate 2 — replay enabled.
|
|
427
430
|
if (!this.enabled)
|
|
428
431
|
return decline("BIR_REPLAY=0", "replay_disabled");
|
|
@@ -452,65 +455,24 @@ export class ReplayController {
|
|
|
452
455
|
}
|
|
453
456
|
// Gate 4 — similarity — is gone. A hand-out is the service's decision that
|
|
454
457
|
// this scenario does what the prompt asks: its match threshold, the
|
|
455
|
-
// same-request question under it, the intent pass
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
// Flatten the chain before the remaining gates, so every one of them judges
|
|
462
|
-
// the steps that will *really* run — the caller's own and, in order, those
|
|
463
|
-
// of every segment it calls (segmented.md R-CALL-29, R-CALL-30).
|
|
464
|
-
let flat;
|
|
465
|
-
try {
|
|
466
|
-
flat = flattenChain(scenario, {
|
|
467
|
-
range: match.segment
|
|
468
|
-
? { stepFrom: match.segment.stepFrom, stepTo: match.segment.stepTo }
|
|
469
|
-
: null,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
catch (err) {
|
|
473
|
-
// A payload we cannot lay out is a payload we cannot run. It costs the
|
|
474
|
-
// turn nothing: the agent does the task (R-FALL-12).
|
|
475
|
-
logLine("replay.flatten_failed", { scenario: scenario.id, error: errText(err) });
|
|
476
|
-
return decline("the chain could not be flattened", "flatten_failed");
|
|
477
|
-
}
|
|
478
|
-
const entries = flat.entries;
|
|
479
|
-
if (entries.length === 0)
|
|
480
|
-
return decline("the chain has no runnable step", "not_ready");
|
|
458
|
+
// same-request question under it, the intent pass. Scoring it again here
|
|
459
|
+
// declined matches the service had made (a checked 0.85, a task line at
|
|
460
|
+
// 0.91), so every match now steers. What a plan cannot do — a step that
|
|
461
|
+
// keeps failing, a tool nobody wrapped, a target the prompt never named —
|
|
462
|
+
// the gates below still stop.
|
|
463
|
+
const steps = scenario.steps;
|
|
481
464
|
// Gate 5 — steps the plan must stop in front of (fallbk.md D3,
|
|
482
465
|
// plan-services.md D8): a step the service parked — failed more than its
|
|
483
|
-
// limit, retry not due — or one the calculation marked non-deterministic
|
|
484
|
-
|
|
485
|
-
// answer stops the plan the same way, and the earliest of them wins.
|
|
486
|
-
const badAt = stopIndex(entries.map((e) => e.step), match.fallback?.maxStepFailures);
|
|
487
|
-
const rawStop = [flat.stopAt, badAt].filter((n) => typeof n === "number");
|
|
488
|
-
// Clamped to the start of the outermost called frame it falls in: a plan
|
|
489
|
-
// never runs half a sub-task and leaves its result mapping unevaluated.
|
|
490
|
-
const stopAt = rawStop.length > 0 ? clampToFrameStart(entries, Math.min(...rawStop)) : undefined;
|
|
466
|
+
// limit, retry not due — or one the calculation marked non-deterministic.
|
|
467
|
+
const stopAt = stopIndex(steps, match.fallback?.maxStepFailures);
|
|
491
468
|
if (stopAt === 0) {
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const judgement = stopping.stop?.kind === "nondeterministic" || decisionFirst;
|
|
500
|
-
const why = unusable
|
|
501
|
-
? `its first step is a call that cannot run: ${unusable.reason}`
|
|
502
|
-
: decisionFirst
|
|
503
|
-
? "its first step is a decision the agent makes — the agent does this task"
|
|
504
|
-
: judgement
|
|
505
|
-
? "its first step needs a judgement the plan cannot compute — the agent does this task"
|
|
506
|
-
: `its first step has failed ${stopping.failureCount ?? 0} times — the agent does this task`;
|
|
507
|
-
return decline(why, unusable
|
|
508
|
-
? "unusable_first_step"
|
|
509
|
-
: judgement
|
|
510
|
-
? "nondeterministic_first_step"
|
|
511
|
-
: "known_bad_first_step");
|
|
512
|
-
}
|
|
513
|
-
const planned = stopAt === undefined ? entries : entries.slice(0, stopAt);
|
|
469
|
+
const stopping = steps[0];
|
|
470
|
+
const judgement = stopping.stop?.kind === "nondeterministic";
|
|
471
|
+
return decline(judgement
|
|
472
|
+
? "its first step needs a judgement the plan cannot compute — the agent does this task"
|
|
473
|
+
: `its first step has failed ${stopping.failureCount ?? 0} times — the agent does this task`, judgement ? "nondeterministic_first_step" : "known_bad_first_step");
|
|
474
|
+
}
|
|
475
|
+
const planned = stopAt === undefined ? steps : steps.slice(0, stopAt);
|
|
514
476
|
// Another host's adapter steps through a plan call by call and knows no
|
|
515
477
|
// tests (AC5): asked for a step its test left out, it is refused, reads
|
|
516
478
|
// that as the plan's end, and the model's own calls after it are booked as
|
|
@@ -530,7 +492,7 @@ export class ReplayController {
|
|
|
530
492
|
// `mcp__fleet__list` is not the runner's to run or a proxy's to reach.
|
|
531
493
|
const allowServers = host ? undefined : this.opts.allowServers;
|
|
532
494
|
const builtinTools = host ? undefined : this.builtinTools;
|
|
533
|
-
const mode = modeFor(planned
|
|
495
|
+
const mode = modeFor(planned, state.wrapped, allowServers, builtinTools);
|
|
534
496
|
if (mode === "none")
|
|
535
497
|
return decline("no step is executable", "coverage");
|
|
536
498
|
// Tool results feed derivation as context, and they are whatever the agent
|
|
@@ -544,7 +506,7 @@ export class ReplayController {
|
|
|
544
506
|
// command nobody asked for.
|
|
545
507
|
let deriveCtx = ctx;
|
|
546
508
|
if (ctx.recentResults?.length && sinkOfPlan(planned, host?.sinks) === "command") {
|
|
547
|
-
deriveCtx = { liveCall: ctx.liveCall, agentDay: ctx.agentDay
|
|
509
|
+
deriveCtx = { liveCall: ctx.liveCall, agentDay: ctx.agentDay };
|
|
548
510
|
logLine("replay.derive_context", {
|
|
549
511
|
scenario: scenario.id,
|
|
550
512
|
shell: hasShellStep(planned, host?.sinks) || undefined,
|
|
@@ -671,15 +633,14 @@ export class ReplayController {
|
|
|
671
633
|
}
|
|
672
634
|
}
|
|
673
635
|
// A plan armed by intent runs inside a task the agent is already doing, so
|
|
674
|
-
// its directive and its bundles say "continue", never "answer the user"
|
|
675
|
-
//
|
|
636
|
+
// its directive and its bundles say "continue", never "answer the user".
|
|
637
|
+
// A continuation is the rest of the user's request: it ends with the answer.
|
|
676
638
|
state.plan = new ScenarioReplayPlan({
|
|
677
639
|
scenario,
|
|
678
640
|
params,
|
|
679
641
|
mode,
|
|
680
642
|
stopAt,
|
|
681
|
-
|
|
682
|
-
subTask: armedBy === "intent",
|
|
643
|
+
subTask: isSubTask(state),
|
|
683
644
|
});
|
|
684
645
|
if (mode === "steer") {
|
|
685
646
|
// A steer directive shows the arguments the steps will run with, so the
|
|
@@ -697,17 +658,11 @@ export class ReplayController {
|
|
|
697
658
|
await state.plan.ready().catch(() => undefined);
|
|
698
659
|
}
|
|
699
660
|
state.mode = mode;
|
|
700
|
-
state.stepsPlanned =
|
|
661
|
+
state.stepsPlanned = steps.length;
|
|
701
662
|
// The chain this plan was served at: what its own calls are marked with
|
|
702
663
|
// when they are recorded into a redo of the turn (AC2).
|
|
703
664
|
if (typeof scenario.chainRevision === "number")
|
|
704
665
|
state.chainRevision = scenario.chainRevision;
|
|
705
|
-
// Why the plan stops, when a call is what stops it: the note names the
|
|
706
|
-
// sub-task rather than a tool (R-CALL-36).
|
|
707
|
-
state.flatStop =
|
|
708
|
-
typeof stopAt === "number" && flat.stopAt !== undefined && stopAt <= flat.stopAt
|
|
709
|
-
? flat.stopReason
|
|
710
|
-
: undefined;
|
|
711
666
|
// Recorded outputs are read only when they may stand in (opt-in), so a
|
|
712
667
|
// plan never fetches a run it will not show.
|
|
713
668
|
if (this.recordedFallback && this.opts.authUrl && this.opts.authToken) {
|
|
@@ -724,22 +679,20 @@ export class ReplayController {
|
|
|
724
679
|
scenario: scenario.id,
|
|
725
680
|
mode,
|
|
726
681
|
host: host?.app,
|
|
727
|
-
steps:
|
|
682
|
+
steps: steps.length,
|
|
728
683
|
stopsAt: stopAt,
|
|
729
|
-
by: armedBy === "intent" ? "intent" : undefined,
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
684
|
+
by: armedBy === "intent" ? (state.continuation ? "continuation" : "intent") : undefined,
|
|
685
|
+
// The plan it continues and the step that plan stopped in front of.
|
|
686
|
+
continues: state.continuation
|
|
687
|
+
? `${state.continuation.ofScenarioId}@${state.continuation.stepIndex}`
|
|
688
|
+
: undefined,
|
|
733
689
|
similarity: match.similarity.toFixed(3),
|
|
734
|
-
// What this chain calls, and what answers each call right now (R-CALL-32).
|
|
735
|
-
calls: callLines(scenario),
|
|
736
|
-
unusable: flat.stopReason?.reason,
|
|
737
690
|
// Which steps the service told this plan to stop in front of, and why.
|
|
738
|
-
stops: stopLines(
|
|
739
|
-
coverage: coverageOf(
|
|
740
|
-
tools:
|
|
741
|
-
// Which steps run only under a test, by
|
|
742
|
-
tests: testLines(
|
|
691
|
+
stops: stopLines(steps),
|
|
692
|
+
coverage: coverageOf(steps, state.wrapped, allowServers, builtinTools).join(","),
|
|
693
|
+
tools: steps.map((s) => s.toolName).join(","),
|
|
694
|
+
// Which steps run only under a test, by position (AC5).
|
|
695
|
+
tests: testLines(steps),
|
|
743
696
|
});
|
|
744
697
|
return state;
|
|
745
698
|
}
|
|
@@ -748,16 +701,16 @@ export class ReplayController {
|
|
|
748
701
|
* stops when that is before its end. For a host that steps through the plan
|
|
749
702
|
* itself and must not have to pick the tool names out of a directive
|
|
750
703
|
* written for a model — a reworded sentence would break every adapter in
|
|
751
|
-
* the field. Built from the same
|
|
704
|
+
* the field. Built from the same steps the directive lists.
|
|
752
705
|
*/
|
|
753
706
|
planData(state) {
|
|
754
707
|
const plan = state.plan;
|
|
755
708
|
if (!plan || state.retired)
|
|
756
709
|
return undefined;
|
|
757
710
|
const steps = plan
|
|
758
|
-
.
|
|
711
|
+
.allSteps()
|
|
759
712
|
.slice(0, plan.stepCount)
|
|
760
|
-
.map((
|
|
713
|
+
.map((step, stepIndex) => ({ stepIndex, toolName: step.toolName }));
|
|
761
714
|
return {
|
|
762
715
|
scenarioId: state.scenarioId,
|
|
763
716
|
mode: state.mode,
|
|
@@ -765,15 +718,6 @@ export class ReplayController {
|
|
|
765
718
|
...(plan.stopsEarly() ? { stopAt: plan.stepCount } : {}),
|
|
766
719
|
};
|
|
767
720
|
}
|
|
768
|
-
/**
|
|
769
|
-
* Whether the plan's steps end in front of a decision (the service's
|
|
770
|
-
* segmentsbreakdown.md SB-6): it runs the steps before it, then hands the
|
|
771
|
-
* task back there. The control server gives such a plan a usage mark, so
|
|
772
|
-
* that it is billed only up to the stop (SB-26).
|
|
773
|
-
*/
|
|
774
|
-
stopsAtDecision(state) {
|
|
775
|
-
return state.plan?.stopEntry()?.step.stop?.kind === "decision";
|
|
776
|
-
}
|
|
777
721
|
/**
|
|
778
722
|
* Take back a plan that armed and has run nothing, as a decline.
|
|
779
723
|
*
|
|
@@ -797,7 +741,6 @@ export class ReplayController {
|
|
|
797
741
|
run: state.matchedRunId,
|
|
798
742
|
scenario: state.scenarioId ?? undefined,
|
|
799
743
|
similarity: state.similarity.toFixed(3),
|
|
800
|
-
kind: state.kind,
|
|
801
744
|
why,
|
|
802
745
|
});
|
|
803
746
|
state.plan = undefined;
|
|
@@ -828,7 +771,6 @@ export class ReplayController {
|
|
|
828
771
|
this.handOver(state, {
|
|
829
772
|
kind: "step_failed",
|
|
830
773
|
step: plan.currentStep(),
|
|
831
|
-
entry: plan.currentEntry(),
|
|
832
774
|
position,
|
|
833
775
|
error: why === "own_call"
|
|
834
776
|
? "the model made a call of its own, so the scenario stopped"
|
|
@@ -852,7 +794,7 @@ export class ReplayController {
|
|
|
852
794
|
directiveFor(state, opts = {}) {
|
|
853
795
|
const asked = opts.previewMs;
|
|
854
796
|
return state.plan?.steeringDirective(DIRECT_TOOL_NAME, {
|
|
855
|
-
subTask: state
|
|
797
|
+
subTask: isSubTask(state),
|
|
856
798
|
// NaN is a caller's arithmetic gone wrong: no time known, no preview.
|
|
857
799
|
previewBudgetMs: typeof asked === "number"
|
|
858
800
|
? Math.max(0, Math.min(Number.isNaN(asked) ? 0 : asked, PREVIEW_BUDGET_MS))
|
|
@@ -889,8 +831,8 @@ export class ReplayController {
|
|
|
889
831
|
return false;
|
|
890
832
|
if (plan.parametersSettled)
|
|
891
833
|
return true;
|
|
892
|
-
const planned = plan.
|
|
893
|
-
if (!planned.some((
|
|
834
|
+
const planned = plan.allSteps().slice(0, plan.stepCount);
|
|
835
|
+
if (!planned.some((s) => pinnedToShape(s.toolName)))
|
|
894
836
|
return false;
|
|
895
837
|
if (state.deriveOnService && !serviceHealth.canWait())
|
|
896
838
|
return false;
|
|
@@ -1023,7 +965,6 @@ export class ReplayController {
|
|
|
1023
965
|
if (step && ready.blame) {
|
|
1024
966
|
this.recordStep(state, {
|
|
1025
967
|
step,
|
|
1026
|
-
entry: plan.currentEntry(),
|
|
1027
968
|
outcome: "failed",
|
|
1028
969
|
stage: ready.stage,
|
|
1029
970
|
error: ready.error,
|
|
@@ -1032,7 +973,6 @@ export class ReplayController {
|
|
|
1032
973
|
this.handOver(state, {
|
|
1033
974
|
kind: "step_failed",
|
|
1034
975
|
step,
|
|
1035
|
-
entry: plan.currentEntry(),
|
|
1036
976
|
position: plan.currentStepIndex,
|
|
1037
977
|
error: ready.error,
|
|
1038
978
|
delivery: "above",
|
|
@@ -1136,7 +1076,6 @@ export class ReplayController {
|
|
|
1136
1076
|
const recorded = err instanceof RecordedValueError;
|
|
1137
1077
|
this.recordStep(state, {
|
|
1138
1078
|
step,
|
|
1139
|
-
entry: plan.currentEntry(),
|
|
1140
1079
|
outcome: recorded ? "skipped" : "failed",
|
|
1141
1080
|
stage: "tool_input_logic",
|
|
1142
1081
|
error: errText(err),
|
|
@@ -1165,7 +1104,6 @@ export class ReplayController {
|
|
|
1165
1104
|
this.handOver(state, {
|
|
1166
1105
|
kind: "step_failed",
|
|
1167
1106
|
step: plan.currentStep(),
|
|
1168
|
-
entry: plan.currentEntry(),
|
|
1169
1107
|
position: plan.currentStepIndex,
|
|
1170
1108
|
error: `the scenario's next step is ${expected ?? "none"}, and the host asked for ${toolName}`,
|
|
1171
1109
|
delivery: "above",
|
|
@@ -1213,7 +1151,6 @@ export class ReplayController {
|
|
|
1213
1151
|
this.handOver(state, {
|
|
1214
1152
|
kind: "step_failed",
|
|
1215
1153
|
step: plan.allSteps()[next],
|
|
1216
|
-
entry: plan.allEntries()[next],
|
|
1217
1154
|
position: next,
|
|
1218
1155
|
delivery: "above",
|
|
1219
1156
|
stopReason: "concurrent",
|
|
@@ -1351,10 +1288,6 @@ export class ReplayController {
|
|
|
1351
1288
|
// reading it after the call would judge the *next* step, and reading it
|
|
1352
1289
|
// before assumes no other pin ever moved it.
|
|
1353
1290
|
const step = plan.allSteps()[pin.stepIndex];
|
|
1354
|
-
// The entry it ran as, so its verdict is kept under the scenario that owns
|
|
1355
|
-
// the step: a called segment numbers its steps from zero too, and without
|
|
1356
|
-
// this its step 0 replaced the caller's (segmented.md R-CALL-13).
|
|
1357
|
-
const entry = plan.allEntries()[pin.stepIndex];
|
|
1358
1291
|
// THE HOST STOPPED THE STEP, and says so. Read before any failure: a
|
|
1359
1292
|
// person's "no" arrives as an error result, and booked as one it would be
|
|
1360
1293
|
// counted against a plan that did nothing wrong — after enough of them the
|
|
@@ -1362,11 +1295,10 @@ export class ReplayController {
|
|
|
1362
1295
|
// `rejected` the tool never ran, and after `edited` its result answers a
|
|
1363
1296
|
// call the plan did not plan, which its later steps were not computed for.
|
|
1364
1297
|
if (how.hostStop && step) {
|
|
1365
|
-
this.hostStopped(state, pin, step,
|
|
1298
|
+
this.hostStopped(state, pin, step, output, how.hostStop);
|
|
1366
1299
|
this.handOver(state, {
|
|
1367
1300
|
kind: "step_failed",
|
|
1368
1301
|
step,
|
|
1369
|
-
entry,
|
|
1370
1302
|
position: pin.stepIndex,
|
|
1371
1303
|
delivery: "above",
|
|
1372
1304
|
stopReason: how.hostStop === "edited" ? "host_edited" : "host_rejected",
|
|
@@ -1387,7 +1319,6 @@ export class ReplayController {
|
|
|
1387
1319
|
`the plan's cursor is at step ${plan.currentStepIndex}, not the pinned step ${pin.stepIndex}`;
|
|
1388
1320
|
const info = {
|
|
1389
1321
|
step,
|
|
1390
|
-
entry,
|
|
1391
1322
|
input: pin.input,
|
|
1392
1323
|
output: answerOf(output),
|
|
1393
1324
|
outcome: "failed",
|
|
@@ -1401,7 +1332,6 @@ export class ReplayController {
|
|
|
1401
1332
|
this.handOver(state, {
|
|
1402
1333
|
kind: "step_failed",
|
|
1403
1334
|
step,
|
|
1404
|
-
entry,
|
|
1405
1335
|
position: pin.stepIndex,
|
|
1406
1336
|
error,
|
|
1407
1337
|
delivery: "above",
|
|
@@ -1423,7 +1353,6 @@ export class ReplayController {
|
|
|
1423
1353
|
if (step) {
|
|
1424
1354
|
this.recordStep(state, {
|
|
1425
1355
|
step,
|
|
1426
|
-
entry,
|
|
1427
1356
|
input: pin.input,
|
|
1428
1357
|
// What the logic threw on — the output is usually the explanation.
|
|
1429
1358
|
output,
|
|
@@ -1462,7 +1391,6 @@ export class ReplayController {
|
|
|
1462
1391
|
if (step && toolError) {
|
|
1463
1392
|
const info = {
|
|
1464
1393
|
step,
|
|
1465
|
-
entry,
|
|
1466
1394
|
input: pin.input,
|
|
1467
1395
|
output,
|
|
1468
1396
|
outcome: "failed",
|
|
@@ -1486,7 +1414,6 @@ export class ReplayController {
|
|
|
1486
1414
|
if (step) {
|
|
1487
1415
|
this.recordStep(state, {
|
|
1488
1416
|
step,
|
|
1489
|
-
entry,
|
|
1490
1417
|
input: pin.input,
|
|
1491
1418
|
output,
|
|
1492
1419
|
outcome: "executed",
|
|
@@ -1510,19 +1437,17 @@ export class ReplayController {
|
|
|
1510
1437
|
return done;
|
|
1511
1438
|
}
|
|
1512
1439
|
/**
|
|
1513
|
-
* A steer plan whose cursor reached its end: in front of a parked step
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
1440
|
+
* A steer plan whose cursor reached its end: in front of a parked step,
|
|
1441
|
+
* which hands over; otherwise it ran to its end — and from here every call
|
|
1442
|
+
* is the model's own.
|
|
1516
1443
|
*/
|
|
1517
1444
|
finishSteer(state, plan) {
|
|
1518
1445
|
if (plan.atStop()) {
|
|
1519
|
-
// Every planned step ran, and the next one is parked (fallbk.md D3)
|
|
1520
|
-
// is a call the service could not answer (segmented.md R-CALL-29).
|
|
1446
|
+
// Every planned step ran, and the next one is parked (fallbk.md D3).
|
|
1521
1447
|
const parked = plan.stopStep();
|
|
1522
1448
|
this.handOver(state, {
|
|
1523
|
-
kind:
|
|
1449
|
+
kind: stopKindOf(parked),
|
|
1524
1450
|
step: parked,
|
|
1525
|
-
entry: plan.stopEntry(),
|
|
1526
1451
|
position: plan.stepCount,
|
|
1527
1452
|
delivery: "above",
|
|
1528
1453
|
});
|
|
@@ -1559,34 +1484,28 @@ export class ReplayController {
|
|
|
1559
1484
|
settleCursor(state, plan, delivery) {
|
|
1560
1485
|
if (!plan.hasTests())
|
|
1561
1486
|
return "ready";
|
|
1562
|
-
// Never throws: a sub-task whose results cannot go back after its last
|
|
1563
|
-
// step was left out is a stop like any other (`result_map`).
|
|
1564
1487
|
const settled = plan.settleTests(this.observeStep(state));
|
|
1565
1488
|
// The recorded outputs of the steps left out are passed over, so a
|
|
1566
1489
|
// divergence's bundle later stands each step's own in (source-run.ts).
|
|
1567
1490
|
const source = state.sourceRun;
|
|
1568
1491
|
if (source) {
|
|
1569
1492
|
for (const l of settled.left) {
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
source.leftOut(entry);
|
|
1574
|
-
}
|
|
1493
|
+
const step = plan.allSteps()[l.position];
|
|
1494
|
+
if (step)
|
|
1495
|
+
source.leftOut(step);
|
|
1575
1496
|
}
|
|
1576
1497
|
}
|
|
1577
1498
|
if (settled.stop) {
|
|
1578
1499
|
const stop = settled.stop;
|
|
1579
|
-
const position = stop.
|
|
1500
|
+
const position = stop.position ?? plan.currentStepIndex;
|
|
1580
1501
|
this.handOver(state, {
|
|
1581
1502
|
kind: stop.kind,
|
|
1582
1503
|
step: plan.allSteps()[position],
|
|
1583
|
-
entry: plan.allEntries()[position],
|
|
1584
1504
|
position,
|
|
1585
1505
|
error: stop.error,
|
|
1586
1506
|
delivery,
|
|
1587
1507
|
stopReason: stop.reason,
|
|
1588
1508
|
whenText: stop.whenText,
|
|
1589
|
-
subTask: stop.subTask,
|
|
1590
1509
|
});
|
|
1591
1510
|
return "stopped";
|
|
1592
1511
|
}
|
|
@@ -1599,7 +1518,6 @@ export class ReplayController {
|
|
|
1599
1518
|
this.handOver(state, {
|
|
1600
1519
|
kind: "other_branch",
|
|
1601
1520
|
step: plan.allSteps()[0],
|
|
1602
|
-
entry: plan.allEntries()[0],
|
|
1603
1521
|
position: 0,
|
|
1604
1522
|
delivery,
|
|
1605
1523
|
noneNeeded: true,
|
|
@@ -1608,9 +1526,8 @@ export class ReplayController {
|
|
|
1608
1526
|
}
|
|
1609
1527
|
state.skipNote = {
|
|
1610
1528
|
text: this.skipNoteText(state, plan, settled.left),
|
|
1611
|
-
//
|
|
1612
|
-
|
|
1613
|
-
tools: new Set(settled.left.flatMap((l) => (l.tools.length > 0 ? l.tools : [l.toolName]))),
|
|
1529
|
+
// A call the model makes to any of these tools is one to a step left out.
|
|
1530
|
+
tools: new Set(settled.left.map((l) => l.toolName)),
|
|
1614
1531
|
delivered: false,
|
|
1615
1532
|
complete: plan.isDone() && !plan.atStop(),
|
|
1616
1533
|
};
|
|
@@ -1618,20 +1535,19 @@ export class ReplayController {
|
|
|
1618
1535
|
}
|
|
1619
1536
|
/** What the model is told about steps a test left out (AC5). */
|
|
1620
1537
|
skipNoteText(state, plan, left) {
|
|
1621
|
-
const named = left.map((l) => `step ${l.
|
|
1538
|
+
const named = left.map((l) => `step ${l.position + 1} (${l.toolName})`);
|
|
1622
1539
|
const texts = [...new Set(left.map((l) => l.text).filter((t) => !!t))];
|
|
1623
1540
|
const why = texts.length === 1 ? ` — only for when ${texts[0]}` : "";
|
|
1624
1541
|
const what = left.length === 1 ? `Not needed for this request: ${named[0]}${why}.` : `Not needed for this request: ${named.join(", ")}${why}.`;
|
|
1625
1542
|
let next;
|
|
1626
1543
|
if (plan.isDone() && !plan.atStop()) {
|
|
1627
|
-
next =
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
: "The sequence is complete: answer the user's request from the tool results.";
|
|
1544
|
+
next = isSubTask(state)
|
|
1545
|
+
? "The sequence is complete: continue your task, calling tools as needed."
|
|
1546
|
+
: "The sequence is complete: answer the user's request from the tool results.";
|
|
1631
1547
|
}
|
|
1632
1548
|
else {
|
|
1633
|
-
const
|
|
1634
|
-
next = `Leave ${left.length === 1 ? "it" : "them"} out, and call step ${plan.currentStepIndex + 1}${
|
|
1549
|
+
const step = plan.currentStep();
|
|
1550
|
+
next = `Leave ${left.length === 1 ? "it" : "them"} out, and call step ${plan.currentStepIndex + 1}${step?.toolName ? ` (${step.toolName})` : ""} next.`;
|
|
1635
1551
|
}
|
|
1636
1552
|
return `[BaseInstRunner calculated replay] ${what} ${next}`;
|
|
1637
1553
|
}
|
|
@@ -1693,7 +1609,7 @@ export class ReplayController {
|
|
|
1693
1609
|
return;
|
|
1694
1610
|
if (how.hostStop) {
|
|
1695
1611
|
// The host stopped it: neither a failure nor work the plan did.
|
|
1696
|
-
this.hostStopped(state, pin, step,
|
|
1612
|
+
this.hostStopped(state, pin, step, output, how.hostStop);
|
|
1697
1613
|
return;
|
|
1698
1614
|
}
|
|
1699
1615
|
const ms = Date.now() - pin.pinnedAt;
|
|
@@ -1705,11 +1621,9 @@ export class ReplayController {
|
|
|
1705
1621
|
? `${failure.error} — it was sent, and may or may not have completed`
|
|
1706
1622
|
: failure.error
|
|
1707
1623
|
: toolResultError(output);
|
|
1708
|
-
const entry = plan.allEntries()[pin.stepIndex];
|
|
1709
1624
|
if (error !== undefined) {
|
|
1710
1625
|
const info = {
|
|
1711
1626
|
step,
|
|
1712
|
-
entry,
|
|
1713
1627
|
input: pin.input,
|
|
1714
1628
|
output: answerOf(output),
|
|
1715
1629
|
outcome: "failed",
|
|
@@ -1721,7 +1635,7 @@ export class ReplayController {
|
|
|
1721
1635
|
this.recordStep(state, info);
|
|
1722
1636
|
return;
|
|
1723
1637
|
}
|
|
1724
|
-
this.recordStep(state, { step,
|
|
1638
|
+
this.recordStep(state, { step, input: pin.input, output, outcome: "executed", ms });
|
|
1725
1639
|
state.stepsPinned += 1;
|
|
1726
1640
|
this.upgrade(state, "fell_back");
|
|
1727
1641
|
logDetail("replay.thread_late", {
|
|
@@ -1736,7 +1650,7 @@ export class ReplayController {
|
|
|
1736
1650
|
* `failed`: the service counts failures against the plan. And not counted
|
|
1737
1651
|
* in `stepsPinned`: the plan's own call did not run.
|
|
1738
1652
|
*/
|
|
1739
|
-
hostStopped(state, pin, step,
|
|
1653
|
+
hostStopped(state, pin, step, output, stop) {
|
|
1740
1654
|
const edited = stop === "edited";
|
|
1741
1655
|
// After `rejected` nothing later can change what the report may claim.
|
|
1742
1656
|
if (state.hostStop !== "rejected")
|
|
@@ -1752,7 +1666,6 @@ export class ReplayController {
|
|
|
1752
1666
|
});
|
|
1753
1667
|
this.recordStep(state, {
|
|
1754
1668
|
step,
|
|
1755
|
-
entry,
|
|
1756
1669
|
// The plan's own arguments are what a refused call was asked with; a
|
|
1757
1670
|
// changed call ran with other ones, which only the host knows.
|
|
1758
1671
|
input: edited ? undefined : pin.input,
|
|
@@ -1829,7 +1742,6 @@ export class ReplayController {
|
|
|
1829
1742
|
if (step && ready.blame) {
|
|
1830
1743
|
this.recordStep(state, {
|
|
1831
1744
|
step,
|
|
1832
|
-
entry: plan.currentEntry(),
|
|
1833
1745
|
outcome: "failed",
|
|
1834
1746
|
stage: ready.stage,
|
|
1835
1747
|
error: ready.error,
|
|
@@ -1838,7 +1750,6 @@ export class ReplayController {
|
|
|
1838
1750
|
this.handOver(state, {
|
|
1839
1751
|
kind: "step_failed",
|
|
1840
1752
|
step,
|
|
1841
|
-
entry: plan.currentEntry(),
|
|
1842
1753
|
position: plan.currentStepIndex,
|
|
1843
1754
|
error: ready.error,
|
|
1844
1755
|
delivery: "none",
|
|
@@ -1872,7 +1783,7 @@ export class ReplayController {
|
|
|
1872
1783
|
// them alone is a baseline sample, not a hand-over "saving".
|
|
1873
1784
|
const ranOk = result.executed - result.errored;
|
|
1874
1785
|
state.stepsPinned += Math.max(0, ranOk);
|
|
1875
|
-
const position = stop.
|
|
1786
|
+
const position = stop.position ?? plan.stepCount;
|
|
1876
1787
|
const step = plan.allSteps()[position];
|
|
1877
1788
|
// Cut short by the clock, nothing failed: the steps that ran did their
|
|
1878
1789
|
// work and none is blamed. A divergence for the ledger — a partial
|
|
@@ -1890,11 +1801,10 @@ export class ReplayController {
|
|
|
1890
1801
|
const live = result.executed > 0;
|
|
1891
1802
|
this.handOver(state, {
|
|
1892
1803
|
// The plan only knows it stopped in front of its end; which kind of
|
|
1893
|
-
// stop that is — parked, a judgement
|
|
1894
|
-
//
|
|
1895
|
-
kind: stop.kind === "known_bad_step" ?
|
|
1804
|
+
// stop that is — parked, or a judgement — is decided here, where the
|
|
1805
|
+
// served reasons are.
|
|
1806
|
+
kind: stop.kind === "known_bad_step" ? stopKindOf(plan.stopStep()) : stop.kind,
|
|
1896
1807
|
step,
|
|
1897
|
-
entry: plan.allEntries()[position],
|
|
1898
1808
|
position,
|
|
1899
1809
|
error: stop.error,
|
|
1900
1810
|
// A cancelled run's note is kept for the model's next call, alone —
|
|
@@ -1905,12 +1815,9 @@ export class ReplayController {
|
|
|
1905
1815
|
ran: live ? undefined : 0,
|
|
1906
1816
|
// An output-logic failure stops *after* its step's tool ran: the step
|
|
1907
1817
|
// is done, its result is in the bundle, and it must not be run again.
|
|
1908
|
-
|
|
1909
|
-
// (`result_map`): that step did not run (AC5).
|
|
1910
|
-
ranThisStep: stop.stage === "tool_output_logic" && stop.reason !== "result_map",
|
|
1818
|
+
ranThisStep: stop.stage === "tool_output_logic",
|
|
1911
1819
|
// The other half (AC5): what the steps from here are for.
|
|
1912
1820
|
whenText: stop.whenText,
|
|
1913
|
-
subTask: stop.subTask,
|
|
1914
1821
|
});
|
|
1915
1822
|
if (stop.reason === "cancelled") {
|
|
1916
1823
|
// The caller is gone and reads nothing. The note stays undelivered: a
|
|
@@ -1947,7 +1854,6 @@ export class ReplayController {
|
|
|
1947
1854
|
this.handOver(state, {
|
|
1948
1855
|
kind: "other_branch",
|
|
1949
1856
|
step: plan.allSteps()[0],
|
|
1950
|
-
entry: plan.allEntries()[0],
|
|
1951
1857
|
position: 0,
|
|
1952
1858
|
delivery: "none",
|
|
1953
1859
|
ran: 0,
|
|
@@ -1962,7 +1868,6 @@ export class ReplayController {
|
|
|
1962
1868
|
this.handOver(state, {
|
|
1963
1869
|
kind: "step_failed",
|
|
1964
1870
|
step: plan.allSteps()[0],
|
|
1965
|
-
entry: plan.allEntries()[0],
|
|
1966
1871
|
position: 0,
|
|
1967
1872
|
error: "none of the scenario's steps could run here",
|
|
1968
1873
|
delivery: "none",
|
|
@@ -2026,23 +1931,20 @@ export class ReplayController {
|
|
|
2026
1931
|
if (state.reported || !state.scenarioId)
|
|
2027
1932
|
return undefined;
|
|
2028
1933
|
state.reported = true;
|
|
2029
|
-
const
|
|
1934
|
+
const deriveCostUsd = state.deriveCostUsd + Math.max(0, d.chainDeriveUsd ?? 0);
|
|
1935
|
+
const total = deriveCostUsd + d.sessionCostUsd + state.fallbackCostUsd;
|
|
2030
1936
|
const steps = this.stepResultsOf(state);
|
|
2031
1937
|
const isBaseline = state.outcome === "not_steered" || state.outcome === "failed";
|
|
2032
|
-
// The plan handed the task back by design, not by a fault: in front of the
|
|
2033
|
-
// other half of an "if … otherwise" (AC5), or at its decision (the
|
|
2034
|
-
// service's segmentsbreakdown.md SB-19). Such a stop is named whatever the
|
|
2035
|
-
// outcome, and the agent's work after it is the service's evidence.
|
|
2036
|
-
const handedBack = state.handover?.kind === "other_branch" || state.handover?.kind === "decision";
|
|
2037
1938
|
// A costless decline is noise on both sides — *unless* it says something the
|
|
2038
1939
|
// recording page cannot learn any other way: a step that actually broke, the
|
|
2039
|
-
// gate that declined, or a plan that
|
|
2040
|
-
//
|
|
2041
|
-
//
|
|
1940
|
+
// gate that declined, or a plan that stood in front of the other half of
|
|
1941
|
+
// its task (AC5), which the service takes the agent's work after as its
|
|
1942
|
+
// evidence of that half. The service accepts a costless report that carries
|
|
1943
|
+
// any of them (its errorshandling.md).
|
|
2042
1944
|
if (isBaseline &&
|
|
2043
1945
|
total <= 0 &&
|
|
2044
1946
|
!state.declined &&
|
|
2045
|
-
|
|
1947
|
+
state.handover?.kind !== "other_branch" &&
|
|
2046
1948
|
!steps?.some((s) => s.status === "failed")) {
|
|
2047
1949
|
return undefined;
|
|
2048
1950
|
}
|
|
@@ -2052,7 +1954,7 @@ export class ReplayController {
|
|
|
2052
1954
|
runId: d.runId,
|
|
2053
1955
|
ticket: state.ticket,
|
|
2054
1956
|
outcome: state.outcome,
|
|
2055
|
-
deriveCostUsd
|
|
1957
|
+
deriveCostUsd,
|
|
2056
1958
|
sessionCostUsd: d.sessionCostUsd,
|
|
2057
1959
|
fallbackCostUsd: state.fallbackCostUsd,
|
|
2058
1960
|
durationMs: d.durationMs,
|
|
@@ -2083,22 +1985,21 @@ export class ReplayController {
|
|
|
2083
1985
|
declined: state.declined,
|
|
2084
1986
|
// Where the plan handed the task to the model (fallbk.md). Only on
|
|
2085
1987
|
// `fell_back`: a hand-over before any step ran is `failed`, and a parked
|
|
2086
|
-
// step found after a divergence is still a divergence. Except
|
|
2087
|
-
//
|
|
2088
|
-
//
|
|
2089
|
-
// outcome: the agent's work after it is the service's evidence, whether
|
|
1988
|
+
// step found after a divergence is still a divergence. Except the other
|
|
1989
|
+
// half of an "if … otherwise" (AC5), named whatever the outcome: the
|
|
1990
|
+
// agent's work after it is the service's evidence of that half, whether
|
|
2090
1991
|
// or not a step ran first.
|
|
2091
|
-
fallbackStepIndex: state.outcome === "fell_back" ||
|
|
1992
|
+
fallbackStepIndex: state.outcome === "fell_back" || state.handover?.kind === "other_branch"
|
|
2092
1993
|
? state.handover?.stepIndex
|
|
2093
1994
|
: undefined,
|
|
2094
|
-
fallbackKind: state.outcome === "fell_back" ||
|
|
1995
|
+
fallbackKind: state.outcome === "fell_back" || state.handover?.kind === "other_branch"
|
|
2095
1996
|
? state.handover?.kind
|
|
2096
1997
|
: undefined,
|
|
2097
1998
|
// The agent did not take the plan's result and redid the task (AC1).
|
|
2098
1999
|
...(d.redoneBy ? { redoneBy: d.redoneBy } : {}),
|
|
2099
|
-
//
|
|
2100
|
-
//
|
|
2101
|
-
|
|
2000
|
+
// A continuation (the service's fallbk.md Part 3): the plan it continued
|
|
2001
|
+
// books the whole turn, so this report books no saving of its own.
|
|
2002
|
+
...(state.continuation ? { continues: { scenarioId: state.continuation.ofScenarioId } } : {}),
|
|
2102
2003
|
// A turn in which another plan ran is not a measurement of what this task
|
|
2103
2004
|
// costs unaided, so it is no baseline sample (segmented.md R-MONEY-5).
|
|
2104
2005
|
// This covers two plans that both armed, and — the case that was silently
|
|
@@ -2142,26 +2043,11 @@ export class ReplayController {
|
|
|
2142
2043
|
* there is nothing to be similar *to* and nothing to decline. It books no
|
|
2143
2044
|
* execution either — there is no ticket, and inventing a saving for a manual
|
|
2144
2045
|
* invocation is exactly the kind of number a ledger must never contain.
|
|
2145
|
-
*
|
|
2146
|
-
* One served stop is kept: a plan's decision (the service's
|
|
2147
|
-
* segmentsbreakdown.md SB-6). The run stops in front of it, and `stopped`
|
|
2148
|
-
* says so with the reason `decision`.
|
|
2149
2046
|
*/
|
|
2150
2047
|
async runAdHoc(scenario, prompt, wrapped) {
|
|
2151
2048
|
if (!isReadyScenario(scenario))
|
|
2152
2049
|
return { ok: false, why: "scenario is not ready" };
|
|
2153
|
-
|
|
2154
|
-
// flat list, one frame per call (segmented.md R-CALL-37).
|
|
2155
|
-
let flat;
|
|
2156
|
-
try {
|
|
2157
|
-
flat = flattenChain(scenario);
|
|
2158
|
-
}
|
|
2159
|
-
catch (err) {
|
|
2160
|
-
return { ok: false, why: `the chain could not be flattened: ${errText(err)}` };
|
|
2161
|
-
}
|
|
2162
|
-
if (flat.entries.length === 0)
|
|
2163
|
-
return { ok: false, why: "the chain has no runnable step" };
|
|
2164
|
-
const mode = modeFor(flat.entries.map((e) => e.step), wrapped, this.opts.allowServers, this.builtinTools);
|
|
2050
|
+
const mode = modeFor(scenario.steps, wrapped, this.opts.allowServers, this.builtinTools);
|
|
2165
2051
|
if (mode === "none")
|
|
2166
2052
|
return { ok: false, why: "no step is executable" };
|
|
2167
2053
|
const state = {
|
|
@@ -2183,15 +2069,11 @@ export class ReplayController {
|
|
|
2183
2069
|
reported: true,
|
|
2184
2070
|
retired: false,
|
|
2185
2071
|
armedBy: "prompt",
|
|
2186
|
-
// `bir replay` runs a scenario an operator named by id: never a segment
|
|
2187
|
-
// hand-out, and nothing to share a baseline with.
|
|
2188
|
-
kind: "scenario",
|
|
2189
|
-
sharedWith: [],
|
|
2190
2072
|
};
|
|
2191
|
-
// `bir replay` keeps its own contract on a missing target
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2194
|
-
//
|
|
2073
|
+
// `bir replay` keeps its own contract on a missing target: an operator
|
|
2074
|
+
// asked for this run by id, so a sample stands in and it never declines.
|
|
2075
|
+
// It says which, as the console's dry replay does (R-PARAM-7), so nobody
|
|
2076
|
+
// reads the output as proof the values were derived.
|
|
2195
2077
|
const schema = scenario.paramsObject ?? {};
|
|
2196
2078
|
const params = this.startDerivation(scenario, prompt, state).then((r) => {
|
|
2197
2079
|
const out = { ...r.params };
|
|
@@ -2210,25 +2092,13 @@ export class ReplayController {
|
|
|
2210
2092
|
}
|
|
2211
2093
|
return out;
|
|
2212
2094
|
});
|
|
2213
|
-
// The one served stop an operator's run keeps: a decision (the service's
|
|
2214
|
-
// segmentsbreakdown.md SB-6). The steps after it are one half of an "if …
|
|
2215
|
-
// otherwise", the half the recording took, and running them by id would
|
|
2216
|
-
// do that half's writes whatever the request. Every other served stop is
|
|
2217
|
-
// the plan's health, which an operator's run is there to try.
|
|
2218
|
-
const decisionAt = flat.entries.findIndex((e) => e.step.stop?.kind === "decision");
|
|
2219
|
-
const stopAt = decisionAt >= 0 && (flat.stopAt === undefined || decisionAt < flat.stopAt)
|
|
2220
|
-
? clampToFrameStart(flat.entries, decisionAt)
|
|
2221
|
-
: flat.stopAt;
|
|
2222
2095
|
const plan = new ScenarioReplayPlan({
|
|
2223
2096
|
scenario,
|
|
2224
2097
|
params,
|
|
2225
2098
|
mode,
|
|
2226
|
-
entries: flat.entries,
|
|
2227
|
-
stopAt,
|
|
2228
2099
|
fallbackTargets: "samples",
|
|
2229
2100
|
});
|
|
2230
2101
|
state.plan = plan;
|
|
2231
|
-
state.flatStop = stopAt === flat.stopAt ? flat.stopReason : undefined;
|
|
2232
2102
|
if (this.opts.authUrl && this.opts.authToken) {
|
|
2233
2103
|
state.sourceRun = new SourceRunOutputs({
|
|
2234
2104
|
baseUrl: this.opts.authUrl,
|
|
@@ -2244,8 +2114,8 @@ export class ReplayController {
|
|
|
2244
2114
|
coverage: coverageOf(scenario.steps, wrapped, this.opts.allowServers, this.builtinTools).join(","),
|
|
2245
2115
|
why: "bir replay — no match, an operator asked for it",
|
|
2246
2116
|
});
|
|
2247
|
-
// An operator's run keeps its own contract
|
|
2248
|
-
//
|
|
2117
|
+
// An operator's run keeps its own contract: out of time, the targets too
|
|
2118
|
+
// take their samples. What it never does is run on `{}`.
|
|
2249
2119
|
const ready = await this.readyFor(state, plan, this.adHocBudgets.deriveMs, { adHoc: true });
|
|
2250
2120
|
if (!ready.ok)
|
|
2251
2121
|
return { ok: false, why: ready.error };
|
|
@@ -2274,16 +2144,7 @@ export class ReplayController {
|
|
|
2274
2144
|
logDetail("replay.response_model_failed", { error: errText(err) });
|
|
2275
2145
|
}
|
|
2276
2146
|
const stop = result.stopped;
|
|
2277
|
-
const stoppedStep = stop ? plan.allSteps()[stop.
|
|
2278
|
-
// The plan's end in front of its decision: said so, by its own word.
|
|
2279
|
-
const atDecision = stop?.kind === "known_bad_step" && stoppedStep?.stop?.kind === "decision";
|
|
2280
|
-
if (atDecision) {
|
|
2281
|
-
logLine("replay.adhoc_decision", {
|
|
2282
|
-
scenario: scenario.id,
|
|
2283
|
-
step: stop.stepIndex,
|
|
2284
|
-
why: "bir replay stops in front of the plan's decision: the steps after it are the half the recording took",
|
|
2285
|
-
});
|
|
2286
|
-
}
|
|
2147
|
+
const stoppedStep = stop ? plan.allSteps()[stop.position ?? plan.stepCount] : undefined;
|
|
2287
2148
|
return {
|
|
2288
2149
|
ok: true,
|
|
2289
2150
|
text: result.text,
|
|
@@ -2295,7 +2156,7 @@ export class ReplayController {
|
|
|
2295
2156
|
? {
|
|
2296
2157
|
step: stop.stepIndex,
|
|
2297
2158
|
tool: stoppedStep?.toolName ?? "",
|
|
2298
|
-
reason:
|
|
2159
|
+
reason: stop.reason ?? stop.stage ?? stop.kind,
|
|
2299
2160
|
error: stop.error,
|
|
2300
2161
|
}
|
|
2301
2162
|
: undefined,
|
|
@@ -2358,8 +2219,6 @@ export class ReplayController {
|
|
|
2358
2219
|
fetchImpl: this.opts.fetchImpl,
|
|
2359
2220
|
liveCall: ctx.liveCall,
|
|
2360
2221
|
recentResults: ctx.recentResults,
|
|
2361
|
-
// A reading for a scenario that follows a plan's decision (offer1.md O4).
|
|
2362
|
-
...(ctx.atDecision ? { atDecision: ctx.atDecision } : {}),
|
|
2363
2222
|
// The request itself ends too. Without this a hung service or model held
|
|
2364
2223
|
// the socket for up to 300 s after every wait on it had given up. Long
|
|
2365
2224
|
// enough to cover both waits that can use its answer (the arm and the
|
|
@@ -2492,16 +2351,7 @@ export class ReplayController {
|
|
|
2492
2351
|
// when a step did its work — so the service takes the agent's work after
|
|
2493
2352
|
// it as that half's evidence. Booked `diverged`, it never did.
|
|
2494
2353
|
const otherHalf = stop?.kind === "other_branch";
|
|
2495
|
-
|
|
2496
|
-
// service's segmentsbreakdown.md SB-17) is a hand-over too: the plan did
|
|
2497
|
-
// all it was for, and the agent decides. Not one of recorded outputs
|
|
2498
|
-
// only, which is no step done here — that stays a plain divergence.
|
|
2499
|
-
const atDecision = !stop &&
|
|
2500
|
-
plan.stopsEarly() &&
|
|
2501
|
-
composed.endedAt === plan.stepCount &&
|
|
2502
|
-
(composed.executed > 0 || composed.recorded === 0) &&
|
|
2503
|
-
this.stopKindOf(state, plan.stopEntry()) === "decision";
|
|
2504
|
-
this.upgrade(state, broke || !didWork ? "failed" : otherHalf || atDecision ? "fell_back" : "diverged");
|
|
2354
|
+
this.upgrade(state, broke || !didWork ? "failed" : otherHalf ? "fell_back" : "diverged");
|
|
2505
2355
|
state.stepsPinned += composed.executed;
|
|
2506
2356
|
// The headline number of this whole design: recovering from a divergence
|
|
2507
2357
|
// costs nothing, because the proxies were already connected.
|
|
@@ -2512,7 +2362,7 @@ export class ReplayController {
|
|
|
2512
2362
|
recorded: composed.recorded,
|
|
2513
2363
|
skipped: composed.skipped,
|
|
2514
2364
|
errored: composed.errored || undefined,
|
|
2515
|
-
stopped: stop ? `${stop.
|
|
2365
|
+
stopped: stop ? `${stop.position ?? "?"}:${stop.reason ?? stop.stage ?? stop.kind}` : undefined,
|
|
2516
2366
|
bytes: composed.text.length,
|
|
2517
2367
|
costUsd: state.fallbackCostUsd.toFixed(2),
|
|
2518
2368
|
});
|
|
@@ -2528,40 +2378,16 @@ export class ReplayController {
|
|
|
2528
2378
|
// of recorded outputs only is last week's work. The model's own call goes
|
|
2529
2379
|
// ahead — unless a step was *sent* and never answered. That one may have
|
|
2530
2380
|
// done its work, and the model must know before it does the task itself.
|
|
2531
|
-
if (stop?.reason === "uncertain" && stop.
|
|
2381
|
+
if (stop?.reason === "uncertain" && stop.position !== undefined) {
|
|
2532
2382
|
const note = this.stopNote(state, plan, stop, "none", composed.left);
|
|
2533
2383
|
return this.deliverInstead(note, toolName, state.host !== undefined);
|
|
2534
2384
|
}
|
|
2535
2385
|
// The rest is the other half (AC5): the model is told so, in place of
|
|
2536
2386
|
// its call, before it does any of the plan's remaining steps.
|
|
2537
|
-
if (stop?.kind === "other_branch" && stop.
|
|
2387
|
+
if (stop?.kind === "other_branch" && stop.position !== undefined) {
|
|
2538
2388
|
const note = this.stopNote(state, plan, stop, "none", composed.left);
|
|
2539
2389
|
return this.deliverInstead(note, toolName, state.host !== undefined);
|
|
2540
2390
|
}
|
|
2541
|
-
// Every step left before the decision was one this request does not
|
|
2542
|
-
// need: the plan has done its part, and the agent decides. Told in
|
|
2543
|
-
// place of its call, like the other half. The steps that ran are the
|
|
2544
|
-
// model's own calls above; the ones left out are named.
|
|
2545
|
-
if (atDecision) {
|
|
2546
|
-
const parked = plan.stopStep();
|
|
2547
|
-
state.handover = {
|
|
2548
|
-
kind: "decision",
|
|
2549
|
-
stepIndex: callerStepIndexOf(plan.stopEntry()) ?? parked.stepIndex,
|
|
2550
|
-
toolName: parked.toolName ?? undefined,
|
|
2551
|
-
note: this.noteFor(state, {
|
|
2552
|
-
kind: "decision",
|
|
2553
|
-
step: parked,
|
|
2554
|
-
entry: plan.stopEntry(),
|
|
2555
|
-
position: plan.stepCount,
|
|
2556
|
-
delivery: "above",
|
|
2557
|
-
executedAbove: plan.currentStepIndex,
|
|
2558
|
-
leftOut: composed.left,
|
|
2559
|
-
}),
|
|
2560
|
-
delivered: true,
|
|
2561
|
-
};
|
|
2562
|
-
this.decidedInDivergence(state, plan);
|
|
2563
|
-
return this.deliverInstead(state.handover.note, toolName, state.host !== undefined);
|
|
2564
|
-
}
|
|
2565
2391
|
// Every remaining step was left out by its test: the steps the session
|
|
2566
2392
|
// ran were the whole of the plan this request needs, and it ran to its
|
|
2567
2393
|
// end (newestRun.md N7 reads that).
|
|
@@ -2576,13 +2402,13 @@ export class ReplayController {
|
|
|
2576
2402
|
// outcome stays as judged above, so the report carries no fallback fields
|
|
2577
2403
|
// (the other half of an "if … otherwise" excepted: see `buildReport`).
|
|
2578
2404
|
let text = composed.text;
|
|
2579
|
-
if (stop && (stop.kind === "step_failed" || stop.kind === "other_branch") && stop.
|
|
2405
|
+
if (stop && (stop.kind === "step_failed" || stop.kind === "other_branch") && stop.position !== undefined) {
|
|
2580
2406
|
const note = this.stopNote(state, plan, stop, "below", composed.left);
|
|
2581
2407
|
text = `${note}\n\n${composed.text}`;
|
|
2582
2408
|
}
|
|
2583
2409
|
else if (plan.stopsEarly() && composed.endedAt === plan.stepCount) {
|
|
2584
2410
|
const parked = plan.stopStep();
|
|
2585
|
-
const stopKind =
|
|
2411
|
+
const stopKind = stopKindOf(parked);
|
|
2586
2412
|
state.handover = {
|
|
2587
2413
|
kind: stopKind,
|
|
2588
2414
|
stepIndex: parked.stepIndex,
|
|
@@ -2590,7 +2416,6 @@ export class ReplayController {
|
|
|
2590
2416
|
note: this.noteFor(state, {
|
|
2591
2417
|
kind: stopKind,
|
|
2592
2418
|
step: parked,
|
|
2593
|
-
entry: plan.stopEntry(),
|
|
2594
2419
|
position: plan.stepCount,
|
|
2595
2420
|
delivery: "below",
|
|
2596
2421
|
executedAbove: plan.currentStepIndex,
|
|
@@ -2600,8 +2425,6 @@ export class ReplayController {
|
|
|
2600
2425
|
delivered: true,
|
|
2601
2426
|
};
|
|
2602
2427
|
text = `${state.handover.note}\n\n${composed.text}`;
|
|
2603
|
-
if (atDecision)
|
|
2604
|
-
this.decidedInDivergence(state, plan);
|
|
2605
2428
|
}
|
|
2606
2429
|
else if (!stop) {
|
|
2607
2430
|
// The bundle carries the rest of the chain, and its header tells the
|
|
@@ -2623,13 +2446,11 @@ export class ReplayController {
|
|
|
2623
2446
|
stopNote(state, plan, stop, delivery,
|
|
2624
2447
|
/** The steps the bundle left out (AC5): no step that ran. */
|
|
2625
2448
|
left = []) {
|
|
2626
|
-
const position = stop.
|
|
2449
|
+
const position = stop.position ?? plan.currentStepIndex;
|
|
2627
2450
|
const step = plan.allSteps()[position];
|
|
2628
|
-
const entry = plan.allEntries()[position];
|
|
2629
2451
|
const note = this.noteFor(state, {
|
|
2630
2452
|
kind: stop.kind,
|
|
2631
2453
|
step,
|
|
2632
|
-
entry,
|
|
2633
2454
|
position,
|
|
2634
2455
|
error: stop.error,
|
|
2635
2456
|
delivery,
|
|
@@ -2639,16 +2460,14 @@ export class ReplayController {
|
|
|
2639
2460
|
// steps that ran; "no step ran" would invite the model to run them again.
|
|
2640
2461
|
ran: delivery === "none" ? plan.currentStepIndex : undefined,
|
|
2641
2462
|
executedAbove: plan.currentStepIndex,
|
|
2642
|
-
// An output-logic failure stops after its step's tool ran (plan.ts)
|
|
2643
|
-
|
|
2644
|
-
ranThisStep: stop.stage === "tool_output_logic" && stop.reason !== "result_map",
|
|
2463
|
+
// An output-logic failure stops after its step's tool ran (plan.ts).
|
|
2464
|
+
ranThisStep: stop.stage === "tool_output_logic",
|
|
2645
2465
|
whenText: stop.whenText,
|
|
2646
|
-
subTask: stop.subTask,
|
|
2647
2466
|
leftOut: left,
|
|
2648
2467
|
});
|
|
2649
2468
|
state.handover ??= {
|
|
2650
2469
|
kind: stop.kind,
|
|
2651
|
-
stepIndex:
|
|
2470
|
+
stepIndex: step?.stepIndex ?? position,
|
|
2652
2471
|
toolName: step?.toolName ?? undefined,
|
|
2653
2472
|
error: stop.error,
|
|
2654
2473
|
note,
|
|
@@ -2669,17 +2488,9 @@ export class ReplayController {
|
|
|
2669
2488
|
return;
|
|
2670
2489
|
const ran = state.stepsPinned;
|
|
2671
2490
|
this.retire(state, ran > 0 ? "fell_back" : "failed");
|
|
2672
|
-
// A plan stopped at its decision did all it was for: its bill ends here,
|
|
2673
|
-
// and the agent's decision and the half it takes are the agent's own
|
|
2674
|
-
// (the service's segmentsbreakdown.md SB-26). Only once a step ran: a
|
|
2675
|
-
// plan that did nothing is a baseline sample of the whole turn.
|
|
2676
|
-
if (h.kind === "decision" && ran > 0)
|
|
2677
|
-
this.markRetired(state);
|
|
2678
2491
|
state.handover = {
|
|
2679
2492
|
kind: h.kind,
|
|
2680
|
-
|
|
2681
|
-
// number the caller's own chain does not have (R-CALL-32, R-FALL-7).
|
|
2682
|
-
stepIndex: callerStepIndexOf(h.entry) ?? h.step?.stepIndex ?? h.position,
|
|
2493
|
+
stepIndex: h.step?.stepIndex ?? h.position,
|
|
2683
2494
|
toolName: h.step?.toolName ?? undefined,
|
|
2684
2495
|
error: h.error,
|
|
2685
2496
|
note: this.noteFor(state, h),
|
|
@@ -2690,83 +2501,38 @@ export class ReplayController {
|
|
|
2690
2501
|
kind: h.kind,
|
|
2691
2502
|
step: `${h.position}/${state.plan?.totalSteps ?? state.stepsPlanned}`,
|
|
2692
2503
|
tool: h.step?.toolName ?? undefined,
|
|
2693
|
-
// The scenario that owns the stopped step, when it is not the matched one.
|
|
2694
|
-
owner: h.entry && h.entry.depth > 0 ? h.entry.scenarioId : undefined,
|
|
2695
|
-
unusable: state.flatStop?.reason,
|
|
2696
|
-
callIntent: state.flatStop?.callIntent ?? undefined,
|
|
2697
|
-
segment: state.flatStop?.segmentId ?? undefined,
|
|
2698
2504
|
ran,
|
|
2699
2505
|
outcome: state.outcome,
|
|
2700
2506
|
why: h.kind === "other_branch"
|
|
2701
2507
|
? h.noneNeeded
|
|
2702
2508
|
? "no step of the plan was needed for this request — the agent does the task"
|
|
2703
2509
|
: `this request is the other half: the steps from here are only for when ${h.whenText ?? "their test says so"} — the agent continues`
|
|
2704
|
-
: h.kind === "
|
|
2705
|
-
? "the
|
|
2706
|
-
: h.kind === "
|
|
2707
|
-
? "a
|
|
2708
|
-
: h.
|
|
2709
|
-
? "the
|
|
2710
|
-
: h.
|
|
2711
|
-
? "the
|
|
2712
|
-
: h.stopReason === "
|
|
2713
|
-
? "
|
|
2714
|
-
: h.stopReason === "
|
|
2715
|
-
? "
|
|
2716
|
-
:
|
|
2717
|
-
? "the host stopped this step itself — no fault of the plan; the agent continues from here"
|
|
2718
|
-
: h.stopReason === "not_run" || h.stopReason === "uncertain"
|
|
2719
|
-
? "a step produced no result here — the agent continues from here"
|
|
2720
|
-
: "a step broke — the agent continues from here",
|
|
2721
|
-
});
|
|
2722
|
-
}
|
|
2723
|
-
/**
|
|
2724
|
-
* Which kind of stop a plan's end is (plan-services.md D8). The stopping
|
|
2725
|
-
* step's own served reason wins — the calculation's verdict is more exact
|
|
2726
|
-
* than a counter — then a call the service could not answer, then a parked
|
|
2727
|
-
* step.
|
|
2728
|
-
*/
|
|
2729
|
-
stopKindOf(state, entry) {
|
|
2730
|
-
// The service serves a decision ahead of any other stop of its step
|
|
2731
|
-
// (the service's segmentsbreakdown.md SB-6), and so it is read first.
|
|
2732
|
-
if (entry?.step.stop?.kind === "decision")
|
|
2733
|
-
return "decision";
|
|
2734
|
-
if (entry?.step.stop?.kind === "nondeterministic")
|
|
2735
|
-
return "nondeterministic_step";
|
|
2736
|
-
return state.flatStop ? "unusable_call" : "known_bad_step";
|
|
2737
|
-
}
|
|
2738
|
-
/**
|
|
2739
|
-
* A divergence's bundle that ended at the plan's decision (SB-17): logged
|
|
2740
|
-
* as `handOver` logs every other hand-over, and its usage mark taken there
|
|
2741
|
-
* when a step of the plan ran (SB-26).
|
|
2742
|
-
*/
|
|
2743
|
-
decidedInDivergence(state, plan) {
|
|
2744
|
-
if (state.stepsPinned > 0)
|
|
2745
|
-
this.markRetired(state);
|
|
2746
|
-
logLine("replay.handover", {
|
|
2747
|
-
scenario: state.scenarioId ?? undefined,
|
|
2748
|
-
kind: "decision",
|
|
2749
|
-
step: `${plan.stepCount}/${plan.totalSteps}`,
|
|
2750
|
-
via: "divergence",
|
|
2751
|
-
ran: state.stepsPinned,
|
|
2752
|
-
outcome: state.outcome,
|
|
2753
|
-
why: "the plan ran the steps before its decision — the agent decides which way applies and continues",
|
|
2510
|
+
: h.kind === "known_bad_step"
|
|
2511
|
+
? "the next step has failed too often — the agent continues from here"
|
|
2512
|
+
: h.kind === "nondeterministic_step"
|
|
2513
|
+
? "the next step needs a judgement the plan cannot compute — the agent makes it"
|
|
2514
|
+
: h.stopReason === "concurrent"
|
|
2515
|
+
? "a second call arrived while the pinned step was still running — the agent continues from here"
|
|
2516
|
+
: h.stopReason === "budget" || h.stopReason === "cancelled"
|
|
2517
|
+
? "the run was cut short before this step — the agent continues from here"
|
|
2518
|
+
: h.stopReason === "host_rejected" || h.stopReason === "host_edited"
|
|
2519
|
+
? "the host stopped this step itself — no fault of the plan; the agent continues from here"
|
|
2520
|
+
: h.stopReason === "not_run" || h.stopReason === "uncertain"
|
|
2521
|
+
? "a step produced no result here — the agent continues from here"
|
|
2522
|
+
: "a step broke — the agent continues from here",
|
|
2754
2523
|
});
|
|
2755
2524
|
}
|
|
2756
2525
|
/**
|
|
2757
|
-
* The steps before
|
|
2758
|
-
*
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
2526
|
+
* The steps before position `bound` that ran, 1-based, and the ones their
|
|
2527
|
+
* tests left out (AC5) — the plan cursor's own, and a divergence bundle's
|
|
2528
|
+
* (`extra`). `left` is empty when no step was left out; `ran` is then not
|
|
2529
|
+
* filled, since every step before `bound` ran.
|
|
2761
2530
|
*/
|
|
2762
2531
|
ranBefore(plan, bound, extra = []) {
|
|
2763
|
-
const left = [...(plan?.leftOut() ?? []), ...extra].filter((l) => l.
|
|
2532
|
+
const left = [...(plan?.leftOut() ?? []), ...extra].filter((l) => l.position < bound);
|
|
2764
2533
|
if (left.length === 0)
|
|
2765
2534
|
return { ran: [], left };
|
|
2766
|
-
const out = new Set();
|
|
2767
|
-
for (const l of left)
|
|
2768
|
-
for (let i = l.flatIndex; i < Math.min(l.until, bound); i += 1)
|
|
2769
|
-
out.add(i);
|
|
2535
|
+
const out = new Set(left.map((l) => l.position));
|
|
2770
2536
|
const ran = [];
|
|
2771
2537
|
for (let i = 0; i < bound; i += 1)
|
|
2772
2538
|
if (!out.has(i))
|
|
@@ -2777,8 +2543,7 @@ export class ReplayController {
|
|
|
2777
2543
|
const plan = state.plan;
|
|
2778
2544
|
// The steps before the stop — or the bound a caller gives — less the ones
|
|
2779
2545
|
// their tests left out (AC5): a note must never say a step ran that did not.
|
|
2780
|
-
|
|
2781
|
-
const bound = h.ran ?? (h.ranThisStep || h.stopReason === "result_map" ? h.position + 1 : h.position);
|
|
2546
|
+
const bound = h.ran ?? (h.ranThisStep ? h.position + 1 : h.position);
|
|
2782
2547
|
const before = this.ranBefore(plan, bound, h.leftOut);
|
|
2783
2548
|
const skipped = before.left.length > 0;
|
|
2784
2549
|
const executed = skipped ? before.ran.length : bound;
|
|
@@ -2787,50 +2552,31 @@ export class ReplayController {
|
|
|
2787
2552
|
: before.ran.filter((n) => n <= h.executedAbove).length;
|
|
2788
2553
|
// A step its host stopped was dealt with there — refused, answered, or
|
|
2789
2554
|
// run with a person's arguments — so it is not among what is left to do.
|
|
2790
|
-
// Nor is the step a test left out, after which a sub-task's results could
|
|
2791
|
-
// not go back (`result_map`, AC5).
|
|
2792
2555
|
const handled = h.ranThisStep === true ||
|
|
2793
2556
|
h.stopReason === "host_rejected" ||
|
|
2794
|
-
h.stopReason === "host_edited"
|
|
2795
|
-
h.stopReason === "result_map";
|
|
2796
|
-
// A chain that holds a decision (the service's segmentsbreakdown.md SB-18)
|
|
2797
|
-
// keeps the decision note's protections in every note, even when the plan
|
|
2798
|
-
// stops earlier for another reason: no intent, no parameters, and no step
|
|
2799
|
-
// from the decision on. Each points at the half the recording took, and an
|
|
2800
|
-
// agent that follows it there runs that half's segment: the plan decided.
|
|
2801
|
-
const decisionAt = plan ? plan.decisionIndex() : -1;
|
|
2557
|
+
h.stopReason === "host_edited";
|
|
2802
2558
|
let params;
|
|
2803
2559
|
try {
|
|
2804
|
-
params =
|
|
2560
|
+
params = plan?.parameters;
|
|
2805
2561
|
}
|
|
2806
2562
|
catch {
|
|
2807
2563
|
params = undefined;
|
|
2808
2564
|
}
|
|
2809
2565
|
return buildHandoverNote({
|
|
2810
|
-
intent:
|
|
2566
|
+
intent: plan?.intent ?? "",
|
|
2811
2567
|
kind: h.kind,
|
|
2812
2568
|
stepIndex: h.position,
|
|
2813
2569
|
stepToolName: h.step?.toolName ?? undefined,
|
|
2814
2570
|
failureCount: h.step?.failureCount,
|
|
2815
2571
|
error: h.error,
|
|
2816
2572
|
stopReason: h.stopReason,
|
|
2817
|
-
unusable: state.flatStop
|
|
2818
|
-
? {
|
|
2819
|
-
reason: state.flatStop.reason,
|
|
2820
|
-
callIntent: state.flatStop.callIntent,
|
|
2821
|
-
segmentId: state.flatStop.segmentId,
|
|
2822
|
-
ownerScenarioId: state.flatStop.scenarioId,
|
|
2823
|
-
}
|
|
2824
|
-
: undefined,
|
|
2825
|
-
ownerScenarioId: h.entry && h.entry.depth > 0 ? h.entry.scenarioId : undefined,
|
|
2826
|
-
inSubTask: h.entry && h.entry.depth > 0 ? (h.entry.frame.callIntent ?? null) : null,
|
|
2827
2573
|
executed: Math.max(0, Math.min(executed, plan?.totalSteps ?? executed)),
|
|
2828
2574
|
...(skipped
|
|
2829
2575
|
? {
|
|
2830
2576
|
ranSteps: before.ran,
|
|
2831
2577
|
leftOut: before.left.map((l) => ({
|
|
2832
|
-
from: l.
|
|
2833
|
-
to:
|
|
2578
|
+
from: l.position + 1,
|
|
2579
|
+
to: l.position + 1,
|
|
2834
2580
|
toolName: l.toolName,
|
|
2835
2581
|
text: l.text,
|
|
2836
2582
|
})),
|
|
@@ -2842,17 +2588,12 @@ export class ReplayController {
|
|
|
2842
2588
|
totalSteps: plan?.totalSteps ?? state.stepsPlanned,
|
|
2843
2589
|
// A step that runs only under a test is marked so: the list must not
|
|
2844
2590
|
// read as steps this request needs (AC5).
|
|
2845
|
-
remainingTools: plan
|
|
2846
|
-
? plan.remainingFrom(handled ? h.position + 1 : h.position, decisionAt >= 0 ? decisionAt : undefined)
|
|
2847
|
-
: [],
|
|
2591
|
+
remainingTools: plan ? plan.remainingFrom(handled ? h.position + 1 : h.position) : [],
|
|
2848
2592
|
params,
|
|
2849
2593
|
delivery: h.delivery,
|
|
2850
2594
|
whenText: h.whenText ?? null,
|
|
2851
|
-
subTask: h.subTask ?? null,
|
|
2852
2595
|
// "None was needed" is the note's own wording for the end of the plan.
|
|
2853
2596
|
...(h.noneNeeded ? { stepIndex: plan?.totalSteps ?? state.stepsPlanned } : {}),
|
|
2854
|
-
// The request's own words for the choice (SB-18); never the service's.
|
|
2855
|
-
...(h.kind === "decision" ? { request: state.request ?? null } : {}),
|
|
2856
2597
|
});
|
|
2857
2598
|
}
|
|
2858
2599
|
/**
|
|
@@ -2991,8 +2732,8 @@ export class ReplayController {
|
|
|
2991
2732
|
return undefined;
|
|
2992
2733
|
// A step a test left out passes over its recorded output, unless a
|
|
2993
2734
|
// correction added it (AC5, source-run.ts).
|
|
2994
|
-
return Object.assign((
|
|
2995
|
-
leftOut: (
|
|
2735
|
+
return Object.assign((step) => source.outputFor(step), {
|
|
2736
|
+
leftOut: (step) => source.leftOut(step),
|
|
2996
2737
|
});
|
|
2997
2738
|
}
|
|
2998
2739
|
/**
|
|
@@ -3071,7 +2812,7 @@ export class ReplayController {
|
|
|
3071
2812
|
}
|
|
3072
2813
|
if (!opts.adHoc) {
|
|
3073
2814
|
const schema = plan.paramsSchema;
|
|
3074
|
-
const planned = plan.
|
|
2815
|
+
const planned = plan.allSteps().slice(0, plan.stepCount);
|
|
3075
2816
|
const blocked = blockingTargets(targetKeys(schema), schema, planned, plan.paramsLogic);
|
|
3076
2817
|
if (blocked.length > 0) {
|
|
3077
2818
|
return {
|
|
@@ -3088,7 +2829,7 @@ export class ReplayController {
|
|
|
3088
2829
|
const missingAgain = state.rederivedMissing;
|
|
3089
2830
|
state.rederivedMissing = undefined;
|
|
3090
2831
|
if (missingAgain && !opts.adHoc) {
|
|
3091
|
-
const planned = plan.
|
|
2832
|
+
const planned = plan.allSteps().slice(0, plan.stepCount);
|
|
3092
2833
|
const blocked = blockingTargets(missingAgain, plan.paramsSchema, planned, plan.paramsLogic);
|
|
3093
2834
|
if (blocked.length > 0) {
|
|
3094
2835
|
return {
|
|
@@ -3101,7 +2842,7 @@ export class ReplayController {
|
|
|
3101
2842
|
}
|
|
3102
2843
|
const sink = opts.adHoc
|
|
3103
2844
|
? undefined
|
|
3104
|
-
: sinkOfPlan(plan.
|
|
2845
|
+
: sinkOfPlan(plan.allSteps().slice(0, plan.stepCount), state.host?.sinks);
|
|
3105
2846
|
if (sink) {
|
|
3106
2847
|
const bad = unsafeParameter(plan.derivedValues, plan.paramsSchema, sink);
|
|
3107
2848
|
if (bad) {
|
|
@@ -3200,12 +2941,7 @@ export class ReplayController {
|
|
|
3200
2941
|
*/
|
|
3201
2942
|
recordStep(state, info) {
|
|
3202
2943
|
const status = info.outcome === "executed" ? "ok" : info.outcome;
|
|
3203
|
-
|
|
3204
|
-
// in one plan both have a step 0, and one would otherwise overwrite the
|
|
3205
|
-
// other's verdict (segmented.md R-CALL-13).
|
|
3206
|
-
const key = info.entry
|
|
3207
|
-
? `${info.entry.scenarioId}#${info.entry.stepIndex}`
|
|
3208
|
-
: `#${info.step.stepIndex}`;
|
|
2944
|
+
const key = `#${info.step.stepIndex}`;
|
|
3209
2945
|
if (state.stepResults.size >= MAX_STEP_RESULTS && !state.stepResults.has(key)) {
|
|
3210
2946
|
return;
|
|
3211
2947
|
}
|
|
@@ -3214,29 +2950,19 @@ export class ReplayController {
|
|
|
3214
2950
|
? `${info.error.slice(0, MAX_STEP_ERROR_CHARS)}…`
|
|
3215
2951
|
: info.error
|
|
3216
2952
|
: undefined;
|
|
3217
|
-
// Payloads only for a step of the reported scenario's own chain. The
|
|
3218
|
-
// console still files an execution's verdicts by step number alone
|
|
3219
|
-
// (plan-services.md W1.9), and a called segment numbers its steps from
|
|
3220
|
-
// zero as well: a called step's call would be shown under the caller's
|
|
3221
|
-
// step of the same number — the very mix-up the payloads are there to end.
|
|
3222
|
-
const own = !info.entry || info.entry.depth === 0;
|
|
3223
2953
|
// A step whose input logic threw never had an input: the `{}` the plan
|
|
3224
2954
|
// reports there is a placeholder, not a call with no arguments. Nor had a
|
|
3225
2955
|
// step its test left out or stopped in front of (AC5).
|
|
3226
2956
|
const noCall = info.stage === "tool_input_logic" || info.stage === "when_logic" || status === "not_taken";
|
|
3227
|
-
const sent =
|
|
3228
|
-
const received =
|
|
2957
|
+
const sent = !noCall ? info.input : undefined;
|
|
2958
|
+
const received = !noCall ? info.output : undefined;
|
|
3229
2959
|
// The call, kept whole for the turn's newest run (AC2): only one that
|
|
3230
2960
|
// really went out — it answered, or it answered with an error.
|
|
3231
2961
|
if (state.keepCalls &&
|
|
3232
2962
|
!noCall &&
|
|
3233
2963
|
info.input !== undefined &&
|
|
3234
2964
|
(status === "ok" || (status === "failed" && info.stage === "tool_call"))) {
|
|
3235
|
-
|
|
3236
|
-
// makes two calls of each of its steps, and both are the turn's (AC5
|
|
3237
|
-
// review). A frame is one call of one sub-task.
|
|
3238
|
-
const callKey = info.entry ? `${info.entry.frame.id}#${info.entry.stepIndex}` : key;
|
|
3239
|
-
this.keepCall(state, callKey, info, status === "failed");
|
|
2965
|
+
this.keepCall(state, key, info, status === "failed");
|
|
3240
2966
|
}
|
|
3241
2967
|
state.stepResults.set(key, {
|
|
3242
2968
|
stepIndex: info.step.stepIndex,
|
|
@@ -3252,10 +2978,6 @@ export class ReplayController {
|
|
|
3252
2978
|
// The tool is gone from its server: the service repairs the plan without
|
|
3253
2979
|
// waiting out the grace (plan-services.md D8, kind 2).
|
|
3254
2980
|
toolMissing: info.toolMissing || undefined,
|
|
3255
|
-
// A verdict about a called segment's step is counted on that segment, at
|
|
3256
|
-
// the revision it was served (R-CALL-13).
|
|
3257
|
-
scenarioId: info.entry && info.entry.depth > 0 ? info.entry.scenarioId : undefined,
|
|
3258
|
-
chainRevision: info.entry && info.entry.depth > 0 ? info.entry.frame.chainRevision : undefined,
|
|
3259
2981
|
});
|
|
3260
2982
|
}
|
|
3261
2983
|
/**
|
|
@@ -3274,14 +2996,11 @@ export class ReplayController {
|
|
|
3274
2996
|
? serializeCapped(redact(info.input ?? {}))
|
|
3275
2997
|
: reportPayload(serializeCapped(redact(info.input ?? {})), true);
|
|
3276
2998
|
const answer = info.output === undefined ? undefined : room ? heldPayload(info.output) : reportPayload(info.output);
|
|
3277
|
-
const inSegment = info.entry && info.entry.depth > 0;
|
|
3278
2999
|
const call = {
|
|
3279
|
-
scenarioId:
|
|
3000
|
+
scenarioId: state.scenarioId ?? "",
|
|
3280
3001
|
stepIndex: info.step.stepIndex,
|
|
3281
|
-
chainRevision:
|
|
3282
|
-
|
|
3283
|
-
: (state.chainRevision ?? null),
|
|
3284
|
-
toolName: info.step.toolName ?? "",
|
|
3002
|
+
chainRevision: state.chainRevision ?? null,
|
|
3003
|
+
toolName: info.step.toolName,
|
|
3285
3004
|
input,
|
|
3286
3005
|
...(failed
|
|
3287
3006
|
? { error: info.error ? capText(redactText(info.error), DEFAULT_MAX_PAYLOAD_BYTES) : answer }
|
|
@@ -3305,8 +3024,6 @@ export class ReplayController {
|
|
|
3305
3024
|
if (info.outcome === "failed") {
|
|
3306
3025
|
logLine("replay.step_failed", {
|
|
3307
3026
|
n: info.step.stepIndex,
|
|
3308
|
-
scenario: info.entry && info.entry.depth > 0 ? info.entry.scenarioId : undefined,
|
|
3309
|
-
frame: info.entry?.frame.id,
|
|
3310
3027
|
tool: info.step.toolName ?? undefined,
|
|
3311
3028
|
stage: info.stage,
|
|
3312
3029
|
why: info.stage === "tool_call"
|
|
@@ -3338,8 +3055,7 @@ export class ReplayController {
|
|
|
3338
3055
|
const text = testWords(info.step.when?.text);
|
|
3339
3056
|
logLine("replay.step_not_taken", {
|
|
3340
3057
|
n: info.step.stepIndex,
|
|
3341
|
-
tool: info.step.toolName
|
|
3342
|
-
scenario: info.entry && info.entry.depth > 0 ? info.entry.scenarioId : undefined,
|
|
3058
|
+
tool: info.step.toolName,
|
|
3343
3059
|
why: `its test said this request does not need it${text ? ` — only for when ${text}` : ""}; left out, and the plan goes on`,
|
|
3344
3060
|
});
|
|
3345
3061
|
return;
|
|
@@ -3363,10 +3079,17 @@ export class ReplayController {
|
|
|
3363
3079
|
this.upgrade(state, outcome);
|
|
3364
3080
|
// Only a plan that *finished* stops the clock (segmented.md R-MONEY-3). A
|
|
3365
3081
|
// hand-over or a divergence takes no mark: the agent's next tokens are the
|
|
3366
|
-
// consequence of this plan stopping, and belong to its bill.
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3082
|
+
// consequence of this plan stopping, and belong to its bill.
|
|
3083
|
+
if (completing) {
|
|
3084
|
+
state.retiredAt = Date.now();
|
|
3085
|
+
try {
|
|
3086
|
+
state.retiredMark = state.markUsage?.();
|
|
3087
|
+
}
|
|
3088
|
+
catch {
|
|
3089
|
+
// An unreadable transcript leaves the mark undefined, and the window
|
|
3090
|
+
// falls back to the next arm or the end of the turn.
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3370
3093
|
state.retired = true;
|
|
3371
3094
|
state.pinned.clear();
|
|
3372
3095
|
// A note naming a step to call next is void once the plan is: after a
|
|
@@ -3378,21 +3101,6 @@ export class ReplayController {
|
|
|
3378
3101
|
// A call waiting for a pin to thread has nothing left to wait for.
|
|
3379
3102
|
this.notifyPins(state);
|
|
3380
3103
|
}
|
|
3381
|
-
/**
|
|
3382
|
-
* Where the plan's work ended, for its bill: now, and the transcript's
|
|
3383
|
-
* watermark now. Taken at a completing retire, and at a decision hand-over
|
|
3384
|
-
* after a step ran (the service's segmentsbreakdown.md SB-26).
|
|
3385
|
-
*/
|
|
3386
|
-
markRetired(state) {
|
|
3387
|
-
state.retiredAt = Date.now();
|
|
3388
|
-
try {
|
|
3389
|
-
state.retiredMark = state.markUsage?.();
|
|
3390
|
-
}
|
|
3391
|
-
catch {
|
|
3392
|
-
// An unreadable transcript leaves the mark undefined, and the window
|
|
3393
|
-
// falls back to the next arm or the end of the turn.
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
3104
|
}
|
|
3397
3105
|
/** Every target of a schema. A parameter with no kind is one (R-PARAM-3). */
|
|
3398
3106
|
function targetKeys(schema) {
|
|
@@ -3530,58 +3238,29 @@ function heldPayload(text) {
|
|
|
3530
3238
|
}
|
|
3531
3239
|
return capText(text, DEFAULT_MAX_PAYLOAD_BYTES, redactText);
|
|
3532
3240
|
}
|
|
3533
|
-
/** Whether
|
|
3534
|
-
function underTest(
|
|
3535
|
-
|
|
3536
|
-
return true;
|
|
3537
|
-
for (let f = entry.frame; f; f = f.parent)
|
|
3538
|
-
if (f.when)
|
|
3539
|
-
return true;
|
|
3540
|
-
return false;
|
|
3241
|
+
/** Whether a step runs only under a test (AC5). */
|
|
3242
|
+
function underTest(step) {
|
|
3243
|
+
return Boolean(step.when);
|
|
3541
3244
|
}
|
|
3542
|
-
/** The tests of a
|
|
3543
|
-
function testLines(
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
const { startsAt } = frameEdges(entries);
|
|
3547
|
-
const lines = entries
|
|
3548
|
-
.map((e, i) => {
|
|
3549
|
-
const own = e.step.when;
|
|
3550
|
-
const call = (startsAt.get(i) ?? []).find((f) => f.when)?.when;
|
|
3551
|
-
const t = own ?? call;
|
|
3552
|
-
return t ? `${i}:${call && !own ? "call/" : ""}${t.otherwise}` : undefined;
|
|
3553
|
-
})
|
|
3245
|
+
/** The tests of a chain, by position, for the `plan.armed` line (AC5). */
|
|
3246
|
+
function testLines(steps) {
|
|
3247
|
+
const lines = steps
|
|
3248
|
+
.map((s, i) => (s.when ? `${i}:${s.when.otherwise}` : undefined))
|
|
3554
3249
|
.filter((s) => s !== undefined);
|
|
3555
3250
|
return lines.length > 0 ? lines.join(",") : undefined;
|
|
3556
3251
|
}
|
|
3557
3252
|
/**
|
|
3558
|
-
*
|
|
3559
|
-
*
|
|
3560
|
-
*
|
|
3561
|
-
* call row that opened its frame does. Reporting the segment's own index would
|
|
3562
|
-
* line the execution up against a step the caller's card does not have.
|
|
3253
|
+
* Which kind of stop a plan's end is (plan-services.md D8): the stopping
|
|
3254
|
+
* step's own served reason wins — the calculation's verdict is more exact
|
|
3255
|
+
* than a counter — then a parked step.
|
|
3563
3256
|
*/
|
|
3564
|
-
function
|
|
3565
|
-
|
|
3566
|
-
return entry?.stepIndex;
|
|
3567
|
-
let frame = entry.frame;
|
|
3568
|
-
while (frame.parent && frame.depth > 1)
|
|
3569
|
-
frame = frame.parent;
|
|
3570
|
-
return frame.callerStepIndex;
|
|
3571
|
-
}
|
|
3572
|
-
/** The calls in a served chain, for the `plan.armed` line (R-CALL-32). */
|
|
3573
|
-
function callLines(scenario) {
|
|
3574
|
-
const calls = (scenario.steps ?? []).filter((s) => s.kind === "segment");
|
|
3575
|
-
if (calls.length === 0)
|
|
3576
|
-
return undefined;
|
|
3577
|
-
return calls
|
|
3578
|
-
.map((s) => `${s.segmentId ?? "-"}:${s.unusable ?? s.resolution?.state ?? "resolved"}`)
|
|
3579
|
-
.join(",");
|
|
3257
|
+
function stopKindOf(step) {
|
|
3258
|
+
return step?.stop?.kind === "nondeterministic" ? "nondeterministic_step" : "known_bad_step";
|
|
3580
3259
|
}
|
|
3581
|
-
/** The served stops in a
|
|
3582
|
-
function stopLines(
|
|
3583
|
-
const lines =
|
|
3584
|
-
.map((
|
|
3260
|
+
/** The served stops in a chain, for the `plan.armed` line (plan-services.md D8). */
|
|
3261
|
+
function stopLines(steps) {
|
|
3262
|
+
const lines = steps
|
|
3263
|
+
.map((s, i) => (s.stop ? `${i}:${s.stop.kind}${s.stop.repairDue ? "/repair" : ""}` : undefined))
|
|
3585
3264
|
.filter((s) => s !== undefined);
|
|
3586
3265
|
return lines.length > 0 ? lines.join(",") : undefined;
|
|
3587
3266
|
}
|
|
@@ -3606,8 +3285,7 @@ function isKnownBad(step, maxStepFailures) {
|
|
|
3606
3285
|
*
|
|
3607
3286
|
* A step that carries `stop` was decided by the service — `null` means run it,
|
|
3608
3287
|
* even when its count is past the limit: its retry is due. A step without the
|
|
3609
|
-
* field came from an older service, and the old counter rule decides it.
|
|
3610
|
-
* can sit in one chain, since a called segment is served on its own terms.
|
|
3288
|
+
* field came from an older service, and the old counter rule decides it.
|
|
3611
3289
|
*/
|
|
3612
3290
|
export function stopIndex(steps, maxStepFailures) {
|
|
3613
3291
|
const i = steps.findIndex((s) => s.stop !== undefined ? s.stop !== null : isKnownBad(s, maxStepFailures));
|