@basein/runner 0.2.11 → 0.3.0

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 (144) hide show
  1. package/README.md +540 -350
  2. package/dist/auth/client.d.ts +65 -3
  3. package/dist/auth/client.js +305 -53
  4. package/dist/bin/bir-hooks.d.ts +72 -31
  5. package/dist/bin/bir-hooks.js +176 -284
  6. package/dist/bin/bir-proxy.d.ts +17 -2
  7. package/dist/bin/bir-proxy.js +222 -28
  8. package/dist/bin/bir-scenario.d.ts +61 -10
  9. package/dist/bin/bir-scenario.js +704 -135
  10. package/dist/bin/bir.d.ts +14 -8
  11. package/dist/bin/bir.js +2937 -488
  12. package/dist/bin/investigate.js +41 -16
  13. package/dist/bin/scenario-edit.d.ts +43 -10
  14. package/dist/bin/scenario-edit.js +281 -80
  15. package/dist/bin/scenario-liveness.d.ts +194 -0
  16. package/dist/bin/scenario-liveness.js +459 -0
  17. package/dist/bin/scenario-stderr.d.ts +43 -0
  18. package/dist/bin/scenario-stderr.js +76 -0
  19. package/dist/bin/service-read.d.ts +88 -0
  20. package/dist/bin/service-read.js +128 -0
  21. package/dist/bin/setup.d.ts +19 -0
  22. package/dist/bin/setup.js +146 -20
  23. package/dist/config/adapters/claude-code.d.ts +92 -14
  24. package/dist/config/adapters/claude-code.js +291 -101
  25. package/dist/config/adapters/generic.d.ts +4 -1
  26. package/dist/config/adapters/generic.js +13 -14
  27. package/dist/config/generate.d.ts +392 -14
  28. package/dist/config/generate.js +666 -19
  29. package/dist/config/mcp-approval.d.ts +76 -0
  30. package/dist/config/mcp-approval.js +210 -0
  31. package/dist/config/node-path.d.ts +42 -0
  32. package/dist/config/node-path.js +102 -0
  33. package/dist/config/overrides.d.ts +62 -0
  34. package/dist/config/overrides.js +136 -0
  35. package/dist/config/resolve.d.ts +70 -8
  36. package/dist/config/resolve.js +186 -15
  37. package/dist/control/client.d.ts +22 -0
  38. package/dist/control/client.js +26 -7
  39. package/dist/control/correlation.d.ts +23 -5
  40. package/dist/control/correlation.js +0 -0
  41. package/dist/control/daemon.d.ts +400 -18
  42. package/dist/control/daemon.js +1105 -102
  43. package/dist/control/discovery.d.ts +77 -5
  44. package/dist/control/discovery.js +186 -11
  45. package/dist/control/ensure-hook.d.ts +61 -0
  46. package/dist/control/ensure-hook.js +160 -8
  47. package/dist/control/paths.d.ts +26 -0
  48. package/dist/control/paths.js +34 -0
  49. package/dist/control/pin-guard.d.ts +83 -0
  50. package/dist/control/pin-guard.js +534 -0
  51. package/dist/control/recorder-keeper.d.ts +229 -0
  52. package/dist/control/recorder-keeper.js +721 -0
  53. package/dist/control/recorder-main.d.ts +73 -0
  54. package/dist/control/recorder-main.js +778 -0
  55. package/dist/control/server.d.ts +707 -31
  56. package/dist/control/server.js +2803 -306
  57. package/dist/control/transcript.d.ts +21 -1
  58. package/dist/control/transcript.js +378 -112
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +5 -1
  61. package/dist/jsonrpc/framing.d.ts +76 -12
  62. package/dist/jsonrpc/framing.js +216 -59
  63. package/dist/proxy/intercept.d.ts +49 -5
  64. package/dist/proxy/intercept.js +188 -59
  65. package/dist/proxy/relay.d.ts +16 -0
  66. package/dist/proxy/relay.js +77 -5
  67. package/dist/proxy/revive.d.ts +122 -0
  68. package/dist/proxy/revive.js +251 -0
  69. package/dist/proxy/session.d.ts +222 -14
  70. package/dist/proxy/session.js +708 -99
  71. package/dist/record/housekeeping.d.ts +71 -0
  72. package/dist/record/housekeeping.js +417 -0
  73. package/dist/record/recorder.d.ts +51 -3
  74. package/dist/record/recorder.js +3 -0
  75. package/dist/record/redact.d.ts +103 -9
  76. package/dist/record/redact.js +659 -30
  77. package/dist/record/remote-recorder.d.ts +327 -32
  78. package/dist/record/remote-recorder.js +1517 -187
  79. package/dist/record/switchable-recorder.d.ts +126 -0
  80. package/dist/record/switchable-recorder.js +351 -0
  81. package/dist/record/truncate.d.ts +40 -1
  82. package/dist/record/truncate.js +111 -4
  83. package/dist/replay/bash-channel.d.ts +35 -0
  84. package/dist/replay/bash-channel.js +133 -0
  85. package/dist/replay/bundle.d.ts +10 -0
  86. package/dist/replay/bundle.js +10 -0
  87. package/dist/replay/controller.d.ts +371 -20
  88. package/dist/replay/controller.js +1185 -111
  89. package/dist/replay/derive.d.ts +1 -0
  90. package/dist/replay/derive.js +81 -24
  91. package/dist/replay/executor.d.ts +51 -16
  92. package/dist/replay/executor.js +114 -41
  93. package/dist/replay/handover.d.ts +54 -1
  94. package/dist/replay/handover.js +55 -5
  95. package/dist/replay/logic.d.ts +62 -10
  96. package/dist/replay/logic.js +140 -22
  97. package/dist/replay/plan.d.ts +201 -27
  98. package/dist/replay/plan.js +581 -141
  99. package/dist/replay/safety.d.ts +72 -0
  100. package/dist/replay/safety.js +303 -0
  101. package/dist/replay/source-run.d.ts +10 -0
  102. package/dist/replay/source-run.js +80 -35
  103. package/dist/replay/targets.d.ts +15 -1
  104. package/dist/replay/targets.js +62 -4
  105. package/dist/replay/tool-error.d.ts +15 -1
  106. package/dist/replay/tool-error.js +33 -1
  107. package/dist/replay/types.d.ts +7 -4
  108. package/dist/upstream/client.d.ts +56 -3
  109. package/dist/upstream/client.js +61 -4
  110. package/dist/upstream/headers.d.ts +43 -0
  111. package/dist/upstream/headers.js +90 -0
  112. package/dist/upstream/http-client.d.ts +213 -22
  113. package/dist/upstream/http-client.js +1034 -168
  114. package/dist/upstream/http-transport.d.ts +143 -0
  115. package/dist/upstream/http-transport.js +391 -0
  116. package/dist/upstream/lazy-client.d.ts +8 -0
  117. package/dist/upstream/lazy-client.js +29 -1
  118. package/dist/upstream/sse.d.ts +78 -0
  119. package/dist/upstream/sse.js +189 -0
  120. package/dist/upstream/stdio-client.d.ts +61 -1
  121. package/dist/upstream/stdio-client.js +356 -33
  122. package/dist/util/atomic-write.d.ts +29 -0
  123. package/dist/util/atomic-write.js +96 -0
  124. package/dist/util/journal.d.ts +15 -0
  125. package/dist/util/journal.js +48 -3
  126. package/dist/util/log.d.ts +10 -0
  127. package/dist/util/log.js +28 -1
  128. package/dist/util/off-switch.d.ts +46 -0
  129. package/dist/util/off-switch.js +91 -0
  130. package/dist/util/service-health.d.ts +133 -0
  131. package/dist/util/service-health.js +217 -0
  132. package/docs/BaseInstRunner.md +694 -621
  133. package/docs/calculatedReplay.md +1625 -1244
  134. package/docs/calculatedReplayGuide.md +1212 -879
  135. package/docs/installRun.md +1013 -772
  136. package/docs/loginWeb.md +627 -607
  137. package/docs/mcpmark.md +49 -16
  138. package/docs/my-first-sample.md +569 -545
  139. package/docs/quickstart.md +384 -271
  140. package/docs/t-bench.md +406 -394
  141. package/package.json +58 -57
  142. package/production.md +1371 -0
  143. package/scripts/install.ps1 +97 -6
  144. package/scripts/install.sh +104 -7
