@basein/runner 0.3.21 → 0.3.22

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.
Files changed (48) hide show
  1. package/dist/bin/bir-hooks.d.ts +7 -16
  2. package/dist/bin/bir-hooks.js +7 -16
  3. package/dist/bin/bir-scenario.js +1 -1
  4. package/dist/bin/bir.js +1 -21
  5. package/dist/bin/investigate.d.ts +0 -9
  6. package/dist/bin/investigate.js +14 -42
  7. package/dist/bin/scenario-edit.js +13 -44
  8. package/dist/control/recorder-main.d.ts +0 -15
  9. package/dist/control/recorder-main.js +7 -39
  10. package/dist/control/server.d.ts +24 -137
  11. package/dist/control/server.js +101 -995
  12. package/dist/record/recorder.d.ts +11 -142
  13. package/dist/record/recorder.js +0 -3
  14. package/dist/record/remote-recorder.d.ts +1 -9
  15. package/dist/record/remote-recorder.js +3 -49
  16. package/dist/record/switchable-recorder.js +0 -2
  17. package/dist/replay/bundle.d.ts +2 -2
  18. package/dist/replay/bundle.js +3 -3
  19. package/dist/replay/controller.d.ts +19 -123
  20. package/dist/replay/controller.js +151 -518
  21. package/dist/replay/derive.d.ts +0 -10
  22. package/dist/replay/derive.js +0 -3
  23. package/dist/replay/handover.d.ts +2 -37
  24. package/dist/replay/handover.js +38 -106
  25. package/dist/replay/logic.d.ts +0 -11
  26. package/dist/replay/logic.js +0 -24
  27. package/dist/replay/plan.d.ts +43 -163
  28. package/dist/replay/plan.js +144 -556
  29. package/dist/replay/safety.d.ts +4 -5
  30. package/dist/replay/safety.js +7 -7
  31. package/dist/replay/source-run.d.ts +15 -24
  32. package/dist/replay/source-run.js +25 -58
  33. package/dist/replay/targets.d.ts +11 -21
  34. package/dist/replay/targets.js +30 -69
  35. package/dist/replay/types.d.ts +9 -96
  36. package/dist/replay/types.js +3 -16
  37. package/dist/util/service-headers.d.ts +1 -2
  38. package/dist/util/service-headers.js +1 -2
  39. package/docs/calculatedReplay.md +17 -74
  40. package/docs/calculatedReplayGuide.md +9 -30
  41. package/package.json +1 -1
  42. package/production.md +5 -35
  43. package/dist/replay/condition-words.d.ts +0 -28
  44. package/dist/replay/condition-words.js +0 -59
  45. package/dist/replay/flatten.d.ts +0 -161
  46. package/dist/replay/flatten.js +0 -222
  47. package/dist/replay/reads-only.d.ts +0 -29
  48. package/dist/replay/reads-only.js +0 -261
