@basein/runner 0.3.17 → 0.3.18
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-scenario.d.ts +5 -1
- package/dist/bin/bir-scenario.js +155 -5
- package/dist/bin/bir.js +2 -2
- package/dist/bin/scenario-edit.d.ts +7 -0
- package/dist/bin/scenario-edit.js +9 -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/server.d.ts +165 -2
- package/dist/control/server.js +719 -21
- 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 +146 -0
- 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/controller.d.ts +33 -1
- package/dist/replay/controller.js +137 -12
- package/dist/replay/derive.d.ts +15 -0
- package/dist/replay/derive.js +5 -0
- package/dist/replay/plan.d.ts +90 -1
- package/dist/replay/plan.js +232 -10
- 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 +6 -5
- package/package.json +1 -1
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;
|
|
@@ -571,7 +710,45 @@ export class ScenarioReplayPlan {
|
|
|
571
710
|
}
|
|
572
711
|
/** The parameters in force. Empty until {@link ready} resolves. */
|
|
573
712
|
get parameters() {
|
|
574
|
-
|
|
713
|
+
// For the agent's eyes (the hand-over note): never a mark.
|
|
714
|
+
if (this.fromStep.length === 0 && this.missingMarked.length === 0)
|
|
715
|
+
return this.params;
|
|
716
|
+
return Object.fromEntries(Object.entries(this.params).filter(([, v]) => markedKey(v) === undefined));
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* The values a body may not use: a value a step works out ({@link
|
|
720
|
+
* FROM_STEP_MARK}) and a value the request did not give ({@link
|
|
721
|
+
* MISSING_VALUE_MARK}). A read of one is the stop {@link valueError} names.
|
|
722
|
+
*/
|
|
723
|
+
watchedKeys() {
|
|
724
|
+
return this.missingMarked.length === 0 ? this.fromStep : [...this.fromStep, ...this.missingMarked];
|
|
725
|
+
}
|
|
726
|
+
/** The stop a read of `key` means: a value the request did not give, or one only the recording had. */
|
|
727
|
+
valueError(key) {
|
|
728
|
+
return this.missingMarked.includes(key) ? new MissingValueError(key) : new RecordedValueError(key);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* `parameters` for one body, with every read of a value a step works out
|
|
732
|
+
* noted (issue 1212): a read is the recorded value used, however the code
|
|
733
|
+
* changes it afterwards — upper-cased, encoded, sliced or counted. A value
|
|
734
|
+
* the request did not give is watched the same way (review item 11).
|
|
735
|
+
*/
|
|
736
|
+
watched(params) {
|
|
737
|
+
const read = new Set();
|
|
738
|
+
const watchedKeys = this.watchedKeys();
|
|
739
|
+
if (watchedKeys.length === 0)
|
|
740
|
+
return { params, read };
|
|
741
|
+
const keys = new Set(watchedKeys);
|
|
742
|
+
return {
|
|
743
|
+
read,
|
|
744
|
+
params: new Proxy(params, {
|
|
745
|
+
get(target, key, receiver) {
|
|
746
|
+
if (typeof key === "string" && keys.has(key))
|
|
747
|
+
read.add(key);
|
|
748
|
+
return Reflect.get(target, key, receiver);
|
|
749
|
+
},
|
|
750
|
+
}),
|
|
751
|
+
};
|
|
575
752
|
}
|
|
576
753
|
/** Everything the steps have accumulated so far. */
|
|
577
754
|
get accumulated() {
|
|
@@ -626,7 +803,25 @@ export class ScenarioReplayPlan {
|
|
|
626
803
|
// the first moment the caller's own accumulation is final (R-CALL-29) —
|
|
627
804
|
// unless the step's test needed the frame first (AC5).
|
|
628
805
|
this.enterFramesAt(this.stepIndex, this.walk);
|
|
629
|
-
return
|
|
806
|
+
return this.inputOf(entry, this.paramsFor(entry), entry.frame.intent, this.respFor(entry));
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* A step's input, refused when it holds a value only the recording had: its
|
|
810
|
+
* code fell back on a parameter a step works out, and the earlier steps did
|
|
811
|
+
* not return it (issue 1212). The caller stops the plan in front of it. So
|
|
812
|
+
* is one that reads, or holds, a value the request did not give (review
|
|
813
|
+
* item 11): {@link MissingValueError}.
|
|
814
|
+
*/
|
|
815
|
+
inputOf(entry, params, intent, resp, opts) {
|
|
816
|
+
const w = this.watched(params);
|
|
817
|
+
const input = evalToolInputLogic(entry.step.toolInputLogic ?? "return {};", w.params, intent, resp, opts);
|
|
818
|
+
const read = [...w.read][0];
|
|
819
|
+
if (read !== undefined)
|
|
820
|
+
throw this.valueError(read);
|
|
821
|
+
const held = markIn(input);
|
|
822
|
+
if (held)
|
|
823
|
+
throw held.missing ? new MissingValueError(held.key) : new RecordedValueError(held.key);
|
|
824
|
+
return input;
|
|
630
825
|
}
|
|
631
826
|
// ── tests (the service's autocorrect.md AC5) ──────────────────────────────
|
|
632
827
|
//
|
|
@@ -680,7 +875,14 @@ export class ScenarioReplayPlan {
|
|
|
680
875
|
/** One test, answered — or the reason it could not be. Never throws. */
|
|
681
876
|
answer(test, params, intent, resp) {
|
|
682
877
|
try {
|
|
683
|
-
|
|
878
|
+
const w = this.watched(params);
|
|
879
|
+
const yes = evalWhenLogic(test.logic, w.params, intent, resp);
|
|
880
|
+
// A test on a value only the recording holds decides nothing (issue
|
|
881
|
+
// 1212), and nor does one on a value the request did not give (item 11).
|
|
882
|
+
const key = [...w.read][0];
|
|
883
|
+
if (key !== undefined)
|
|
884
|
+
return { error: this.valueError(key).message };
|
|
885
|
+
return { yes };
|
|
684
886
|
}
|
|
685
887
|
catch (err) {
|
|
686
888
|
return { error: errText(err) };
|
|
@@ -910,9 +1112,25 @@ export class ScenarioReplayPlan {
|
|
|
910
1112
|
}
|
|
911
1113
|
/** Build the final response model from everything the steps accumulated. */
|
|
912
1114
|
responseModel() {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1115
|
+
const w = this.watched(this.params);
|
|
1116
|
+
const model = this.scenario.responseParamsLogic
|
|
1117
|
+
? evalResponseParamsLogic(this.scenario.responseParamsLogic, this.respParams, w.params, this.intent)
|
|
1118
|
+
: { ...this.respParams };
|
|
1119
|
+
// The answer's code read a value only the recording had (issue 1212): its
|
|
1120
|
+
// summary would name the recorded address and the recorded lead. Left out.
|
|
1121
|
+
// So is one that read a value the request did not give (review item 11):
|
|
1122
|
+
// it would name a mark, or what the code made of one.
|
|
1123
|
+
const read = [...w.read];
|
|
1124
|
+
const held = markIn(model);
|
|
1125
|
+
if (read.length > 0 || held) {
|
|
1126
|
+
const missing = read.length > 0 ? read.every((k) => this.missingMarked.includes(k)) : held.missing;
|
|
1127
|
+
return {
|
|
1128
|
+
summaryLeftOut: missing
|
|
1129
|
+
? "The plan's summary read a value this request did not give, so it is left out: answer from the steps' own results."
|
|
1130
|
+
: "The plan's summary read values recorded from another run, so it is left out: answer from the steps' own results.",
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
return model;
|
|
916
1134
|
}
|
|
917
1135
|
/**
|
|
918
1136
|
* The arguments each planned step will run with, where they are known before
|
|
@@ -963,7 +1181,7 @@ export class ScenarioReplayPlan {
|
|
|
963
1181
|
});
|
|
964
1182
|
let input;
|
|
965
1183
|
try {
|
|
966
|
-
input =
|
|
1184
|
+
input = this.inputOf(entry, params, entry.frame.intent, resp, { timeoutMs: Math.min(bodyMs, left) });
|
|
967
1185
|
}
|
|
968
1186
|
catch (err) {
|
|
969
1187
|
// A body that throws (often on an earlier result it does not have
|
|
@@ -1220,7 +1438,7 @@ export class ScenarioReplayPlan {
|
|
|
1220
1438
|
computed = reportingStage(onStep, entry, "tool_input_logic", {}, startedAt, () => {
|
|
1221
1439
|
this.enterFramesAt(flatIndex, walk);
|
|
1222
1440
|
const respParams = resp.get(entry.frame.id) ?? {};
|
|
1223
|
-
return
|
|
1441
|
+
return this.inputOf(entry, this.paramsFor(entry), entry.frame.intent, respParams);
|
|
1224
1442
|
});
|
|
1225
1443
|
}
|
|
1226
1444
|
catch (err) {
|
|
@@ -1232,6 +1450,7 @@ export class ScenarioReplayPlan {
|
|
|
1232
1450
|
flatIndex,
|
|
1233
1451
|
stage: "tool_input_logic",
|
|
1234
1452
|
error: errText(err),
|
|
1453
|
+
...(err instanceof RecordedValueError ? { reason: "not_run" } : {}),
|
|
1235
1454
|
};
|
|
1236
1455
|
break;
|
|
1237
1456
|
}
|
|
@@ -1330,6 +1549,7 @@ export class ScenarioReplayPlan {
|
|
|
1330
1549
|
// left out a sub-task reaching past the parked step did not stop short.
|
|
1331
1550
|
handover: (opts.handover === true && flatIndex <= this.stopAt) || stopped !== undefined,
|
|
1332
1551
|
subTask: this.subTask,
|
|
1552
|
+
reportTool: opts.reportTool,
|
|
1333
1553
|
}),
|
|
1334
1554
|
executed,
|
|
1335
1555
|
recorded: recordedCount,
|
|
@@ -1418,6 +1638,7 @@ export class ScenarioReplayPlan {
|
|
|
1418
1638
|
flatIndex,
|
|
1419
1639
|
stage: "tool_input_logic",
|
|
1420
1640
|
error: errText(err),
|
|
1641
|
+
...(err instanceof RecordedValueError ? { reason: "not_run" } : {}),
|
|
1421
1642
|
};
|
|
1422
1643
|
break;
|
|
1423
1644
|
}
|
|
@@ -1516,6 +1737,7 @@ export class ScenarioReplayPlan {
|
|
|
1516
1737
|
text: assembleBundle(entries, maxChars, {
|
|
1517
1738
|
handover: stopped !== undefined,
|
|
1518
1739
|
subTask: this.subTask,
|
|
1740
|
+
reportTool: opts.reportTool,
|
|
1519
1741
|
}),
|
|
1520
1742
|
executed,
|
|
1521
1743
|
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`,
|
package/dist/replay/types.d.ts
CHANGED
|
@@ -83,13 +83,14 @@ export interface SerializedScenarioStep {
|
|
|
83
83
|
when?: StepTest | null;
|
|
84
84
|
/**
|
|
85
85
|
* The correction that wrote this step, and what it did (the service's
|
|
86
|
-
* autocorrect.md AC6). `"added"` is a step taken from the agent's redo
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
86
|
+
* autocorrect.md AC6). `"added"` is a step taken from the agent's redo,
|
|
87
|
+
* and `"written"` one written from the agent's answer (AC12): neither made
|
|
88
|
+
* a call in the recording the plan was calculated from, so it pairs with no
|
|
89
|
+
* recorded output (source-run.ts). Absent or null on every other step, and
|
|
90
|
+
* on every step of an older service.
|
|
90
91
|
*/
|
|
91
92
|
correctionId?: string | null;
|
|
92
|
-
correctionKind?: "added" | "tested" | "changed" | null;
|
|
93
|
+
correctionKind?: "added" | "tested" | "changed" | "written" | null;
|
|
93
94
|
/**
|
|
94
95
|
* `"segment"` is a call: a sub-task this chain runs by calling a recorded
|
|
95
96
|
* scenario instead of carrying a copy of its steps (R-CALL-1). Absent means
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basein/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|