@@ -2,8 +2,7 @@
2
2
  * logic — the four stored-JS evaluators (docs/calculatedReplay.md §9).
3
3
  *
4
4
  * A calculated scenario carries JavaScript function *bodies*, one per hook
5
- * point, each ending in `return { … }`. They are compiled with `new Function`
6
- * and run here.
5
+ * point, each ending in `return { … }`. They are compiled and run here.
7
6
  *
8
7
  * PORTED FROM THE SERVICE, DELIBERATELY. BaseIn's `src/scenarios/logic.ts` is
9
8
  * the copy the service itself evaluates in a dry replay, so it is the one that
@@ -12,18 +11,71 @@
12
11
  * another way here — an untestable difference. Argument order and the
13
12
  * plain-object guard both match the service exactly.
14
13
  *
15
- * THERE IS NO SANDBOX, and the doc says so plainly (§13.1). A `new Function`
16
- * body gets its declared arguments and the global scope: `require`/`import` are
17
- * unreachable under ESM, but `fetch`, `process` and `globalThis` are not. What
18
- * makes this acceptable is provenance, not containment — the payload arrives
19
- * only in an authenticated response from the configured `BIR_AUTH_URL`,
20
- * describing the caller's own recorded runs — and replay is opt-in for exactly
21
- * this reason.
14
+ * THERE IS NO SANDBOX, and the doc says so plainly (§13.1). A body gets its
15
+ * declared arguments and the global scope: `require`/`import` are unreachable
16
+ * under ESM, but `fetch`, `process` and `globalThis` are not. What makes this
17
+ * acceptable is provenance, not containment — the payload arrives only in an
18
+ * authenticated response from the configured `BIR_AUTH_URL`, describing the
19
+ * caller's own recorded runs — and replay is opt-in for exactly this reason.
20
+ *
21
+ * THERE IS A CLOCK. Every body runs inside the one process that answers every
22
+ * hook of the project, on its only thread. The service ran the same body under
23
+ * a time limit — but on the *recording's* values, and a regex written against a
24
+ * 2 KB output can backtrack for minutes on a 2 MB one, or a hand edit saved with
25
+ * `--force` can loop. Without a limit that froze the event loop, and every
26
+ * `PreToolUse`, `PostToolUse` and prompt hook of every session in the project
27
+ * waited out its timeout on every call until someone killed `bir-hooks`.
28
+ *
29
+ * So each body runs through `vm.Script#runInThisContext` with a `timeout`
30
+ * ({@link logicTimeoutMs}). *This* context, not a new one: the body keeps the
31
+ * realm, the globals and the argument objects it always had — `instanceof
32
+ * Array` still holds for a value `JSON.parse` made outside, a body that writes
33
+ * into `respParams` still writes into the plan's own — so the only behaviour
34
+ * that changes is that a runaway body throws instead of hanging. That throw is
35
+ * an ordinary logic failure, and the plan hands over at that step. The limit
36
+ * covers synchronous work, which is all a body returning a plain object does;
37
+ * it does not bound memory.
38
+ *
39
+ * WHAT THE CLOCK DOES NOT COVER. `runInThisContext`'s timeout ends when the
40
+ * body returns. A continuation the body *queues* — `Promise.resolve().then(…)`,
41
+ * `queueMicrotask`, `setTimeout` — runs afterwards, outside it, on the same
42
+ * thread, and nothing here bounds it. A body that returns a promise is refused
43
+ * (it is no plain object, and it is the shape such work comes in); one that
44
+ * queues work and still returns a plain object is not caught. Bounding that
45
+ * would take a separate context (`microtaskMode: "afterEvaluate"`), which gives
46
+ * up the realm and the shared argument objects this relies on. The threat the
47
+ * clock answers — a backtracking regex, a loop saved with `--force` — is
48
+ * synchronous; a body written to get around it already has the process (see
49
+ * THERE IS NO SANDBOX above).
50
+ */
51
+ /** How long one logic body may run before it is stopped. */
52
+ export declare const DEFAULT_LOGIC_TIMEOUT_MS = 1000;
53
+ /**
54
+ * The most `BIR_LOGIC_TIMEOUT_MS` may ask for. A body runs on the only thread
55
+ * of the process that answers every hook of the project, so each second it is
56
+ * allowed is a second every session's hooks can stand still; and a direct run
57
+ * adds a few of them past its own deadline (controller.ts, `clampDirectRun`).
22
58
  */
