@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.
- package/dist/bin/bir-hooks.d.ts +11 -8
- package/dist/bin/bir-hooks.js +11 -8
- package/dist/bin/bir-scenario.d.ts +5 -1
- package/dist/bin/bir-scenario.js +155 -5
- package/dist/bin/bir.js +22 -9
- package/dist/bin/investigate.d.ts +9 -0
- package/dist/bin/investigate.js +26 -12
- package/dist/bin/scenario-edit.d.ts +7 -0
- package/dist/bin/scenario-edit.js +19 -2
- package/dist/bin/scenario-liveness.d.ts +12 -1
- package/dist/bin/scenario-liveness.js +8 -4
- package/dist/bin/setup.js +3 -3
- package/dist/control/client.d.ts +5 -0
- package/dist/control/client.js +7 -0
- package/dist/control/ensure-hook.d.ts +14 -3
- package/dist/control/ensure-hook.js +18 -5
- package/dist/control/recorder-main.d.ts +15 -0
- package/dist/control/recorder-main.js +33 -8
- package/dist/control/server.d.ts +192 -13
- package/dist/control/server.js +819 -41
- package/dist/control/transcript.d.ts +19 -0
- package/dist/control/transcript.js +57 -0
- package/dist/proxy/intercept.js +13 -0
- package/dist/proxy/session.d.ts +23 -0
- package/dist/proxy/session.js +71 -3
- package/dist/record/recorder.d.ts +156 -2
- package/dist/record/recorder.js +9 -0
- package/dist/record/remote-recorder.d.ts +59 -1
- package/dist/record/remote-recorder.js +297 -20
- package/dist/record/switchable-recorder.js +6 -0
- package/dist/replay/bundle.d.ts +8 -0
- package/dist/replay/bundle.js +20 -6
- package/dist/replay/condition-words.d.ts +28 -0
- package/dist/replay/condition-words.js +59 -0
- package/dist/replay/controller.d.ts +78 -7
- package/dist/replay/controller.js +318 -62
- package/dist/replay/derive.d.ts +15 -0
- package/dist/replay/derive.js +5 -0
- package/dist/replay/handover.d.ts +7 -0
- package/dist/replay/handover.js +89 -51
- package/dist/replay/plan.d.ts +101 -5
- package/dist/replay/plan.js +260 -18
- package/dist/replay/source-run.d.ts +2 -2
- package/dist/replay/source-run.js +3 -3
- package/dist/replay/targets.d.ts +40 -1
- package/dist/replay/targets.js +105 -3
- package/dist/replay/types.d.ts +25 -8
- package/docs/calculatedReplay.md +45 -5
- package/docs/calculatedReplayGuide.md +13 -4
- package/package.json +1 -1
- package/production.md +21 -7
package/dist/replay/plan.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
662
|
-
* that runs only under a test marked so — for a
|
|
663
|
-
* planned steps", which must not read as steps
|
|
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
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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 =
|
|
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
|
-
? [
|
|
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${
|
|
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
|
-
...(
|
|
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
|
|
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);
|
package/dist/replay/targets.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/replay/targets.js
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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`,
|