@@ -1,59 +0,0 @@
1
- /**
2
- * condition-words — the request's own words for its decision (the service's
3
- * segmentsbreakdown.md SB-18).
4
- *
5
- * When a plan stops at its decision, the hand-over note quotes the sentences
6
- * of the live request that hold a word for a condition ("if … otherwise"), so
7
- * the agent sees which choice is its own to make. Only the request's words:
8
- * never the condition the service keeps, which is written from the recorded
9
- * run and would hint at the half the recording took.
10
- *
11
- * The word lists are the service's (its src/scenarios/condition.ts), copied
12
- * exactly. Keep the two the same: the service decides which requests may hold
13
- * a condition with them, and this quotes the sentences they find.
14
- */
15
- /** Words that say a part of a task depends on something: English, and the Hebrew the service reads. */
16
- const CONDITION_WORDS = /\b(if|otherwise|else|unless|only when|only if|in case|whether)\b/i;
17
- const HEBREW_CONDITION = /(^|[\s,.;:!?"'()\-])(אם|אחרת|אלא אם|במקרה ש|רק אם|רק כאשר)($|[\s,.;:!?"'()\-])/;
18
- /** The longest quote a decision note carries, in characters. */
19
- export const MAX_DECISION_QUOTE = 600;
20
- /** Whether a text has a word for a condition in it: the service's `mayHoldCondition`. */
21
- export function mayHoldCondition(text) {
22
- return CONDITION_WORDS.test(text) || HEBREW_CONDITION.test(text);
23
- }
24
- /**
25
- * Where one sentence ends and the next begins: after `.`, `!`, `?` or `…` and
26
- * white space, in front of a capital letter, a letter of a script with no
27
- * capitals (Hebrew), a quote mark or a bracket. Never in front of a digit or a
28
- * small letter, so "Nov. 20" and "e.g. the" stay in one sentence.
29
- */
30
- const SENTENCE_BREAK = /(?<=[.!?…])\s+(?=[\p{Lu}\p{Lo}"'“‘(\[])/u;
31
- /**
32
- * The sentences of `request` that hold a word for a condition, in order,
33
- * each on one line of single spaces, joined by a space. Cut to at most `max`
34
- * characters, ending in "…" when cut. Null when no sentence holds one.
35
- *
36
- * Lines are sentences of their own: the request is split at line breaks
37
- * first, then at {@link SENTENCE_BREAK}.
38
- */
39
- export function conditionSentences(request, max = MAX_DECISION_QUOTE) {
40
- if (typeof request !== "string" || request.trim() === "")
41
- return null;
42
- const sentences = request
43
- .replace(/\r\n?/g, "\n")
44
- .split("\n")
45
- .flatMap((line) => line.split(SENTENCE_BREAK))
46
- .map((s) => s.replace(/\s+/g, " ").trim())
47
- .filter((s) => s !== "" && mayHoldCondition(s));
48
- if (sentences.length === 0)
49
- return null;
50
- const quote = sentences.join(" ");
51
- if (quote.length <= max)
52
- return quote;
53
- let cut = quote.slice(0, Math.max(0, max - 1));
54
- // Never half of a character written as two code units (an emoji).
55
- if (/[\uD800-\uDBFF]$/.test(cut))
56
- cut = cut.slice(0, -1);
57
- return `${cut.trimEnd()}…`;
58
- }
59
- //# sourceMappingURL=condition-words.js.map
@@ -1,161 +0,0 @@
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, StepTest } 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
- /**
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;
59
- /** Filled by the plan when the frame is entered. */
60
- parameters?: Record<string, unknown>;
61
- respParams: Record<string, unknown>;
62
- }
63
- /** One step that will really run, with the frame it runs in. */
64
- export interface FlatEntry {
65
- scenarioId: string;
66
- runId: string;
67
- stepFrom: number | null;
68
- stepTo: number | null;
69
- /** The owning scenario's own `scenario_steps.step_index`, not a flat position. */
70
- stepIndex: number;
71
- /** Always a tool row: a call row becomes a frame, or a stop. */
72
- step: SerializedScenarioStep;
73
- frame: Frame;
74
- depth: number;
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
- */
80
- first: boolean;
81
- last: boolean;
82
- }
83
- /** Why the flat list stops short, and which call row said so. */
84
- export interface FlatStop {
85
- kind: "unusable";
86
- reason: CallUnusable;
87
- /** The scenario whose chain holds the call row. */
88
- scenarioId: string;
89
- /** That call row's own `stepIndex`. */
90
- stepIndex: number;
91
- segmentId?: string | null;
92
- callIntent?: string | null;
93
- }
94
- export interface FlatChain {
95
- entries: FlatEntry[];
96
- /** Flat position the plan stops in front of. Undefined means the whole chain. */
97
- stopAt?: number;
98
- stopReason?: FlatStop;
99
- }
100
- /**
101
- * Mirrors the service's `SEGMENT_MAX_DEPTH`. A constant, not a setting: one
102
- * call row holds one answer, which is only correct because a call inside a
103
- * level-two segment is never answered (R-CALL-12).
104
- */
105
- export declare const MAX_CALL_DEPTH = 2;
106
- /**
107
- * Lay a served chain out as one flat list of steps, with a frame per call.
108
- *
109
- * Stops at the first call it cannot run. The stop is *clamped* to the start of
110
- * the outermost called frame that contains it: a plan must never run half a
111
- * sub-task and leave its `resultMapLogic` unevaluated, because the caller's
112
- * later steps read what that body returns (R-CALL-30).
113
- */
114
- export declare function flattenChain(scenario: SerializedScenario, opts?: {
115
- maxDepth?: number;
116
- /** The range a segment armed on its own covers (its own recording). */
117
- range?: {
118
- stepFrom: number;
119
- stepTo: number;
120
- } | null;
121
- }): FlatChain;
122
- /**
123
- * Move a stop back to the start of the outermost called frame it falls in
124
- * (R-CALL-30, 10.6 step 3).
125
- *
126
- * Used for the known-bad-step gate, which judges entries: a parked step in the
127
- * middle of a sub-task stops the plan in front of the *sub-task*, not in front
128
- * of the step, because stopping inside it would skip the body that hands its
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.
131
- */
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;
155
- /**
156
- * One frame per chain, for a payload with no calls in it and for the callers
157
- * that build a plan by hand (tests, `bir replay`). Byte for byte the old
158
- * behaviour: one frame, one scenario, one range.
159
- */
160
- export declare function flatEntriesOf(scenario: SerializedScenario): FlatEntry[];
161
- //# sourceMappingURL=flatten.d.ts.map
@@ -1,222 +0,0 @@
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
- // 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,
128
- respParams: {},
129
- };
130
- frameStart.set(child.id, entries.length);
131
- walk(segment.steps ?? [], child, [...stack, segment.id]);
132
- }
133
- };
134
- frameStart.set(root.id, 0);
135
- walk(scenario.steps ?? [], root, [scenario.id]);
136
- // `first` / `last` per frame, marked once the whole list is known: they are
137
- // where the two mapping bodies run. A frame's entries are not necessarily
138
- // contiguous — a call sitting in the middle of a chain interleaves its
139
- // callee's entries with its own — so these are the frame's first and last
140
- // entries overall, never the edges of a run of them.
141
- const firstOf = new Map();
142
- const lastOf = new Map();
143
- entries.forEach((entry, index) => {
144
- if (!firstOf.has(entry.frame.id))
145
- firstOf.set(entry.frame.id, index);
146
- lastOf.set(entry.frame.id, index);
147
- });
148
- entries.forEach((entry, index) => {
149
- entry.first = firstOf.get(entry.frame.id) === index;
150
- entry.last = lastOf.get(entry.frame.id) === index;
151
- });
152
- return { entries, stopAt, stopReason };
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
- }
161
- /**
162
- * Move a stop back to the start of the outermost called frame it falls in
163
- * (R-CALL-30, 10.6 step 3).
164
- *
165
- * Used for the known-bad-step gate, which judges entries: a parked step in the
166
- * middle of a sub-task stops the plan in front of the *sub-task*, not in front
167
- * of the step, because stopping inside it would skip the body that hands its
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.
170
- */
171
- export function clampToFrameStart(entries, index) {
172
- const at = entries[index];
173
- if (!at || at.depth === 0)
174
- return index;
175
- let outermost = at.frame;
176
- while (outermost && outermost.depth > 1)
177
- outermost = outermost.parent;
178
- if (!outermost)
179
- return index;
180
- for (let i = 0; i < entries.length; i += 1) {
181
- if (within(entries[i].frame, outermost))
182
- return i;
183
- }
184
- return index;
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
- }
214
- /**
215
- * One frame per chain, for a payload with no calls in it and for the callers
216
- * that build a plan by hand (tests, `bir replay`). Byte for byte the old
217
- * behaviour: one frame, one scenario, one range.
218
- */
219
- export function flatEntriesOf(scenario) {
220
- return flattenChain(scenario).entries;
221
- }
222
- //# sourceMappingURL=flatten.js.map
@@ -1,29 +0,0 @@
1
- /**
2
- * Whether a tool call may change something, judged by its name alone — the
3
- * runner's copy of the service's own rule (BaseIn src/scenarios/correctionCheck.ts,
4
- * `readsOnly`, `commandReadsOnly`, `callMayChange`), kept word for word so the
5
- * two never disagree about the same call.
6
- *
7
- * Used where a call after a plan's decision is the agent starting a way of its
8
- * own (the service's offer1.md O7), and where a plan started after a call
9
- * would make it a second time (O8). When in doubt, a call is a change: that
10
- * only ends a set of scenarios early or keeps a plan from starting, never lets
11
- * a second write through.
12
- */
13
- /**
14
- * A tool that only reads: Claude Code's read-only tools, then the name — a
15
- * reading word and no writing word (`calendar_search_events`, `getJiraIssue`;
16
- * `run_sql` no: `run` writes). A name with both (`get_or_create_contact`)
17
- * changes something.
18
- */
19
- export declare function readsOnly(toolName: string): boolean;
20
- /** Whether a shell command only looks: every part of it is a looking command, and nothing is written to a file. */
21
- export declare function commandReadsOnly(command: string): boolean;
22
- /**
23
- * Whether one call may change something. A Bash call changes nothing only
24
- * when its command is there and only looks; any other tool is a change unless
25
- * {@link readsOnly} says it only reads — a tool it cannot tell about counts as
26
- * a change. `input` is the call's arguments, as an object or as JSON text.
27
- */
28
- export declare function callMayChange(toolName: string, input: unknown): boolean;
29
- //# sourceMappingURL=reads-only.d.ts.map