59
+ export declare const MAX_LOGIC_TIMEOUT_MS = 10000;
60
+ /** `BIR_LOGIC_TIMEOUT_MS` (at most {@link MAX_LOGIC_TIMEOUT_MS}), or {@link DEFAULT_LOGIC_TIMEOUT_MS}. */
61
+ export declare function logicTimeoutMs(): number;
62
+ /** A body stopped by its time limit — as opposed to one that threw on its own. */
63
+ export declare class LogicTimeoutError extends Error {
64
+ constructor(label: string, timeoutMs: number);
65
+ }
66
+ /** Per-call options for an evaluator. */
67
+ export interface LogicRunOptions {
68
+ /**
69
+ * A tighter limit than {@link logicTimeoutMs} for this one evaluation — for
70
+ * a look-ahead nobody waits on the answer of, such as the directive's
71
+ * preview of a step's arguments. Never raises the limit.
72
+ */
73
+ timeoutMs?: number;
74
+ }
23
75
  /** Transform the derived parameters once, before any step runs. */
24
76
  export declare function evalParamsLogic(code: string, parameters: Record<string, unknown>, intent: string): Record<string, unknown>;
25
77
  /** Derive one step's tool input from params, intent and the accumulated respParams. */
26
- export declare function evalToolInputLogic(code: string, parameters: Record<string, unknown>, intent: string, respParams?: Record<string, unknown>): Record<string, unknown>;
78
+ export declare function evalToolInputLogic(code: string, parameters: Record<string, unknown>, intent: string, respParams?: Record<string, unknown>, opts?: LogicRunOptions): Record<string, unknown>;
27
79
  /** Extract keys from a step's real output, to merge into respParams for later steps. */
28
80
  export declare function evalToolOutputLogic(code: string, toolOutput: string, parameters: Record<string, unknown>, intent: string, respParams?: Record<string, unknown>): Record<string, unknown>;
29
81
  /** Build the final response model from everything the steps accumulated. */
@@ -2,8 +2,7 @@
2
2
  * logic — the four stored-JS evaluators (docs/calculatedReplay.md §9).
3
3
  *
4
4
  * A calculated scenario carries JavaScript function *bodies*, one per hook
5
- * point, each ending in `return { … }`. They are compiled with `new Function`
6
- * and run here.
5
+ * point, each ending in `return { … }`. They are compiled and run here.
7
6
  *
8
7
  * PORTED FROM THE SERVICE, DELIBERATELY. BaseIn's `src/scenarios/logic.ts` is
9
8
  * the copy the service itself evaluates in a dry replay, so it is the one that
@@ -12,40 +11,152 @@
12
11
  * another way here — an untestable difference. Argument order and the
13
12
  * plain-object guard both match the service exactly.
14
13
  *
