@basein/runner 0.3.20 → 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 (51) hide show
  1. package/README.md +6 -5
  2. package/dist/bin/bir-hooks.d.ts +17 -19
  3. package/dist/bin/bir-hooks.js +17 -19
  4. package/dist/bin/bir-scenario.js +1 -1
  5. package/dist/bin/bir.js +1 -21
  6. package/dist/bin/investigate.d.ts +0 -9
  7. package/dist/bin/investigate.js +15 -43
  8. package/dist/bin/scenario-edit.js +13 -44
  9. package/dist/control/recorder-main.d.ts +0 -15
  10. package/dist/control/recorder-main.js +22 -42
  11. package/dist/control/server.d.ts +24 -137
  12. package/dist/control/server.js +107 -1001
  13. package/dist/record/recorder.d.ts +11 -142
  14. package/dist/record/recorder.js +0 -3
  15. package/dist/record/remote-recorder.d.ts +1 -9
  16. package/dist/record/remote-recorder.js +3 -49
  17. package/dist/record/switchable-recorder.js +0 -2
  18. package/dist/replay/builtins.d.ts +45 -9
  19. package/dist/replay/builtins.js +131 -59
  20. package/dist/replay/bundle.d.ts +2 -2
  21. package/dist/replay/bundle.js +3 -3
  22. package/dist/replay/controller.d.ts +34 -125
  23. package/dist/replay/controller.js +171 -528
  24. package/dist/replay/derive.d.ts +0 -10
  25. package/dist/replay/derive.js +0 -3
  26. package/dist/replay/handover.d.ts +2 -37
  27. package/dist/replay/handover.js +38 -106
  28. package/dist/replay/logic.d.ts +0 -11
  29. package/dist/replay/logic.js +0 -24
  30. package/dist/replay/plan.d.ts +43 -163
  31. package/dist/replay/plan.js +144 -556
  32. package/dist/replay/safety.d.ts +4 -5
  33. package/dist/replay/safety.js +7 -7
  34. package/dist/replay/source-run.d.ts +15 -24
  35. package/dist/replay/source-run.js +25 -58
  36. package/dist/replay/targets.d.ts +11 -21
  37. package/dist/replay/targets.js +30 -69
  38. package/dist/replay/types.d.ts +9 -96
  39. package/dist/replay/types.js +3 -16
  40. package/dist/util/service-headers.d.ts +1 -2
  41. package/dist/util/service-headers.js +1 -2
  42. package/docs/calculatedReplay.md +30 -81
  43. package/docs/calculatedReplayGuide.md +17 -34
  44. package/package.json +1 -1
  45. package/production.md +7 -36
  46. package/dist/replay/condition-words.d.ts +0 -28
  47. package/dist/replay/condition-words.js +0 -59
  48. package/dist/replay/flatten.d.ts +0 -161
  49. package/dist/replay/flatten.js +0 -222
  50. package/dist/replay/reads-only.d.ts +0 -29
  51. package/dist/replay/reads-only.js +0 -261
