@basein/runner 0.3.17 → 0.3.19

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.
Files changed (51) hide show
  1. package/dist/bin/bir-hooks.d.ts +11 -8
  2. package/dist/bin/bir-hooks.js +11 -8
  3. package/dist/bin/bir-scenario.d.ts +5 -1
  4. package/dist/bin/bir-scenario.js +155 -5
  5. package/dist/bin/bir.js +22 -9
  6. package/dist/bin/investigate.d.ts +9 -0
  7. package/dist/bin/investigate.js +26 -12
  8. package/dist/bin/scenario-edit.d.ts +7 -0
  9. package/dist/bin/scenario-edit.js +19 -2
  10. package/dist/bin/scenario-liveness.d.ts +12 -1
  11. package/dist/bin/scenario-liveness.js +8 -4
  12. package/dist/bin/setup.js +3 -3
  13. package/dist/control/client.d.ts +5 -0
  14. package/dist/control/client.js +7 -0
  15. package/dist/control/ensure-hook.d.ts +14 -3
  16. package/dist/control/ensure-hook.js +18 -5
  17. package/dist/control/recorder-main.d.ts +15 -0
  18. package/dist/control/recorder-main.js +33 -8
  19. package/dist/control/server.d.ts +192 -13
  20. package/dist/control/server.js +819 -41
  21. package/dist/control/transcript.d.ts +19 -0
  22. package/dist/control/transcript.js +57 -0
  23. package/dist/proxy/intercept.js +13 -0
  24. package/dist/proxy/session.d.ts +23 -0
  25. package/dist/proxy/session.js +71 -3
  26. package/dist/record/recorder.d.ts +156 -2
  27. package/dist/record/recorder.js +9 -0
  28. package/dist/record/remote-recorder.d.ts +59 -1
  29. package/dist/record/remote-recorder.js +297 -20
  30. package/dist/record/switchable-recorder.js +6 -0
  31. package/dist/replay/bundle.d.ts +8 -0
  32. package/dist/replay/bundle.js +20 -6
  33. package/dist/replay/condition-words.d.ts +28 -0
  34. package/dist/replay/condition-words.js +59 -0
  35. package/dist/replay/controller.d.ts +78 -7
  36. package/dist/replay/controller.js +318 -62
  37. package/dist/replay/derive.d.ts +15 -0
  38. package/dist/replay/derive.js +5 -0
  39. package/dist/replay/handover.d.ts +7 -0
  40. package/dist/replay/handover.js +89 -51
  41. package/dist/replay/plan.d.ts +101 -5
  42. package/dist/replay/plan.js +260 -18
  43. package/dist/replay/source-run.d.ts +2 -2
  44. package/dist/replay/source-run.js +3 -3
  45. package/dist/replay/targets.d.ts +40 -1
  46. package/dist/replay/targets.js +105 -3
  47. package/dist/replay/types.d.ts +25 -8
  48. package/docs/calculatedReplay.md +45 -5
  49. package/docs/calculatedReplayGuide.md +13 -4
  50. package/package.json +1 -1
  51. package/production.md +21 -7
@@ -25,9 +25,93 @@ import { flatEntriesOf, frameEdges } from "./flatten.js";
25
25
  import { assembleBundle, bundleInput, MAX_REPLAY_REASON, testWords } from "./bundle.js";
26
26
  import { redact, redactText } from "../record/redact.js";
27
27
  import { sinkOfPlan, unsafeParameter } from "./safety.js";
28
+ import { testedOnlyTargets } from "./targets.js";
28
29
  import { errText } from "../util/log.js";
29
30
  import { awaitWithin } from "../util/service-health.js";
30
31
  import { isMissingToolError, toolResultError } from "./tool-error.js";