15
- * THERE IS NO SANDBOX, and the doc says so plainly (§13.1). A `new Function`
16
- * body gets its declared arguments and the global scope: `require`/`import` are
17
- * unreachable under ESM, but `fetch`, `process` and `globalThis` are not. What
18
- * makes this acceptable is provenance, not containment — the payload arrives
19
- * only in an authenticated response from the configured `BIR_AUTH_URL`,
20
- * describing the caller's own recorded runs — and replay is opt-in for exactly
21
- * this reason.
14
+ * THERE IS NO SANDBOX, and the doc says so plainly (§13.1). A body gets its
15
+ * declared arguments and the global scope: `require`/`import` are unreachable
16
+ * under ESM, but `fetch`, `process` and `globalThis` are not. What makes this
17
+ * acceptable is provenance, not containment — the payload arrives only in an
18
+ * authenticated response from the configured `BIR_AUTH_URL`, describing the
19
+ * caller's own recorded runs — and replay is opt-in for exactly this reason.
20
+ *
21
+ * THERE IS A CLOCK. Every body runs inside the one process that answers every
22
+ * hook of the project, on its only thread. The service ran the same body under
23
+ * a time limit — but on the *recording's* values, and a regex written against a
24
+ * 2 KB output can backtrack for minutes on a 2 MB one, or a hand edit saved with
25
+ * `--force` can loop. Without a limit that froze the event loop, and every
26
+ * `PreToolUse`, `PostToolUse` and prompt hook of every session in the project
27
+ * waited out its timeout on every call until someone killed `bir-hooks`.
28
+ *
29
+ * So each body runs through `vm.Script#runInThisContext` with a `timeout`
30
+ * ({@link logicTimeoutMs}). *This* context, not a new one: the body keeps the
31
+ * realm, the globals and the argument objects it always had — `instanceof
32
+ * Array` still holds for a value `JSON.parse` made outside, a body that writes
33
+ * into `respParams` still writes into the plan's own — so the only behaviour
34
+ * that changes is that a runaway body throws instead of hanging. That throw is
35
+ * an ordinary logic failure, and the plan hands over at that step. The limit
36
+ * covers synchronous work, which is all a body returning a plain object does;
37
+ * it does not bound memory.
38
+ *
39
+ * WHAT THE CLOCK DOES NOT COVER. `runInThisContext`'s timeout ends when the
40
+ * body returns. A continuation the body *queues* — `Promise.resolve().then(…)`,
41
+ * `queueMicrotask`, `setTimeout` — runs afterwards, outside it, on the same
42
+ * thread, and nothing here bounds it. A body that returns a promise is refused
43
+ * (it is no plain object, and it is the shape such work comes in); one that
44
+ * queues work and still returns a plain object is not caught. Bounding that
45
+ * would take a separate context (`microtaskMode: "afterEvaluate"`), which gives
46
+ * up the realm and the shared argument objects this relies on. The threat the
47
+ * clock answers — a backtracking regex, a loop saved with `--force` — is
48
+ * synchronous; a body written to get around it already has the process (see
49
+ * THERE IS NO SANDBOX above).
50
+ */
51
+ import { Script } from "node:vm";
52
+ /** How long one logic body may run before it is stopped. */
53
+ export const DEFAULT_LOGIC_TIMEOUT_MS = 1_000;
54
+ /**
55
+ * The most `BIR_LOGIC_TIMEOUT_MS` may ask for. A body runs on the only thread
56
+ * of the process that answers every hook of the project, so each second it is
57
+ * allowed is a second every session's hooks can stand still; and a direct run
58
+ * adds a few of them past its own deadline (controller.ts, `clampDirectRun`).
59
+ */
60
+ export const MAX_LOGIC_TIMEOUT_MS = 10_000;
61
+ /** `BIR_LOGIC_TIMEOUT_MS` (at most {@link MAX_LOGIC_TIMEOUT_MS}), or {@link DEFAULT_LOGIC_TIMEOUT_MS}. */
62
+ export function logicTimeoutMs() {
63
+ const n = Number(process.env.BIR_LOGIC_TIMEOUT_MS);
64
+ return Number.isFinite(n) && n > 0 ? Math.min(n, MAX_LOGIC_TIMEOUT_MS) : DEFAULT_LOGIC_TIMEOUT_MS;
65
+ }
66
+ /** A body stopped by its time limit — as opposed to one that threw on its own. */
67
+ export class LogicTimeoutError extends Error {
68
+ constructor(label, timeoutMs) {
69
+ super(`${label} ran longer than ${timeoutMs} ms and was stopped`);
70
+ this.name = "LogicTimeoutError";
71
+ }
72
+ }
73
+ /**
74
+ * Where a call's arguments wait for the script to pick them up. Set and cleared
75
+ * around one synchronous evaluation, so no two calls can ever see each other's.
22
76
  */
77
+ const ARGS_SLOT = "__bir_logic_args__";
78
+ /** Compiled bodies, by argument list and source. A plan evaluates each many times. */
79
+ const compiled = new Map();
80
+ const MAX_COMPILED = 256;
81
+ function compile(argNames, code) {
82
+ const key = `${argNames.join(",")}\u0000${code}`;
83
+ const hit = compiled.get(key);
84
+ if (hit)
85
+ return hit;
86
+ // Parsed on its own first, exactly as it always was: a body that is not
87
+ // valid JavaScript fails with the same SyntaxError as before, and one that
88
+ // tries to close the wrapper below early cannot parse at all.
89
+ new Function(...argNames, code);
90
+ const script = new Script(`(function(){var __bir_a=globalThis[${JSON.stringify(ARGS_SLOT)}];` +
91
+ `return (function(${argNames.join(",")}){\n${code}\n}).apply(this,__bir_a);}).call(this)`, { filename: "bir-scenario-logic.js" });
92
+ if (compiled.size >= MAX_COMPILED)
93
+ compiled.clear();
94
+ compiled.set(key, script);
95
+ return script;
96
+ }
97
+ /** Run one body with `args`, under the time limit. Throws what the body throws. */
98
+ function run(label, argNames, code, args, opts = {}) {
99
+ const script = compile(argNames, code);
100
+ const slot = globalThis;
101
+ const previous = slot[ARGS_SLOT];
102
+ slot[ARGS_SLOT] = args;
103
+ const asked = opts.timeoutMs;
104
+ const timeout = typeof asked === "number" && Number.isFinite(asked) && asked > 0
105
+ ? Math.max(1, Math.min(Math.floor(asked), logicTimeoutMs()))
106
+ : logicTimeoutMs();
107
+ try {
108
+ return script.runInThisContext({ timeout, displayErrors: false });
109
+ }
110
+ catch (err) {
111
+ if (err?.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
112
+ throw new LogicTimeoutError(label, timeout);
113
+ }
114
+ throw err;
115
+ }
116
+ finally {
117
+ if (previous === undefined)
118
+ delete slot[ARGS_SLOT];
119
+ else
120
+ slot[ARGS_SLOT] = previous;
121
+ }
122
+ }
23
123
  /** Every logic body must return a plain object; anything else is a bug upstream. */
