@basein/runner 0.3.14 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@
22
22
  * the service did that long before this payload was built. It only lays out
23
23
  * what it was served, and stops in front of anything it cannot run.
24
24
  */
25
- import type { SerializedScenario, SerializedScenarioStep, CallUnusable } from "./types.js";
25
+ import type { SerializedScenario, SerializedScenarioStep, CallUnusable, StepTest } from "./types.js";
26
26
  import type { ParamsSchema } from "./types.js";
27
27
  /** The scope one stretch of steps runs in. Frame 0 is the caller's own. */
28
28
  export interface Frame {
@@ -47,6 +47,15 @@ export interface Frame {
47
47
  chainRevision?: number;
48
48
  /** The segment's own parameters, for the settings a mapping omits (R-CALL-21). */
49
49
  paramsObject?: ParamsSchema | null;
50
+ /**
51
+ * The call row that opened this frame, when it carries a test (the
52
+ * service's autocorrect.md AC5): the whole sub-task runs only when the test
53
+ * says so. It is evaluated in the *caller's* frame, before this one is
54
+ * entered.
55
+ */
56
+ when?: StepTest;
57
+ /** That call row itself, for the verdict a sub-task left out gets. */
58
+ callStep?: SerializedScenarioStep;
50
59
  /** Filled by the plan when the frame is entered. */
51
60
  parameters?: Record<string, unknown>;
52
61
  respParams: Record<string, unknown>;
@@ -63,7 +72,11 @@ export interface FlatEntry {
63
72
  step: SerializedScenarioStep;
64
73
  frame: Frame;
65
74
  depth: number;
66
- /** First / last entry of its frame — where the two mapping bodies run. */
75
+ /**
76
+ * First / last of its frame's *own* entries. Where a frame is entered and
77
+ * left is {@link frameEdges}: a frame whose chain begins or ends with a
78
+ * call begins or ends with that call's steps.
79
+ */
67
80
  first: boolean;
68
81
  last: boolean;
69
82
  }
@@ -113,9 +126,32 @@ export declare function flattenChain(scenario: SerializedScenario, opts?: {
113
126
  * Used for the known-bad-step gate, which judges entries: a parked step in the
114
127
  * middle of a sub-task stops the plan in front of the *sub-task*, not in front
115
128
  * of the step, because stopping inside it would skip the body that hands its
116
- * results back.
129
+ * results back. The sub-task starts at its first entry *or its callee's*: one
130
+ * whose chain begins with a call starts where that call's steps do.
117
131
  */
118
132
  export declare function clampToFrameStart(entries: readonly FlatEntry[], index: number): number;
133
+ /**
134
+ * Where each called frame begins and ends in the flat list, its callees'
135
+ * entries included — not where its *own* entries do (`first` / `last`).
136
+ *
137
+ * The two differ when a sub-task's chain begins or ends with a call: for
138
+ * F = [call G, f1] the flat order is g0, g1, f1, and F begins at g0. Entering
139
+ * F there, before G, is what gives G's mapping (and G's test) F's parameters;
140
+ * deciding F's own test there is what keeps G from running when F is not
141
+ * needed (the service's autocorrect.md AC5). Leaving F after the last of
142
+ * them is what hands G's results on through F.
143
+ */
144
+ export interface FrameEdges {
145
+ /** Per frame id, the flat position of its first entry, its callees' included. */
146
+ start: Map<number, number>;
147
+ /** Per frame id, the flat position of its last entry, its callees' included. */
148
+ end: Map<number, number>;
149
+ /** Per flat position, the called frames that begin there, outermost first. */
150
+ startsAt: Map<number, Frame[]>;
151
+ /** Per flat position, the called frames that end there, innermost first. */
152
+ endsAt: Map<number, Frame[]>;
153
+ }
154
+ export declare function frameEdges(entries: readonly FlatEntry[]): FrameEdges;
119
155
  /**
120
156
  * One frame per chain, for a payload with no calls in it and for the callers
121
157
  * that build a plan by hand (tests, `bir replay`). Byte for byte the old
@@ -121,6 +121,10 @@ export function flattenChain(scenario, opts = {}) {
121
121
  segmentId: segment.id,
122
122
  chainRevision: segment.chainRevision,
123
123
  paramsObject: segment.paramsObject ?? null,
124
+ // A sub-task may run only when a test says so (AC5): the plan decides
125
+ // the whole frame at its first entry, in the caller's scope.
126
+ ...(step.when ? { when: step.when } : {}),
127
+ callStep: step,
124
128
  respParams: {},
125
129
  };
126
130
  frameStart.set(child.id, entries.length);
@@ -147,6 +151,13 @@ export function flattenChain(scenario, opts = {}) {
147
151
  });
148
152
  return { entries, stopAt, stopReason };
149
153
  }
154
+ /** Whether `frame` is `outer` or lies inside it. */
155
+ function within(frame, outer) {
156
+ for (let f = frame; f; f = f.parent)
157
+ if (f.id === outer.id)
158
+ return true;
159
+ return false;
160
+ }
150
161
  /**
151
162
  * Move a stop back to the start of the outermost called frame it falls in
152
163
  * (R-CALL-30, 10.6 step 3).
@@ -154,7 +165,8 @@ export function flattenChain(scenario, opts = {}) {
154
165
  * Used for the known-bad-step gate, which judges entries: a parked step in the
155
166
  * middle of a sub-task stops the plan in front of the *sub-task*, not in front
156
167
  * of the step, because stopping inside it would skip the body that hands its
157
- * results back.
168
+ * results back. The sub-task starts at its first entry *or its callee's*: one
169
+ * whose chain begins with a call starts where that call's steps do.
158
170
  */
159
171
  export function clampToFrameStart(entries, index) {
160
172
  const at = entries[index];
@@ -166,11 +178,39 @@ export function clampToFrameStart(entries, index) {
166
178
  if (!outermost)
167
179
  return index;
168
180
  for (let i = 0; i < entries.length; i += 1) {
169
- if (entries[i].frame.id === outermost.id)
181
+ if (within(entries[i].frame, outermost))
170
182
  return i;
171
183
  }
172
184
  return index;
173
185
  }
186
+ export function frameEdges(entries) {
187
+ const start = new Map();
188
+ const end = new Map();
189
+ const frames = new Map();
190
+ entries.forEach((entry, index) => {
191
+ for (let f = entry.frame; f; f = f.parent) {
192
+ frames.set(f.id, f);
193
+ if (!start.has(f.id))
194
+ start.set(f.id, index);
195
+ end.set(f.id, index);
196
+ }
197
+ });
198
+ const startsAt = new Map();
199
+ const endsAt = new Map();
200
+ for (const f of frames.values()) {
201
+ if (f.depth === 0)
202
+ continue;
203
+ const s = start.get(f.id);
204
+ const e = end.get(f.id);
205
+ startsAt.set(s, [...(startsAt.get(s) ?? []), f]);
206
+ endsAt.set(e, [...(endsAt.get(e) ?? []), f]);
207
+ }
208
+ for (const list of startsAt.values())
209
+ list.sort((a, b) => a.depth - b.depth);
210
+ for (const list of endsAt.values())
211
+ list.sort((a, b) => b.depth - a.depth);
212
+ return { start, end, startsAt, endsAt };
213
+ }
174
214
  /**
175
215
  * One frame per chain, for a payload with no calls in it and for the callers
176
216
  * that build a plan by hand (tests, `bir replay`). Byte for byte the old
@@ -57,7 +57,19 @@ export type HandoverStopReason =
57
57
  * place of the plan's. Its result is real; the plan's later steps were
58
58
  * computed for the call it planned, so it stops there.
59
59
  */
60
- | "host_edited";
60
+ | "host_edited"
61
+ /**
62
+ * The step's test (the service's autocorrect.md AC5) threw, or answered
63
+ * neither true nor false: nothing says whether this request needs the
64
+ * step, and nothing is guessed. The step did not run.
65
+ */
66
+ | "test_failed"
67
+ /**
68
+ * A sub-task's last step was left out by its test (AC5), and the sub-task
69
+ * could not hand its results back to its caller: the body that maps them
70
+ * threw. Its steps that ran did their work; the one left out did not run.
71
+ */
72
+ | "result_map";
61
73
  export interface HandoverNoteInput {
62
74
  intent: string;
63
75
  kind: FallbackKind;
@@ -72,6 +84,25 @@ export interface HandoverNoteInput {
72
84
  stopReason?: HandoverStopReason;
73
85
  /** Steps that ran under the scenario before the stop. */
74
86
  executed: number;
87
+ /**
88
+ * When steps before the stop were left out by their tests (AC5): the
89
+ * 1-based numbers of the steps that ran, so the note names them rather than
90
+ * "steps 1–N". `executed` is then their count.
91
+ */
92
+ ranSteps?: number[];
93
+ /**
94
+ * The steps before the stop that their tests left out, 1-based and
95
+ * inclusive (a sub-task left out whole spans several): named, so the model
96
+ * neither thinks they ran nor runs them.
97
+ */
98
+ leftOut?: Array<{
99
+ from: number;
100
+ to: number;
101
+ toolName: string;
102
+ text: string | null;
103
+ }>;
104
+ /** For `result_map`: the sub-task whose results could not go back. */
105
+ subTask?: string | null;
75
106
  /**
76
107
  * Of {@link executed}, how many are the model's own tool calls above — the
77
108
  * steps a steered session ran before a divergence took over. Only read when
@@ -120,6 +151,13 @@ export interface HandoverNoteInput {
120
151
  ownerScenarioId?: string | null;
121
152
  /** The sub-task the stopped step belongs to, when it is inside one. */
122
153
  inSubTask?: string | null;
154
+ /**
155
+ * For `other_branch`: the stopped step's test in words — what the steps
156
+ * from there on are for, which this request is not (autocorrect.md AC5).
157
+ */
158
+ whenText?: string | null;
123
159
  }
124
160
  export declare function buildHandoverNote(n: HandoverNoteInput): string;
161
+ /** "Step 2", "Steps 1–3", "Steps 1, 3 and 5–6": 1-based numbers in order, runs joined. */
162
+ export declare function stepsPhrase(numbers: readonly number[]): string;
125
163
  //# sourceMappingURL=handover.d.ts.map
@@ -35,7 +35,28 @@ export function buildHandoverNote(n) {
35
35
  ];
36
36
  if (intent)
37
37
  lines.push(`Intent: ${intent}`);
38
- if (n.executed > 0) {
38
+ // Steps left out by their tests sit among those before the stop (AC5): the
39
+ // ones that ran are then named, never "steps 1–N", which would say a step
40
+ // ran that did not.
41
+ const numbered = n.ranSteps && n.ranSteps.length > 0 ? n.ranSteps.slice(0, n.executed) : undefined;
42
+ if (n.executed > 0 && numbered) {
43
+ const above = Math.max(0, Math.min(n.executedAbove ?? 0, n.executed));
44
+ const delivery = above >= n.executed ? "above" : n.delivery;
45
+ const one = numbered.length === 1;
46
+ const ran = `${stepsPhrase(numbered)} of ${n.totalSteps} ran under the scenario`;
47
+ const them = one ? "it" : "them";
48
+ const aboveAre = `${stepsPhrase(numbered.slice(0, above)).toLowerCase()} ${above === 1 ? "is the tool call" : "are the tool calls"} above`;
49
+ lines.push(delivery === "above"
50
+ ? `${ran}; ${one ? "its call and result are the tool call" : "their calls and results are the tool calls"} above. Do not repeat ${them}.`
51
+ : above > 0
52
+ ? delivery === "none"
53
+ ? `${ran}: ${aboveAre}, and the side effects of the rest are in place. Do not repeat ${them}.`
54
+ : `${ran}: ${aboveAre}, and the rest ${n.executed - above === 1 ? "is" : "are"} below. Do not repeat ${them}.`
55
+ : delivery === "none"
56
+ ? `${ran}, and ${one ? "its" : "their"} side effects are in place. Do not repeat ${them}.`
57
+ : `${ran}; ${one ? "its call and result are" : "their calls and results are"} below. Do not repeat ${them}.`);
58
+ }
59
+ else if (n.executed > 0) {
39
60
  // Part of the work may be the model's own calls above (a steered session
40
61
  // that diverged), and the rest the bundle below or side effects only.
41
62
  const above = Math.max(0, Math.min(n.executedAbove ?? 0, n.executed));
@@ -65,6 +86,14 @@ export function buildHandoverNote(n) {
65
86
  else {
66
87
  lines.push("No step of the scenario ran; the whole task is yours.");
67
88
  }
89
+ if (n.leftOut && n.leftOut.length > 0) {
90
+ // Named, so the model neither counts them as done nor runs them now.
91
+ const named = n.leftOut.map((l) => `${l.from === l.to ? `step ${l.from}` : `steps ${l.from}–${l.to}`} (${l.toolName})`);
92
+ const texts = [...new Set(n.leftOut.map((l) => l.text).filter((t) => !!t))];
93
+ const why = texts.length === 1 ? ` — only for when ${texts[0]}` : "";
94
+ const one = n.leftOut.length === 1 && n.leftOut[0].from === n.leftOut[0].to;
95
+ lines.push(`Not needed for this request, so not run: ${joinList(named)}${why}. Do not run ${one ? "it" : "them"}.`);
96
+ }
68
97
  if (n.inFlight) {
69
98
  // Never "ran", never "did not run": the host has not said which.
70
99
  const inTool = n.inFlight.toolName ? ` (${n.inFlight.toolName})` : "";
@@ -93,10 +122,22 @@ export function buildHandoverNote(n) {
93
122
  // judgement, and the agent is the one to make it (plan-services.md D8).
94
123
  lines.push(`Step ${human} of ${n.totalSteps}${tool} did not run: its input needs a judgement the scenario cannot compute. Make that choice yourself and carry on.`);
95
124
  }
125
+ else if (n.kind === "other_branch") {
126
+ // The plan holds one half of an "if … otherwise" and this request is the
127
+ // other (autocorrect.md AC5). The steps from here are not wrong, they are
128
+ // not this request's: the model is told what they are for, never to run
129
+ // them, and the list of their tools is left out below for that reason.
130
+ const why = n.whenText ? `they are only for when ${n.whenText}, and that is not so for this request` : "this request is the other case of the task, which they are not for";
131
+ lines.push(n.stepIndex >= n.totalSteps
132
+ ? "None of the scenario's steps was needed for this request. Do the task as this request needs it."
133
+ : `Step ${human} of ${n.totalSteps}${tool} and the steps after it did not run: ${why}. Do the rest of the task yourself, as this request needs it.`);
134
+ }
96
135
  else {
97
136
  lines.push(stepFailedLine(n, human, tool));
98
137
  }
99
- if (n.remainingTools.length > 0) {
138
+ // Not for the other half: those tools are the ones this request must not
139
+ // run, and naming them invites exactly that.
140
+ if (n.remainingTools.length > 0 && n.kind !== "other_branch") {
100
141
  lines.push(`The remaining planned steps were: ${n.remainingTools.join(", ")}.`);
101
142
  }
102
143
  if (n.params && Object.keys(n.params).length > 0) {
@@ -111,6 +152,32 @@ export function buildHandoverNote(n) {
111
152
  }
112
153
  return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
113
154
  }
155
+ /** "Step 2", "Steps 1–3", "Steps 1, 3 and 5–6": 1-based numbers in order, runs joined. */
156
+ export function stepsPhrase(numbers) {
157
+ if (numbers.length === 0)
158
+ return "No step";
159
+ if (numbers.length === 1)
160
+ return `Step ${numbers[0]}`;
161
+ const runs = [];
162
+ let from = numbers[0];
163
+ let to = from;
164
+ for (const n of numbers.slice(1)) {
165
+ if (n === to + 1) {
166
+ to = n;
167
+ continue;
168
+ }
169
+ runs.push(from === to ? `${from}` : `${from}–${to}`);
170
+ from = to = n;
171
+ }
172
+ runs.push(from === to ? `${from}` : `${from}–${to}`);
173
+ return `Steps ${joinList(runs)}`;
174
+ }
175
+ /** "a", "a and b", "a, b and c". */
176
+ function joinList(items) {
177
+ if (items.length <= 1)
178
+ return items.join("");
179
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
180
+ }
114
181
  /** The line that names a stopped step and says what may be assumed about it. */
115
182
  function stepFailedLine(n, human, tool) {
116
183
  const at = `Step ${human} of ${n.totalSteps}${tool}`;
@@ -135,6 +202,18 @@ function stepFailedLine(n, human, tool) {
135
202
  `could not read its output${detail ? ` (${detail})` : ""}, so it stops there. Do not run it again.`);
136
203
  }
137
204
  switch (n.stopReason) {
205
+ case "result_map": {
206
+ // Nothing of the stopped position ran: its sub-task's last step was left
207
+ // out, and what failed is handing the sub-task's results on.
208
+ const what = n.subTask ? ` "${truncate(n.subTask, 120)}"` : "";
209
+ return (`The sub-task${what} could not hand back its results${detail ? ` (${detail})` : ""}, ` +
210
+ "so the scenario stops after it. Do the rest of the task yourself.");
211
+ }
212
+ case "test_failed":
213
+ // The test could not answer, so whether the step is needed is open: the
214
+ // model decides, as it would without the scenario.
215
+ return (`${at} did not run: the scenario could not tell whether this request needs it` +
216
+ `${detail ? ` (${detail})` : ""}. Decide that yourself and carry on.`);
138
217
  case "not_run":
139
218
  return `${at} did not run${detail ? `: ${detail}` : ""}. Nothing of it happened; do it yourself.`;
140
219
  case "uncertain":
@@ -74,6 +74,14 @@ export interface LogicRunOptions {
74
74
  }
75
75
  /** Transform the derived parameters once, before any step runs. */
76
76
  export declare function evalParamsLogic(code: string, parameters: Record<string, unknown>, intent: string): Record<string, unknown>;
77
+ /**
78
+ * A step's test (the service's autocorrect.md AC5): whether this request needs
79
+ * the step, from the parameters and what the earlier steps accumulated. The
80
+ * answer must be exactly `true` or `false` — a truthy string or a missing key
81
+ * is not an answer, and a test that cannot answer is never guessed at: it
82
+ * throws, and the plan stops in front of the step.
83
+ */
84
+ export declare function evalWhenLogic(code: string, parameters: Record<string, unknown>, intent: string, respParams?: Record<string, unknown>): boolean;
77
85
  /** Derive one step's tool input from params, intent and the accumulated respParams. */
78
86
  export declare function evalToolInputLogic(code: string, parameters: Record<string, unknown>, intent: string, respParams?: Record<string, unknown>, opts?: LogicRunOptions): Record<string, unknown>;
79
87
  /** Extract keys from a step's real output, to merge into respParams for later steps. */
@@ -137,6 +137,25 @@ function assertPlainObject(result, label) {
137
137
  export function evalParamsLogic(code, parameters, intent) {
138
138
  return assertPlainObject(run("paramsLogic", ["parameters", "intent"], code, [parameters, intent]), "paramsLogic");
139
139
  }
140
+ /**
141
+ * A step's test (the service's autocorrect.md AC5): whether this request needs
142
+ * the step, from the parameters and what the earlier steps accumulated. The
143
+ * answer must be exactly `true` or `false` — a truthy string or a missing key
144
+ * is not an answer, and a test that cannot answer is never guessed at: it
145
+ * throws, and the plan stops in front of the step.
146
+ */
147
+ export function evalWhenLogic(code, parameters, intent, respParams = {}) {
148
+ const answer = run("whenLogic", ["parameters", "intent", "respParams"], code, [
149
+ parameters,
150
+ intent,
151
+ respParams,
152
+ ]);
153
+ if (answer !== true && answer !== false) {
154
+ const shown = answer === undefined ? "undefined" : typeof answer === "string" ? `"${answer.slice(0, 40)}"` : typeof answer;
155
+ throw new Error(`whenLogic must return true or false, and returned ${shown}`);
156
+ }
157
+ return answer;
158
+ }
140
159
  /** Derive one step's tool input from params, intent and the accumulated respParams. */
141
160
  export function evalToolInputLogic(code, parameters, intent, respParams = {}, opts) {
142
161
  return assertPlainObject(run("toolInputLogic", ["parameters", "intent", "respParams"], code, [parameters, intent, respParams], opts), "toolInputLogic");
@@ -83,8 +83,14 @@ export interface DriveOptions {
83
83
  * It takes the *entry*, not the step: a step inside a called segment takes its
84
84
  * recorded output from that segment's own recording, within its own range
85
85
  * (segmented.md R-CALL-31).
86
+ *
87
+ * `leftOut`, when there, is told of every step a test left out (the service's
88
+ * autocorrect.md AC5): one that was in the recording made its call there, and
89
+ * its output is passed over, so the next step of its tool takes its own.
86
90
  */
87
- export type RecordedOutputFor = (entry: FlatEntry) => Promise<string | undefined>;
91
+ export type RecordedOutputFor = ((entry: FlatEntry) => Promise<string | undefined>) & {
92
+ leftOut?: (entry: FlatEntry) => void;
93
+ };
88
94
  /**
89
95
  * What one step did, told to the caller as it happens.
90
96
  *
@@ -111,7 +117,12 @@ export interface StepInfo {
111
117
  * call produced none — `skipped`, or a failure before the tool ran.
112
118
  */
113
119
  output?: string;
114
- outcome: "executed" | "recorded" | "skipped" | "failed";
120
+ /**
121
+ * `not_taken`: the step's test said this request does not need it, and it
122
+ * was left out on purpose (the service's autocorrect.md AC5). No call, no
123
+ * output logic; a sub-task's call row left out whole is one such verdict.
124
+ */
125
+ outcome: "executed" | "recorded" | "skipped" | "failed" | "not_taken";
115
126
  /** Which part of the step broke. Only set for `failed`. */
116
127
  stage?: ExecutionStage;
117
128
  error?: string;
@@ -152,8 +163,18 @@ export interface ComposeResult {
152
163
  * and yet the work did not happen, and an outcome must not say it did.
153
164
  */
154
165
  errored: number;
166
+ /** Steps a test left out because this request does not need them (AC5). */
167
+ notTaken: number;
168
+ /** Those steps, in order: what a note counting the steps that ran leaves out. */
169
+ left: LeftOut[];
155
170
  /** Where, and why, the run stopped short of its end, when it did. */
156
171
  stopped?: PlanStop;
172
+ /**
173
+ * The flat position the walk ended at. At or past the plan's `stepCount`
174
+ * when it reached the end of the planned steps — past it when a test left
175
+ * out a sub-task that reaches beyond a parked step.
176
+ */
177
+ endedAt: number;
157
178
  }
158
179
  /** Where, and why, a run stopped short of the chain's end (fallbk.md). */
159
180
  export interface PlanStop {
@@ -170,6 +191,58 @@ export interface PlanStop {
170
191
  * Absent means its logic threw or its tool reported an error.
171
192
  */
172
193
  reason?: HandoverStopReason;
194
+ /** For `other_branch`: the test in words — what the steps from here are for (AC5). */
195
+ whenText?: string | null;
196
+ /**
197
+ * For `result_map`: the sub-task whose results could not go back to its
198
+ * caller, after its last step was left out by its test (AC5).
199
+ */
200
+ subTask?: string | null;
201
+ }
202
+ /**
203
+ * What a step's test decided at one flat position (the service's
204
+ * autocorrect.md AC5).
205
+ *
206
+ * - `run`: no test, or its test said yes.
207
+ * - `skip`: this request does not need it — the step, or the whole sub-task
208
+ * whose call row's test said no. The walk goes on at `until`. `leave` says
209
+ * the step left out was the last of an entered sub-task, so the sub-task's
210
+ * results still go back to its caller.
211
+ * - `stop`: the test said `hand_over` (this request is the other half), or
212
+ * could not answer. `info` is the step's verdict when there is one to give.
213
+ */
214
+ export type Gate = {
215
+ kind: "run";
216
+ } | {
217
+ kind: "skip";
218
+ until: number;
219
+ info: StepInfo;
220
+ entry: FlatEntry;
221
+ text: string | null;
222
+ tools: string[];
223
+ } | {
224
+ kind: "stop";
225
+ stop: PlanStop;
226
+ info?: StepInfo;
227
+ };
228
+ /** A step left out by its test, as the steer note and the bundle name it. */
229
+ export interface LeftOut {
230
+ /** The owning scenario's own `stepIndex` — the caller's for a sub-task. */
231
+ stepIndex: number;
232
+ /** Its flat position. */
233
+ flatIndex: number;
234
+ /**
235
+ * The flat position after it: past every step of a sub-task left out
236
+ * whole, `flatIndex + 1` for one step.
237
+ */
238
+ until: number;
239
+ toolName: string;
240
+ /**
241
+ * Every tool the steps left out call — the sub-task's, for a sub-task left
242
+ * out whole — so a call the model makes to any of them is known for one.
243
+ */
244
+ tools: string[];
245
+ text: string | null;
173
246
  }
174
247
  export interface PlanOptions {
175
248
  scenario: SerializedScenario;
@@ -230,7 +303,22 @@ export declare class ScenarioReplayPlan {
230
303
  private readonly stopAt;
231
304
  /** This plan is a sub-task inside the agent's own task (segmented.md R-OUT-13). */
232
305
  readonly subTask: boolean;
306
+ /**
307
+ * Where each called frame begins and ends, its callees' entries included:
308
+ * where it is entered, its test decided and it is left, and where a
309
+ * sub-task its test leaves out ends (AC5).
310
+ */
311
+ private readonly edges;
312
+ /** Each entry's flat position. */
313
+ private readonly positions;
314
+ /** Every frame of the flat list, by id. */
315
+ private readonly frames;
316
+ /** The plan's own walk: what its cursor has entered and decided (AC5). */
317
+ private readonly walk;
233
318
  constructor(opts: PlanOptions);
319
+ private frameById;
320
+ /** The called frames that begin at flat position `flatIndex`, outermost first. */
321
+ private framesStartingAt;
234
322
  /** The caller's own frame — the one at depth 0. */
235
323
  private rootFrame;
236
324
  /** The parameters one entry's logic runs with (segmented.md R-CALL-29). */
@@ -246,7 +334,27 @@ export declare class ScenarioReplayPlan {
246
334
  * out. A mapping that omits a setting therefore cannot pass the service's
247
335
  * check and then run `undefined` here.
248
336
  */
337
+ private enterFramesAt;
249
338
  private enterFrame;
339
+ /**
340
+ * Leave every called frame that ends at `flatIndex` and that `walk`
341
+ * entered, innermost first: a sub-task whose chain ends with a call is left
342
+ * after its callee has handed it its results.
343
+ */
344
+ private leaveFramesAt;
345
+ /**
346
+ * After a skip that ends at flat position `last`: leave every entered
347
+ * sub-task that ends there — its results still go back to its caller. A
348
+ * result mapping that throws ends the walk: its verdict is filed on the
349
+ * call row, in the caller's chain, and the stop says the step at `last` was
350
+ * left out, not run (`result_map`). Rethrown instead when `rethrow`.
351
+ */
352
+ private leaveAfterSkip;
353
+ /**
354
+ * The call row that opened `frame`, as a step of its caller's chain: where
355
+ * a sub-task's test and its result mapping are filed (R-CALL-13).
356
+ */
357
+ private callRowOf;
250
358
  /**
251
359
  * Leaving a called frame: hand what the segment emitted back to its caller
252
360
  * (R-CALL-29). `out` is the frame's own accumulated `respParams`.
@@ -295,8 +403,6 @@ export declare class ScenarioReplayPlan {
295
403
  stopStep(): SerializedScenarioStep | undefined;
296
404
  /** The entry the plan stops in front of, with the frame that owns it. */
297
405
  stopEntry(): FlatEntry | undefined;
298
- /** Tool names from position `from` to the end of the flat chain. */
299
- toolsFrom(from: number): string[];
300
406
  /** Every entry in order — for the report, and for the owning scenario ids. */
301
407
  allEntries(): readonly FlatEntry[];
302
408
  /** The entry awaiting execution, or undefined when the plan is done. */
@@ -333,6 +439,61 @@ export declare class ScenarioReplayPlan {
333
439
  * a throw as divergence.
334
440
  */
335
441
  toolInputForCurrentStep(): Record<string, unknown>;
442
+ /** Whether the plan's cursor rests in front of the step it is parked before. */
443
+ atStop(): boolean;
444
+ /**
445
+ * The test an entry runs under, in words, when it has one: its own, or that
446
+ * of a sub-task beginning at it — one whose chain begins with a call begins
447
+ * at its callee's first step.
448
+ */
449
+ onlyWhen(entry: FlatEntry): {
450
+ text: string | null;
451
+ } | undefined;
452
+ /**
453
+ * The tool names of the steps from flat position `from` to the end, each
454
+ * that runs only under a test marked so — for a hand-over note's "remaining
455
+ * planned steps", which must not read as steps this request needs.
456
+ */
457
+ remainingFrom(from: number): string[];
458
+ /** The steps the plan's own cursor has left out so far, in order (AC5). */
459
+ leftOut(): readonly LeftOut[];
460
+ /** Whether any planned step runs only under a test. */
461
+ hasTests(): boolean;
462
+ /** One test, answered — or the reason it could not be. Never throws. */
463
+ private answer;
464
+ /**
465
+ * What the tests decide at flat position `flatIndex` for `walk`. Enters the
466
+ * position's frame when its own test needs the frame's parameters, and
467
+ * nothing else changes: the caller moves its cursor.
468
+ */
469
+ private gate;
470
+ /**
471
+ * A sub-task's test, decided in its caller's scope at flat position
472
+ * `flatIndex`, where the sub-task begins. Undefined when it says yes.
473
+ */
474
+ private decideCall;
475
+ /** What a skip the gate decided leaves out, as the notes name it. */
476
+ private leftOutOf;
477
+ /**
478
+ * Tell a recorded-output source of the steps one skip left out — every step
479
+ * of a sub-task left out whole — so its queues pass over the outputs of the
480
+ * ones that were in the recording ({@link RecordedOutputFor}).
481
+ */
482
+ private passOver;
483
+ /**
484
+ * Move the plan's own cursor past every step this request does not need,
485
+ * deciding each test as its step comes due (AC5). Stops at a step that will
486
+ * run, at the end, or at a test that stops the plan — which it returns, with
487
+ * the steps it left out. The observer is told each verdict as it is made.
488
+ * A sub-task whose results cannot go back after its last step was left out
489
+ * stops the plan too (`result_map`), its verdict filed on the call row.
490
+ */
491
+ settleTests(onStep?: StepObserver, opts?: {
492
+ rethrow?: boolean;
493
+ }): {
494
+ stop?: PlanStop;
495
+ left: LeftOut[];
496
+ };
336
497
  /**
337
498
  * Thread a real tool output into `respParams` and advance.
338
499
  *