@basein/runner 0.2.0 → 0.2.1

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.
@@ -34,11 +34,11 @@ export function reachOf(toolName, wrapped, allow) {
34
34
  export function modeFor(steps, wrapped, allow) {
35
35
  if (steps.length === 0)
36
36
  return "none";
37
- return steps.every((s) => reachOf(s.toolName, wrapped, allow) === "direct") ? "direct" : "steer";
37
+ return steps.every((s) => reachOf(s.toolName ?? "", wrapped, allow) === "direct") ? "direct" : "steer";
38
38
  }
39
39
  /** Per-step reach, in step order — what `plan.armed` logs and `bir doctor` shows. */
40
40
  export function coverageOf(steps, wrapped, allow) {
41
- return steps.map((s) => reachOf(s.toolName, wrapped, allow));
41
+ return steps.map((s) => reachOf(s.toolName ?? "", wrapped, allow));
42
42
  }
43
43
  /**
44
44
  * Parse `BIR_REPLAY_ALLOW_SERVERS`. Unset (or empty) means *every wrapped
@@ -23,7 +23,18 @@
23
23
  */
24
24
  import type { ParamsSchema } from "./types.js";
25
25
  export declare const DEFAULT_DERIVE_MODEL = "claude-haiku-4-5-20251001";
26
- export interface DeriveOptions {
26
+ /** The call the agent was about to make when a plan armed mid-task. */
27
+ export interface LiveCall {
28
+ toolName: string;
29
+ /** Redacted and capped, exactly as the probe sent it. */
30
+ toolInput: string;
31
+ }
32
+ /** What a turn can tell derivation beyond its prompt (segmented.md R-PARAM-2). */
33
+ export interface DeriveContext {
34
+ liveCall?: LiveCall;
35
+ recentResults?: string[];
36
+ }
37
+ export interface DeriveOptions extends DeriveContext {
27
38
  prompt: string;
28
39
  intent: string;
29
40
  paramsObject: ParamsSchema | null;
@@ -42,12 +53,20 @@ export interface DeriveResult {
42
53
  model?: string;
43
54
  inputTokens?: number;
44
55
  outputTokens?: number;
56
+ /**
57
+ * Targets that came back `null` (segmented.md R-PARAM-3). Non-empty means the
58
+ * plan must not run: the turn does not say what this task is to act on.
59
+ */
60
+ missing: string[];
61
+ /** Settings filled from their recorded sample (R-PARAM-2). */
62
+ sampled: string[];
45
63
  }
46
64
  /**
47
65
  * First balanced `{…}` in a text blob. Models wrap JSON in prose and fences more
48
66
  * often than they emit it bare, and a failed parse here costs a whole replay.
49
67
  */
50
68
  export declare function extractJsonObject(text: string): string | undefined;
69
+ export declare function buildExtractionPrompt(prompt: string, intent: string, schema: ParamsSchema, ctx?: DeriveContext): string;
51
70
  /**
52
71
  * Derive the scenario's parameters from `prompt`. Never throws: every failure
53
72
  * path resolves with the recorded sample values, because a replay with stale
@@ -32,6 +32,10 @@ function sampleValues(schema) {
32
32
  out[key] = schema[key].sampleValue;
33
33
  return out;
34
34
  }
35
+ /** A parameter with no kind reads as a target (segmented.md R-PARAM-3). */
36
+ function isTarget(schema, key) {
37
+ return schema[key]?.kind !== "setting";
38
+ }
35
39
  /**
36
40
  * First balanced `{…}` in a text blob. Models wrap JSON in prose and fences more
37
41
  * often than they emit it bare, and a failed parse here costs a whole replay.
@@ -66,24 +70,40 @@ export function extractJsonObject(text) {
66
70
  }
67
71
  return undefined;
68
72
  }
69
- function buildExtractionPrompt(prompt, intent, schema) {
73
+ export function buildExtractionPrompt(prompt, intent, schema, ctx = {}) {
74
+ // Each parameter wears its kind, because the last rule below treats the two
75
+ // differently and the model cannot tell them apart from the name alone
76
+ // (segmented.md R-PARAM-2).
70
77
  const descriptions = Object.keys(schema)
71
78
  .map((key) => {
72
79
  const { description, sampleValue } = schema[key];
73
- return `- ${key}: ${description} (sample: ${JSON.stringify(sampleValue)})`;
80
+ const kind = isTarget(schema, key) ? "target" : "setting";
81
+ return `- ${key} (${kind}): ${description} (sample: ${JSON.stringify(sampleValue)})`;
74
82
  })
75
83
  .join("\n");
84
+ // The call the agent was about to make is the most specific thing the turn
85
+ // says about a target — more specific than the prompt, which may not mention
86
+ // it at all (R-PARAM-8).
87
+ const live = ctx.liveCall
88
+ ? `\nThe agent was about to call ${ctx.liveCall.toolName} with ${ctx.liveCall.toolInput}.\n`
89
+ : "";
90
+ const results = (ctx.recentResults ?? []).filter((r) => r.trim().length > 0);
91
+ const recent = results.length > 0
92
+ ? `\nRecent tool results from this task, oldest first:\n${results
93
+ .map((r) => `\`\`\`\n${r}\n\`\`\``)
94
+ .join("\n")}\n`
95
+ : "";
76
96
  return `You are extracting parameter values from a user prompt to execute a pre-defined scenario.
77
97
 
78
98
  Scenario intent: ${intent}
79
-
99
+ ${live}
80
100
  Parameters needed (with sample values from past runs):
81
101
  ${descriptions}
82
102
 
83
103
  User prompt:
84
104
  ${prompt}
85
-
86
- Extract the parameter values from the user prompt. If a value is not explicitly mentioned, infer a reasonable default from the sample values above.
105
+ ${recent}
106
+ Extract the parameter values. A setting you cannot find takes its sample value. A target you cannot find in the prompt, the reasoning, the call or the results is null — never invent it and never copy the sample.
87
107
 
88
108
  Respond ONLY with a valid JSON object mapping each parameter name to its value. No markdown fences, no prose.
89
109
  Example: { "directory": "src/utils", "pattern": "*.ts" }`;
@@ -98,10 +118,19 @@ export async function deriveParameters(opts) {
98
118
  const schema = opts.paramsObject ?? {};
99
119
  const keys = Object.keys(schema);
100
120
  if (keys.length === 0)
101
- return { params: {}, costUsd: 0, derived: false };
121
+ return { params: {}, costUsd: 0, derived: false, missing: [], sampled: [] };
102
122
  const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
123
+ // No key is a supported configuration, and `derived: false` is how the caller
124
+ // tells it apart from a real extraction: it declines any scenario that has a
125
+ // target and arms an all-settings one on its recorded values (R-PARAM-5).
103
126
  if (!apiKey)
104
- return { params: sampleValues(schema), costUsd: 0, derived: false };
127
+ return {
128
+ params: sampleValues(schema),
129
+ costUsd: 0,
130
+ derived: false,
131
+ missing: [],
132
+ sampled: keys,
133
+ };
105
134
  const model = opts.model ?? process.env.BIR_DERIVE_MODEL ?? DEFAULT_DERIVE_MODEL;
106
135
  const doFetch = opts.fetchImpl ?? fetch;
107
136
  const res = await doFetch(ANTHROPIC_URL, {
@@ -115,7 +144,13 @@ export async function deriveParameters(opts) {
115
144
  model,
116
145
  max_tokens: 2024,
117
146
  messages: [
118
- { role: "user", content: buildExtractionPrompt(opts.prompt, opts.intent, schema) },
147
+ {
148
+ role: "user",
149
+ content: buildExtractionPrompt(opts.prompt, opts.intent, schema, {
150
+ liveCall: opts.liveCall,
151
+ recentResults: opts.recentResults,
152
+ }),
153
+ },
119
154
  ],
120
155
  }),
121
156
  signal: opts.signal,
@@ -155,12 +190,34 @@ export async function deriveParameters(opts) {
155
190
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
156
191
  throw new Error("derive returned a non-object");
157
192
  }
158
- // A missing or null key falls back to its recorded sample, so a partial
159
- // extraction still yields a runnable parameter set.
193
+ // A setting nobody stated takes its recorded sample — that is what a setting
194
+ // is. A target nobody stated stays null and is named in `missing`, and the
195
+ // caller declines the plan (segmented.md R-PARAM-2, R-PARAM-3). Copying the
196
+ // sample here is exactly the guess the design forbids: it is how a replay
197
+ // ends up querying last quarter's region because this prompt named no region.
198
+ const missing = [];
199
+ const sampled = [];
160
200
  for (const key of keys) {
161
- if (parsed[key] === null || parsed[key] === undefined)
201
+ if (parsed[key] !== null && parsed[key] !== undefined)
202
+ continue;
203
+ if (isTarget(schema, key)) {
204
+ parsed[key] = null;
205
+ missing.push(key);
206
+ }
207
+ else {
162
208
  parsed[key] = schema[key].sampleValue;
209
+ sampled.push(key);
210
+ }
163
211
  }
164
- return { params: parsed, costUsd, derived: true, model, inputTokens, outputTokens };
212
+ return {
213
+ params: parsed,
214
+ costUsd,
215
+ derived: true,
216
+ model,
217
+ inputTokens,
218
+ outputTokens,
219
+ missing,
220
+ sampled,
221
+ };
165
222
  }
166
223
  //# sourceMappingURL=derive.js.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Flattening calls into frames (segmented.md 10.6).
3
+ *
4
+ * A chain may hold a **call**: one row that says "here the agent does this
5
+ * sub-task", answered by a recorded scenario the service inlines into the
6
+ * payload. Running it is not a second plan. It is one plan over one flat list
7
+ * of steps, in which some steps belong to another scenario and run with *its*
8
+ * parameters, *its* intent and *its* accumulated values.
9
+ *
10
+ * That is what a **frame** is: the scope one stretch of steps runs in. Frame 0
11
+ * is the caller's own. Entering a called frame evaluates the call's
12
+ * `paramMapLogic` to build the segment's parameters; leaving it evaluates
13
+ * `resultMapLogic` and merges the result back into the caller's `respParams`
14
+ * (R-CALL-29).
15
+ *
16
+ * Everything downstream reads the flat list rather than the chain: the gates
17
+ * judge the steps that will really run (R-CALL-30), recorded outputs come from
18
+ * the right recording and range (R-CALL-31), and a failure is reported on the
19
+ * scenario that owns the step (R-CALL-13).
20
+ *
21
+ * The flattener itself decides nothing about *which* segment answers a call —
22
+ * the service did that long before this payload was built. It only lays out
23
+ * what it was served, and stops in front of anything it cannot run.
24
+ */
25
+ import type { SerializedScenario, SerializedScenarioStep, CallUnusable } from "./types.js";
26
+ import type { ParamsSchema } from "./types.js";
27
+ /** The scope one stretch of steps runs in. Frame 0 is the caller's own. */
28
+ export interface Frame {
29
+ id: number;
30
+ /** 0 for the caller, 1 for a segment it calls, 2 for one that segment calls. */
31
+ depth: number;
32
+ scenarioId: string;
33
+ runId: string;
34
+ /** The recorded range the frame's outputs come from. Null means the whole run. */
35
+ stepFrom: number | null;
36
+ stepTo: number | null;
37
+ /** The intent the frame's step logic runs with (R-INTENT-12). */
38
+ intent: string;
39
+ parent?: Frame;
40
+ /** The caller's chain `stepIndex` of the call row that opened this frame. */
41
+ callerStepIndex?: number;
42
+ callIntent?: string;
43
+ paramMapLogic?: string;
44
+ resultMapLogic?: string;
45
+ segmentId?: string;
46
+ /** The inlined segment's `chainRevision` as served (R-CALL-13). */
47
+ chainRevision?: number;
48
+ /** The segment's own parameters, for the settings a mapping omits (R-CALL-21). */
49
+ paramsObject?: ParamsSchema | null;
50
+ /** Filled by the plan when the frame is entered. */
51
+ parameters?: Record<string, unknown>;
52
+ respParams: Record<string, unknown>;
53
+ }
54
+ /** One step that will really run, with the frame it runs in. */
55
+ export interface FlatEntry {
56
+ scenarioId: string;
57
+ runId: string;
58
+ stepFrom: number | null;
59
+ stepTo: number | null;
60
+ /** The owning scenario's own `scenario_steps.step_index`, not a flat position. */
61
+ stepIndex: number;
62
+ /** Always a tool row: a call row becomes a frame, or a stop. */
63
+ step: SerializedScenarioStep;
64
+ frame: Frame;
65
+ depth: number;
66
+ /** First / last entry of its frame — where the two mapping bodies run. */
67
+ first: boolean;
68
+ last: boolean;
69
+ }
70
+ /** Why the flat list stops short, and which call row said so. */
71
+ export interface FlatStop {
72
+ kind: "unusable";
73
+ reason: CallUnusable;
74
+ /** The scenario whose chain holds the call row. */
75
+ scenarioId: string;
76
+ /** That call row's own `stepIndex`. */
77
+ stepIndex: number;
78
+ segmentId?: string | null;
79
+ callIntent?: string | null;
80
+ }
81
+ export interface FlatChain {
82
+ entries: FlatEntry[];
83
+ /** Flat position the plan stops in front of. Undefined means the whole chain. */
84
+ stopAt?: number;
85
+ stopReason?: FlatStop;
86
+ }
87
+ /**
88
+ * Mirrors the service's `SEGMENT_MAX_DEPTH`. A constant, not a setting: one
89
+ * call row holds one answer, which is only correct because a call inside a
90
+ * level-two segment is never answered (R-CALL-12).
91
+ */
92
+ export declare const MAX_CALL_DEPTH = 2;
93
+ /**
94
+ * Lay a served chain out as one flat list of steps, with a frame per call.
95
+ *
96
+ * Stops at the first call it cannot run. The stop is *clamped* to the start of
97
+ * the outermost called frame that contains it: a plan must never run half a
98
+ * sub-task and leave its `resultMapLogic` unevaluated, because the caller's
99
+ * later steps read what that body returns (R-CALL-30).
100
+ */
101
+ export declare function flattenChain(scenario: SerializedScenario, opts?: {
102
+ maxDepth?: number;
103
+ /** The range a segment armed on its own covers (its own recording). */
104
+ range?: {
105
+ stepFrom: number;
106
+ stepTo: number;
107
+ } | null;
108
+ }): FlatChain;
109
+ /**
110
+ * Move a stop back to the start of the outermost called frame it falls in
111
+ * (R-CALL-30, 10.6 step 3).
112
+ *
113
+ * Used for the known-bad-step gate, which judges entries: a parked step in the
114
+ * middle of a sub-task stops the plan in front of the *sub-task*, not in front
115
+ * of the step, because stopping inside it would skip the body that hands its
116
+ * results back.
117
+ */
118
+ export declare function clampToFrameStart(entries: readonly FlatEntry[], index: number): number;
119
+ /**
120
+ * One frame per chain, for a payload with no calls in it and for the callers
121
+ * that build a plan by hand (tests, `bir replay`). Byte for byte the old
122
+ * behaviour: one frame, one scenario, one range.
123
+ */
124
+ export declare function flatEntriesOf(scenario: SerializedScenario): FlatEntry[];
125
+ //# sourceMappingURL=flatten.d.ts.map
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Flattening calls into frames (segmented.md 10.6).
3
+ *
4
+ * A chain may hold a **call**: one row that says "here the agent does this
5
+ * sub-task", answered by a recorded scenario the service inlines into the
6
+ * payload. Running it is not a second plan. It is one plan over one flat list
7
+ * of steps, in which some steps belong to another scenario and run with *its*
8
+ * parameters, *its* intent and *its* accumulated values.
9
+ *
10
+ * That is what a **frame** is: the scope one stretch of steps runs in. Frame 0
11
+ * is the caller's own. Entering a called frame evaluates the call's
12
+ * `paramMapLogic` to build the segment's parameters; leaving it evaluates
13
+ * `resultMapLogic` and merges the result back into the caller's `respParams`
14
+ * (R-CALL-29).
15
+ *
16
+ * Everything downstream reads the flat list rather than the chain: the gates
17
+ * judge the steps that will really run (R-CALL-30), recorded outputs come from
18
+ * the right recording and range (R-CALL-31), and a failure is reported on the
19
+ * scenario that owns the step (R-CALL-13).
20
+ *
21
+ * The flattener itself decides nothing about *which* segment answers a call —
22
+ * the service did that long before this payload was built. It only lays out
23
+ * what it was served, and stops in front of anything it cannot run.
24
+ */
25
+ /**
26
+ * Mirrors the service's `SEGMENT_MAX_DEPTH`. A constant, not a setting: one
27
+ * call row holds one answer, which is only correct because a call inside a
28
+ * level-two segment is never answered (R-CALL-12).
29
+ */
30
+ export const MAX_CALL_DEPTH = 2;
31
+ /**
32
+ * Lay a served chain out as one flat list of steps, with a frame per call.
33
+ *
34
+ * Stops at the first call it cannot run. The stop is *clamped* to the start of
35
+ * the outermost called frame that contains it: a plan must never run half a
36
+ * sub-task and leave its `resultMapLogic` unevaluated, because the caller's
37
+ * later steps read what that body returns (R-CALL-30).
38
+ */
39
+ export function flattenChain(scenario, opts = {}) {
40
+ const maxDepth = opts.maxDepth ?? MAX_CALL_DEPTH;
41
+ const entries = [];
42
+ let frameId = 0;
43
+ let stopAt;
44
+ let stopReason;
45
+ const root = {
46
+ id: frameId++,
47
+ depth: 0,
48
+ scenarioId: scenario.id,
49
+ runId: scenario.runId,
50
+ stepFrom: opts.range?.stepFrom ?? null,
51
+ stepTo: opts.range?.stepTo ?? null,
52
+ intent: scenario.intent ?? "",
53
+ respParams: {},
54
+ };
55
+ /** The entry a frame's steps start at, for the clamp. */
56
+ const frameStart = new Map();
57
+ const walk = (steps, frame, stack) => {
58
+ for (const step of steps) {
59
+ if (stopAt !== undefined)
60
+ return;
61
+ if (step.kind !== "segment") {
62
+ entries.push({
63
+ scenarioId: frame.scenarioId,
64
+ runId: frame.runId,
65
+ stepFrom: frame.stepFrom,
66
+ stepTo: frame.stepTo,
67
+ stepIndex: step.stepIndex,
68
+ step,
69
+ frame,
70
+ depth: frame.depth,
71
+ first: false,
72
+ last: false,
73
+ });
74
+ continue;
75
+ }
76
+ // A call the service already marked unrunnable, a third level, or a
77
+ // segment already on this stack (which would be a cycle): all the same
78
+ // thing to a plan — it stops here and the agent takes over (R-CALL-29).
79
+ const cycles = !!step.segment && stack.includes(step.segment.id);
80
+ const tooDeep = frame.depth + 1 > maxDepth;
81
+ const reason = step.unusable
82
+ ? step.unusable
83
+ : !step.segment || cycles || tooDeep
84
+ ? "depth"
85
+ : undefined;
86
+ if (reason) {
87
+ // The clamp: a stop inside a called frame moves back to the first entry
88
+ // of the outermost called frame containing it, so the caller never runs
89
+ // part of a sub-task (R-CALL-30).
90
+ let outermost = frame;
91
+ while (outermost && outermost.depth > 1)
92
+ outermost = outermost.parent;
93
+ stopAt =
94
+ frame.depth > 0 && outermost
95
+ ? (frameStart.get(outermost.id) ?? entries.length)
96
+ : entries.length;
97
+ stopReason = {
98
+ kind: "unusable",
99
+ reason,
100
+ scenarioId: frame.scenarioId,
101
+ stepIndex: step.stepIndex,
102
+ segmentId: step.segmentId ?? step.segment?.id ?? null,
103
+ callIntent: step.callIntent ?? null,
104
+ };
105
+ return;
106
+ }
107
+ const segment = step.segment;
108
+ const child = {
109
+ id: frameId++,
110
+ depth: frame.depth + 1,
111
+ scenarioId: segment.id,
112
+ runId: segment.runId,
113
+ stepFrom: segment.stepFrom,
114
+ stepTo: segment.stepTo,
115
+ intent: segment.intent ?? "",
116
+ parent: frame,
117
+ callerStepIndex: step.stepIndex,
118
+ callIntent: step.callIntent ?? undefined,
119
+ paramMapLogic: step.paramMapLogic ?? undefined,
120
+ resultMapLogic: step.resultMapLogic ?? undefined,
121
+ segmentId: segment.id,
122
+ chainRevision: segment.chainRevision,
123
+ paramsObject: segment.paramsObject ?? null,
124
+ respParams: {},
125
+ };
126
+ frameStart.set(child.id, entries.length);
127
+ walk(segment.steps ?? [], child, [...stack, segment.id]);
128
+ }
129
+ };
130
+ frameStart.set(root.id, 0);
131
+ walk(scenario.steps ?? [], root, [scenario.id]);
132
+ // `first` / `last` per frame, marked once the whole list is known: they are
133
+ // where the two mapping bodies run. A frame's entries are not necessarily
134
+ // contiguous — a call sitting in the middle of a chain interleaves its
135
+ // callee's entries with its own — so these are the frame's first and last
136
+ // entries overall, never the edges of a run of them.
137
+ const firstOf = new Map();
138
+ const lastOf = new Map();
139
+ entries.forEach((entry, index) => {
140
+ if (!firstOf.has(entry.frame.id))
141
+ firstOf.set(entry.frame.id, index);
142
+ lastOf.set(entry.frame.id, index);
143
+ });
144
+ entries.forEach((entry, index) => {
145
+ entry.first = firstOf.get(entry.frame.id) === index;
146
+ entry.last = lastOf.get(entry.frame.id) === index;
147
+ });
148
+ return { entries, stopAt, stopReason };
149
+ }
150
+ /**
151
+ * Move a stop back to the start of the outermost called frame it falls in
152
+ * (R-CALL-30, 10.6 step 3).
153
+ *
154
+ * Used for the known-bad-step gate, which judges entries: a parked step in the
155
+ * middle of a sub-task stops the plan in front of the *sub-task*, not in front
156
+ * of the step, because stopping inside it would skip the body that hands its
157
+ * results back.
158
+ */
159
+ export function clampToFrameStart(entries, index) {
160
+ const at = entries[index];
161
+ if (!at || at.depth === 0)
162
+ return index;
163
+ let outermost = at.frame;
164
+ while (outermost && outermost.depth > 1)
165
+ outermost = outermost.parent;
166
+ if (!outermost)
167
+ return index;
168
+ for (let i = 0; i < entries.length; i += 1) {
169
+ if (entries[i].frame.id === outermost.id)
170
+ return i;
171
+ }
172
+ return index;
173
+ }
174
+ /**
175
+ * One frame per chain, for a payload with no calls in it and for the callers
176
+ * that build a plan by hand (tests, `bir replay`). Byte for byte the old
177
+ * behaviour: one frame, one scenario, one range.
178
+ */
179
+ export function flatEntriesOf(scenario) {
180
+ return flattenChain(scenario).entries;
181
+ }
182
+ //# sourceMappingURL=flatten.js.map
@@ -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