24
124
  function assertPlainObject(result, label) {
25
125
  if (typeof result !== "object" || result === null || Array.isArray(result)) {
26
126
  throw new Error(`${label} must return a plain object`);
27
127
  }
128
+ // A promise is an object too, and used to pass. It is also the one shape of
129
+ // body whose work outlives its clock: what it queues runs after the time
130
+ // limit has stopped counting (see the file comment).
131
+ if (typeof result.then === "function") {
132
+ throw new Error(`${label} must return a plain object, not a promise`);
133
+ }
28
134
  return result;
29
135
  }
30
136
  /** Transform the derived parameters once, before any step runs. */
31
137
  export function evalParamsLogic(code, parameters, intent) {
32
- const fn = new Function("parameters", "intent", code);
33
- return assertPlainObject(fn(parameters, intent), "paramsLogic");
138
+ return assertPlainObject(run("paramsLogic", ["parameters", "intent"], code, [parameters, intent]), "paramsLogic");
34
139
  }
35
140
  /** Derive one step's tool input from params, intent and the accumulated respParams. */
36
- export function evalToolInputLogic(code, parameters, intent, respParams = {}) {
37
- const fn = new Function("parameters", "intent", "respParams", code);
38
- return assertPlainObject(fn(parameters, intent, respParams), "toolInputLogic");
141
+ export function evalToolInputLogic(code, parameters, intent, respParams = {}, opts) {
142
+ return assertPlainObject(run("toolInputLogic", ["parameters", "intent", "respParams"], code, [parameters, intent, respParams], opts), "toolInputLogic");
39
143
  }
40
144
  /** Extract keys from a step's real output, to merge into respParams for later steps. */
41
145
  export function evalToolOutputLogic(code, toolOutput, parameters, intent, respParams = {}) {
42
- const fn = new Function("toolOutput", "parameters", "intent", "respParams", code);
43
- return assertPlainObject(fn(toolOutput, parameters, intent, respParams), "toolOutputLogic");
146
+ return assertPlainObject(run("toolOutputLogic", ["toolOutput", "parameters", "intent", "respParams"], code, [
147
+ toolOutput,
148
+ parameters,
149
+ intent,
150
+ respParams,
151
+ ]), "toolOutputLogic");
44
152
  }
45
153
  /** Build the final response model from everything the steps accumulated. */
46
154
  export function evalResponseParamsLogic(code, respParams, parameters, intent) {
47
- const fn = new Function("respParams", "parameters", "intent", code);
48
- return assertPlainObject(fn(respParams, parameters, intent), "responseParamsLogic");
155
+ return assertPlainObject(run("responseParamsLogic", ["respParams", "parameters", "intent"], code, [
156
+ respParams,
157
+ parameters,
158
+ intent,
159
+ ]), "responseParamsLogic");
49
160
  }
50
161
  /**
51
162
  * The two mapping bodies of a call (segmented.md R-CALL-20).
@@ -57,11 +168,18 @@ export function evalResponseParamsLogic(code, respParams, parameters, intent) {
57
168
  * two of them.
58
169
  */
59
170
  export function evalParamMapLogic(code, parameters, intent, respParams = {}) {
60
- const fn = new Function("parameters", "intent", "respParams", code);
61
- return assertPlainObject(fn(parameters, intent, respParams), "paramMapLogic");
171
+ return assertPlainObject(run("paramMapLogic", ["parameters", "intent", "respParams"], code, [
172
+ parameters,
173
+ intent,
174
+ respParams,
175
+ ]), "paramMapLogic");
62
176
  }
63
177
  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");
178
+ return assertPlainObject(run("resultMapLogic", ["out", "parameters", "intent", "respParams"], code, [
179
+ out,
180
+ parameters,
181
+ intent,
182
+ respParams,
183
+ ]), "resultMapLogic");
66
184
  }
67
185
  //# sourceMappingURL=logic.js.map
@@ -22,15 +22,61 @@
22
22
  */
23
23
  import { type FlatEntry } from "./flatten.js";
24
24
  import type { ReplayMode } from "./coverage.js";
25
- import type { ExecutionStage, FallbackKind, SerializedScenario, SerializedScenarioStep } from "./types.js";
25
+ import type { HandoverStopReason } from "./handover.js";
26
+ import type { ExecutionStage, FallbackKind, ParamsSchema, SerializedScenario, SerializedScenarioStep } from "./types.js";
26
27
  /**
27
28
  * Runs one step's tool and returns its output as a string.
28
29
  *
29
30
  * Resolving with a failure message is a *result* — a tool that ran and failed.
30
- * Rejecting means the step could not be **run here at all**, which is the only
31
- * case a recorded output may stand in for.
31
+ * Rejecting means the step produced **no result here**: it could not be sent
32
+ * (`not_run`), it was sent and never answered (`uncertain`), or the upstream
33
+ * refused it (`failed`). Reject with a {@link StepRunError} to say which; any
34
+ * other rejection reads as `not_run`, the old contract.
32
35
  */
33
36
  export type ExecuteStep = (step: SerializedScenarioStep, input: Record<string, unknown>) => Promise<string>;
