@basein/runner 0.3.7 → 0.3.9
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.
- package/README.md +10 -6
- package/dist/bin/bir-hooks.d.ts +8 -8
- package/dist/bin/bir-hooks.js +8 -8
- package/dist/bin/investigate.js +20 -6
- package/dist/control/recorder-main.js +4 -4
- package/dist/control/server.js +7 -4
- package/dist/control/transcript.d.ts +9 -0
- package/dist/control/transcript.js +26 -7
- package/dist/record/remote-recorder.d.ts +9 -1
- package/dist/record/remote-recorder.js +77 -10
- package/dist/record/truncate.d.ts +11 -0
- package/dist/record/truncate.js +11 -0
- package/dist/replay/builtins.d.ts +21 -16
- package/dist/replay/builtins.js +38 -21
- package/dist/replay/controller.d.ts +22 -9
- package/dist/replay/controller.js +103 -12
- package/dist/replay/coverage.d.ts +8 -7
- package/dist/replay/coverage.js +8 -7
- package/dist/replay/plan.d.ts +6 -0
- package/dist/replay/plan.js +13 -3
- package/dist/replay/types.d.ts +16 -0
- package/docs/calculatedReplay.md +48 -8
- package/docs/calculatedReplayGuide.md +11 -5
- package/package.json +1 -1
- package/production.md +3 -2
package/docs/calculatedReplay.md
CHANGED
|
@@ -45,7 +45,7 @@ this design has a ladder whose cheapest rung is free — and the more of a stack
|
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| Runs MCP tools itself | No (`replay.md` §12: *"rrepeat never executes tools — not even MCP"*) | **Yes**, on the connection it already owns |
|
|
47
47
|
| Divergence fallback | A second Agent SDK session — the dominant cost | Direct `tools/call` through the proxies — **$0** |
|
|
48
|
-
| Reaches built-ins (`Bash`, `Read`) | Only by steering the live session |
|
|
48
|
+
| Reaches built-ins (`Bash`, `Read`) | Only by steering the live session | `Read`, `Glob`, `Grep`, `Edit`, `Write` and `Bash` run on the runner itself, jailed to the project (`BIR_BUILTIN_TOOLS`, `BIR_BASH_ALLOW`); any other built-in is steered |
|
|
49
49
|
| Needs `@anthropic-ai/*` at runtime | Yes | No (§16.3) |
|
|
50
50
|
|
|
51
51
|
---
|
|
@@ -252,21 +252,26 @@ export function modeFor(steps: SerializedScenarioStep[], wrapped: ReadonlySet<st
|
|
|
252
252
|
| — | `none` | nowhere | declines at gate 5 |
|
|
253
253
|
|
|
254
254
|
`live` covers three genuinely different cases and treats them identically, correctly: a built-in
|
|
255
|
-
off the `BIR_BUILTIN_TOOLS` allow-list (by default everything but
|
|
256
|
-
`
|
|
255
|
+
off the `BIR_BUILTIN_TOOLS` allow-list (by default everything but the six the runner implements —
|
|
256
|
+
so `Task`, `WebFetch`; up to 0.3.7 also `Bash`, `Edit`, `Write`), an MCP server the user chose not to wrap, and
|
|
257
257
|
`claude-in-chrome` — which is `scope: "dynamic"`, appears in no config file, and therefore can never
|
|
258
258
|
be wrapped (v1 design §1). None of them can be reached except through the session.
|
|
259
259
|
|
|
260
260
|
A built-in **on** the allow-list runs on the runner, in the session's own project directory and
|
|
261
261
|
confined to it — a path that resolves outside the project, symlinks included, is refused and the
|
|
262
|
-
step is `not_run`, the clean hand-over. `Bash`
|
|
263
|
-
command prefixes
|
|
262
|
+
step is `not_run`, the clean hand-over. `Bash` has a second switch on top (`BIR_BASH_ALLOW`,
|
|
263
|
+
command prefixes; unset means any command, `none` keeps `Bash` in the session), because a
|
|
264
|
+
runner-side step never reaches the host's permission prompts. The
|
|
264
265
|
results mimic the hook's view of each tool, which is what the recorder stored for built-in steps
|
|
265
266
|
and what their `toolOutputLogic` was authored against; they pass through the same
|
|
266
267
|
`serializeCapped(redact(…))` as every recorded payload. Bash keeps the host's own semantics a
|
|
267
268
|
recorded chain was written against: each call is a fresh `-l` shell (variables and functions do
|
|
268
269
|
not persist), while the **working directory carries over** from the plan's previous Bash step —
|
|
269
|
-
`cd sub` then `cat file` replays as it ran.
|
|
270
|
+
`cd sub` then `cat file` replays as it ran. Two Bash calls the host runs differently are never
|
|
271
|
+
started on the runner, and are `not_run` instead: one with `run_in_background` (there is no shell
|
|
272
|
+
here to leave it in), and one whose own `timeout` is longer than the step's clock
|
|
273
|
+
(`BIR_STEP_TIMEOUT_MS`, clipped to what the plan has left) — cut off part-way, an `npm i -g` or a
|
|
274
|
+
migration can leave things half done.
|
|
270
275
|
|
|
271
276
|
A **mixed** scenario in `steer` mode still gets direct execution for its wrapped steps once it
|
|
272
277
|
diverges (§8), so coverage is not all-or-nothing at the divergence boundary — only at the arm-time
|
|
@@ -898,9 +903,38 @@ steps: Array<{
|
|
|
898
903
|
stage?: ExecutionStage; // where in the step, for `failed` / `skipped`
|
|
899
904
|
error?: string; // message only — this is rendered in a web page
|
|
900
905
|
durationMs?: number;
|
|
906
|
+
toolInput?: string; // what the step sent its tool, as JSON text
|
|
907
|
+
toolOutput?: string; // what its output logic was given
|
|
901
908
|
}>
|
|
902
909
|
```
|
|
903
910
|
|
|
911
|
+
**What the step sent and received.** `toolInput` is the arguments the step's input logic computed
|
|
912
|
+
on this turn, and `toolOutput` what its output logic was given: the tool's answer, or for `recorded`
|
|
913
|
+
the source run's output that stood in. They are this run's, and the service can learn them nowhere
|
|
914
|
+
else — a matched turn is not recorded (§12). Without them the recording page can only put the
|
|
915
|
+
recording's own sample under this run's verdict and time: a transfer of 3500 beside "passed", after
|
|
916
|
+
a replay that sent 3600.
|
|
917
|
+
|
|
918
|
+
- **Redacted and capped.** Each is serialized and redacted as a recorded step's payload is
|
|
919
|
+
(`serializeCapped(redact(…))`; an output is redacted again here, as JSON, because the hook's view
|
|
920
|
+
of a steered built-in is threaded as the host gave it). Then it is cut to 3,800
|
|
921
|
+
bytes (`REPORT_PAYLOAD_MAX_BYTES`), as text and with a marker that says how much was dropped: these
|
|
922
|
+
are read on a page, never parsed, and with its marker a cut payload still fits the 4,000
|
|
923
|
+
characters the service keeps of each — so the page shows this cut and its sizes, not a second one.
|
|
924
|
+
The recorder redacts once more as it sends.
|
|
925
|
+
- **Inside one small request.** The service reads `POST /scenarios/:id/executions` with its 100 kB
|
|
926
|
+
JSON parser (only `/recordings` has the 5 MB one) and answers a larger body 413, which would lose
|
|
927
|
+
the turn's cost with it. So the recorder holds the whole report to 96 KiB (`REPORT_MAX_BYTES`):
|
|
928
|
+
it writes the costs and the verdicts first, then adds payloads while they fit — the failed steps'
|
|
929
|
+
first, then every step's input, then the outputs, in the order the steps ran. A payload that does
|
|
930
|
+
not fit is left out, and its step keeps its verdict.
|
|
931
|
+
- **Absent where there was none.** A step whose input logic threw has no input. A `skipped` step
|
|
932
|
+
has no output, and neither has a call the host says failed without a result.
|
|
933
|
+
- **Only for the reported scenario's own steps.** A step inside a called segment is reported with
|
|
934
|
+
its verdict and its `scenarioId`, without payloads: the console still files verdicts by step
|
|
935
|
+
number alone (the service's plan-services.md, W1.9), and a called segment numbers its steps from
|
|
936
|
+
zero too.
|
|
937
|
+
|
|
904
938
|
`ok` and `recorded` are passes (`recorded` with an asterisk: the values came from the source run,
|
|
905
939
|
which happens only with `BIR_REPLAY_RECORDED_OUTPUTS=1` and in `bir replay`). `skipped` produced no
|
|
906
940
|
result here: its call could not be sent (a built-in outside the session, no proxy for its server,
|
|
@@ -921,8 +955,10 @@ The verdicts come from `plan.ts`'s `onStep`, which has always known them — wit
|
|
|
921
955
|
logic failure is announced *before* the throw propagates (`reportingStage`), because after it
|
|
922
956
|
nothing knows which step the stack trace belonged to. In steer mode there is no loop to observe, so
|
|
923
957
|
the controller records the verdict at both hooks: `PreToolUse` for an input-logic throw, `PostToolUse`
|
|
924
|
-
for the pass or the output-logic throw
|
|
925
|
-
|
|
958
|
+
for the pass or the output-logic throw — and keeps the pinned arguments on the pin, so the verdict
|
|
959
|
+
`PostToolUse` writes can say what the call ran with. `ReplayState.stepResults` is keyed by the
|
|
960
|
+
scenario that owns the step and its index, so the second word on a step replaces the first and a
|
|
961
|
+
called segment's step 0 never replaces the caller's.
|
|
926
962
|
|
|
927
963
|
One consequence for the rule that a costless decline is not worth sending (`buildReport`, and the
|
|
928
964
|
service's matching `202 { recorded: false }`): it now *is* worth sending when a step broke. No cost
|
|
@@ -986,6 +1022,10 @@ trusting it.
|
|
|
986
1022
|
would 404. `run.recording = false` already handles it — `record()` early-returns and `sealRun` skips
|
|
987
1023
|
both the final answer and `finishRun`.
|
|
988
1024
|
|
|
1025
|
+
What the turn's steps sent and received is not lost with it: the execution report carries them, on
|
|
1026
|
+
each step's verdict (§11.2a). That is the only copy the service gets, and it is what the recording
|
|
1027
|
+
page shows as the scenario's last run.
|
|
1028
|
+
|
|
989
1029
|
Three consequences to keep straight:
|
|
990
1030
|
|
|
991
1031
|
1. **Step indices are still allocated.** `ordering` runs regardless, correlations are still minted
|
|
@@ -234,7 +234,10 @@ When replay arms:
|
|
|
234
234
|
when it plausibly *is* the step — the same command, the same file, an argument in common (design
|
|
235
235
|
§7) — and never in plan mode.
|
|
236
236
|
- **Direct steps are not visible to the permission system at all.** They are dispatched straight to a
|
|
237
|
-
`bir-proxy`, which calls the upstream on a connection it already holds
|
|
237
|
+
`bir-proxy`, which calls the upstream on a connection it already holds — or, for Claude Code's own
|
|
238
|
+
`Read`, `Glob`, `Grep`, `Edit`, `Write` and `Bash`, run by the recorder itself in the project
|
|
239
|
+
folder, any `Bash` command included (`BIR_BUILTIN_TOOLS`, `BIR_BASH_ALLOW`; up to 0.3.7 only the
|
|
240
|
+
first three). Claude Code never sees them.
|
|
238
241
|
The `run_scenario` call that starts them is answered `allow` too, while the runner has announced a
|
|
239
242
|
direct plan; your own deny or ask rules for it still apply.
|
|
240
243
|
|
|
@@ -243,7 +246,9 @@ That is what makes replay fast, and it is the whole risk. Two things follow:
|
|
|
243
246
|
1. Only enable it in a project whose scenarios you have dry-replayed and read (§3.1).
|
|
244
247
|
2. Run `bir replay allow <servers>` (§5.3). An allowlist of the servers you are comfortable having
|
|
245
248
|
called unattended is cheap insurance, and it is the difference between "replay reads a dashboard"
|
|
246
|
-
and "replay can do anything any wrapped server can do".
|
|
249
|
+
and "replay can do anything any wrapped server can do". The same goes for built-ins:
|
|
250
|
+
`BIR_BUILTIN_TOOLS=Read,Glob,Grep` keeps writes and commands with the model, and
|
|
251
|
+
`BIR_BASH_ALLOW=git,npm test` (for example) limits the commands the recorder runs itself.
|
|
247
252
|
|
|
248
253
|
Nothing else is approved on your behalf. Outside a matched scenario's own steps, a wrapped MCP call
|
|
249
254
|
keeps its permission prompt, as it would without the runner (`bir doctor` says so in a note);
|
|
@@ -354,8 +359,8 @@ so there is exactly one switch and it cannot get out of step with itself.
|
|
|
354
359
|
| `BIR_STEP_TIMEOUT_MS` | `60000` | Per-step ceiling for one direct `tools/call`, clipped to what the plan has left |
|
|
355
360
|
| `BIR_LOGIC_TIMEOUT_MS` | `1000` | How long one scenario logic body may run before it is stopped. At most 10 000 |
|
|
356
361
|
| `BIR_REPLAY_RECORDED_OUTPUTS` | *(unset)* | `1` lets a step that could not be sent (a built-in off `BIR_BUILTIN_TOOLS`, no proxy, a server off the allowlist) stand on its recorded output instead of stopping the plan. Never for a timeout or an upstream error |
|
|
357
|
-
| `BIR_BUILTIN_TOOLS` | `Read,Glob,Grep` | Built-ins the runner executes itself during a replay, in the session's own project directory and confined to it. A chain whose only non-MCP steps are on this list arms `direct`.
|
|
358
|
-
| `BIR_BASH_ALLOW` | *(unset)* | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset
|
|
362
|
+
| `BIR_BUILTIN_TOOLS` | `Read,Glob,Grep,Edit,Write,Bash` | Built-ins the runner executes itself during a replay, in the session's own project directory and confined to it. A chain whose only non-MCP steps are on this list arms `direct`. A runner-side step never reaches the host's permission prompts, so leave off any tool you want the model to call itself; `Read,Glob,Grep` is the read-only set (the default up to 0.3.7). `none`: every built-in step is steered in the session |
|
|
363
|
+
| `BIR_BASH_ALLOW` | *(unset: any command)* | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset means any command; `none` means no command, and `Bash` steps are steered in the session. A plan's Bash steps share a working directory, exactly as the host's own Bash tool does — variables and functions do not persist, also as the host's. A call with `run_in_background`, or with its own `timeout` longer than the step's clock (`BIR_STEP_TIMEOUT_MS`), is never started on the runner: the plan hands over in front of it |
|
|
359
364
|
| `BIR_INTENT_ARM` | *(unset)* | `1` lets a match found mid-turn arm a plan in the calling tool's place, which means waiting for it (`BIR_INTENT_ARM_BUDGET_MS`, 500); `0` never waits; unset follows `BIR_SEGMENT_ARM` |
|
|
360
365
|
| `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
|
|
361
366
|
|
|
@@ -553,7 +558,7 @@ matches. A prompt that missed both is matched once more on its task line (`PROMP
|
|
|
553
558
|
| `replay.vetoed … the model's first call was …, not run_scenario` | A direct plan, and the model reached for another tool first — usually because it has no `run_scenario` | As for `no_delivery_tool`. Nothing ran; the model did the task |
|
|
554
559
|
| `replay.pin_declined` on most steered turns | The model's calls to the expected tool are not the step — another command, another file — so nothing is pinned; if it then moves on, `replay.pin_abandoned` retires the plan | Harmless for those calls, which ran as the model wrote them. Persistent declines on one step usually mean its arguments cannot be shown in the directive (they depend on an earlier step's output, or the command runs to several lines), and the model writes its own |
|
|
555
560
|
| `why="no step is executable"` | None of the scenario's tools is a wrapped MCP server and none is a built-in reachable in this session | Wrap the servers the scenario uses (`bir install --server …`) and recalculate |
|
|
556
|
-
| `plan.armed mode=steer` where you expected `direct` | At least one step is a built-in off `BIR_BUILTIN_TOOLS` (default
|
|
561
|
+
| `plan.armed mode=steer` where you expected `direct` | At least one step is a built-in off `BIR_BUILTIN_TOOLS` (by default one the runner does not implement, such as `WebFetch` or `Task`; a `Bash` step too when `BIR_BASH_ALLOW=none`), an unwrapped MCP server, or `claude-in-chrome`. Before 0.3.8 the default list was `Read,Glob,Grep`, so `Edit`, `Write` and `Bash` steered too | `bir status` shows what is wrapped; the `plan.armed` line's `coverage=` names the `live` step. `claude-in-chrome` is `scope: "dynamic"` and can never be wrapped |
|
|
557
562
|
| Model ignores the directive and diverges every turn | Steering is advisory — the model chooses; `bir` only pins arguments | Expected occasionally. Persistent divergence usually means the scenario's tools do not fit the live task; check `intent` |
|
|
558
563
|
| A `replay.thread` line with no `emitted=` (under `BIR_VERBOSE=1`) | `toolOutputLogic` derived nothing — usually the output shape changed since recording | Dry-replay (§3.1). If the dry run emits and the live run does not, the tool's output shape has drifted; recalculate |
|
|
559
564
|
| `replay.thread_fallback` on a wrapped step | The proxy did not report within the grace window | Check that proxy is alive (`bir doctor`). Repeats mean a slow or dying upstream |
|
|
@@ -567,6 +572,7 @@ matches. A prompt that missed both is matched once more on its task line (`PROMP
|
|
|
567
572
|
| A parked step ran again after an hour, or a plan was recalculated by itself | The service's grace period (D8): a parked step is served with `stop: null` once an hour for a day and a success clears its count; if it keeps failing, or its tool is gone, the next repeat of the prompt is recorded in full and the plan is calculated again from it | Nothing: that is the repair. `bir investigate` says which it is — a step still being retried, a parked step whose repair is due, or a plan being calculated again. To repair now by hand: `bir scenario calc <runId> --force` for a whole-run plan; from the next version, `bir scenario calc <scnId> --force` works for any plan, a sub-task one included. A plan with steps edited by hand is not repaired by itself: the parked step stays parked, and `bir investigate` says the repair was skipped. Then fix the step (§9.2); or take the edits back with `bir scenario undo`, once per edit, and it is repaired as usual; or recalculate now and drop the edits: `bir scenario calc <scnId> --force --discard-edits` |
|
|
568
573
|
| `sessionCostUsd` grows every prompt in a session | The per-run usage watermark is missing or not taken | Design §11.4. Reports without a mark must carry `measured: false` |
|
|
569
574
|
| Everything works, nothing is saved | A replayed turn is deliberately not recorded (design §12) | Correct. The matched run stays canonical and its `iterations` is bumped |
|
|
575
|
+
| The console shows a replayed step's input and output with the recording's values, beside this run's verdict and time | The run was reported by a runner up to 0.3.8, which sent each step's verdict and duration and nothing of what it sent or received. The page then falls back to the recording's sample (design §11.2a) | Update the runner: a newer one sends each replayed step's input and output with its verdict, redacted and cut to under 4 KB, and the card shows them as *last run*. To see what an older replay ran with: the result of `mcp__bir__run_scenario` in the session's transcript, or `lastParams` in `bir scenario show scn_…` |
|
|
570
576
|
| `bir doctor`: the SessionStart hook points at a Node that is gone | The hook names Node by absolute path. That Node was uninstalled, moved, or was a version manager's per-shell copy; the hook then fails on every session, no recorder is started, and runs record Tier 2 | `bir setup` here again — it rewrites the hook to the Node that runs it |
|
|
571
577
|
| `control.port_busy` in the log; the recorder is not running | Another program holds this project's port, or the system excludes it (on Windows, a range Hyper-V or WSL reserved). A background recorder with a recorded port does not fall back to another, because every hook but SessionStart names this one; only a `bir-hooks` you run by hand in a terminal still does | `bir setup --port <another>` in this directory — the fix the log line itself names. It rewrites the hook URLs and the scenario server's address together and restarts the recorder |
|
|
572
578
|
| The recorder was started by the hook, and `bir doctor` says `servers=(all wrapped)` although you set `BIR_REPLAY_ALLOW_SERVERS` | The hook starts the recorder with Claude Code's environment, not your terminal's; a variable set in one terminal is not seen | `bir replay allow <a,b>` — stored for the project, read by every recorder — then `bir up --restart` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basein/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/production.md
CHANGED
|
@@ -518,6 +518,7 @@ Over 2-minute runs with one turn every 8 s (92 hooks):
|
|
|
518
518
|
- Non-JSON tool errors: 8 KiB.
|
|
519
519
|
- Report errors: 2 KiB.
|
|
520
520
|
- Tool payloads: 64 KiB.
|
|
521
|
+
- A replayed step's input and output, in the execution report: 3,800 bytes each, redacted like a recorded step's. The whole report stays under 96 KiB, because the service reads that route with its 100 kB parser: a payload that does not fit is left out, and its step keeps its verdict.
|
|
521
522
|
|
|
522
523
|
Truncation is always marked with the original size.
|
|
523
524
|
|
|
@@ -991,8 +992,8 @@ Defaults were checked in `src/`.
|
|
|
991
992
|
| `BIR_STEP_TIMEOUT_MS` | recorder | 60000 | One direct `tools/call`. |
|
|
992
993
|
| `BIR_LOGIC_TIMEOUT_MS` | recorder | 1000 (capped at 10000) | Synchronous time limit for one scenario logic body. |
|
|
993
994
|
| `BIR_REPLAY_RECORDED_OUTPUTS` | recorder | unset | `1`: a step that could not be sent may use its recorded output, labelled as such. |
|
|
994
|
-
| `BIR_BUILTIN_TOOLS` | recorder | `Read,Glob,Grep` | Built-ins the runner executes itself during a replay, in the session's project directory and confined to it.
|
|
995
|
-
| `BIR_BASH_ALLOW` | recorder | unset | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset
|
|
995
|
+
| `BIR_BUILTIN_TOOLS` | recorder | `Read,Glob,Grep,Edit,Write,Bash` | Built-ins the runner executes itself during a replay, in the session's project directory and confined to it (up to 0.3.7 the default was `Read,Glob,Grep`). Runner-side steps bypass the host's permission prompts; leave a tool off to have the model call it. `none` turns it off. |
|
|
996
|
+
| `BIR_BASH_ALLOW` | recorder | unset (any command) | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset means any command; `none` means no command, and `Bash` steps are steered. A plan's Bash steps share a working directory, as the host's do; variables do not persist, also as the host's. |
|
|
996
997
|
| `BIR_DERIVE_RECENT_RESULTS` | recorder | 5 | How many of this prompt's tool results derivation may read (`0`: none). Never for plans with a command sink. |
|
|
997
998
|
| `BIR_INTENT_MATCH` | recorder | on | `0` turns mid-turn intent probes off. |
|
|
998
999
|
| `BIR_INTENT_MATCH_BUDGET_MS` | recorder | 4000 | One probe's own ceiling. It is not a hook wait. |
|