@basein/runner 0.2.1 → 0.2.2
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 +2 -2
- package/dist/bin/bir-hooks.d.ts +5 -3
- package/dist/bin/bir-hooks.js +12 -4
- package/dist/bin/bir.js +14 -1
- package/dist/control/server.js +9 -0
- package/dist/replay/controller.d.ts +15 -1
- package/dist/replay/controller.js +31 -4
- package/dist/replay/derive.d.ts +34 -6
- package/dist/replay/derive.js +108 -8
- package/docs/calculatedReplay.md +16 -8
- package/docs/calculatedReplayGuide.md +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -201,8 +201,8 @@ what to do when they register, so there is one switch and no second copy of it:
|
|
|
201
201
|
| `BIR_REPLAY=1` | Enable replay. Nothing below matters until it is set. |
|
|
202
202
|
| `BIR_REPLAY_ALLOW_SERVERS` | Server keys eligible for **direct** execution. Unset means every wrapped server; setting it is the recommendation. |
|
|
203
203
|
| `BIR_MIN_STEER_SIMILARITY` | Minimum match similarity to replay (default `0.92`, above the service's own `0.9` detection threshold). |
|
|
204
|
-
| `ANTHROPIC_API_KEY` |
|
|
205
|
-
| `BIR_DERIVE_MODEL` | Derivation model (default `claude-haiku-4-5-20251001`). |
|
|
204
|
+
| `ANTHROPIC_API_KEY` | **Optional.** Working out what a new request is about — which fleet, which file, which date — is done for you by the service on its own key, as long as you are signed in. Set this only to keep that reading on your machine, on your key. Signed out *and* unset, a scenario whose values change between requests is declined rather than replayed on stale ones. |
|
|
205
|
+
| `BIR_DERIVE_MODEL` | Derivation model, when you set a key of your own (default `claude-haiku-4-5-20251001`). |
|
|
206
206
|
| `BIR_MATCH_BUDGET_MS` | How long the prompt hook waits for a match (default `2500`). |
|
|
207
207
|
| `BIR_DERIVE_BUDGET_MS` | How long the first `PreToolUse` waits for parameters (default `8000`). |
|
|
208
208
|
| `BIR_REPLAY_BUDGET_MS` / `BIR_STEP_TIMEOUT_MS` | Whole-plan and per-step ceilings for direct execution (default `120000` / `60000`). |
|
package/dist/bin/bir-hooks.d.ts
CHANGED
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
* BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
|
|
28
28
|
* execution. Unset means every wrapped server
|
|
29
29
|
* BIR_MIN_STEER_SIMILARITY minimum match similarity to replay (default 0.92)
|
|
30
|
-
* ANTHROPIC_API_KEY
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* ANTHROPIC_API_KEY OPTIONAL. Derivation — reading what this turn acts
|
|
31
|
+
* on — is done by the service for a signed-in
|
|
32
|
+
* runner. Set this only to keep that reading on this
|
|
33
|
+
* machine; signed out and unset, a scenario with a
|
|
34
|
+
* target does not run (segmented.md R-PARAM-5)
|
|
33
35
|
* BIR_DERIVE_MODEL derivation model (default claude-haiku-4-5-…)
|
|
34
36
|
* BIR_MATCH_BUDGET_MS prompt-hook match wait (default 2500)
|
|
35
37
|
* BIR_DERIVE_BUDGET_MS first PreToolUse derivation wait (default 8000)
|
package/dist/bin/bir-hooks.js
CHANGED
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
* BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
|
|
28
28
|
* execution. Unset means every wrapped server
|
|
29
29
|
* BIR_MIN_STEER_SIMILARITY minimum match similarity to replay (default 0.92)
|
|
30
|
-
* ANTHROPIC_API_KEY
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* ANTHROPIC_API_KEY OPTIONAL. Derivation — reading what this turn acts
|
|
31
|
+
* on — is done by the service for a signed-in
|
|
32
|
+
* runner. Set this only to keep that reading on this
|
|
33
|
+
* machine; signed out and unset, a scenario with a
|
|
34
|
+
* target does not run (segmented.md R-PARAM-5)
|
|
33
35
|
* BIR_DERIVE_MODEL derivation model (default claude-haiku-4-5-…)
|
|
34
36
|
* BIR_MATCH_BUDGET_MS prompt-hook match wait (default 2500)
|
|
35
37
|
* BIR_DERIVE_BUDGET_MS first PreToolUse derivation wait (default 8000)
|
|
@@ -182,7 +184,13 @@ function buildReplayOptions(auth) {
|
|
|
182
184
|
logLine("replay.enabled", {
|
|
183
185
|
minSimilarity: opts.minSimilarity,
|
|
184
186
|
allowServers: allowServers ? [...allowServers].join(",") : "(all wrapped)",
|
|
185
|
-
|
|
187
|
+
// Who reads what this turn acts on (segmented.md R-PARAM-5). Named at
|
|
188
|
+
// startup because the third state is a replay that silently never runs.
|
|
189
|
+
derive: process.env.ANTHROPIC_API_KEY
|
|
190
|
+
? "this machine (ANTHROPIC_API_KEY)"
|
|
191
|
+
: auth.session && auth.baseUrl
|
|
192
|
+
? "the service"
|
|
193
|
+
: "recorded sample values — sign in, or a scenario with a target will not run",
|
|
186
194
|
intentMatch: opts.intentMatch?.enabled ? "on" : "off",
|
|
187
195
|
why: "matched prompts will run their calculated scenario — steered steps are auto-approved",
|
|
188
196
|
});
|
package/dist/bin/bir.js
CHANGED
|
@@ -600,7 +600,20 @@ async function doctor(args) {
|
|
|
600
600
|
if (replay?.enabled) {
|
|
601
601
|
out(`Replay : ON servers=${replay.allowServers?.join(",") || "(all wrapped)"} ` +
|
|
602
602
|
`minSimilarity=${replay.minSimilarity ?? "?"}`);
|
|
603
|
-
|
|
603
|
+
// An older control server sends only `deriveKey`; read it as the two states
|
|
604
|
+
// it could describe then.
|
|
605
|
+
const via = replay.deriveVia ?? (replay.deriveKey ? "key" : "samples");
|
|
606
|
+
const derive = via === "key"
|
|
607
|
+
? "this machine (ANTHROPIC_API_KEY)"
|
|
608
|
+
: via === "service"
|
|
609
|
+
? "the service (no key needed here)"
|
|
610
|
+
: "recorded sample values — scenarios with a target will NOT run";
|
|
611
|
+
out(` derive=${derive}`);
|
|
612
|
+
if (via === "samples") {
|
|
613
|
+
notes.push("nothing can read what this turn acts on: sign in with `bir login` so the " +
|
|
614
|
+
"service derives, or set ANTHROPIC_API_KEY to derive here. Until then a " +
|
|
615
|
+
"matched scenario with a target is declined and the agent does the task");
|
|
616
|
+
}
|
|
604
617
|
const idle = wantWrapped.filter((n) => !(replay.pollingProxies ?? []).includes(n));
|
|
605
618
|
if (idle.length > 0) {
|
|
606
619
|
notes.push(`these proxies are not polling for replay work: ${idle.join(", ")} — ` +
|
package/dist/control/server.js
CHANGED
|
@@ -212,6 +212,15 @@ export class ControlServer {
|
|
|
212
212
|
minSimilarity: this.opts.replay?.minSimilarity ?? null,
|
|
213
213
|
allowServers: this.opts.replay?.allowServers ? [...this.opts.replay.allowServers] : null,
|
|
214
214
|
deriveKey: Boolean(this.opts.replay?.apiKey ?? process.env.ANTHROPIC_API_KEY),
|
|
215
|
+
// Who reads the turn for its parameters (segmented.md R-PARAM-5). The
|
|
216
|
+
// ordinary answer is `service`; a key here is an override; `samples`
|
|
217
|
+
// means no scenario with a target can run, which is the one state an
|
|
218
|
+
// operator must be able to see without reading a log.
|
|
219
|
+
deriveVia: (this.opts.replay?.apiKey ?? process.env.ANTHROPIC_API_KEY)
|
|
220
|
+
? "key"
|
|
221
|
+
: this.opts.replay?.authUrl && this.opts.replay?.authToken
|
|
222
|
+
? "service"
|
|
223
|
+
: "samples",
|
|
215
224
|
pollingProxies: this.replay.work.pollingServers(),
|
|
216
225
|
},
|
|
217
226
|
lossy: this.lossy,
|
|
@@ -54,7 +54,10 @@ export interface ReplayOptions {
|
|
|
54
54
|
/** `BIR_REPLAY_ALLOW_SERVERS`; undefined means every wrapped server. */
|
|
55
55
|
allowServers?: ReadonlySet<string>;
|
|
56
56
|
budgets?: Partial<ReplayBudgets>;
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The service: the lazy source-run fetch, and the parameter derivation this
|
|
59
|
+
* runner no longer needs a key of its own for (segmented.md R-PARAM-5).
|
|
60
|
+
*/
|
|
58
61
|
authUrl?: string;
|
|
59
62
|
authToken?: () => string;
|
|
60
63
|
/** Injected by tests. */
|
|
@@ -378,6 +381,17 @@ export declare class ReplayController {
|
|
|
378
381
|
* directive the moment the match lands; the first `PreToolUse` — or
|
|
379
382
|
* `/scenario/run`, which has no hook timeout at all — is where the wait lands.
|
|
380
383
|
*/
|
|
384
|
+
/**
|
|
385
|
+
* Where to ask the service to read this turn (segmented.md R-PARAM-5).
|
|
386
|
+
*
|
|
387
|
+
* Undefined when there is nobody to ask — an unauthenticated session, or one
|
|
388
|
+
* whose token has gone. `derive` then falls back to the recorded samples, and
|
|
389
|
+
* a scenario with a target declines, exactly as a keyless runner always did.
|
|
390
|
+
*
|
|
391
|
+
* The token is read here rather than captured, because the recorder refreshes
|
|
392
|
+
* it as a session outlives it.
|
|
393
|
+
*/
|
|
394
|
+
private deriveService;
|
|
381
395
|
private startDerivation;
|
|
382
396
|
/**
|
|
383
397
|
* Divergence (§8). Execute the remaining steps for real, then deliver.
|
|
@@ -246,10 +246,13 @@ export class ReplayController {
|
|
|
246
246
|
return decline(`target ${first ?? "(unknown)"} not found — ${errText(err)}`, "missing_target");
|
|
247
247
|
}
|
|
248
248
|
if (!result.derived) {
|
|
249
|
-
//
|
|
250
|
-
//
|
|
249
|
+
// Nobody read the turn — no key here and no service to ask, or the
|
|
250
|
+
// service declined. Settings can still take their recorded values, but a
|
|
251
|
+
// target is a guess nobody is allowed to make (R-PARAM-5). The reason
|
|
252
|
+
// travels into the line, because "it did not run" without one is how
|
|
253
|
+
// this stayed invisible before.
|
|
251
254
|
const first = firstTargetKey(scenario.paramsObject);
|
|
252
|
-
return decline(
|
|
255
|
+
return decline(`${result.reason ?? "no_derive_key"}: target ${first ?? "(unknown)"}`, "no_derive_key");
|
|
253
256
|
}
|
|
254
257
|
if (result.missing.length > 0) {
|
|
255
258
|
return decline(`target ${result.missing[0]} not found`, "missing_target");
|
|
@@ -886,6 +889,22 @@ export class ReplayController {
|
|
|
886
889
|
* directive the moment the match lands; the first `PreToolUse` — or
|
|
887
890
|
* `/scenario/run`, which has no hook timeout at all — is where the wait lands.
|
|
888
891
|
*/
|
|
892
|
+
/**
|
|
893
|
+
* Where to ask the service to read this turn (segmented.md R-PARAM-5).
|
|
894
|
+
*
|
|
895
|
+
* Undefined when there is nobody to ask — an unauthenticated session, or one
|
|
896
|
+
* whose token has gone. `derive` then falls back to the recorded samples, and
|
|
897
|
+
* a scenario with a target declines, exactly as a keyless runner always did.
|
|
898
|
+
*
|
|
899
|
+
* The token is read here rather than captured, because the recorder refreshes
|
|
900
|
+
* it as a session outlives it.
|
|
901
|
+
*/
|
|
902
|
+
deriveService(scenarioId) {
|
|
903
|
+
const token = this.opts.authToken?.();
|
|
904
|
+
if (!this.opts.authUrl || !token)
|
|
905
|
+
return undefined;
|
|
906
|
+
return { baseUrl: this.opts.authUrl, token, scenarioId };
|
|
907
|
+
}
|
|
889
908
|
startDerivation(scenario, prompt, state, ctx = {}) {
|
|
890
909
|
const derive = this.opts.deriveImpl ?? deriveParameters;
|
|
891
910
|
return derive({
|
|
@@ -893,6 +912,9 @@ export class ReplayController {
|
|
|
893
912
|
intent: scenario.intent ?? "",
|
|
894
913
|
paramsObject: scenario.paramsObject,
|
|
895
914
|
apiKey: this.opts.apiKey ?? process.env.ANTHROPIC_API_KEY,
|
|
915
|
+
// Where to ask when there is no key here, which is the ordinary case
|
|
916
|
+
// (segmented.md R-PARAM-5).
|
|
917
|
+
service: this.deriveService(scenario.id),
|
|
896
918
|
fetchImpl: this.opts.fetchImpl,
|
|
897
919
|
liveCall: ctx.liveCall,
|
|
898
920
|
recentResults: ctx.recentResults,
|
|
@@ -903,7 +925,12 @@ export class ReplayController {
|
|
|
903
925
|
scenario: scenario.id,
|
|
904
926
|
params: Object.keys(r.params).length,
|
|
905
927
|
costUsd: r.costUsd.toFixed(4),
|
|
906
|
-
source: r.derived
|
|
928
|
+
source: r.derived
|
|
929
|
+
? r.via === "service"
|
|
930
|
+
? "the service"
|
|
931
|
+
: "prompt"
|
|
932
|
+
: "recorded samples",
|
|
933
|
+
why: r.derived ? undefined : r.reason,
|
|
907
934
|
missing: r.missing.length > 0 ? r.missing.join(",") : undefined,
|
|
908
935
|
sampled: r.sampled.length > 0 ? r.sampled.join(",") : undefined,
|
|
909
936
|
});
|
package/dist/replay/derive.d.ts
CHANGED
|
@@ -10,11 +10,20 @@
|
|
|
10
10
|
* most sessions never arm. This is ~40 lines of `fetch` and it keeps the package
|
|
11
11
|
* at zero runtime dependencies.
|
|
12
12
|
*
|
|
13
|
-
* NO KEY IS
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* NO KEY IS THE ORDINARY CONFIGURATION (segmented.md R-PARAM-5). Without
|
|
14
|
+
* `ANTHROPIC_API_KEY` the reading is done by the **service**, over one authorized
|
|
15
|
+
* POST to `/scenarios/:id/derive`, under the key it already uses to calculate
|
|
16
|
+
* scenarios and to ask its live question. That is the path nearly every session
|
|
17
|
+
* takes: the people this runs for are inside Claude Code on a subscription and
|
|
18
|
+
* have no API key to give it.
|
|
19
|
+
*
|
|
20
|
+
* A key here is an override, not a requirement. It keeps the reading on this
|
|
21
|
+
* machine — the prompt never reaches the service — which is what an in-house
|
|
22
|
+
* deployment wants, and it is one round trip faster.
|
|
23
|
+
*
|
|
24
|
+
* NEITHER is still supported and still means the same thing it always did:
|
|
25
|
+
* settings take their recorded samples and a scenario with a target declines,
|
|
26
|
+
* because a target is never guessed.
|
|
18
27
|
*
|
|
19
28
|
* The prompt and the parsing tolerances mirror RRepeat's
|
|
20
29
|
* `deriveParametersFromScenarioPayload` exactly, so the two produce the same
|
|
@@ -34,12 +43,27 @@ export interface DeriveContext {
|
|
|
34
43
|
liveCall?: LiveCall;
|
|
35
44
|
recentResults?: string[];
|
|
36
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The service, which derives when this runner has no key of its own
|
|
48
|
+
* (R-PARAM-5). All three fields or none: a session that is not signed in has
|
|
49
|
+
* nowhere to ask.
|
|
50
|
+
*/
|
|
51
|
+
export interface DeriveService {
|
|
52
|
+
/** `BIR_AUTH_URL`, the same base the recorder reports to. */
|
|
53
|
+
baseUrl: string;
|
|
54
|
+
/** A live access token. Read late — the recorder refreshes it mid-session. */
|
|
55
|
+
token: string;
|
|
56
|
+
/** Whose parameters to read. The service takes the schema from its own row. */
|
|
57
|
+
scenarioId: string;
|
|
58
|
+
}
|
|
37
59
|
export interface DeriveOptions extends DeriveContext {
|
|
38
60
|
prompt: string;
|
|
39
61
|
intent: string;
|
|
40
62
|
paramsObject: ParamsSchema | null;
|
|
41
|
-
/**
|
|
63
|
+
/** An override that keeps the reading on this machine. Absent → the service. */
|
|
42
64
|
apiKey?: string;
|
|
65
|
+
/** Used when there is no `apiKey`. Absent too → recorded samples. */
|
|
66
|
+
service?: DeriveService;
|
|
43
67
|
model?: string;
|
|
44
68
|
signal?: AbortSignal;
|
|
45
69
|
/** Injected by tests. Defaults to global `fetch`. */
|
|
@@ -50,6 +74,10 @@ export interface DeriveResult {
|
|
|
50
74
|
costUsd: number;
|
|
51
75
|
/** False when the values came from the recorded samples rather than the model. */
|
|
52
76
|
derived: boolean;
|
|
77
|
+
/** Who read the turn. For the audit line, and for nothing else. */
|
|
78
|
+
via?: "key" | "service" | "samples";
|
|
79
|
+
/** Why nothing was derived, when the service said. */
|
|
80
|
+
reason?: string;
|
|
53
81
|
model?: string;
|
|
54
82
|
inputTokens?: number;
|
|
55
83
|
outputTokens?: number;
|
package/dist/replay/derive.js
CHANGED
|
@@ -10,11 +10,20 @@
|
|
|
10
10
|
* most sessions never arm. This is ~40 lines of `fetch` and it keeps the package
|
|
11
11
|
* at zero runtime dependencies.
|
|
12
12
|
*
|
|
13
|
-
* NO KEY IS
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* NO KEY IS THE ORDINARY CONFIGURATION (segmented.md R-PARAM-5). Without
|
|
14
|
+
* `ANTHROPIC_API_KEY` the reading is done by the **service**, over one authorized
|
|
15
|
+
* POST to `/scenarios/:id/derive`, under the key it already uses to calculate
|
|
16
|
+
* scenarios and to ask its live question. That is the path nearly every session
|
|
17
|
+
* takes: the people this runs for are inside Claude Code on a subscription and
|
|
18
|
+
* have no API key to give it.
|
|
19
|
+
*
|
|
20
|
+
* A key here is an override, not a requirement. It keeps the reading on this
|
|
21
|
+
* machine — the prompt never reaches the service — which is what an in-house
|
|
22
|
+
* deployment wants, and it is one round trip faster.
|
|
23
|
+
*
|
|
24
|
+
* NEITHER is still supported and still means the same thing it always did:
|
|
25
|
+
* settings take their recorded samples and a scenario with a target declines,
|
|
26
|
+
* because a target is never guessed.
|
|
18
27
|
*
|
|
19
28
|
* The prompt and the parsing tolerances mirror RRepeat's
|
|
20
29
|
* `deriveParametersFromScenarioPayload` exactly, so the two produce the same
|
|
@@ -120,14 +129,20 @@ export async function deriveParameters(opts) {
|
|
|
120
129
|
if (keys.length === 0)
|
|
121
130
|
return { params: {}, costUsd: 0, derived: false, missing: [], sampled: [] };
|
|
122
131
|
const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
123
|
-
// No key
|
|
124
|
-
//
|
|
125
|
-
|
|
132
|
+
// No key of our own is the ordinary case: the service reads the turn instead,
|
|
133
|
+
// under the key it already spends on this account (R-PARAM-5).
|
|
134
|
+
if (!apiKey && opts.service)
|
|
135
|
+
return deriveViaService(opts, opts.service, schema, keys);
|
|
136
|
+
// No key and nowhere to ask — an unauthenticated session, or a deployment that
|
|
137
|
+
// turned service derivation off. `derived: false` is how the caller tells this
|
|
138
|
+
// apart from a real extraction: it declines any scenario that has a target and
|
|
139
|
+
// arms an all-settings one on its recorded values.
|
|
126
140
|
if (!apiKey)
|
|
127
141
|
return {
|
|
128
142
|
params: sampleValues(schema),
|
|
129
143
|
costUsd: 0,
|
|
130
144
|
derived: false,
|
|
145
|
+
via: "samples",
|
|
131
146
|
missing: [],
|
|
132
147
|
sampled: keys,
|
|
133
148
|
};
|
|
@@ -213,6 +228,7 @@ export async function deriveParameters(opts) {
|
|
|
213
228
|
params: parsed,
|
|
214
229
|
costUsd,
|
|
215
230
|
derived: true,
|
|
231
|
+
via: "key",
|
|
216
232
|
model,
|
|
217
233
|
inputTokens,
|
|
218
234
|
outputTokens,
|
|
@@ -220,4 +236,88 @@ export async function deriveParameters(opts) {
|
|
|
220
236
|
sampled,
|
|
221
237
|
};
|
|
222
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Ask the service to read the turn (segmented.md R-PARAM-5).
|
|
241
|
+
*
|
|
242
|
+
* One authorized POST. The body carries the turn — the prompt, and whatever else
|
|
243
|
+
* this turn has said about its target — and never the schema: the service reads
|
|
244
|
+
* that from the scenario row it owns, so a stale copy here cannot change what is
|
|
245
|
+
* asked for.
|
|
246
|
+
*
|
|
247
|
+
* Never throws, and never worse than no key. A service that is off, out of
|
|
248
|
+
* budget, too slow or simply down answers `derived: false`, and so does every
|
|
249
|
+
* failure on this side, which is precisely the keyless behaviour this replaced:
|
|
250
|
+
* settings stand on their samples, and a target does not run.
|
|
251
|
+
*/
|
|
252
|
+
async function deriveViaService(opts, service, schema, keys) {
|
|
253
|
+
const samples = (reason) => ({
|
|
254
|
+
params: sampleValues(schema),
|
|
255
|
+
costUsd: 0,
|
|
256
|
+
derived: false,
|
|
257
|
+
via: "service",
|
|
258
|
+
reason,
|
|
259
|
+
missing: [],
|
|
260
|
+
sampled: keys,
|
|
261
|
+
});
|
|
262
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
263
|
+
const url = `${service.baseUrl.replace(/\/+$/, "")}/scenarios/${service.scenarioId}/derive`;
|
|
264
|
+
let body;
|
|
265
|
+
try {
|
|
266
|
+
const res = await doFetch(url, {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: {
|
|
269
|
+
"content-type": "application/json",
|
|
270
|
+
authorization: `Bearer ${service.token}`,
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
prompt: opts.prompt,
|
|
274
|
+
liveCall: opts.liveCall,
|
|
275
|
+
recentResults: opts.recentResults,
|
|
276
|
+
}),
|
|
277
|
+
signal: opts.signal,
|
|
278
|
+
});
|
|
279
|
+
if (!res.ok)
|
|
280
|
+
return samples(`service HTTP ${res.status}`);
|
|
281
|
+
body = (await res.json());
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
return samples(err instanceof Error ? err.message : String(err));
|
|
285
|
+
}
|
|
286
|
+
if (!body || typeof body !== "object")
|
|
287
|
+
return samples("service sent no answer");
|
|
288
|
+
if (body.derived !== true)
|
|
289
|
+
return samples(`service: ${body.reason ?? "declined"}`);
|
|
290
|
+
// Normalised again on this side, over *our* copy of the schema. The service
|
|
291
|
+
// answers in the same shape, but the plan runs on these values and the gate
|
|
292
|
+
// reads this `missing`: an older or newer service must not be able to leave a
|
|
293
|
+
// target unaccounted for.
|
|
294
|
+
const params = { ...(body.params ?? {}) };
|
|
295
|
+
const missing = [];
|
|
296
|
+
const sampled = [];
|
|
297
|
+
for (const key of keys) {
|
|
298
|
+
const value = params[key];
|
|
299
|
+
if (value !== null && value !== undefined)
|
|
300
|
+
continue;
|
|
301
|
+
if (isTarget(schema, key)) {
|
|
302
|
+
params[key] = null;
|
|
303
|
+
missing.push(key);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
params[key] = schema[key].sampleValue;
|
|
307
|
+
sampled.push(key);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
params,
|
|
312
|
+
// What the service spent on this turn, reported back on the execution so a
|
|
313
|
+
// replay's cost stays the whole truth about it (R-MONEY-2). The account is
|
|
314
|
+
// not billed for it; the service books it on its own ledger as well.
|
|
315
|
+
costUsd: typeof body.costUsd === "number" ? body.costUsd : 0,
|
|
316
|
+
derived: true,
|
|
317
|
+
via: "service",
|
|
318
|
+
model: body.model,
|
|
319
|
+
missing,
|
|
320
|
+
sampled,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
223
323
|
//# sourceMappingURL=derive.js.map
|
package/docs/calculatedReplay.md
CHANGED
|
@@ -853,7 +853,8 @@ fail because of BaseInstRunner.**
|
|
|
853
853
|
| Failure | Behaviour |
|
|
854
854
|
|---|---|
|
|
855
855
|
| `getMatch()` slower than the match budget | No plan. Ordinary turn. One `replay.decision` line |
|
|
856
|
-
| No `ANTHROPIC_API_KEY` |
|
|
856
|
+
| No `ANTHROPIC_API_KEY` | The service derives instead (segmented.md R-PARAM-5). Signed out as well: settings take their recorded `sampleValue`s, a scenario with a target declines `no_derive_key`, `deriveCostUsd: 0` |
|
|
857
|
+
| The service declines, is slow, or cannot be reached | The same, with its reason carried into the `replay.derived` and `replay.decision` lines. Never a throw |
|
|
857
858
|
| Derivation call fails or times out | Same as above, plus a `replay.derive_failed` line |
|
|
858
859
|
| `toolInputLogic` throws | Divergence (§8) — never a crash |
|
|
859
860
|
| `toolOutputLogic` throws | Retire the plan, abort to an ordinary turn, outcome `failed` |
|
|
@@ -982,7 +983,8 @@ export async function deriveParameters(opts: {
|
|
|
982
983
|
prompt: string;
|
|
983
984
|
intent: string;
|
|
984
985
|
paramsObject: Record<string, { sampleValue: unknown; description: string }> | null;
|
|
985
|
-
apiKey?: string; // ANTHROPIC_API_KEY
|
|
986
|
+
apiKey?: string; // ANTHROPIC_API_KEY — an override
|
|
987
|
+
service?: { baseUrl: string; token: string; scenarioId: string }; // the default path
|
|
986
988
|
model?: string; // default claude-haiku-4-5-20251001
|
|
987
989
|
signal?: AbortSignal;
|
|
988
990
|
}): Promise<{ params: Record<string, unknown>; costUsd: number; derived: boolean }>;
|
|
@@ -992,11 +994,16 @@ Behaviour, matching RRepeat's `deriveParametersFromScenarioPayload` exactly so t
|
|
|
992
994
|
same parameters from the same prompt:
|
|
993
995
|
|
|
994
996
|
- No `paramsObject` keys → `{}`, `costUsd: 0`.
|
|
995
|
-
- **No `apiKey`
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
from
|
|
997
|
+
- **No `apiKey` but a `service` → `POST {baseUrl}/scenarios/{id}/derive`** with the turn (prompt,
|
|
998
|
+
live call, recent results) and a bearer token, `derived` as the service answered. This is the
|
|
999
|
+
ordinary path: nearly every session runs inside Claude Code on a subscription and has no key of
|
|
1000
|
+
its own (segmented.md R-PARAM-5). The answer is re-normalised here against this runner's own copy
|
|
1001
|
+
of the schema, so the gate reads a `missing` computed from what will actually run.
|
|
1002
|
+
- **Neither → return every key's recorded `sampleValue`**, `costUsd: 0`, `derived: false`. Settings
|
|
1003
|
+
stand on their samples; a scenario with a target declines rather than run on a stale one.
|
|
1004
|
+
- With an `apiKey`, `POST https://api.anthropic.com/v1/messages` with the same extraction prompt,
|
|
1005
|
+
tolerant JSON extraction (first balanced `{…}`, `undefined` → `null`), and any `null`/missing
|
|
1006
|
+
**setting** filled from its `sampleValue` — a `null` target is named in `missing` instead.
|
|
1000
1007
|
|
|
1001
1008
|
Keeping this dependency-free is not purity. `bir-proxy` is spawned inside the host's process tree for
|
|
1002
1009
|
*every* wrapped server; an npm install that pulls a model SDK into that path is a startup-latency and
|
|
@@ -1041,7 +1048,8 @@ bir replay --scenario <scnId> --prompt "…" [--dry] alias; the Tier 2 / deb
|
|
|
1041
1048
|
source run's *recorded* outputs — no real tools, no side effects, one Haiku call. Without `--dry`,
|
|
1042
1049
|
`bir replay` runs the same plan through the same executor against the live proxies. `bir doctor`
|
|
1043
1050
|
gains a `replay` block: on/off, the effective `BIR_REPLAY_ALLOW_SERVERS` allowlist,
|
|
1044
|
-
`BIR_MIN_STEER_SIMILARITY`, and
|
|
1051
|
+
`BIR_MIN_STEER_SIMILARITY`, and who derives — this machine, the service, or nobody, which is the
|
|
1052
|
+
state in which a scenario with a target never runs (§13.2, mitigation 3).
|
|
1045
1053
|
|
|
1046
1054
|
---
|
|
1047
1055
|
|
|
@@ -34,7 +34,7 @@ end-to-end smoke test.
|
|
|
34
34
|
| 3 | `SIMILARITY_DETECTION_ENABLED=true` on the server (default) | otherwise no prompt ever matches |
|
|
35
35
|
| 4 | BaseInstRunnerMCP built and installed in your project | `bir status` |
|
|
36
36
|
| 5 | Tier 1 — the hooks wired and `bir-hooks` running | `bir doctor` |
|
|
37
|
-
| 6 | *(replay only)*
|
|
37
|
+
| 6 | *(replay only)* signed in, so the service can read what each request acts on | `bir doctor` — `derive=the service`; an `ANTHROPIC_API_KEY` here replaces it, see §5.3 |
|
|
38
38
|
|
|
39
39
|
Tier 1 is not optional for replay. A match is a match on **the prompt**, and a standalone proxy never
|
|
40
40
|
sees one (design §1.1). If `bir doctor` says `tier: standalone`, replay cannot arm, and that is the
|
|
@@ -229,7 +229,7 @@ bir doctor
|
|
|
229
229
|
|
|
230
230
|
```
|
|
231
231
|
replay ON servers=chrome-devtools,fleet-api minSimilarity=0.92
|
|
232
|
-
derive=
|
|
232
|
+
derive=the service (no key needed here)
|
|
233
233
|
control http://127.0.0.1:53411 sess=birsess_…
|
|
234
234
|
wrapped chrome-devtools ✓ proxy pid 41822 fleet-api ✓ proxy pid 41823
|
|
235
235
|
recording yes (https://your-basein-service)
|
|
@@ -246,8 +246,8 @@ there is exactly one switch and it cannot get out of step with itself.
|
|
|
246
246
|
| `BIR_REPLAY` | *(unset)* | `1` enables replay. Nothing below matters until it is set |
|
|
247
247
|
| `BIR_REPLAY_ALLOW_SERVERS` | *(all wrapped)* | Comma-separated server keys eligible for **direct** execution. **Set this** |
|
|
248
248
|
| `BIR_MIN_STEER_SIMILARITY` | `0.92` | Below this a match is detected but not replayed (§8) |
|
|
249
|
-
| `ANTHROPIC_API_KEY` | *(unset)* |
|
|
250
|
-
| `BIR_DERIVE_MODEL` | `claude-haiku-4-5-20251001` | The derivation model |
|
|
249
|
+
| `ANTHROPIC_API_KEY` | *(unset)* | **Not required.** Derivation — reading what this request acts on — is done by the service on its key for a signed-in runner. Set this to keep the reading on this machine instead: the prompt then never leaves it, and it is one round trip faster. Signed out *and* unset, only a scenario with nothing to work out replays |
|
|
250
|
+
| `BIR_DERIVE_MODEL` | `claude-haiku-4-5-20251001` | The derivation model, when this machine does the reading |
|
|
251
251
|
| `BIR_MATCH_BUDGET_MS` | `2500` | How long the prompt hook waits for a match before giving up |
|
|
252
252
|
| `BIR_DERIVE_BUDGET_MS` | `8000` | How long the first `PreToolUse` waits for parameters |
|
|
253
253
|
| `BIR_REPLAY_BUDGET_MS` | `120000` | Whole-plan ceiling for direct execution |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basein/runner",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|