37
+ /** Why a step's call produced no result. */
38
+ export type StepRejection = "not_run" | "uncertain" | "failed";
39
+ /** A rejection from an {@link ExecuteStep} that says what happened to the call. */
40
+ export declare class StepRunError extends Error {
41
+ readonly rejection: StepRejection;
42
+ constructor(message: string, rejection: StepRejection);
43
+ }
44
+ /** What a rejection means for the step — see {@link ExecuteStep}. */
45
+ export declare function rejectionOf(err: unknown): StepRejection;
46
+ /**
47
+ * How a plan driver treats the steps it cannot get a real result for.
48
+ *
49
+ * THE RULE: a result shown to the model is a result that happened, here, now.
50
+ * A step that could not run, did not answer or was refused is never replaced by
51
+ * the output recorded for it last week — "INSERT 0 1" under a header that says
52
+ * the work is done is how a user is told a write happened that did not. The run
53
+ * stops at that step, the steps before it are shown as they are, and the rest
54
+ * of the task is handed back with a note that names the step and what may be
55
+ * assumed about it (fallbk.md).
56
+ */
57
+ export interface DriveOptions {
58
+ /**
59
+ * Stop at the first step that breaks — its logic throws, or its tool reports
60
+ * an error — instead of threading the error on (fallbk.md §Runner 3). A step
61
+ * after a failed one computes on a result that does not exist. Off keeps the
62
+ * old contract: a logic throw propagates, a tool error is threaded and
63
+ * counted. A step with *no* result stops the run either way.
64
+ */
65
+ stopOnFailure?: boolean;
66
+ /**
67
+ * Serve the recorded output of a step whose tool could not be *sent* here
68
+ * (`not_run`: a built-in, a server with no proxy), labelled `(recorded)`.
69
+ * `BIR_REPLAY_RECORDED_OUTPUTS=1`, and `bir replay`. Never for a step that
70
+ * was sent and failed or did not answer.
71
+ */
72
+ recordedFallback?: boolean;
73
+ /**
74
+ * The caller went away — `bir-scenario`'s request was cancelled or timed out
75
+ * (C4). No further step is *started*; one already sent runs to its own end,
76
+ * so what the run reports about it is the truth.
77
+ */
78
+ signal?: AbortSignal;
79
+ }
34
80
  /**
35
81
  * Supplies a step's recorded output when its tool cannot run here (§8.1).
36
82
  *
@@ -69,10 +115,30 @@ export interface StepInfo {
69
115
  toolMissing?: boolean;
70
116
  }
71
117
  export type StepObserver = (info: StepInfo) => void;
118
+ /**
119
+ * One step's input logic, evaluated only to show its arguments, runs no longer
120
+ * than this (nor than `logicTimeoutMs()`).
121
+ *
122
+ * The limit is wall-clock time (a `vm` timeout), so it must tolerate a busy
123
+ * machine: with ten CPU-bound processes on eight cores, a trivial
124
+ * `return { command: 'ls ' + parameters.x }` took up to ~170 ms, and a 50 ms
125
+ * limit stopped more than half of them — ending the preview, and every
126
+ * argument with it, whenever the machine was loaded. At 250 ms none was
127
+ * stopped. A body stopped at this limit really is slow, and ends the preview.
128
+ */
129
+ export declare const PREVIEW_LOGIC_MS = 250;
130
+ /**
131
+ * All of a directive's argument previews together: it is written inside the
132
+ * prompt hook, so this is the most the preview adds to it, a stopped body
133
+ * included — each body's limit is clipped to what is left of it. A caller with
134
+ * less time left passes less (`steeringDirective`'s `previewBudgetMs`).
135
+ */
136
+ export declare const PREVIEW_BUDGET_MS = 500;
72
137
  export interface ComposeResult {
73
138
  text: string;
74
139
  executed: number;
75
140
  recorded: number;
141
+ /** Steps whose call produced no result. The run stopped at the first. */
76
142
  skipped: number;
77
143
  /**
78
144
  * Of `executed`, how many tools ran and reported an error. Counted separately
@@ -80,6 +146,8 @@ export interface ComposeResult {
80
146
  * and yet the work did not happen, and an outcome must not say it did.
81
147
  */
82
148
  errored: number;
149
+ /** Where, and why, the run stopped short of its end, when it did. */
150
+ stopped?: PlanStop;
83
151
  }
84
152
  /** Where, and why, a run stopped short of the chain's end (fallbk.md). */
