@basein/runner 0.2.0 → 0.2.2

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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * handover — what the model is told when a calculated scenario stops part-way
3
+ * and the rest of the task is its own again (the service's fallbk.md).
4
+ *
5
+ * Before this existed a failing step retired the plan silently: every later
6
+ * tool call passed through, and the model finished the task without ever being
7
+ * told a plan had been steering it, which step broke, or what was left. So a
8
+ * hand-over is always a message, never silence (D4).
9
+ *
10
+ * Message only in the error position — this text reaches the model's context,
11
+ * and a tool's own payload is already in front of it where it ran.
12
+ */
13
+ import type { FallbackKind } from "./types.js";
14
+ /** Hard ceiling on a note. Long enough to carry derived params, short enough to read. */
15
+ export declare const MAX_HANDOVER_NOTE = 4000;
16
+ /** Where the executed steps are, from the model's point of view. */
17
+ export type HandoverDelivery =
18
+ /** Steer mode: the steps that ran are the model's own tool calls, just above. */
19
+ "above"
20
+ /** Direct mode: the steps that ran are in the bundle that follows the note. */
21
+ | "below";
22
+ export interface HandoverNoteInput {
23
+ intent: string;
24
+ kind: FallbackKind;
25
+ /** The step the plan stopped at — the scenario's own `stepIndex`. */
26
+ stepIndex: number;
27
+ stepToolName?: string;
28
+ /** How often the stopped step has failed before (known_bad_step). */
29
+ failureCount?: number;
30
+ /** Why it stopped (step_failed). */
31
+ error?: string;
32
+ /** Steps that ran under the scenario before the stop. */
33
+ executed: number;
34
+ totalSteps: number;
35
+ /** Tool names of the planned steps from the stopped one to the end. */
36
+ remainingTools: string[];
37
+ params?: Readonly<Record<string, unknown>>;
38
+ delivery: HandoverDelivery;
39
+ /**
40
+ * The plan stopped in front of a *call* it cannot run (segmented.md
41
+ * R-CALL-29, R-CALL-36). The agent is told what that stretch of work was and
42
+ * why it is not running, so it does that part itself rather than wondering
43
+ * what is missing.
44
+ */
45
+ unusable?: {
46
+ reason: string;
47
+ callIntent?: string | null;
48
+ segmentId?: string | null;
49
+ ownerScenarioId?: string | null;
50
+ };
51
+ /**
52
+ * The scenario that owns the stopped step, when it is not the one that was
53
+ * matched — a parked step inside a called segment (R-CALL-30).
54
+ */
55
+ ownerScenarioId?: string | null;
56
+ /** The sub-task the stopped step belongs to, when it is inside one. */
57
+ inSubTask?: string | null;
58
+ }
59
+ export declare function buildHandoverNote(n: HandoverNoteInput): string;
60
+ //# sourceMappingURL=handover.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * handover — what the model is told when a calculated scenario stops part-way
3
+ * and the rest of the task is its own again (the service's fallbk.md).
4
+ *
5
+ * Before this existed a failing step retired the plan silently: every later
6
+ * tool call passed through, and the model finished the task without ever being
7
+ * told a plan had been steering it, which step broke, or what was left. So a
8
+ * hand-over is always a message, never silence (D4).
9
+ *
10
+ * Message only in the error position — this text reaches the model's context,
11
+ * and a tool's own payload is already in front of it where it ran.
12
+ */
13
+ import { truncate } from "./bundle.js";
14
+ /** Hard ceiling on a note. Long enough to carry derived params, short enough to read. */
15
+ export const MAX_HANDOVER_NOTE = 4_000;
16
+ const MAX_ERROR_CHARS = 500;
17
+ const MAX_PARAMS_CHARS = 1_500;
18
+ /** What the note says about each reason a call cannot run (R-CALL-36). */
19
+ const UNUSABLE_LINE = {
20
+ switched_off: "whose recorded scenario is switched off",
21
+ not_ready: "whose recorded scenario is not ready",
22
+ missing: "whose recorded scenario was deleted",
23
+ stale: "whose recorded scenario is out of date",
24
+ unresolved: "with no matching recorded scenario right now",
25
+ depth: "whose recorded scenario is nested too deep to run here",
26
+ unsupported: "this runner cannot run",
27
+ };
28
+ export function buildHandoverNote(n) {
29
+ const intent = n.intent.length > 240 ? `${n.intent.slice(0, 240)}…` : n.intent;
30
+ const human = n.stepIndex + 1;
31
+ const tool = n.stepToolName ? ` (${n.stepToolName})` : "";
32
+ const lines = [
33
+ "[BaseInstRunner calculated replay] The calculated scenario for this request stopped",
34
+ "before finishing. Continue the task yourself from here, calling tools as needed.",
35
+ ];
36
+ if (intent)
37
+ lines.push(`Intent: ${intent}`);
38
+ if (n.executed > 0) {
39
+ lines.push(n.delivery === "above"
40
+ ? `Steps 1–${n.executed} of ${n.totalSteps} ran under the scenario; their calls and results are the tool calls above. Do not repeat them.`
41
+ : `Steps 1–${n.executed} of ${n.totalSteps} ran under the scenario; their calls and results are below. Do not repeat them.`);
42
+ }
43
+ else {
44
+ lines.push("No step of the scenario ran; the whole task is yours.");
45
+ }
46
+ if (n.kind === "unusable_call") {
47
+ // A call is a stretch of work, not a tool: the agent is told what the
48
+ // sub-task was, not which tool did not run.
49
+ const what = n.unusable?.callIntent ? ` ("${truncate(n.unusable.callIntent, 200)}")` : "";
50
+ const why = UNUSABLE_LINE[n.unusable?.reason ?? ""] ?? "that cannot run here";
51
+ lines.push(`Step ${human} of ${n.totalSteps} is a sub-task${what} ${why}.`);
52
+ }
53
+ else if (n.kind === "known_bad_step") {
54
+ const times = n.failureCount ? ` ${n.failureCount} times` : "";
55
+ // A parked step inside a called segment names the scenario that owns it:
56
+ // that is where the repair happens, and the agent's own step numbering has
57
+ // no row for it (R-CALL-30).
58
+ const owner = n.inSubTask
59
+ ? `, in sub-task "${truncate(n.inSubTask, 120)}"${n.ownerScenarioId ? `, scenario ${n.ownerScenarioId}` : ""}`
60
+ : "";
61
+ lines.push(`Step ${human} of ${n.totalSteps}${tool.replace(/\)$/, `${owner})`)} did not run: it has failed${times} before, so the scenario stops in front of it.`);
62
+ }
63
+ else {
64
+ const error = n.error ? `: ${truncate(n.error, MAX_ERROR_CHARS)}` : "";
65
+ lines.push(`Step ${human} of ${n.totalSteps}${tool} failed${error}`);
66
+ }
67
+ if (n.remainingTools.length > 0) {
68
+ lines.push(`The remaining planned steps were: ${n.remainingTools.join(", ")}.`);
69
+ }
70
+ if (n.params && Object.keys(n.params).length > 0) {
71
+ let json;
72
+ try {
73
+ json = JSON.stringify(n.params);
74
+ }
75
+ catch {
76
+ json = "[unserializable]";
77
+ }
78
+ lines.push(`Parameters derived for this request: ${truncate(json, MAX_PARAMS_CHARS)}`);
79
+ }
80
+ return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
81
+ }
82
+ //# sourceMappingURL=handover.js.map
@@ -28,4 +28,15 @@ export declare function evalToolInputLogic(code: string, parameters: Record<stri
28
28
  export declare function evalToolOutputLogic(code: string, toolOutput: string, parameters: Record<string, unknown>, intent: string, respParams?: Record<string, unknown>): Record<string, unknown>;
29
29
  /** Build the final response model from everything the steps accumulated. */
30
30
  export declare function evalResponseParamsLogic(code: string, respParams: Record<string, unknown>, parameters: Record<string, unknown>, intent: string): Record<string, unknown>;
31
+ /**
32
+ * The two mapping bodies of a call (segmented.md R-CALL-20).
33
+ *
34
+ * `paramMapLogic` builds the called segment's parameters from the caller's
35
+ * state; `resultMapLogic` turns what the segment emitted into the keys the
36
+ * caller's later steps read. Both belong to the *(call, segment) pair* rather
37
+ * than to either side alone, which is why two chains calling one segment carry
38
+ * two of them.
39
+ */
40
+ export declare function evalParamMapLogic(code: string, parameters: Record<string, unknown>, intent: string, respParams?: Record<string, unknown>): Record<string, unknown>;
41
+ export declare function evalResultMapLogic(code: string, out: Record<string, unknown>, parameters: Record<string, unknown>, intent: string, respParams?: Record<string, unknown>): Record<string, unknown>;
31
42
  //# sourceMappingURL=logic.d.ts.map
@@ -47,4 +47,21 @@ export function evalResponseParamsLogic(code, respParams, parameters, intent) {
47
47
  const fn = new Function("respParams", "parameters", "intent", code);
48
48
  return assertPlainObject(fn(respParams, parameters, intent), "responseParamsLogic");
49
49
  }
50
+ /**
51
+ * The two mapping bodies of a call (segmented.md R-CALL-20).
52
+ *
53
+ * `paramMapLogic` builds the called segment's parameters from the caller's
54
+ * state; `resultMapLogic` turns what the segment emitted into the keys the
55
+ * caller's later steps read. Both belong to the *(call, segment) pair* rather
56
+ * than to either side alone, which is why two chains calling one segment carry
57
+ * two of them.
58
+ */
59
+ export function evalParamMapLogic(code, parameters, intent, respParams = {}) {
60
+ const fn = new Function("parameters", "intent", "respParams", code);
61
+ return assertPlainObject(fn(parameters, intent, respParams), "paramMapLogic");
62
+ }
63
+ export function evalResultMapLogic(code, out, parameters, intent, respParams = {}) {
64
+ const fn = new Function("out", "parameters", "intent", "respParams", code);
65
+ return assertPlainObject(fn(out, parameters, intent, respParams), "resultMapLogic");
66
+ }
50
67
  //# sourceMappingURL=logic.js.map
@@ -20,8 +20,9 @@
20
20
  * step to know its reach, so it can choose between the proxy, a recorded
21
21
  * output, and skipping.
22
22
  */
23
+ import { type FlatEntry } from "./flatten.js";
23
24
  import type { ReplayMode } from "./coverage.js";
24
- import type { ExecutionStage, SerializedScenario, SerializedScenarioStep } from "./types.js";
25
+ import type { ExecutionStage, FallbackKind, SerializedScenario, SerializedScenarioStep } from "./types.js";
25
26
  /**
26
27
  * Runs one step's tool and returns its output as a string.
27
28
  *
@@ -30,8 +31,14 @@ import type { ExecutionStage, SerializedScenario, SerializedScenarioStep } from
30
31
  * case a recorded output may stand in for.
31
32
  */
32
33
  export type ExecuteStep = (step: SerializedScenarioStep, input: Record<string, unknown>) => Promise<string>;
33
- /** Supplies a step's recorded output when its tool cannot run here (§8.1). */
34
- export type RecordedOutputFor = (step: SerializedScenarioStep) => Promise<string | undefined>;
34
+ /**
35
+ * Supplies a step's recorded output when its tool cannot run here (§8.1).
36
+ *
37
+ * It takes the *entry*, not the step: a step inside a called segment takes its
38
+ * recorded output from that segment's own recording, within its own range
39
+ * (segmented.md R-CALL-31).
40
+ */
41
+ export type RecordedOutputFor = (entry: FlatEntry) => Promise<string | undefined>;
35
42
  /**
36
43
  * What one step did, told to the caller as it happens.
37
44
  *
@@ -45,6 +52,12 @@ export type RecordedOutputFor = (step: SerializedScenarioStep) => Promise<string
45
52
  */
46
53
  export interface StepInfo {
47
54
  step: SerializedScenarioStep;
55
+ /**
56
+ * The flat entry this step ran as: which scenario owns it, which frame it
57
+ * ran in, and where it sits in the flat order (segmented.md R-CALL-13).
58
+ * Absent only where a caller builds a StepInfo by hand.
59
+ */
60
+ entry?: FlatEntry;
48
61
  input: Record<string, unknown>;
49
62
  outcome: "executed" | "recorded" | "skipped" | "failed";
50
63
  /** Which part of the step broke. Only set for `failed`. */
@@ -66,12 +79,41 @@ export interface ComposeResult {
66
79
  */
67
80
  errored: number;
68
81
  }
82
+ /** Where, and why, a run stopped short of the chain's end (fallbk.md). */
83
+ export interface PlanStop {
84
+ kind: FallbackKind;
85
+ /** The owning scenario's own `stepIndex` of the step it stopped at. */
86
+ stepIndex: number;
87
+ /** Its position in the flat list, which is what the caller indexes by. */
88
+ flatIndex?: number;
89
+ stage?: ExecutionStage;
90
+ error?: string;
91
+ }
69
92
  export interface PlanOptions {
70
93
  scenario: SerializedScenario;
71
94
  /** Resolves with the derived parameters. Rejection degrades to `{}`. */
72
95
  params: Promise<Record<string, unknown>>;
73
96
  mode: ReplayMode;
74
97
  respParamsInit?: Record<string, unknown>;
98
+ /**
99
+ * Position of the first known-bad step (fallbk.md D3). The plan ends in front
100
+ * of it: the steps before it are the plan, the rest is handed to the model.
101
+ * Omitted means the whole chain.
102
+ */
103
+ stopAt?: number;
104
+ /**
105
+ * The plan is a sub-task inside the agent's own task (segmented.md R-OUT-13):
106
+ * it was armed by intent, not by the prompt, so its directive and its bundles
107
+ * tell the agent to continue its task rather than to stop calling tools.
108
+ */
109
+ subTask?: boolean;
110
+ /**
111
+ * The flattened chain: the steps that will really run, each with the frame it
112
+ * runs in (segmented.md 10.6). A chain with no calls flattens to one frame
113
+ * and behaves exactly as it did before calls existed, which is why this is
114
+ * optional — a caller that does not flatten gets that same single frame.
115
+ */
116
+ entries?: FlatEntry[];
75
117
  }
76
118
  export declare class ScenarioReplayPlan {
77
119
  readonly scenarioId: string;
@@ -79,13 +121,39 @@ export declare class ScenarioReplayPlan {
79
121
  readonly mode: ReplayMode;
80
122
  readonly intent: string;
81
123
  private readonly scenario;
82
- private readonly steps;
124
+ /** The flat list this plan walks. One entry per step that will really run. */
125
+ private readonly entries;
83
126
  private readonly paramsPromise;
84
127
  private params;
85
128
  private respParams;
86
129
  private stepIndex;
87
130
  private readyPromise?;
131
+ /** Exclusive end of the planned steps. Equal to the chain length unless a step is parked. */
132
+ private readonly stopAt;
133
+ /** This plan is a sub-task inside the agent's own task (segmented.md R-OUT-13). */
134
+ readonly subTask: boolean;
88
135
  constructor(opts: PlanOptions);
136
+ /** The caller's own frame — the one at depth 0. */
137
+ private rootFrame;
138
+ /** The parameters one entry's logic runs with (segmented.md R-CALL-29). */
139
+ private paramsFor;
140
+ /** The accumulated values one entry's logic runs with. */
141
+ private respFor;
142
+ /**
143
+ * Entering a called frame: build the segment's parameters from the caller's
144
+ * state (R-CALL-29).
145
+ *
146
+ * The recipe is the dry run's, exactly (9.8 step 10): the mapping's output,
147
+ * plus the segment's own recorded sample for every *setting* the mapping left
148
+ * out. A mapping that omits a setting therefore cannot pass the service's
149
+ * check and then run `undefined` here.
150
+ */
151
+ private enterFrame;
152
+ /**
153
+ * Leaving a called frame: hand what the segment emitted back to its caller
154
+ * (R-CALL-29). `out` is the frame's own accumulated `respParams`.
155
+ */
156
+ private leaveFrame;
89
157
  /**
90
158
  * Await the derivation, then apply `paramsLogic` — once, however many callers
91
159
  * race here. A rejected derivation resolves to `{}` rather than throwing: the
@@ -95,7 +163,22 @@ export declare class ScenarioReplayPlan {
95
163
  ready(): Promise<void>;
96
164
  /** 0-based index of the step awaiting execution (== completed step count). */
97
165
  get currentStepIndex(): number;
166
+ /** Steps this plan will run — the chain up to the first known-bad step. */
98
167
  get stepCount(): number;
168
+ /** Every step this plan could run, planned or not. */
169
+ get totalSteps(): number;
170
+ /** True when the plan ends in front of a known-bad step (fallbk.md D3). */
171
+ stopsEarly(): boolean;
172
+ /** The known-bad step the plan stops in front of, if any. */
173
+ stopStep(): SerializedScenarioStep | undefined;
174
+ /** The entry the plan stops in front of, with the frame that owns it. */
175
+ stopEntry(): FlatEntry | undefined;
176
+ /** Tool names from position `from` to the end of the flat chain. */
177
+ toolsFrom(from: number): string[];
178
+ /** Every entry in order — for the report, and for the owning scenario ids. */
179
+ allEntries(): readonly FlatEntry[];
180
+ /** The entry awaiting execution, or undefined when the plan is done. */
181
+ currentEntry(): FlatEntry | undefined;
99
182
  /** The parameters in force. Empty until {@link ready} resolves. */
100
183
  get parameters(): Readonly<Record<string, unknown>>;
101
184
  /** Everything the steps have accumulated so far. */
@@ -104,7 +187,7 @@ export declare class ScenarioReplayPlan {
104
187
  currentStep(): SerializedScenarioStep | undefined;
105
188
  /** The tool the current step expects, or undefined when done. */
106
189
  expectedTool(): string | undefined;
107
- /** True once every step has been applied — the plan should be retired. */
190
+ /** True once every planned step has been applied — the plan should be retired. */
108
191
  isDone(): boolean;
109
192
  /** Every step in order — for the directive, and for coverage reporting. */
110
193
  allSteps(): readonly SerializedScenarioStep[];
@@ -143,7 +226,9 @@ export declare class ScenarioReplayPlan {
143
226
  * model initiates the expected calls — the arguments are supplied by the
144
227
  * system, so the model need not compute them.
145
228
  */
146
- steeringDirective(directToolName?: string): string;
229
+ steeringDirective(directToolName?: string, opts?: {
230
+ subTask?: boolean;
231
+ }): string;
147
232
  /**
148
233
  * Execute every **remaining** step and lay the results out as one bundle.
149
234
  *
@@ -159,7 +244,9 @@ export declare class ScenarioReplayPlan {
159
244
  * Skipping instead would drop the step *and* stop threading `respParams`, so
160
245
  * every later step reading from it fails too.
161
246
  */
162
- composeBundle(maxChars: number, execute: ExecuteStep, recordedOutputFor?: RecordedOutputFor, onStep?: StepObserver): Promise<ComposeResult>;
247
+ composeBundle(maxChars: number, execute: ExecuteStep, recordedOutputFor?: RecordedOutputFor, onStep?: StepObserver, opts?: {
248
+ handover?: boolean;
249
+ }): Promise<ComposeResult>;
163
250
  /**
164
251
  * Run the whole plan from the current cursor, advancing it as it goes.
165
252
  *
@@ -174,8 +261,18 @@ export declare class ScenarioReplayPlan {
174
261
  * an unbounded payload into the model's context for a scenario whose tools
175
262
  * answer in megabytes (docs/mcpmark.md §12).
176
263
  */
177
- runToCompletion(execute: ExecuteStep, recordedOutputFor?: RecordedOutputFor, onStep?: StepObserver, deadline?: number, maxChars?: number): Promise<ComposeResult & {
264
+ runToCompletion(execute: ExecuteStep, recordedOutputFor?: RecordedOutputFor, onStep?: StepObserver, deadline?: number, maxChars?: number, opts?: {
265
+ /**
266
+ * Stop at the first step that breaks — its logic throws, or its tool
267
+ * reports an error — instead of threading the error on (fallbk.md
268
+ * §Runner 3). A step after a failed one computes on a result that does not
269
+ * exist. Off keeps the old contract: a logic throw propagates, a tool
270
+ * error is threaded and counted.
271
+ */
272
+ stopOnFailure?: boolean;
273
+ }): Promise<ComposeResult & {
178
274
  partial: boolean;
275
+ stopped?: PlanStop;
179
276
  }>;
180
277
  }
181
278
  //# sourceMappingURL=plan.d.ts.map