32
+ /**
33
+ * A value a step of the recording worked out — an address the directory
34
+ * returned, the id a search found; the service labels it `from: "step"` —
35
+ * is never replayed from the recording (issue 1212). The plan holds this mark
36
+ * in its place. A step whose input still holds it, because its earlier steps
37
+ * did not return the value and its code fell back on the parameter, stops the
38
+ * plan in front of it, for the agent; a summary that holds it is left out.
39
+ * Never the recorded e-mail address, never the recorded lead.
40
+ */
41
+ export const FROM_STEP_MARK = "\u2063bir-from-step:";
42
+ const markOf = (key) => `${FROM_STEP_MARK}${key}\u2063`;
43
+ /**
44
+ * A value the request did not give, which only the steps of one half of an
45
+ * "if \u2026 otherwise" read (the service's review of calcscenarioautocorrect.md,
46
+ * item 11; targets.ts, `testedOnlyTargets`). The plan arms without it \u2014 the
47
+ * other half needs nothing it lacks \u2014 and holds this mark in its place, never
48
+ * `null` and never the recorded value. A step whose input reads or holds it
49
+ * stops in front of its call, and the agent does that half; a test that reads
50
+ * it decides nothing; a summary that reads it is left out.
51
+ */
52
+ export const MISSING_VALUE_MARK = "\u2063bir-missing:";
53
+ const missingMarkOf = (key) => `${MISSING_VALUE_MARK}${key}\u2063`;
54
+ /**
55
+ * The plan's values a step worked out in the recording: settings labelled
56
+ * `step` that code found in an earlier step's output (`verified`), so the
57
+ * plan's own steps have them. One the calculation only claimed has no such
58
+ * source, and keeps its recorded value as before.
59
+ */
60
+ function fromStepKeys(schema) {
61
+ if (!schema || typeof schema !== "object")
62
+ return [];
63
+ return Object.entries(schema)
64
+ .filter(([, p]) => p?.kind === "setting" && p.from === "step" && p.verified === true)
65
+ .map(([key]) => key);
66
+ }
67
+ /**
68
+ * A step's code read a value a step works out, which here only the recording
69
+ * holds (issue 1212). Not a fault of the step: its earlier steps did not
70
+ * return the value this time. The plan stops in front of it, the step counts
71
+ * as not run, and the agent does the rest.
72
+ */
73
+ export class RecordedValueError extends Error {
74
+ key;
75
+ constructor(key, message) {
76
+ super(message ?? `the value "${key}" was not in what the earlier steps returned, and a replay never takes it from the recording`);
77
+ this.name = "RecordedValueError";
78
+ this.key = key;
79
+ }
80
+ }
81
+ /**
82
+ * A step's code read a value the request did not give ({@link
83
+ * MISSING_VALUE_MARK}). The same stop as {@link RecordedValueError} \u2014 not a
84
+ * fault of the step: it counts as not run, nothing is parked or repaired, and
85
+ * the agent does the rest \u2014 so it is one, and every place that tells that
86
+ * stop apart tells this one apart too. Its words name the value.
87
+ */
88
+ export class MissingValueError extends RecordedValueError {
89
+ constructor(key) {
90
+ super(key, `a value the request did not give: ${key}`);
91
+ this.name = "MissingValueError";
92
+ }
93
+ }
94
+ /** The first mark `value` holds anywhere, and which kind it is. */
95
+ function markIn(value) {
96
+ let text;
97
+ try {
98
+ text = typeof value === "string" ? value : (JSON.stringify(value) ?? "");
99
+ }
100
+ catch {
101
+ return undefined;
102
+ }
103
+ const step = text.indexOf(FROM_STEP_MARK);
104
+ const missing = text.indexOf(MISSING_VALUE_MARK);
105
+ if (step === -1 && missing === -1)
106
+ return undefined;
107
+ const isMissing = missing !== -1 && (step === -1 || missing < step);
108
+ const at = isMissing ? missing + MISSING_VALUE_MARK.length : step + FROM_STEP_MARK.length;
109
+ return { key: text.slice(at).split("\u2063")[0], missing: isMissing };
110
+ }
111
+ /** The value a mark stands for, when `value` holds one anywhere \u2014 either kind. */
112
+ export function markedKey(value) {
113
+ return markIn(value)?.key;
114
+ }
31
115
  /** A rejection from an {@link ExecuteStep} that says what happened to the call. */