85
153
  export interface PlanStop {
@@ -90,11 +158,28 @@ export interface PlanStop {
90
158
  flatIndex?: number;
91
159
  stage?: ExecutionStage;
92
160
  error?: string;
161
+ /**
162
+ * For `step_failed`: what kind of stop — the step did not run, did not
163
+ * answer, was refused, or the budget or the caller ended the run before it.
164
+ * Absent means its logic threw or its tool reported an error.
165
+ */
166
+ reason?: HandoverStopReason;
93
167
  }
94
168
  export interface PlanOptions {
95
169
  scenario: SerializedScenario;
96
- /** Resolves with the derived parameters. Rejection degrades to `{}`. */
170
+ /**
171
+ * Resolves with the derived parameters. A rejection settles them the way
172
+ * {@link ScenarioReplayPlan.useFallbackParams} does: settings at their
173
+ * recorded samples, targets `null` — never `{}`.
174
+ */
97
175
  params: Promise<Record<string, unknown>>;
176
+ /**
177
+ * What a *target* takes when the parameters settle without the derivation.
178
+ * `null` (the default) is the rule: a target is never guessed. `samples` is
179
+ * `bir replay`'s own contract — an operator named the scenario by id, so a
180
+ * recorded sample stands in (R-CALL-37).
181
+ */
182
+ fallbackTargets?: "null" | "samples";
98
183
  mode: ReplayMode;
99
184
  respParamsInit?: Record<string, unknown>;
100
185
  /**
@@ -127,6 +212,11 @@ export declare class ScenarioReplayPlan {
127
212
  private readonly entries;
128
213
  private readonly paramsPromise;
129
214
  private params;
215
+ /** The derivation's own answer, before `paramsLogic`. What safety.ts judges. */
216
+ private derivedParams;
217
+ /** Set once the parameters are final — derived, or settled without the derivation. */
218
+ private paramsSettled;
219
+ private readonly fallbackTargets;
130
220
  private respParams;
131
221
  private stepIndex;
132
222
  private readyPromise?;
@@ -158,11 +248,35 @@ export declare class ScenarioReplayPlan {
158
248
  private leaveFrame;
159
249
  /**
160
250
  * Await the derivation, then apply `paramsLogic` — once, however many callers
161
- * race here. A rejected derivation resolves to `{}` rather than throwing: the
162
- * caller decided long ago that a replay with fallback parameters beats no
163
- * replay, and a throw here would abort a turn the user is waiting on.
251
+ * race here. A rejected derivation settles on the fallback values
252
+ * ({@link useFallbackParams}) rather than throwing.
253
+ *
254
+ * Rejects only when `paramsLogic` throws. That is real: the scenario's own
255
+ * logic is broken, and the caller must hand over rather than run steps on
256
+ * parameters nobody processed.
164
257
  */
165
258
  ready(): Promise<void>;
259
+ /**
260
+ * Settle the parameters without waiting any longer for the derivation: every
261
+ * *setting* at its recorded sample, every *target* `null` (R-PARAM-3), then
262
+ * `paramsLogic`. What the caller does when the derivation's budget runs out.
263
+ *
264
+ * Never `{}`. Running on an empty object put `undefined` into every argument
265
+ * — `{"command":"query undefined"}` — and a steered step was auto-approved
266
+ * with it. A target stays null here exactly as it does when the derivation
267
+ * answers without one, and the caller decides whether the plan can run
268
+ * without it (targets.ts).
269
+ *
270
+ * A derivation that answers later is ignored. Throws what `paramsLogic`
271
+ * throws.
272
+ */
273
+ useFallbackParams(): void;
274
+ /** Whether the parameters are final. */
275
+ get parametersSettled(): boolean;
276
+ /** The derivation's own values, before `paramsLogic`. Empty until settled. */
277
+ get derivedValues(): Readonly<Record<string, unknown>>;
278
+ private fallbackValues;
279
+ private settleParams;
166
280
  /** 0-based index of the step awaiting execution (== completed step count). */
167
281
  get currentStepIndex(): number;
168
282
  /** Steps this plan will run — the chain up to the first known-bad step. */
@@ -185,6 +299,10 @@ export declare class ScenarioReplayPlan {
185
299
  get parameters(): Readonly<Record<string, unknown>>;
186
300
  /** Everything the steps have accumulated so far. */
187
301
  get accumulated(): Readonly<Record<string, unknown>>;
302
+ /** The scenario's parameter schema — what the target and safety checks judge. */
303
+ get paramsSchema(): ParamsSchema | null;
304
+ /** The scenario's `paramsLogic`, which targets.ts reads as a consumer. */
305
+ get paramsLogic(): string | null;
188
306
  /** The step awaiting execution, or undefined when the plan is done. */
189
307
  currentStep(): SerializedScenarioStep | undefined;
190
308
  /** The tool the current step expects, or undefined when done. */
@@ -220,6 +338,24 @@ export declare class ScenarioReplayPlan {
220
338
  applyOutput(realOutput: string): string[];
221
339
  /** Build the final response model from everything the steps accumulated. */
222
340
  responseModel(): Record<string, unknown>;
341
+ /**
342
+ * The arguments each planned step will run with, where they are known before
343
+ * any step runs — for the steer directive (see {@link steeringDirective}).
344
+ *
345
+ * Known means: the parameters are settled, the step is the caller's own
346
+ * (a called segment's parameters are only built when it is entered), and
347
+ * its input logic neither reads nor passes on anything an earlier step
348
+ * accumulates — or it is the step at the cursor, whose accumulation is
349
+ * already final. Everything else is undefined, and the directive leaves
350
+ * those steps' arguments to the system.
351
+ *
352
+ * A look-ahead only: every body runs on copies, under a short limit, and a
353
+ * body that throws or runs long just leaves its step without arguments —
354
+ * the pin, not this, is what runs the step. The whole preview takes no more
355
+ * than `budgetMs` ({@link PREVIEW_BUDGET_MS}), a stopped body included; each
356
+ * body gets at most `bodyMs` ({@link PREVIEW_LOGIC_MS}) of it.
357
+ */
358
+ private previewArguments;
223
359
  /**
224
360
  * The directive injected via `UserPromptSubmit`'s `additionalContext`.
225
361
  *
@@ -227,9 +363,25 @@ export declare class ScenarioReplayPlan {
227
363
  * whole sequence server-side. In `steer` mode it names the sequence so the
228
364
  * model initiates the expected calls — the arguments are supplied by the
229
365
  * system, so the model need not compute them.
366
+ *
367
+ * WHY A STEER DIRECTIVE SHOWS ARGUMENTS. The pin replaces the model's
368
+ * arguments with the step's only when the call is plausibly the step
369
+ * (control/pin-guard.ts): the same command head, the same file, an argument
370
+ * in common. Told merely that "the system supplies the arguments", a model
371
+ * writes a placeholder call — `ls`, `{"query":"..."}` — that the guard rightly
372
+ * declines, and the plan retires having run nothing. So where a step's
373
+ * arguments are already known they are shown under it, as data, so that the
374
+ * call the model writes, and the user sees, is the call that runs.
375
+ *
376
+ * `previewBudgetMs` / `previewBodyMs` bound that preview (default
377
+ * {@link PREVIEW_BUDGET_MS} / {@link PREVIEW_LOGIC_MS}): a caller with less
378
+ * of its hook's time left passes less, and under
379
+ * {@link MIN_PREVIEW_BODY_MS} there is no preview at all.
230
380
  */
231
381
  steeringDirective(directToolName?: string, opts?: {
232
382
  subTask?: boolean;
383
+ previewBudgetMs?: number;
384
+ previewBodyMs?: number;
233
385
  }): string;
234
386
  /**
235
387
  * Execute every **remaining** step and lay the results out as one bundle.
@@ -238,16 +390,20 @@ export declare class ScenarioReplayPlan {
238
390
  * live session already ran — with side effects — every step before the one it
239
391
  * diverged on, so re-running them would be wrong.
240
392
  *
241
- * There is no try/catch around the input or output logic: a throw there
242
- * propagates out, and the caller abandons the whole bundle rather than
243
- * shipping a half-computed one. The tool call itself is the exception — a
244
- * rejection means the step could not be run *here*, and then a recorded output
245
- * is not a shortcut past a real result, it is the only result that exists.
246
- * Skipping instead would drop the step *and* stop threading `respParams`, so
247
- * every later step reading from it fails too.
393
+ * A step whose call produced no result ends the bundle there
394
+ * ({@link DriveOptions}): later steps would compute on a result that does not
395
+ * exist, and nothing may be shown in its place. The stop is returned, and the
396
+ * bundle says the task is not finished. With `stopOnFailure` a logic throw or
397
+ * a tool error ends it the same way; without it they keep their old contract
398
+ * (a throw propagates, an error is threaded and counted).
399
+ *
400
+ * `deadline` bounds the whole bundle: this runs inside a `PreToolUse` hook
401
+ * with a timeout of its own, and a step started after the host stopped
402
+ * waiting is a side effect nobody is told about.
248
403
  */
249
- composeBundle(maxChars: number, execute: ExecuteStep, recordedOutputFor?: RecordedOutputFor, onStep?: StepObserver, opts?: {
404
+ composeBundle(maxChars: number, execute: ExecuteStep, recordedOutputFor?: RecordedOutputFor, onStep?: StepObserver, opts?: DriveOptions & {
250
405
  handover?: boolean;
406
+ deadline?: number;
251
407
  }): Promise<ComposeResult>;
252
408
  /**
253
409
  * Run the whole plan from the current cursor, advancing it as it goes.
@@ -255,26 +411,44 @@ export declare class ScenarioReplayPlan {
255
411
  * This is `direct` mode's driver, and unlike {@link composeBundle} it mutates
256
412
  * the plan — the steps really did run, so the cursor and `respParams` must say
257
413
  * so, which is what lets the outcome be `steered_full` and `stepsPinned` be
258
- * truthful. A step that cannot run at all still advances the cursor: the plan
259
- * is spent either way, and stalling it would leave a retired turn looking live.
414
+ * truthful.
415
+ *
416
+ * A step whose call produced no result stops the run there, with the cursor
417
+ * on it ({@link DriveOptions}). So does the deadline, checked before each
418
+ * step, and the caller's `signal`. A step already sent is never abandoned:
419
+ * its result arrives (or its own timeout does) and is reported as it is.
260
420
  *
261
421
  * `maxChars` caps the bundle exactly as a divergence bundle is capped. The
262
422
  * direct path once passed `Number.MAX_SAFE_INTEGER` here — fine at 10 KB, and
263
423
  * an unbounded payload into the model's context for a scenario whose tools
264
424
  * answer in megabytes (docs/mcpmark.md §12).
265
425
  */
266
- runToCompletion(execute: ExecuteStep, recordedOutputFor?: RecordedOutputFor, onStep?: StepObserver, deadline?: number, maxChars?: number, opts?: {
267
- /**
268
- * Stop at the first step that breaks — its logic throws, or its tool
269
- * reports an error — instead of threading the error on (fallbk.md
270
- * §Runner 3). A step after a failed one computes on a result that does not
271
- * exist. Off keeps the old contract: a logic throw propagates, a tool
272
- * error is threaded and counted.
273
- */
274
- stopOnFailure?: boolean;
275
- }): Promise<ComposeResult & {
426
+ runToCompletion(execute: ExecuteStep, recordedOutputFor?: RecordedOutputFor, onStep?: StepObserver, deadline?: number, maxChars?: number, opts?: DriveOptions): Promise<ComposeResult & {
276
427
  partial: boolean;
277
428
  stopped?: PlanStop;
429
+ aborted?: boolean;
278
430
  }>;
431
+ /**
432
+ * A reason not to *start* the next step: the caller is gone, or the time is
433
+ * up. Undefined means go ahead.
434
+ */
435
+ private stopBefore;
436
+ /**
437
+ * Call one step's tool. Resolves with its result, with the recorded output
438
+ * that may stand in for it (only under `recordedFallback`, only for a step
439
+ * that could not be sent), or with the stop a missing result means.
440
+ */
441
+ private callStep;
442
+ /**
443
+ * A step's recorded output from the source run, waited on no longer than the
444
+ * driver's deadline.
445
+ *
446
+ * The lookup is a service fetch (source-run.ts) of up to its own timeout,
447
+ * and a divergence runs it inside a `PreToolUse` whose deadline the hook
448
+ * must keep (C1, C9): so it is a hot-path wait on the service, through the
449
+ * breaker, and a lookup that has not answered in time is no recorded output.
450
+ * A sliver of time left is not worth a request, nor a verdict on the service.
451
+ */
452
+ private recordedWithin;
279
453
  }
280
454
  //# sourceMappingURL=plan.d.ts.map