@@ -26,7 +26,7 @@ import { DISCOVERY_CHECK_MS, HEARTBEAT_MS, pidAlive, readDiscovery, removeDiscov
26
26
  import { DAEMON_CWD_ENV, DAEMON_ENV, DAEMON_LOG_ENV, DERIVE_KEY_ENV, DaemonLog, USE_API_KEY_ENV, clearPortProblem, compareVersions, probeHealth, projectEndpoint, rediscover, validPort, writePortProblem, } from "./daemon.js";
27
27
  import { configDir, normalizePath, supersededLogPath } from "./paths.js";
28
28
  import { RecorderKeeper } from "./recorder-keeper.js";
29
- import { parseBashAllow, parseBuiltinTools } from "../replay/builtins.js";
29
+ import { FILE_BUILTINS, parseBashAllow, parseBuiltinTools, parsePathAllow } from "../replay/builtins.js";
30
30
  import { parseAllowList } from "../replay/coverage.js";
31
31
  import { sweepDeliveries } from "../replay/bash-channel.js";
32
32
  import { exemptLoopbackFromProxy, isWrapped, projectRecord, projectToken, readSidecar, } from "../config/generate.js";
@@ -85,9 +85,8 @@ function wrappedServersFor(cwd) {
85
85
  * `BIR_INTENT_SOURCES` — which text a step's intent may be read from
86
86
  * (segmented.md R-INTENT-3). Unset or unparseable means all three.
87
87
  *
88
- * It governs only what is *sent*. Turning every source off does not stop a
89
- * probe: the request still goes out with an empty text and the server builds a
90
- * line from the tool name and arguments (R-HIT-4).
88
+ * It governs only what is *sent*. With every source off, a step goes out with
89
+ * no text, and no intent probe is sent for it.
91
90
  */
92
91
  function parseIntentSources(value) {
93
92
  if (value === undefined)
@@ -152,33 +151,12 @@ export function derivationKey(daemon = process.env[DAEMON_ENV] === "1") {
152
151
  }
153
152
  return {};
154
153
  }
155
- /**
156
- * `BIR_SEGMENT_ARM`: whether a segment the service hands out may run mid-task.
157
- * On unless it is `0`, from runner 0.3.19 (the service's segmentsbreakdown.md
158
- * SB-21): segments found by repeated work, and the halves after a plan's
159
- * decision. `0` is observe-only: what would have armed is logged, and nothing
160
- * is replaced. While it is on, whole plans may start mid-task by intent too:
161
- * `BIR_INTENT_ARM` follows it unless that is set.
162
- */
163
- export function segmentArmFromEnv(env = process.env) {
164
- return env.BIR_SEGMENT_ARM !== "0";
165
- }
166
- /**
167
- * What the `replay.enabled` startup line says segments do. A segment starts
168
- * only where the probe is waited on, so `BIR_INTENT_ARM=0` starts nothing
169
- * mid-task, segments included — the rule `bir doctor` words from /health.
170
- */
171
- export function segmentsAtStartup(segmentArm, env = process.env) {
172
- if (!segmentArm)
173
- return "observed only (BIR_SEGMENT_ARM=0)";
174
- return env.BIR_INTENT_ARM === "0" ? "observed only (BIR_INTENT_ARM=0)" : "run mid-task";
175
- }
176
154
  /**
177
155
  * Assemble the replay configuration (docs/calculatedReplay.md §5.3 of the guide).
178
156
  *
179
157
  * There is no steer threshold: a match the service hands out is steered
180
158
  * (controller.ts, gate 4). A recorder started with `BIR_MIN_STEER_SIMILARITY`
181
- * or `BIR_MIN_SEGMENT_STEER_SIMILARITY` set ignores them.
159
+ * set ignores it.
182
160
  */
183
161
  function buildReplayOptions(keeper, policy, key) {
184
162
  // The environment still wins — an operator at a terminal is the most
@@ -197,15 +175,18 @@ function buildReplayOptions(keeper, policy, key) {
197
175
  : undefined;
198
176
  // Which built-ins the runner may execute itself (replay/builtins.ts):
199
177
  // all six by default, `BIR_BUILTIN_TOOLS` to narrow them, `BIR_BASH_ALLOW`
200
- // to narrow the commands a `Bash` step may run. Environment-only knobs,
201
- // like `BIR_REPLAY_RECORDED_OUTPUTS`.
178
+ // to narrow the commands a `Bash` step may run, `BIR_PATH_ALLOW` the
179
+ // folders a file step may touch (unset: any, as the agent's own tools).
180
+ // Environment-only knobs, like `BIR_REPLAY_RECORDED_OUTPUTS`.
202
181
  const builtinTools = parseBuiltinTools(process.env.BIR_BUILTIN_TOOLS);
203
182
  const bashAllow = parseBashAllow(process.env.BIR_BASH_ALLOW);
183
+ const pathAllow = parsePathAllow(process.env.BIR_PATH_ALLOW);
204
184
  const opts = {
205
185
  enabled,
206
186
  allowServers,
207
187
  builtinTools,
208
188
  bashAllow,
189
+ pathAllow,
209
190
  budgets: {
210
191
  // What a prompt may wait for its match (K5). Past it the prompt goes on
211
192
  // without one; a service slower than this must not cost every prompt.
@@ -224,22 +205,14 @@ function buildReplayOptions(keeper, policy, key) {
224
205
  // BIR_INTENT_MATCH=0 turns it off on its own.
225
206
  intentMatch: {
226
207
  enabled: process.env.BIR_INTENT_MATCH !== "0",
227
- // The probe's own ceiling, not a wait on the hook's path: `PreToolUse`
228
- // waits for it only while arming is on — the default, unless
229
- // BIR_SEGMENT_ARM=0 or BIR_INTENT_ARM=0 — and then at most
208
+ // The probe's own ceiling, not a wait on the hook's path: a probe is sent
209
+ // only with BIR_INTENT_ARM=1, and `PreToolUse` waits for it at most
230
210
  // BIR_INTENT_ARM_BUDGET_MS (500 ms), never while the service is failing
231
- // (control/server.ts); otherwise it answers without it (hit counting
232
- // only). 4 000 leaves a request whose best segment lands in the
233
- // not-clear band its one live question under the service's own 2 500 ms
234
- // attempt (segmented.md R-HIT-11).
235
- budgetMs: positiveInt(process.env.BIR_INTENT_MATCH_BUDGET_MS, 4_000),
211
+ // (control/server.ts).
212
+ budgetMs: positiveInt(process.env.BIR_INTENT_MATCH_BUDGET_MS, 1_500),
236
213
  maxPerTurn: positiveInt(process.env.BIR_INTENT_MATCHES_PER_TURN, 3),
237
214
  maxRequestsPerTurn: positiveInt(process.env.BIR_INTENT_REQUESTS_PER_TURN, 40),
238
215
  },
239
- // On by default from 0.3.19 (the service's segmentsbreakdown.md SB-21): a
240
- // plan that stops at its decision hands the rest to segments of each half,
241
- // and they run only mid-task. BIR_SEGMENT_ARM=0 brings back observe-only.
242
- segmentArm: segmentArmFromEnv(),
243
216
  // Both read live, never captured (C5). The recorder signs in after it
244
217
  // listens, refreshes the token as the session outlives it, and may sign
245
218
  // in or out while it runs; replay reads whatever holds *now*. The old
@@ -263,6 +236,15 @@ function buildReplayOptions(keeper, policy, key) {
263
236
  // all — loud for the same reason as the rest of this line.
264
237
  builtins: builtinTools.size > 0 ? [...builtinTools].join(",") : "(none)",
265
238
  bashAllow: builtinTools.has("Bash") ? (bashAllow.length > 0 ? bashAllow.join(",") : "(none — Bash steps are steered)") : undefined,
239
+ // Where the file tools may reach: anywhere, as the agent's own, unless
240
+ // BIR_PATH_ALLOW names folders (up to 0.3.20 the project alone).
241
+ pathAllow: [...builtinTools].some((name) => FILE_BUILTINS.has(name))
242
+ ? pathAllow.includes("*")
243
+ ? "(any folder)"
244
+ : pathAllow.length > 0
245
+ ? pathAllow.join(",")
246
+ : "(none — file steps are steered)"
247
+ : undefined,
266
248
  // Who reads what this turn acts on (segmented.md R-PARAM-5). Named at
267
249
  // startup because the third state is a replay that silently never runs.
268
250
  derive: key.source ??
@@ -270,8 +252,6 @@ function buildReplayOptions(keeper, policy, key) {
270
252
  ? "the service"
271
253
  : "recorded sample values — sign in, or a scenario with a target will not run"),
272
254
  intentMatch: opts.intentMatch?.enabled ? "on" : "off",
273
- // Whether a segment the service hands out runs, or is only logged.
274
- segments: segmentsAtStartup(opts.segmentArm === true),
275
255
  source,
276
256
  why: "a prompt that matches a calculated scenario runs it: the scenario's own steps run without a " +
277
257
  "permission prompt of their own (never in plan mode); every other tool call is asked about as " +
@@ -272,17 +272,14 @@ export interface ControlServerOptions {
272
272
  */
273
273
  onStopRequested?: () => void;
274
274
  /**
275
- * Whether an intent probe may arm a plan inside the `PreToolUse` that sent it
276
- * (fallbk.md §Runner 4).
275
+ * Whether a match found mid-turn may arm a plan in the place of the call
276
+ * that found it (fallbk.md §Runner 4).
277
277
  *
278
278
  * Arming there means *waiting* there, on every unsteered tool call, for a
279
- * service round trip. It waits (at most `BIR_INTENT_ARM_BUDGET_MS`, 500 ms,
280
- * and never while the service is failing) when segment arming is on — the
281
- * recorder's default from 0.3.19, `BIR_SEGMENT_ARM` unset or `1` — or when
282
- * `BIR_INTENT_ARM=1` asks for whole-scenario intent arming. `BIR_INTENT_ARM=0`
283
- * never waits. Otherwise, as in a server built with neither (a test, an
284
- * embedder), the probe is sent without waiting, for hit counting only, and
285
- * nothing arms mid-turn.
279
+ * service round trip — so it is off by default: no probe is sent and nothing
280
+ * arms mid-turn. `BIR_INTENT_ARM=1` turns it on, and then each call waits at
281
+ * most `BIR_INTENT_ARM_BUDGET_MS` (500 ms), and never while the service is
282
+ * failing. Unset or `BIR_INTENT_ARM=0` is off.
286
283
  */
287
284
  intentArm?: boolean;
288
285
  /**
@@ -441,8 +438,8 @@ export declare class ControlServer {
441
438
  private readonly deriveRecentResults;
442
439
  /**
443
440
  * Which rungs of R-INTENT-1 the runner may send (`BIR_INTENT_SOURCES`). It
444
- * governs only what is *sent*: with every source off, the probe still goes out
445
- * with an empty text and the server builds the tool line (R-HIT-4).
441
+ * governs only what is *sent*: with every source off, a step goes out with no
442
+ * text, and no intent probe is sent for it.
446
443
  */
447
444
  private readonly intentSources;
448
445
  constructor(opts: ControlServerOptions);
@@ -1144,154 +1141,44 @@ export declare class ControlServer {
1144
1141
  */
1145
1142
  private replayOverran;
1146
1143
  /**
1147
- * Recordings this turn must not count a step hit against (segmented.md
1148
- * R-HIT-7).
1144
+ * Intent matching in the ReAct loop (fallbk.md §Runner 4).
1149
1145
  *
1150
- * A repeat of a recording's own prompt is *prompt* recurrence, counted as
1151
- * `iterations`, never step recurrence. Without this, three prompt repeats
1152
- * would reach `SEGMENT_MIN_HITS` on every step of the whole recording and the
1153
- * detector would build a whole-run-sized segment beside the whole-run
1154
- * scenario.
1155
- */
1156
- private excludeRunsFor;
1157
- /**
1158
- * Per plan of this turn, its recording and how far the plan got in its own
1159
- * chain (segmented.md R-OUT-6).
1160
- *
1161
- * The server uses it to refuse a segment that starts *before* where this turn
1162
- * already is: re-running steps the turn has just done is worse than not
1163
- * replaying at all. The position is the plan's own: the step it handed over
1164
- * at (its decision included), or its last step when it ran to its end. The
1165
- * server maps it onto the recording — a segment's first position, a
1166
- * decision's step before it — and takes the largest per recording (the
1167
- * service's segmentsbreakdown.md D18). One entry per plan, so that two plans
1168
- * of one recording, whose own positions are on different scales, never
1169
- * hide each other. A plan declined for anything but a known-bad first step
1170
- * reports nothing — it ran none of the recording, so it blocks none of it.
1171
- *
1172
- * Rebuilt on every probe rather than cached: a plan that hands over between
1173
- * two calls changes the answer.
1174
- */
1175
- private ranThroughFor;
1176
- /**
1177
- * Intent matching in the ReAct loop (fallbk.md §Runner 4, segmented.md 10.3).
1178
- *
1179
- * Called only while the model is driving. Every unsteered, non-housekeeping
1180
- * tool call is sent: the service counts it as a hit on whatever recorded step
1181
- * it matches, and may hand back a scenario or a segment to run in its place.
1146
+ * Called only while the model is driving, and only when a match may arm a
1147
+ * plan in the call's place ({@link intentArming}, off unless
1148
+ * `BIR_INTENT_ARM=1`): the reasoning the call was made with is sent, and the
1149
+ * service may hand back a whole scenario whose intent it matches. A call made
1150
+ * with no reasoning sends nothing — there is no intent to match. Every
1151
+ * failure is a miss.
1182
1152
  *
1183
- * A call made with **no reasoning at all** still probes (R-HIT-4): the server
1184
- * builds an intent from the tool name and arguments, and a step the agent took
1185
- * without narrating it is exactly as much a recurrence as one it explained.
1186
- * Every failure is a miss.
1187
- *
1188
- * THE PROBE IS NOT WAITED ON unless it may arm ({@link intentArming}). Hit
1189
- * counting needs no answer at all, and waiting for one put a service round
1190
- * trip — 70-450 ms healthy, the whole 4 s budget on a hung service — in front
1191
- * of every Read, Grep, Edit and Bash of every signed-in user. When it may arm,
1192
- * the wait is {@link intentArmBudgetMs} at most, and none while the service is
1153
+ * The probe sits in front of the call, so it is waited on
1154
+ * {@link intentArmBudgetMs} at most, and not at all while the service is
1193
1155
  * failing. Delivering a match on a *later* call instead is not an option: the
1194
1156
  * matched call has run by then, and the plan's first step would repeat it.
1195
1157
  */
1196
1158
  private tryIntentMatch;
1197
1159
  /**
1198
1160
  * Whether the model's call is plausibly `state`'s first step: its tool, and
1199
- * arguments the pin guard would pin to that step's input. The input is
1200
- * computed only for a step of the plan's own frame (a called segment's first
1201
- * step would build that segment's parameters here); anything that cannot be
1202
- * computed is "no" — the directive then rides on the call's PostToolUse.
1161
+ * arguments the pin guard would pin to that step's input. Anything that
1162
+ * cannot be computed is "no" — the directive then rides on the call's
1163
+ * PostToolUse.
1203
1164
  */
1204
1165
  private isFirstStep;
1205
1166
  /**
1206
- * The agent's call against `state`'s first step, value for value (the
1207
- * service's offer1.md O5): `hit` when every argument is the step's own,
1208
- * `missing` when the only differences are values the plan does not have
1209
- * (a target the reading left empty, which the agent's call may hold), and
1210
- * `other` otherwise.
1211
- */
1212
- private firstStepVerdict;
1213
- /**
1214
- * Whether `state`'s plan calls `toolName` in a step it would run: what makes
1215
- * a plan started after a call of that tool make it a second time (the
1216
- * service's offer1.md O8). A tool that only reads never counts — reading
1217
- * twice changes nothing — by the service's own rule ({@link readsOnly}): a
1218
- * `crm_search_customers` mid-task never keeps a plan that searches later from
1219
- * starting.
1220
- */
1221
- private planCallsTool;
1222
- /**
1223
- * A plan of this turn stopped at its decision: ask the service, once per
1224
- * plan, which calculated scenarios can follow it, and arm each in the
1225
- * background while the agent reads the note and decides (offer1.md O3, O4).
1226
- * Called wherever a hand-over is first seen; a second call for the same plan
1227
- * does nothing.
1228
- */
1229
- private noteDecision;
1230
- /**
1231
- * Ask for a decision's next scenarios (offer1.md O3): each is checkable as
1232
- * soon as the service answers, and armed in the background, each on its own
1233
- * (O4). Resolves when the service has answered, not when the arms have.
1234
- * Never rejects.
1235
- */
1236
- private fetchNext;
1237
- /**
1238
- * Arm one next scenario in the background (offer1.md O4): its values read
1239
- * from the turn's request and its results so far — or, once more, with the
1240
- * agent's own call (O5) — and settled, so a call is compared with the first
1241
- * step as it would really run. Never rejects.
1242
- */
1243
- private armNext;
1244
- /**
1245
- * The agent's call after a decision, against the scenarios that can follow
1246
- * it (offer1.md O5, O6). A hit — the call is a scenario's first step, value
1247
- * for value — is answered in the call's place with that scenario's directive:
1248
- * the agent calls `run_scenario`, and the scenario runs every step, this one
1249
- * included, once. Undefined otherwise: the call runs as the agent wrote it.
1250
- */
1251
- private tryNextScenarios;
1252
- /** A next scenario's hit: bookkeeping as for any plan armed mid-task, then its directive in the call's place. */
1253
- private nextHit;
1254
- /**
1255
- * A next scenario hit after the agent's own calls — looks — opened the
1256
- * fragment of its work after the decision: the fragment ends here, with what
1257
- * the agent did itself, and nothing more of this turn is recorded into it.
1258
- * The scenario's own calls, pinned in steer mode or run in direct mode, are
1259
- * not the agent's work, and would make the fragment a copy of that scenario
1260
- * (offer1.md O7). A fragment still due is dropped instead
1261
- * ({@link dropDecisionFragment}).
1262
- */
1263
- private closeDecisionFragment;
1264
- /**
1265
- * End this turn's next scenarios (offer1.md O6): every one but the hit is
1266
- * never announced, so never runs and is never reported.
1267
- */
1268
- private endNext;
1269
- /**
1270
- * Whether an intent probe may arm a plan in its own `PreToolUse` — and so
1271
- * whether that hook waits for it. See {@link ControlServerOptions.intentArm}.
1167
+ * Whether a match found mid-turn may arm a plan in its own `PreToolUse` — and
1168
+ * so whether a probe is sent and waited for at all. Off unless
1169
+ * `BIR_INTENT_ARM=1`. See {@link ControlServerOptions.intentArm}.
1272
1170
  */
1273
1171
  private intentArming;
1274
1172
  /** How long an arming probe is waited on: `BIR_INTENT_ARM_BUDGET_MS`, never past the probe's own budget. */
1275
1173
  private intentArmBudgetMs;
1276
- /** The bookkeeping half of a probe's answer: what `/health` and the log show. */
1174
+ /** A probe's answer, for the log. Never throws: a malformed answer is a log line. */
1277
1175
  private noteProbe;
1278
- private noteProbeUnsafe;
1279
1176
  /**
1280
1177
  * Schedule a fragment once a matched turn's plan has handed over (fallbk.md
1281
1178
  * D7). Only a turn that is not already recording needs one: an unmatched turn
1282
1179
  * whose intent-armed plan handed over is recording the model's work anyway.
1283
1180
  */
1284
1181
  private noteHandover;
1285
- /**
1286
- * Drop a fragment scheduled at a decision (the service's
1287
- * segmentsbreakdown.md SB-16) when a plan the intent check armed takes the
1288
- * agent's next call: what follows is that plan's half, not the other one,
1289
- * and recorded as the other half it would become a wrong segment C. Cleared
1290
- * rather than ended, so a later hand-over of that plan schedules its own
1291
- * fragment, as any plan's does in a turn with none. Any other fragment is
1292
- * left as it is.
1293
- */
1294
- private dropDecisionFragment;
1295
1182
  /**
1296
1183
  * Open the fragment run and record from this call on (fallbk.md D7).
1297
1184
  *