32
116
  export class StepRunError extends Error {
33
117
  rejection;
@@ -229,7 +313,8 @@ function reportingStage(onStep, entry, stage, input, startedAt, fn, output) {
229
313
  entry,
230
314
  input,
231
315
  output,
232
- outcome: "failed",
316
+ // A value only the recording holds is no fault of the step's (issue 1212).
317
+ outcome: err instanceof RecordedValueError ? "skipped" : "failed",
233
318
  stage,
234
319
  error: err instanceof Error ? err.message : String(err),
235
320
  ms: Date.now() - startedAt,
@@ -246,6 +331,16 @@ export class ScenarioReplayPlan {
246
331
  /** The flat list this plan walks. One entry per step that will really run. */
247
332
  entries;
248
333
  paramsPromise;
334
+ /** The values a step worked out in the recording ({@link FROM_STEP_MARK}). */
335
+ fromStep;
336
+ /**
337
+ * The targets only the steps of one half read (targets.ts,
338
+ * `testedOnlyTargets`): each held as {@link MISSING_VALUE_MARK} when the
339
+ * request does not give it.
340
+ */
341
+ oneHalf;
342
+ /** Of {@link oneHalf}, the ones the request did not give: held as the mark now. */
343
+ missingMarked = [];
249
344
  params = {};
250
345
  /** The derivation's own answer, before `paramsLogic`. What safety.ts judges. */
251
346
  derivedParams = {};
@@ -279,6 +374,7 @@ export class ScenarioReplayPlan {
279
374
  this.entries = opts.entries ?? flatEntriesOf(opts.scenario);
280
375
  this.mode = opts.mode;
281
376
  this.paramsPromise = opts.params;
377
+ this.fromStep = fromStepKeys(opts.scenario.paramsObject);
282
378
  this.fallbackTargets = opts.fallbackTargets ?? "null";
283
379
  this.respParams = { ...(opts.respParamsInit ?? {}) };
284
380
  // The caller's own frame accumulates into the plan's `respParams`, so
@@ -291,6 +387,12 @@ export class ScenarioReplayPlan {
291
387
  typeof stop === "number" && stop >= 0 && stop < this.entries.length
292
388
  ? stop
293
389
  : this.entries.length;
390
+ // Judged over the steps this plan will run, as the gate that armed it was.
391
+ const schema = opts.scenario.paramsObject;
392
+ this.oneHalf =
393
+ schema && typeof schema === "object"
394
+ ? testedOnlyTargets(Object.keys(schema).filter((k) => schema[k]?.kind !== "setting"), schema, this.entries.slice(0, this.stopAt), opts.scenario.paramsLogic)
395
+ : [];
294
396
  this.edges = frameEdges(this.entries);
295
397
  this.entries.forEach((entry, index) => {
296
398
  this.positions.set(entry, index);
@@ -477,13 +579,17 @@ export class ScenarioReplayPlan {
477
579
  if (this.paramsSettled)
478
580
  return Promise.resolve();
479
581
  if (!this.readyPromise) {
480
- this.readyPromise = this.paramsPromise
582
+ const source = this.paramsPromise;
583
+ this.readyPromise = source
481
584
  .then((derived) => derived, () => undefined)
482
585
  .then((derived) => {
483
586
  // Settled already, without the derivation: a late answer must not
484
587
  // change the parameters under steps that ran on the others.
485
588
  if (this.paramsSettled)
486
589
  return;
590
+ // Read again since (`resetParams`): this answer is no one's now.
591
+ if (this.paramsPromise !== source)
592
+ return;
487
593
  this.settleParams(derived ?? this.fallbackValues());
488
594
  });
489
595
  }
@@ -508,6 +614,20 @@ export class ScenarioReplayPlan {
508
614
  return;
509
615
  this.settleParams(this.fallbackValues());
510
616
  }
617
+ /**
618
+ * The values read again, before any step ran (issue 1212): the agent's own
619
+ * day showed in the session only after the first reading. False once a step
620
+ * has run, when the values the steps ran on are the ones that stand.
621
+ */
622
+ resetParams(params) {
623
+ if (this.stepIndex > 0)
624
+ return false;
625
+ params.catch(() => undefined);
626
+ this.paramsPromise = params;
627
+ this.readyPromise = undefined;
628
+ this.paramsSettled = false;
629
+ return true;
630
+ }
511
631
  /** Whether the parameters are final. */
512
632
  get parametersSettled() {
513
633
  return this.paramsSettled;
@@ -530,9 +650,28 @@ export class ScenarioReplayPlan {
530
650
  settleParams(derived) {
531
651
  const own = { ...derived };
532
652
  let params = { ...derived };
653
+ // A value a step worked out: its mark, never the recorded value (issue 1212).
654
+ for (const key of this.fromStep) {
655
+ delete own[key];
656
+ params[key] = markOf(key);
657
+ }
658
+ // A value only one half reads, which this request did not give (review
659
+ // item 11): its mark, never `null` — a step with a null in its input would
660
+ // still run — and never the recorded value.
661
+ const missing = this.oneHalf.filter((key) => derived[key] === null || derived[key] === undefined);
662
+ for (const key of missing)
663
+ params[key] = missingMarkOf(key);
533
664
  if (this.scenario.paramsLogic) {
534
665
  params = evalParamsLogic(this.scenario.paramsLogic, params, this.intent);
666
+ // And after `paramsLogic` too, whatever it made of the key: dropped, a
667
+ // step would read `undefined`; replaced, a default someone typed —
668
+ // either way a value this request did not give (targets.ts). On a copy:
669
+ // what the body returned may be frozen.
670
+ if (missing.length > 0) {
671
+ params = { ...params, ...Object.fromEntries(missing.map((key) => [key, missingMarkOf(key)])) };
672
+ }
535
673
  }
674
+ this.missingMarked = missing;
536
675
  this.derivedParams = own;
537
676
  this.params = params;
538
677
  this.paramsSettled = true;
@@ -561,6 +700,14 @@ export class ScenarioReplayPlan {
561
700
  stopEntry() {
562
701
  return this.stopsEarly() ? this.entries[this.stopAt] : undefined;
563
702
  }
703
+ /**
704
+ * The flat position of the chain's decision stop (the service's
705
+ * segmentsbreakdown.md SB-18), or -1 when it has none. The steps from there
706
+ * on are the half the recording took, whatever stops the plan first.
707
+ */
708
+ decisionIndex() {
709
+ return this.entries.findIndex((e) => e.step.stop?.kind === "decision");
710
+ }
564
711
  /** Every entry in order — for the report, and for the owning scenario ids. */
565
712
  allEntries() {
566
713
  return this.entries;
@@ -571,7 +718,45 @@ export class ScenarioReplayPlan {
571
718
  }
572
719
  /** The parameters in force. Empty until {@link ready} resolves. */
573
720
  get parameters() {
574
- return this.params;
721
+ // For the agent's eyes (the hand-over note): never a mark.
722
+ if (this.fromStep.length === 0 && this.missingMarked.length === 0)
723
+ return this.params;
724
+ return Object.fromEntries(Object.entries(this.params).filter(([, v]) => markedKey(v) === undefined));
725
+ }
726
+ /**
727
+ * The values a body may not use: a value a step works out ({@link
728
+ * FROM_STEP_MARK}) and a value the request did not give ({@link
729
+ * MISSING_VALUE_MARK}). A read of one is the stop {@link valueError} names.
730
+ */
731
+ watchedKeys() {
732
+ return this.missingMarked.length === 0 ? this.fromStep : [...this.fromStep, ...this.missingMarked];
733
+ }
734
+ /** The stop a read of `key` means: a value the request did not give, or one only the recording had. */
735
+ valueError(key) {
736
+ return this.missingMarked.includes(key) ? new MissingValueError(key) : new RecordedValueError(key);
737
+ }
738
+ /**
739
+ * `parameters` for one body, with every read of a value a step works out
740
+ * noted (issue 1212): a read is the recorded value used, however the code
741
+ * changes it afterwards — upper-cased, encoded, sliced or counted. A value
742
+ * the request did not give is watched the same way (review item 11).
743
+ */
744
+ watched(params) {
745
+ const read = new Set();
746
+ const watchedKeys = this.watchedKeys();
747
+ if (watchedKeys.length === 0)
748
+ return { params, read };
749
+ const keys = new Set(watchedKeys);
750
+ return {
751
+ read,
752
+ params: new Proxy(params, {
753
+ get(target, key, receiver) {
754
+ if (typeof key === "string" && keys.has(key))
755
+ read.add(key);
756
+ return Reflect.get(target, key, receiver);
757
+ },
758
+ }),
759
+ };
575
760
  }
576
761
  /** Everything the steps have accumulated so far. */
577
762
  get accumulated() {
@@ -626,7 +811,25 @@ export class ScenarioReplayPlan {
626
811
  // the first moment the caller's own accumulation is final (R-CALL-29) —
627
812
  // unless the step's test needed the frame first (AC5).
628
813
  this.enterFramesAt(this.stepIndex, this.walk);
629
- return evalToolInputLogic(entry.step.toolInputLogic ?? "return {};", this.paramsFor(entry), entry.frame.intent, this.respFor(entry));
814
+ return this.inputOf(entry, this.paramsFor(entry), entry.frame.intent, this.respFor(entry));
815
+ }
816
+ /**
817
+ * A step's input, refused when it holds a value only the recording had: its
818
+ * code fell back on a parameter a step works out, and the earlier steps did
819
+ * not return it (issue 1212). The caller stops the plan in front of it. So
820
+ * is one that reads, or holds, a value the request did not give (review
821
+ * item 11): {@link MissingValueError}.
822
+ */
823
+ inputOf(entry, params, intent, resp, opts) {
824
+ const w = this.watched(params);
825
+ const input = evalToolInputLogic(entry.step.toolInputLogic ?? "return {};", w.params, intent, resp, opts);
826
+ const read = [...w.read][0];
827
+ if (read !== undefined)
828
+ throw this.valueError(read);
829
+ const held = markIn(input);
830
+ if (held)
831
+ throw held.missing ? new MissingValueError(held.key) : new RecordedValueError(held.key);
832
+ return input;
630
833
  }
631
834
  // ── tests (the service's autocorrect.md AC5) ──────────────────────────────
632
835
  //
@@ -658,12 +861,13 @@ export class ScenarioReplayPlan {
658
861
  return undefined;
659
862
  }
660
863
  /**
661
- * The tool names of the steps from flat position `from` to the end, each
662
- * that runs only under a test marked so — for a hand-over note's "remaining
663
- * planned steps", which must not read as steps this request needs.
864
+ * The tool names of the steps from flat position `from` to the end (or to
865
+ * `until`, not included), each that runs only under a test marked so — for a
866
+ * hand-over note's "remaining planned steps", which must not read as steps
867
+ * this request needs.
664
868
  */
665
- remainingFrom(from) {
666
- return this.entries.slice(Math.max(0, from)).map((e) => {
869
+ remainingFrom(from, until) {
870
+ return this.entries.slice(Math.max(0, from), until).map((e) => {
667
871
  const name = e.step.toolName ?? "";
668
872
  const t = this.onlyWhen(e);
669
873
  return t ? `${name} (only when ${t.text ?? "a request needs it"})` : name;
@@ -680,7 +884,14 @@ export class ScenarioReplayPlan {
680
884
  /** One test, answered — or the reason it could not be. Never throws. */
681
885
  answer(test, params, intent, resp) {
682
886
  try {
683
- return { yes: evalWhenLogic(test.logic, params, intent, resp) };
887
+ const w = this.watched(params);
888
+ const yes = evalWhenLogic(test.logic, w.params, intent, resp);
889
+ // A test on a value only the recording holds decides nothing (issue
890
+ // 1212), and nor does one on a value the request did not give (item 11).
891
+ const key = [...w.read][0];
892
+ if (key !== undefined)
893
+ return { error: this.valueError(key).message };
894
+ return { yes };
684
895
  }
685
896
  catch (err) {
686
897
  return { error: errText(err) };
@@ -910,9 +1121,25 @@ export class ScenarioReplayPlan {
910
1121
  }
911
1122
  /** Build the final response model from everything the steps accumulated. */
912
1123
  responseModel() {
913
- if (!this.scenario.responseParamsLogic)
914
- return { ...this.respParams };
915
- return evalResponseParamsLogic(this.scenario.responseParamsLogic, this.respParams, this.params, this.intent);
1124
+ const w = this.watched(this.params);
1125
+ const model = this.scenario.responseParamsLogic
1126
+ ? evalResponseParamsLogic(this.scenario.responseParamsLogic, this.respParams, w.params, this.intent)
1127
+ : { ...this.respParams };
1128
+ // The answer's code read a value only the recording had (issue 1212): its
1129
+ // summary would name the recorded address and the recorded lead. Left out.
1130
+ // So is one that read a value the request did not give (review item 11):
1131
+ // it would name a mark, or what the code made of one.
1132
+ const read = [...w.read];
1133
+ const held = markIn(model);
1134
+ if (read.length > 0 || held) {
1135
+ const missing = read.length > 0 ? read.every((k) => this.missingMarked.includes(k)) : held.missing;
1136
+ return {
1137
+ summaryLeftOut: missing
1138
+ ? "The plan's summary read a value this request did not give, so it is left out: answer from the steps' own results."
1139
+ : "The plan's summary read values recorded from another run, so it is left out: answer from the steps' own results.",
1140
+ };
1141
+ }
1142
+ return model;
916
1143
  }
917
1144
  /**
918
1145
  * The arguments each planned step will run with, where they are known before
@@ -963,7 +1190,7 @@ export class ScenarioReplayPlan {
963
1190
  });
964
1191
  let input;
965
1192
  try {
966
- input = evalToolInputLogic(entry.step.toolInputLogic ?? "return {};", params, entry.frame.intent, resp, { timeoutMs: Math.min(bodyMs, left) });
1193
+ input = this.inputOf(entry, params, entry.frame.intent, resp, { timeoutMs: Math.min(bodyMs, left) });
967
1194
  }
968
1195
  catch (err) {
969
1196
  // A body that throws (often on an earlier result it does not have
@@ -1061,10 +1288,21 @@ export class ScenarioReplayPlan {
1061
1288
  break;
1062
1289
  }
1063
1290
  const anyArgs = args.slice(0, argsShown).some((a) => a !== undefined);
1291
+ // A plan whose chain holds a decision (the service's segmentsbreakdown.md
1292
+ // SB-18) never shows its intent, even when it stops earlier for another
1293
+ // reason: the intent names the recorded task, the half the recording took
1294
+ // included, and which way applies is the agent's choice.
1295
+ const atDecision = this.stopStep()?.stop?.kind === "decision";
1296
+ const shownIntent = this.decisionIndex() >= 0 ? "" : intent;
1064
1297
  // A parked step (fallbk.md D3): the model is told up front that the plan is
1065
1298
  // partial, so the hand-over that follows is expected rather than a surprise.
1066
1299
  const tail = this.stopsEarly()
1067
- ? ["", "The system will then hand the rest of the task back to you."]
1300
+ ? [
1301
+ "",
1302
+ atDecision
1303
+ ? "The system will then hand the task back to you at a decision."
1304
+ : "The system will then hand the rest of the task back to you.",
1305
+ ]
1068
1306
  : [];
1069
1307
  if (this.mode === "direct") {
1070
1308
  // The sequence is listed even though the model does not call it itself:
@@ -1073,7 +1311,7 @@ export class ScenarioReplayPlan {
1073
1311
  // place an operator watching the session can see what a replay is doing.
1074
1312
  return [
1075
1313
  "[BaseInstRunner calculated replay] A known-good tool sequence was recorded",
1076
- `for this request${intent ? ` (intent: ${intent})` : ""}.`,
1314
+ `for this request${shownIntent ? ` (intent: ${shownIntent})` : ""}.`,
1077
1315
  "",
1078
1316
  anyTest
1079
1317
  ? 'It runs these tools, in order, and returns their results. A step marked "only when" runs only when this request needs it, and the results say which ones did not:'
@@ -1095,7 +1333,7 @@ export class ScenarioReplayPlan {
1095
1333
  "recorded for this request. Call exactly these tools, in order, one at a time:",
1096
1334
  "",
1097
1335
  ...lines,
1098
- ...(intent ? ["", `Context: ${intent}`] : []),
1336
+ ...(shownIntent ? ["", `Context: ${shownIntent}`] : []),
1099
1337
  ...(anyTest
1100
1338
  ? [
1101
1339
  "",
@@ -1220,7 +1458,7 @@ export class ScenarioReplayPlan {
1220
1458
  computed = reportingStage(onStep, entry, "tool_input_logic", {}, startedAt, () => {
1221
1459
  this.enterFramesAt(flatIndex, walk);
1222
1460
  const respParams = resp.get(entry.frame.id) ?? {};
1223
- return evalToolInputLogic(step.toolInputLogic ?? "return {};", this.paramsFor(entry), entry.frame.intent, respParams);
1461
+ return this.inputOf(entry, this.paramsFor(entry), entry.frame.intent, respParams);
1224
1462
  });
1225
1463
  }
1226
1464
  catch (err) {
@@ -1232,6 +1470,7 @@ export class ScenarioReplayPlan {
1232
1470
  flatIndex,
1233
1471
  stage: "tool_input_logic",
1234
1472
  error: errText(err),
1473
+ ...(err instanceof RecordedValueError ? { reason: "not_run" } : {}),
1235
1474
  };
1236
1475
  break;
1237
1476
  }
@@ -1330,6 +1569,7 @@ export class ScenarioReplayPlan {
1330
1569
  // left out a sub-task reaching past the parked step did not stop short.
1331
1570
  handover: (opts.handover === true && flatIndex <= this.stopAt) || stopped !== undefined,
1332
1571
  subTask: this.subTask,
1572
+ reportTool: opts.reportTool,
1333
1573
  }),
1334
1574
  executed,
1335
1575
  recorded: recordedCount,
@@ -1418,6 +1658,7 @@ export class ScenarioReplayPlan {
1418
1658
  flatIndex,
1419
1659
  stage: "tool_input_logic",
1420
1660
  error: errText(err),
1661
+ ...(err instanceof RecordedValueError ? { reason: "not_run" } : {}),
1421
1662
  };
1422
1663
  break;
1423
1664
  }
@@ -1516,6 +1757,7 @@ export class ScenarioReplayPlan {
1516
1757
  text: assembleBundle(entries, maxChars, {
1517
1758
  handover: stopped !== undefined,
1518
1759
  subTask: this.subTask,
1760
+ reportTool: opts.reportTool,
1519
1761
  }),
1520
1762
  executed,
1521
1763
  recorded: recordedCount,
@@ -68,8 +68,8 @@ export declare class SourceRunCache {
68
68
  /**
69
69
  * A step its test left out (the service's autocorrect.md AC5). Every step
70
70
  * of a plan was in the recording, and made its call there — unless a
71
- * correction added it from the agent's redo (`correctionKind: "added"`),
72
- * which made none. So one that was in it passes over its recorded output,
71
+ * correction added it from the agent's redo (`correctionKind: "added"`)
72
+ * or wrote it from the agent's answer (`"written"`, AC12), which made none. So one that was in it passes over its recorded output,
73
73
  * and the next step of its tool takes its own; an added one passes over
74
74
  * nothing. Nothing is fetched: the cursor is a count of this frame's calls
75
75
  * of the tool, whichever of them are read later (docs/calculatedReplay.md
@@ -90,8 +90,8 @@ export class SourceRunCache {
90
90
  /**
91
91
  * A step its test left out (the service's autocorrect.md AC5). Every step
92
92
  * of a plan was in the recording, and made its call there — unless a
93
- * correction added it from the agent's redo (`correctionKind: "added"`),
94
- * which made none. So one that was in it passes over its recorded output,
93
+ * correction added it from the agent's redo (`correctionKind: "added"`)
94
+ * or wrote it from the agent's answer (`"written"`, AC12), which made none. So one that was in it passes over its recorded output,
95
95
  * and the next step of its tool takes its own; an added one passes over
96
96
  * nothing. Nothing is fetched: the cursor is a count of this frame's calls
97
97
  * of the tool, whichever of them are read later (docs/calculatedReplay.md
@@ -99,7 +99,7 @@ export class SourceRunCache {
99
99
  */
100
100
  leftOut(entry) {
101
101
  const toolName = entry.step.toolName;
102
- if (!toolName || entry.step.correctionKind === "added")
102
+ if (!toolName || entry.step.correctionKind === "added" || entry.step.correctionKind === "written")
103
103
  return;
104
104
  const key = cursorKey(entry, toolName);
105
105
  this.cursor.set(key, (this.cursor.get(key) ?? 0) + 1);
@@ -39,6 +39,18 @@
39
39
  * call's `paramMapLogic` builds, not on these, so that body is scanned as a
40
40
  * consumer and the segment's own steps are not (R-CALL-29).
41
41
  *
42
+ * A THIRD WAY NOT TO BLOCK: A VALUE ONLY ONE HALF NEEDS (the service's review
43
+ * of calcscenarioautocorrect.md, item 11). A plan that holds both halves of an
44
+ * "if … otherwise" reads values only one half needs — the task's name, its
45
+ * board, the meeting's day — and a prompt for the other half names none of
46
+ * them. Declined as a whole (`missing_target`), the half that needed nothing
47
+ * the prompt lacked never ran. Such a value does not block when every use of
48
+ * it is in the input of a step that runs only under a test of its own: the
49
+ * step is reached only when this request is its half. The plan holds a mark
50
+ * in its place then, never `null` and never the recorded value (plan.ts,
51
+ * `MISSING_VALUE_MARK`), and a step whose input reads it stops in front of its
52
+ * call, for the agent. See {@link testedOnlyTargets}.
53
+ *
42
54
  * WHEN IN DOUBT, IT BLOCKS. This reads source text, not a parse tree, so it can
43
55
  * only prove "safe" for the shapes it recognises: `parameters.key`,
44
56
  * `parameters?.key` and `parameters["key"]`. Any other use of `parameters` —
@@ -51,7 +63,7 @@
51
63
  * the steps then read under another name.
52
64
  */
53
65
  import type { FlatEntry } from "./flatten.js";
54
- import type { ParamsSchema } from "./types.js";
66
+ import { type ParamsSchema } from "./types.js";
55
67
  /**
56
68
  * The subset of `missing` this plan must decline on.
57
69
  *
@@ -63,4 +75,31 @@ import type { ParamsSchema } from "./types.js";
63
75
  * parameters before step 0, and what it returns is what every step reads.
64
76
  */
65
77
  export declare function blockingTargets(missing: readonly string[], schema: ParamsSchema | null | undefined, planned: readonly FlatEntry[], paramsLogic?: string | null): string[];
78
+ /**
79
+ * Of `missing`, the targets that only the steps of one half read (see A THIRD
80
+ * WAY NOT TO BLOCK above): the plan holds them as a mark, and they do not stop
81
+ * it. A key is one only when
82
+ *
83
+ * - some step of frame 0 that runs under a test of its own (`when`, either
84
+ * `otherwise`) reads it in its input logic; and
85
+ * - nothing else names it: no step without a test, no output logic, no test,
86
+ * no sub-task's parameter mapping or test — the mark is never something a
87
+ * test decides on, or that an output passes on to another value; and
88
+ * - `paramsLogic`, when there is one, does not name the key at all, not even
89
+ * as a bare word (`{ ...parameters, task_name: 'x' }` is a typed default:
90
+ * a guessed target), and uses `parameters` only as a spread that hands
91
+ * every key on; and
92
+ * - no body carries the recorded sample as a literal.
93
+ *
94
+ * Anything this cannot follow — `parameters` destructured, passed or aliased —
95
+ * and it answers none: every missing target blocks, as before. Nor in a plan
96
+ * that calls a sub-task: the sub-task's mappings — the one that builds its
97
+ * parameters, the one that hands its results back — run on the caller's
98
+ * values out of every watch, and a mark is a truthy text a mapping could
99
+ * branch on, change past recognising, or pass into what a later test decides
100
+ * on. The plan also puts the mark back after `paramsLogic` has run, so a body
101
+ * that dropped the key or wrote one of its own in its place still stops in
102
+ * front of the step.
103
+ */
104
+ export declare function testedOnlyTargets(missing: readonly string[], schema: ParamsSchema | null | undefined, planned: readonly FlatEntry[], paramsLogic?: string | null): string[];
66
105
  //# sourceMappingURL=targets.d.ts.map
@@ -39,6 +39,18 @@
39
39
  * call's `paramMapLogic` builds, not on these, so that body is scanned as a
40
40
  * consumer and the segment's own steps are not (R-CALL-29).
41
41
  *
42
+ * A THIRD WAY NOT TO BLOCK: A VALUE ONLY ONE HALF NEEDS (the service's review
43
+ * of calcscenarioautocorrect.md, item 11). A plan that holds both halves of an
44
+ * "if … otherwise" reads values only one half needs — the task's name, its
45
+ * board, the meeting's day — and a prompt for the other half names none of
46
+ * them. Declined as a whole (`missing_target`), the half that needed nothing
47
+ * the prompt lacked never ran. Such a value does not block when every use of
48
+ * it is in the input of a step that runs only under a test of its own: the
49
+ * step is reached only when this request is its half. The plan holds a mark
50
+ * in its place then, never `null` and never the recorded value (plan.ts,
51
+ * `MISSING_VALUE_MARK`), and a step whose input reads it stops in front of its
52
+ * call, for the agent. See {@link testedOnlyTargets}.
53
+ *
42
54
  * WHEN IN DOUBT, IT BLOCKS. This reads source text, not a parse tree, so it can
43
55
  * only prove "safe" for the shapes it recognises: `parameters.key`,
44
56
  * `parameters?.key` and `parameters["key"]`. Any other use of `parameters` —
@@ -50,6 +62,7 @@
50
62
  * more strictly still, because it runs before any step and can rename a target
51
63
  * the steps then read under another name.
52
64
  */
65
+ import { isStepTest } from "./types.js";
53
66
  /**
54
67
  * The subset of `missing` this plan must decline on.
55
68
  *
@@ -82,9 +95,98 @@ export function blockingTargets(missing, schema, planned, paramsLogic) {
82
95
  const logic = paramsLogic?.trim() ? paramsLogic : undefined;
83
96
  if (logic && readsWhole(withoutSpread(logic)))
84
97
  return [...missing];
85
- return missing.filter((key) => (logic !== undefined &&
86
- (mentions(logic, key) || carriesLiteral(logic, schema[key]?.sampleValue))) ||
87
- bodies.some((body) => readsOutright(body, key) || carriesLiteral(body, schema[key]?.sampleValue)));
98
+ // Read only by the steps of one half: not blocking, and held as a mark.
99
+ const oneHalf = new Set(testedOnlyTargets(missing, schema, planned, paramsLogic));
100
+ return missing.filter((key) => !oneHalf.has(key) &&
101
+ ((logic !== undefined &&
102
+ (mentions(logic, key) || carriesLiteral(logic, schema[key]?.sampleValue))) ||
103
+ bodies.some((body) => readsOutright(body, key) || carriesLiteral(body, schema[key]?.sampleValue))));
104
+ }
105
+ /**
106
+ * Of `missing`, the targets that only the steps of one half read (see A THIRD
107
+ * WAY NOT TO BLOCK above): the plan holds them as a mark, and they do not stop
108
+ * it. A key is one only when
109
+ *
110
+ * - some step of frame 0 that runs under a test of its own (`when`, either
111
+ * `otherwise`) reads it in its input logic; and
112
+ * - nothing else names it: no step without a test, no output logic, no test,
113
+ * no sub-task's parameter mapping or test — the mark is never something a
114
+ * test decides on, or that an output passes on to another value; and
115
+ * - `paramsLogic`, when there is one, does not name the key at all, not even
116
+ * as a bare word (`{ ...parameters, task_name: 'x' }` is a typed default:
117
+ * a guessed target), and uses `parameters` only as a spread that hands
118
+ * every key on; and
119
+ * - no body carries the recorded sample as a literal.
120
+ *
121
+ * Anything this cannot follow — `parameters` destructured, passed or aliased —
122
+ * and it answers none: every missing target blocks, as before. Nor in a plan
123
+ * that calls a sub-task: the sub-task's mappings — the one that builds its
124
+ * parameters, the one that hands its results back — run on the caller's
125
+ * values out of every watch, and a mark is a truthy text a mapping could
126
+ * branch on, change past recognising, or pass into what a later test decides
127
+ * on. The plan also puts the mark back after `paramsLogic` has run, so a body
128
+ * that dropped the key or wrote one of its own in its place still stops in
129
+ * front of the step.
130
+ */
131
+ export function testedOnlyTargets(missing, schema, planned, paramsLogic) {
132
+ if (missing.length === 0 || !schema || planned.length === 0)
133
+ return [];
134
+ if (planned.some((entry) => entry.depth > 0))
135
+ return [];
136
+ const { tested, others } = consumerBodies(planned);
137
+ const all = [...tested, ...others];
138
+ if (all.length === 0 || all.some(readsWhole))
139
+ return [];
140
+ const logic = paramsLogic?.trim() ? paramsLogic : undefined;
141
+ if (logic && readsWhole(withoutSpread(logic)))
142
+ return [];
143
+ return missing.filter((key) => {
144
+ const sample = schema[key]?.sampleValue;
145
+ if (logic !== undefined && (namesKey(logic, key) || carriesLiteral(logic, sample)))
146
+ return false;
147
+ if (all.some((body) => carriesLiteral(body, sample)))
148
+ return false;
149
+ if (others.some((body) => mentions(body, key)))
150
+ return false;
151
+ return tested.some((body) => mentions(body, key));
152
+ });
153
+ }
154
+ /**
155
+ * The bodies that run with the caller's own `parameters`, as
156
+ * {@link callerBodies} lists them, told apart: the input logic of each step of
157
+ * frame 0 that runs under a test of its own, and every other body.
158
+ */
159
+ function consumerBodies(planned) {
160
+ const tested = [];
161
+ const others = [];
162
+ const seenFrames = new Set();
163
+ for (const entry of planned) {
164
+ if (entry.depth === 0) {
165
+ const input = entry.step.toolInputLogic;
166
+ if (input)
167
+ (isStepTest(entry.step.when) ? tested : others).push(input);
168
+ if (entry.step.toolOutputLogic)
169
+ others.push(entry.step.toolOutputLogic);
170
+ if (entry.step.when?.logic)
171
+ others.push(entry.step.when.logic);
172
+ continue;
173
+ }
174
+ const frame = entry.frame;
175
+ if (frame.depth !== 1 || seenFrames.has(frame.id))
176
+ continue;
177
+ seenFrames.add(frame.id);
178
+ if (frame.paramMapLogic)
179
+ others.push(frame.paramMapLogic);
180
+ if (frame.when?.logic)
181
+ others.push(frame.when.logic);
182
+ }
183
+ return { tested, others };
184
+ }
185
+ /** Whether `body` names `key` at all — `parameters.key`, or the bare word, as a key of an object it returns. */
186
+ function namesKey(body, key) {
187
+ if (mentions(body, key))
188
+ return true;
189
+ return new RegExp(`(?<![\\w$])${escapeRegExp(key)}(?![\\w$])`).test(body);
88
190
  }
89
191
  /**
90
192
  * `parameters` used in any shape other than `parameters.key`,