@basein/runner 0.1.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.
- package/LICENSE +201 -0
- package/README.md +276 -0
- package/dist/auth/client.d.ts +85 -0
- package/dist/auth/client.js +284 -0
- package/dist/bin/bir-hooks.d.ts +48 -0
- package/dist/bin/bir-hooks.js +201 -0
- package/dist/bin/bir-proxy.d.ts +45 -0
- package/dist/bin/bir-proxy.js +207 -0
- package/dist/bin/bir-scenario.d.ts +24 -0
- package/dist/bin/bir-scenario.js +177 -0
- package/dist/bin/bir.d.ts +21 -0
- package/dist/bin/bir.js +876 -0
- package/dist/config/adapters/claude-code.d.ts +76 -0
- package/dist/config/adapters/claude-code.js +181 -0
- package/dist/config/adapters/generic.d.ts +17 -0
- package/dist/config/adapters/generic.js +36 -0
- package/dist/config/generate.d.ts +127 -0
- package/dist/config/generate.js +114 -0
- package/dist/config/resolve.d.ts +68 -0
- package/dist/config/resolve.js +132 -0
- package/dist/control/client.d.ts +56 -0
- package/dist/control/client.js +86 -0
- package/dist/control/correlation.d.ts +86 -0
- package/dist/control/correlation.js +0 -0
- package/dist/control/discovery.d.ts +50 -0
- package/dist/control/discovery.js +123 -0
- package/dist/control/ordering.d.ts +38 -0
- package/dist/control/ordering.js +44 -0
- package/dist/control/paths.d.ts +32 -0
- package/dist/control/paths.js +56 -0
- package/dist/control/server.d.ts +272 -0
- package/dist/control/server.js +1131 -0
- package/dist/control/transcript.d.ts +75 -0
- package/dist/control/transcript.js +241 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +32 -0
- package/dist/jsonrpc/framing.d.ts +49 -0
- package/dist/jsonrpc/framing.js +143 -0
- package/dist/jsonrpc/types.d.ts +52 -0
- package/dist/jsonrpc/types.js +46 -0
- package/dist/proxy/intercept.d.ts +55 -0
- package/dist/proxy/intercept.js +147 -0
- package/dist/proxy/relay.d.ts +97 -0
- package/dist/proxy/relay.js +166 -0
- package/dist/proxy/session.d.ts +116 -0
- package/dist/proxy/session.js +319 -0
- package/dist/record/housekeeping.d.ts +34 -0
- package/dist/record/housekeeping.js +39 -0
- package/dist/record/queue.d.ts +48 -0
- package/dist/record/queue.js +96 -0
- package/dist/record/recorder.d.ts +111 -0
- package/dist/record/recorder.js +39 -0
- package/dist/record/redact.d.ts +37 -0
- package/dist/record/redact.js +119 -0
- package/dist/record/remote-recorder.d.ts +110 -0
- package/dist/record/remote-recorder.js +301 -0
- package/dist/record/truncate.d.ts +36 -0
- package/dist/record/truncate.js +85 -0
- package/dist/replay/bundle.d.ts +36 -0
- package/dist/replay/bundle.js +89 -0
- package/dist/replay/controller.d.ts +300 -0
- package/dist/replay/controller.js +807 -0
- package/dist/replay/coverage.d.ts +41 -0
- package/dist/replay/coverage.js +56 -0
- package/dist/replay/derive.d.ts +58 -0
- package/dist/replay/derive.js +166 -0
- package/dist/replay/executor.d.ts +78 -0
- package/dist/replay/executor.js +233 -0
- package/dist/replay/logic.d.ts +31 -0
- package/dist/replay/logic.js +50 -0
- package/dist/replay/plan.d.ts +181 -0
- package/dist/replay/plan.js +397 -0
- package/dist/replay/pricing.d.ts +41 -0
- package/dist/replay/pricing.js +76 -0
- package/dist/replay/source-run.d.ts +50 -0
- package/dist/replay/source-run.js +98 -0
- package/dist/replay/tool-error.d.ts +22 -0
- package/dist/replay/tool-error.js +60 -0
- package/dist/replay/types.d.ts +116 -0
- package/dist/replay/types.js +35 -0
- package/dist/upstream/client.d.ts +78 -0
- package/dist/upstream/client.js +114 -0
- package/dist/upstream/http-client.d.ts +78 -0
- package/dist/upstream/http-client.js +261 -0
- package/dist/upstream/lazy-client.d.ts +31 -0
- package/dist/upstream/lazy-client.js +53 -0
- package/dist/upstream/stdio-client.d.ts +57 -0
- package/dist/upstream/stdio-client.js +203 -0
- package/dist/util/log.d.ts +27 -0
- package/dist/util/log.js +51 -0
- package/dist/util/version.d.ts +2 -0
- package/dist/util/version.js +40 -0
- package/docs/BaseInstRunner.md +621 -0
- package/docs/calculatedReplay.md +1185 -0
- package/docs/calculatedReplayGuide.md +448 -0
- package/docs/installRun.md +413 -0
- package/docs/mcpmark.md +752 -0
- package/docs/quickstart.md +201 -0
- package/docs/t-bench.md +394 -0
- package/package.json +56 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# Calculated Replay — instructions
|
|
2
|
+
|
|
3
|
+
> The operator's half of [calculatedReplay.md](calculatedReplay.md). That document says *why*; this
|
|
4
|
+
> one says *what to type*, in order, from an empty machine to a prompt that answers itself.
|
|
5
|
+
>
|
|
6
|
+
> **Read this first.** Replay executes tool calls with arguments a scenario computed, and it does so
|
|
7
|
+
> **without the permission prompts you would normally see** (design §13.2). It is off by default and
|
|
8
|
+
> it should stay off until you have read §5. Nothing in §1–§4 changes any behaviour.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 0. The shape of it, in thirty seconds
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
1. Record you work normally; bir records the run
|
|
16
|
+
2. Calculate BaseIn analyses that run into a reusable scenario
|
|
17
|
+
3. Match next time you ask the same thing in different words,
|
|
18
|
+
the server recognises it and hands the scenario back
|
|
19
|
+
4. Replay bir runs the scenario's tools instead of the model
|
|
20
|
+
rediscovering the plan
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
All four are implemented — see §10 for the map from design phase to code, and for how to run the
|
|
24
|
+
end-to-end smoke test.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Prerequisites
|
|
29
|
+
|
|
30
|
+
| # | You need | Check it with |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| 1 | A BaseIn service you can reach | `curl -fsS "$BIR_AUTH_URL/health"` |
|
|
33
|
+
| 2 | Anthropic configured **on the server** — calculation and derivation both need it | a `503 anthropic_not_configured` from `/calculate` means it is not |
|
|
34
|
+
| 3 | `SIMILARITY_DETECTION_ENABLED=true` on the server (default) | otherwise no prompt ever matches |
|
|
35
|
+
| 4 | BaseInstRunnerMCP built and installed in your project | `bir status` |
|
|
36
|
+
| 5 | Tier 1 — the hooks wired and `bir-hooks` running | `bir doctor` |
|
|
37
|
+
| 6 | *(replay only)* `ANTHROPIC_API_KEY` in the `bir-hooks` environment | optional — see §5.3 |
|
|
38
|
+
|
|
39
|
+
Tier 1 is not optional for replay. A match is a match on **the prompt**, and a standalone proxy never
|
|
40
|
+
sees one (design §1.1). If `bir doctor` says `tier: standalone`, replay cannot arm, and that is the
|
|
41
|
+
honest ceiling rather than a bug.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export BIR_AUTH_URL=https://your-basein-service
|
|
45
|
+
npm install && npm run build
|
|
46
|
+
node dist/bin/bir.js install # or `bir install` once linked/published
|
|
47
|
+
bir login
|
|
48
|
+
bir-hooks # leave running in its own terminal
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. Record a run worth reusing
|
|
54
|
+
|
|
55
|
+
Nothing special — work normally. Two server-side facts decide whether the run becomes reusable, and
|
|
56
|
+
both bite quietly:
|
|
57
|
+
|
|
58
|
+
**A run needs at least `RECORDING_MIN_ACTIONS` tool actions (default 4).** Below that it is not
|
|
59
|
+
embedded, does not enter the similarity pool, does not appear in the list, and is eventually pruned.
|
|
60
|
+
A two-tool run will never match anything, no matter how many times you repeat it.
|
|
61
|
+
|
|
62
|
+
**The prompt is what gets embedded.** `findSimilarRun` embeds `run.input` — the text of your
|
|
63
|
+
`UserPromptSubmit` — not the transcript, not the answer. So the prompt that *starts* the turn is the
|
|
64
|
+
one future prompts are compared against. Write the first prompt of a reusable task as if someone else
|
|
65
|
+
will have to phrase it again.
|
|
66
|
+
|
|
67
|
+
Watch it land:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
[bir] … run.start run=run_… sess=birsess_… tier=bound
|
|
71
|
+
[bir] … run.finish run=run_… steps=14 durationMs=48210 lossy=false recorded=true
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`recorded=false` means the service declined the run — usually because it matched something you
|
|
75
|
+
already have. That is §4 working.
|
|
76
|
+
|
|
77
|
+
List what you have:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bir scenario list # or: GET /recordings/runs
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
RUN ITER STEPS SCENARIO STATE TITLE
|
|
85
|
+
run_5f3a… 3 14 scn_9c1b… ready Fleet risk sweep
|
|
86
|
+
run_a812… 1 9 — — Rewrite the billing README
|
|
87
|
+
run_c004… 1 6 scn_44d0… calculating Check open PRs for stale reviews
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 3. Calculate the scenario
|
|
93
|
+
|
|
94
|
+
A recorded run is a transcript. A **scenario** is what BaseIn derives from it: an intent, a parameter
|
|
95
|
+
schema, and one JavaScript logic body per step that recomputes that step's tool arguments from new
|
|
96
|
+
parameters. Until a run has one, a match can only stop recording — there is nothing to run.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bir scenario calc run_5f3a… # POST /recordings/runs/:runId/calculate → 202
|
|
100
|
+
bir scenario show run_5f3a… # GET /recordings/runs/:runId/scenario (poll)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Raw, if you prefer — the token lives in `~/.baseinstrunner/credentials.json`:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
TOKEN=$(node -p "require(require('os').homedir()+'/.baseinstrunner/credentials.json').accessToken")
|
|
107
|
+
curl -fsS -X POST "$BIR_AUTH_URL/recordings/runs/run_5f3a…/calculate" \
|
|
108
|
+
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{}'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```powershell
|
|
112
|
+
$t = (Get-Content "$HOME\.baseinstrunner\credentials.json" | ConvertFrom-Json).accessToken
|
|
113
|
+
Invoke-RestMethod -Method Post -Uri "$env:BIR_AUTH_URL/recordings/runs/run_5f3a…/calculate" `
|
|
114
|
+
-Headers @{ authorization = "Bearer $t" } -ContentType 'application/json' -Body '{}'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Calculation is a background job. Poll `show` until `state` leaves `calculating`:
|
|
118
|
+
|
|
119
|
+
| `state` | Meaning | Next |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `calculating` | job running | poll again in a few seconds |
|
|
122
|
+
| `ready` | usable | §4 |
|
|
123
|
+
| `failed` | see the `error` field | fix and recalculate with `--force` |
|
|
124
|
+
|
|
125
|
+
`409 scenario_exists` means this run already has one. Re-derive it in place — keeping the id and its
|
|
126
|
+
replay history — with `bir scenario calc run_… --force`.
|
|
127
|
+
|
|
128
|
+
### 3.1 Check the scenario before you trust it
|
|
129
|
+
|
|
130
|
+
**Dry replay is the cheapest thing you will do all day. Do it every time.** It derives parameters
|
|
131
|
+
from a fresh prompt and runs the stored logic against the *original run's recorded outputs* — no real
|
|
132
|
+
tools, no side effects, one Haiku call:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
bir scenario replay scn_9c1b… --prompt "which devices in the fleet look risky right now?" --dry
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
params { "fleetId": "eu-west-1", "riskThreshold": 0.8 }
|
|
140
|
+
step 0 mcp__chrome-devtools__navigate_page { "url": "https://fleet.internal/eu-west-1" }
|
|
141
|
+
step 1 mcp__chrome-devtools__take_snapshot {}
|
|
142
|
+
→ emitted: devices[24]
|
|
143
|
+
step 2 mcp__fleet-api__device_detail { "id": "dev_88f1" }
|
|
144
|
+
→ emitted: highestRiskDeviceId, riskScore
|
|
145
|
+
response { "fleet": "eu-west-1", "atRisk": 3, "worst": "dev_88f1" }
|
|
146
|
+
cost $0.0012
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Read it for three things:
|
|
150
|
+
|
|
151
|
+
1. **Did the parameters come out right** for a prompt phrased differently from the original?
|
|
152
|
+
2. **Do the step inputs look computed**, or are they the original run's constants copied through? A
|
|
153
|
+
scenario whose inputs never vary with the parameters is a recording, not a scenario.
|
|
154
|
+
3. **Is `emitted` non-empty** for steps that later steps depend on? An empty `emitted` means
|
|
155
|
+
`toolOutputLogic` derived nothing, and every downstream input reading it will be wrong.
|
|
156
|
+
|
|
157
|
+
A dry replay is recorded as `outcome: 'dry'` and is excluded from savings by construction — it costs
|
|
158
|
+
you a Haiku call and nothing else.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 4. Confirm the match fires
|
|
163
|
+
|
|
164
|
+
Now ask the same thing in different words, in a new session.
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
you: can you check the eu-west fleet and tell me which boxes are in trouble?
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
[bir] … run.matched run=run_… matchedRun=run_5f3a… similarity=0.94
|
|
172
|
+
why="similar prompt — the service kept its own run; not recording this one"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
That line is the whole feature working up to the point of replay. If it does not appear, the prompt
|
|
176
|
+
did not clear the server's `SIMILARITY_THRESHOLD` (default 0.9) — see §8.
|
|
177
|
+
|
|
178
|
+
**With replay off (the default), this is where it stops.** The server keeps its canonical run, bumps
|
|
179
|
+
its `iterations`, this turn is not recorded, and the model answers the ordinary way. Nothing is lost
|
|
180
|
+
and nothing is skipped.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 5. Turn replay on
|
|
185
|
+
|
|
186
|
+
> Read §5.1 before running anything in this section.
|
|
187
|
+
|
|
188
|
+
### 5.1 What you are agreeing to
|
|
189
|
+
|
|
190
|
+
A calculated scenario is, in effect, **a pre-approved list of tool calls with computed arguments.**
|
|
191
|
+
When replay arms:
|
|
192
|
+
|
|
193
|
+
- **Steered steps are auto-approved.** `PreToolUse` is answered `allow`, so a pinned `Bash` or `Edit`
|
|
194
|
+
runs without the prompt you would normally see.
|
|
195
|
+
- **Direct steps are not visible to the permission system at all.** They are dispatched straight to a
|
|
196
|
+
`bir-proxy`, which calls the upstream on a connection it already holds. Claude Code never sees them.
|
|
197
|
+
|
|
198
|
+
That is what makes replay fast, and it is the whole risk. Two things follow:
|
|
199
|
+
|
|
200
|
+
1. Only enable it in a project whose scenarios you have dry-replayed and read (§3.1).
|
|
201
|
+
2. Set `BIR_REPLAY_ALLOW_SERVERS` (§5.3). An allowlist of the servers you are comfortable having
|
|
202
|
+
called unattended is cheap insurance, and it is the difference between "replay reads a dashboard"
|
|
203
|
+
and "replay can do anything any wrapped server can do".
|
|
204
|
+
|
|
205
|
+
### 5.2 Enable it
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
bir install --replay # adds the `bir` MCP server; raises the prompt-hook timeout
|
|
209
|
+
export BIR_REPLAY=1 # in the terminal that runs bir-hooks
|
|
210
|
+
bir-hooks # restart it
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`bir install --replay` does exactly two things beyond a normal install, and `bir uninstall` reverses
|
|
214
|
+
both:
|
|
215
|
+
|
|
216
|
+
- registers a first-party MCP server under the key **`bir`**, exposing one tool,
|
|
217
|
+
`mcp__bir__run_scenario` — the channel a fully-wrapped scenario's results come back through;
|
|
218
|
+
- raises `UserPromptSubmit`'s hook timeout from 5 s to 15 s, so the match round trip fits.
|
|
219
|
+
|
|
220
|
+
The `bir` tool is visible to the model in every session in this project. That is a real cost — one
|
|
221
|
+
more tool in the list, a little context per turn — and it is why replay is a separate flag rather
|
|
222
|
+
than part of `bir install`.
|
|
223
|
+
|
|
224
|
+
Confirm:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
bir doctor
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
replay ON servers=chrome-devtools,fleet-api minSimilarity=0.92
|
|
232
|
+
derive=claude-haiku-4-5-20251001 (ANTHROPIC_API_KEY present)
|
|
233
|
+
control http://127.0.0.1:53411 sess=birsess_…
|
|
234
|
+
wrapped chrome-devtools ✓ proxy pid 41822 fleet-api ✓ proxy pid 41823
|
|
235
|
+
recording yes (https://your-basein-service)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 5.3 Environment
|
|
239
|
+
|
|
240
|
+
**Set these in one place: the terminal that runs `bir-hooks`.** The proxies need no replay
|
|
241
|
+
configuration of their own — each one is told whether to open its work loop when it registers, so
|
|
242
|
+
there is exactly one switch and it cannot get out of step with itself.
|
|
243
|
+
|
|
244
|
+
| Variable | Default | What it does |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `BIR_REPLAY` | *(unset)* | `1` enables replay. Nothing below matters until it is set |
|
|
247
|
+
| `BIR_REPLAY_ALLOW_SERVERS` | *(all wrapped)* | Comma-separated server keys eligible for **direct** execution. **Set this** |
|
|
248
|
+
| `BIR_MIN_STEER_SIMILARITY` | `0.92` | Below this a match is detected but not replayed (§8) |
|
|
249
|
+
| `ANTHROPIC_API_KEY` | *(unset)* | Enables parameter derivation. Without it, replay uses the scenario's **recorded sample values** — free, and often still correct |
|
|
250
|
+
| `BIR_DERIVE_MODEL` | `claude-haiku-4-5-20251001` | The derivation model |
|
|
251
|
+
| `BIR_MATCH_BUDGET_MS` | `2500` | How long the prompt hook waits for a match before giving up |
|
|
252
|
+
| `BIR_DERIVE_BUDGET_MS` | `8000` | How long the first `PreToolUse` waits for parameters |
|
|
253
|
+
| `BIR_REPLAY_BUDGET_MS` | `120000` | Whole-plan ceiling for direct execution |
|
|
254
|
+
| `BIR_STEP_TIMEOUT_MS` | `60000` | Per-step ceiling for one direct `tools/call` |
|
|
255
|
+
| `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
|
|
256
|
+
|
|
257
|
+
**No API key is a supported configuration, not a broken one.** Derivation is skipped and every
|
|
258
|
+
parameter takes the value it had in the recorded run. For a scenario whose parameters rarely change
|
|
259
|
+
— a dashboard sweep, a fixed report — that is a complete, free replay. For one that keys off the
|
|
260
|
+
prompt ("check *eu-west*"), you want the key.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 6. Run it, and read what happened
|
|
265
|
+
|
|
266
|
+
Ask the matching question again, with replay on.
|
|
267
|
+
|
|
268
|
+
### 6.1 A fully-wrapped scenario — direct
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
[bir] … run.matched run=run_… matchedRun=run_5f3a… similarity=0.94
|
|
272
|
+
[bir] … plan.armed scenario=scn_9c1b… mode=direct steps=3
|
|
273
|
+
tools="mcp__chrome-devtools__navigate_page,mcp__chrome-devtools__take_snapshot,mcp__fleet-api__device_detail"
|
|
274
|
+
[bir] … replay.derived params=2 costUsd=0.0011 ms=840
|
|
275
|
+
[bir] … replay.step n=0 tool=navigate_page server=chrome-devtools ms=612 ok=true
|
|
276
|
+
[bir] … replay.step n=1 tool=take_snapshot server=chrome-devtools ms=1104 ok=true emitted=devices
|
|
277
|
+
[bir] … replay.step n=2 tool=device_detail server=fleet-api ms=210 ok=true emitted=highestRiskDeviceId,riskScore
|
|
278
|
+
[bir] … replay.done scenario=scn_9c1b… mode=direct steps=3/3 outcome=steered_full ms=2766
|
|
279
|
+
[bir] … execution.reported scenario=scn_9c1b… outcome=steered_full derive=0.0011 session=0.0083 fallback=0 savedUsd=0.114
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
In the session you will see one tool call, `mcp__bir__run_scenario`, returning the composed results.
|
|
283
|
+
The model reads them and answers. It never emitted the three underlying calls.
|
|
284
|
+
|
|
285
|
+
### 6.2 A mixed scenario — steer
|
|
286
|
+
|
|
287
|
+
The model emits each call itself; `bir` overrides the arguments.
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
[bir] … plan.armed scenario=scn_44d0… mode=steer steps=4
|
|
291
|
+
[bir] … replay.pin n=0 tool=mcp__github__list_pull_requests
|
|
292
|
+
[bir] … replay.thread n=0 via=proxy emitted=prNumbers
|
|
293
|
+
[bir] … replay.pin n=1 tool=Read
|
|
294
|
+
[bir] … replay.thread n=1 via=hook emitted=reviewers
|
|
295
|
+
[bir] … replay.done scenario=scn_44d0… mode=steer steps=4/4 outcome=steered_full
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`via=proxy` versus `via=hook` matters: a wrapped MCP step's output is threaded from the **proxy's**
|
|
299
|
+
report, because that is the shape the scenario's logic was written against (design §7.2). A
|
|
300
|
+
`via=hook fallback=true` line means the proxy did not report in time and the hook's differently-shaped
|
|
301
|
+
view was used instead — expect derived values to be empty, and treat repeats as a bug.
|
|
302
|
+
|
|
303
|
+
### 6.3 The model goes off-script
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
[bir] … replay.diverge scenario=scn_44d0… expected=Read called=Bash step=2/4
|
|
307
|
+
[bir] … replay.compose remaining=2 direct=1 recorded=1 skipped=0 bytes=8412 costUsd=0
|
|
308
|
+
[bir] … replay.done outcome=diverged steps=4/4
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`costUsd=0` on the compose line is the point of the whole design: the remaining steps ran through
|
|
312
|
+
proxies that were already connected, so recovering from a divergence cost no tokens at all.
|
|
313
|
+
|
|
314
|
+
### 6.4 The audit vocabulary
|
|
315
|
+
|
|
316
|
+
Everything replay decides is one line. `grep` for these:
|
|
317
|
+
|
|
318
|
+
| Line | Means |
|
|
319
|
+
|---|---|
|
|
320
|
+
| `run.matched` | The server recognised the prompt |
|
|
321
|
+
| `replay.decision … verdict=no-steer` | Matched, and declined. `why=` says which gate |
|
|
322
|
+
| `plan.armed` | A plan exists. Carries mode, step count and the ordered tool names — **before** anything runs |
|
|
323
|
+
| `replay.derived` / `replay.derive_failed` | Parameters resolved, or fell back to sample values |
|
|
324
|
+
| `replay.pin` / `replay.thread` | One steered step's input pinned / output threaded |
|
|
325
|
+
| `replay.step` | One direct step executed |
|
|
326
|
+
| `replay.diverge` / `replay.compose` | Off-script, and the recovery |
|
|
327
|
+
| `replay.done` | Final outcome, steps completed, elapsed |
|
|
328
|
+
| `execution.reported` | The ticket was redeemed and the saving booked |
|
|
329
|
+
|
|
330
|
+
Keep them: `bir-hooks 2>&1 | tee -a ~/.baseinstrunner/audit.log`.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 7. What each outcome means for your numbers
|
|
335
|
+
|
|
336
|
+
Reported once per matched turn, to `POST /scenarios/:id/executions`, redeeming the match's
|
|
337
|
+
`executionTicket`.
|
|
338
|
+
|
|
339
|
+
| Outcome | When | Where it lands |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| `steered_full` | Every planned step ran under the scenario's control | savings ledger |
|
|
342
|
+
| `diverged` | Off-script; the composed bundle answered | savings ledger |
|
|
343
|
+
| `not_steered` | Matched, declined at a gate — the agent ran the task normally | **baseline sample** |
|
|
344
|
+
| `failed` | Armed, then gave up — the agent ran the task normally | **baseline sample** |
|
|
345
|
+
|
|
346
|
+
`not_steered` is not an error, and it is not wasted. A saving is `baseline − actual`, and the baseline
|
|
347
|
+
is *what this task costs when the agent does it*. Every declined match is a fresh measurement of
|
|
348
|
+
exactly that. A deployment that never declines has no baseline to measure against — which is a good
|
|
349
|
+
reason to leave `BIR_MIN_STEER_SIMILARITY` where it is rather than driving it to zero.
|
|
350
|
+
|
|
351
|
+
An **unredeemed** ticket books nothing at all. Silence loses a saving; it never invents one.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 8. Tuning
|
|
356
|
+
|
|
357
|
+
Only one knob matters in practice.
|
|
358
|
+
|
|
359
|
+
**`BIR_MIN_STEER_SIMILARITY` (default 0.92).** The server detects matches at 0.9 and uses that to
|
|
360
|
+
decide *"don't record this again"* — cheap: the worst case is a lost recording. Steering makes a much
|
|
361
|
+
stronger claim, *"don't think about this again"*, and a 0.90-similar prompt usually covers only part
|
|
362
|
+
of the live task. Steering it produces a confident, thin, wrong answer.
|
|
363
|
+
|
|
364
|
+
Tune it from evidence, not taste. Run for a week and read the declines:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
grep 'replay.decision' audit.log | grep 'why="similarity below threshold"'
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
- Prompts you *would* have wanted replayed, sitting at 0.90–0.92 → lower it, in steps of 0.01.
|
|
371
|
+
- Replays that answered the wrong question → raise it, and re-read the scenario's `intent`.
|
|
372
|
+
|
|
373
|
+
If a specific pair of prompts *should* match and does not, the fix is usually upstream of the
|
|
374
|
+
threshold: the two prompts genuinely mean different things to the embedding model. Re-record with a
|
|
375
|
+
first prompt that states the task plainly.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## 9. Troubleshooting
|
|
380
|
+
|
|
381
|
+
| Symptom | Cause | Fix |
|
|
382
|
+
|---|---|---|
|
|
383
|
+
| No `run.matched`, ever | Prompt below the server's `SIMILARITY_THRESHOLD` | Rephrase closer, or lower it server-side. Confirm the original run is in the list — a sub-`RECORDING_MIN_ACTIONS` run is never embedded |
|
|
384
|
+
| `run.matched` but no `plan.armed` | A gate declined | Read the `replay.decision` line's `why=`. Ranked by frequency: `BIR_REPLAY` unset · scenario not `ready` · similarity below threshold · no step is executable |
|
|
385
|
+
| `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 |
|
|
386
|
+
| `plan.armed mode=steer` where you expected `direct` | At least one step is a built-in, an unwrapped MCP server, or `claude-in-chrome` | `bir status` shows what is wrapped. `claude-in-chrome` is `scope: "dynamic"` and can never be wrapped |
|
|
387
|
+
| 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` |
|
|
388
|
+
| `replay.thread … emitted=` empty | `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 |
|
|
389
|
+
| `via=hook fallback=true` 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 |
|
|
390
|
+
| `run_scenario` returns "no scenario is armed" | The model called it on a turn with no plan | Harmless. It happens when the model remembers the tool from an earlier turn |
|
|
391
|
+
| `execution.reported … 409 invalid_ticket` | The ticket was already redeemed, or belongs to another scenario | Harmless if `duplicate: true`. Otherwise a rollover raced `SessionEnd`; the server books once |
|
|
392
|
+
| `savedUsd` looks impossible | Pricing drift between the runner's table and the server's | Compare `PRICING_VERSION` on both sides. Design §11.3 documents a known live drift between two existing copies |
|
|
393
|
+
| `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` |
|
|
394
|
+
| 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 |
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## 10. What is built
|
|
399
|
+
|
|
400
|
+
All of it. Phases R0–R7 of [calculatedReplay.md](calculatedReplay.md) §17 are implemented and
|
|
401
|
+
tested; everything this guide describes is code you can run.
|
|
402
|
+
|
|
403
|
+
| Phase | What landed | Where |
|
|
404
|
+
|---|---|---|
|
|
405
|
+
| **R0** | Plan, logic shims, coverage, bundle | [src/replay/](../src/replay/) — `plan.ts`, `logic.ts`, `coverage.ts`, `bundle.ts` |
|
|
406
|
+
| **R1** | Derivation over raw `fetch`; pricing | [derive.ts](../src/replay/derive.ts), [pricing.ts](../src/replay/pricing.ts) |
|
|
407
|
+
| **R2** | Gate ladder, mode choice, arming, directive | [controller.ts](../src/replay/controller.ts), [control/server.ts](../src/control/server.ts) |
|
|
408
|
+
| **R3** | Pin, thread, retire | `onToolPre` / `onToolPost` / `onProxyStep` in [control/server.ts](../src/control/server.ts) |
|
|
409
|
+
| **R4** | Long-poll work queue; direct execution | [executor.ts](../src/replay/executor.ts), [proxy/session.ts](../src/proxy/session.ts) |
|
|
410
|
+
| **R5** | The `bir` MCP server, `/scenario/run`, `--replay` | [bin/bir-scenario.ts](../src/bin/bir-scenario.ts), [bin/bir.ts](../src/bin/bir.ts) |
|
|
411
|
+
| **R6** | Divergence, source-run fallback, `bir replay` | `diverge()` in [controller.ts](../src/replay/controller.ts), [source-run.ts](../src/replay/source-run.ts) |
|
|
412
|
+
| **R7** | Usage watermark, reporting, ticket, doctor | [control/transcript.ts](../src/control/transcript.ts), [remote-recorder.ts](../src/record/remote-recorder.ts) |
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
npm test # 195 tests, incl. test/replay.test.ts and test/replay-server.test.ts
|
|
416
|
+
npm run build && node test/smoke-replay.mjs
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The smoke test is the one worth running after any change to the seams. It starts a stand-in BaseIn
|
|
420
|
+
service, a real `bir-hooks`, a real `bir-proxy` wrapping a real MCP server, and a real `bir-scenario`,
|
|
421
|
+
then drives a two-step scenario end to end through actual stdio — proving that a pinned argument
|
|
422
|
+
reaches the real tool, that step 1's input was computed from step 0's real output, and that the turn
|
|
423
|
+
books `steered_full` with `fallbackCostUsd: 0`.
|
|
424
|
+
|
|
425
|
+
Three things the implementation settled that the design left open:
|
|
426
|
+
|
|
427
|
+
- **`SerializedScenarioStep.recordedOutput` really is never sent.** Recorded outputs come from a lazy
|
|
428
|
+
`GET /recordings/runs/:runId` ([source-run.ts](../src/replay/source-run.ts)), as design §8.1 anticipated.
|
|
429
|
+
- **The work queue must hold work for a proxy that is mid-round-trip.** Failing fast when no poller is
|
|
430
|
+
*parked* lost every step after the first in a multi-step direct plan, because the proxy is between
|
|
431
|
+
polls exactly then. It now fails fast only for a server no proxy has served in the last minute.
|
|
432
|
+
- **The proxies need no replay configuration.** `/proxy/register` tells each one whether to open its
|
|
433
|
+
work loop, so `BIR_REPLAY` is a single switch on the control server.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## 11. Turning it off
|
|
438
|
+
|
|
439
|
+
Three levels, least to most.
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
unset BIR_REPLAY # stop arming. Matches still detected; recording still stops on a match
|
|
443
|
+
bir uninstall --replay # remove the `bir` MCP server, restore the prompt-hook timeout
|
|
444
|
+
bir uninstall # remove everything: proxies, hooks, files restored byte-for-byte
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
With `BIR_REPLAY` unset the system is exactly v1 again: it recognises a repeated prompt, declines to
|
|
448
|
+
record it a second time, and lets the model do the work.
|