@basein/runner 0.3.23 → 0.3.24
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/dist/control/pin-guard.d.ts +0 -16
- package/dist/control/pin-guard.js +0 -36
- package/dist/control/server.d.ts +29 -12
- package/dist/control/server.js +201 -82
- package/dist/record/recorder.d.ts +14 -4
- package/dist/replay/controller.d.ts +22 -9
- package/dist/replay/controller.js +9 -9
- package/docs/calculatedReplayGuide.md +1 -1
- package/package.json +1 -1
|
@@ -85,20 +85,4 @@ export declare function commandShape(command: string, depth?: number, escape?: s
|
|
|
85
85
|
* only the general rule is applied there.
|
|
86
86
|
*/
|
|
87
87
|
export declare function pinCompatible(toolName: string, modelArgs: unknown, pinnedArgs: unknown, otherHost?: boolean): PinVerdict;
|
|
88
|
-
/**
|
|
89
|
-
* Whether the model's own call is a continuation's first step: the same call,
|
|
90
|
-
* not only the same kind of call (the service's fallbk.md Part 3).
|
|
91
|
-
*
|
|
92
|
-
* Stricter than {@link pinCompatible}, on purpose. A pin takes a call the
|
|
93
|
-
* model was told to make — the directive named the step — and one argument in
|
|
94
|
-
* common is enough to know it is that call. A continuation starts on a call
|
|
95
|
-
* nobody told the model to make: after a hand-over, the call is the model's
|
|
96
|
-
* own choice of what to do next, and the plan may run only when that choice
|
|
97
|
-
* is the one the recording made. Looking up another person, restarting
|
|
98
|
-
* another worker, reading another file is another choice. So the call must
|
|
99
|
-
* pass the pin guard, share at least one argument with the step, and hold
|
|
100
|
-
* the same value in every argument they share — text compared without
|
|
101
|
-
* surrounding space or letter case, anything else exactly.
|
|
102
|
-
*/
|
|
103
|
-
export declare function sameFirstCall(toolName: string, modelArgs: unknown, stepArgs: unknown): PinVerdict;
|
|
104
88
|
//# sourceMappingURL=pin-guard.d.ts.map
|
|
@@ -545,40 +545,4 @@ export function pinCompatible(toolName, modelArgs, pinnedArgs, otherHost = false
|
|
|
545
545
|
? { ok: true }
|
|
546
546
|
: { ok: false, why: "it shares no argument with the step" };
|
|
547
547
|
}
|
|
548
|
-
/**
|
|
549
|
-
* Whether the model's own call is a continuation's first step: the same call,
|
|
550
|
-
* not only the same kind of call (the service's fallbk.md Part 3).
|
|
551
|
-
*
|
|
552
|
-
* Stricter than {@link pinCompatible}, on purpose. A pin takes a call the
|
|
553
|
-
* model was told to make — the directive named the step — and one argument in
|
|
554
|
-
* common is enough to know it is that call. A continuation starts on a call
|
|
555
|
-
* nobody told the model to make: after a hand-over, the call is the model's
|
|
556
|
-
* own choice of what to do next, and the plan may run only when that choice
|
|
557
|
-
* is the one the recording made. Looking up another person, restarting
|
|
558
|
-
* another worker, reading another file is another choice. So the call must
|
|
559
|
-
* pass the pin guard, share at least one argument with the step, and hold
|
|
560
|
-
* the same value in every argument they share — text compared without
|
|
561
|
-
* surrounding space or letter case, anything else exactly.
|
|
562
|
-
*/
|
|
563
|
-
export function sameFirstCall(toolName, modelArgs, stepArgs) {
|
|
564
|
-
const verdict = pinCompatible(toolName, modelArgs, stepArgs);
|
|
565
|
-
if (!verdict.ok)
|
|
566
|
-
return verdict;
|
|
567
|
-
const model = record(modelArgs);
|
|
568
|
-
const step = record(stepArgs);
|
|
569
|
-
const shared = Object.keys(model).filter((key) => Object.prototype.hasOwnProperty.call(step, key));
|
|
570
|
-
if (shared.length === 0) {
|
|
571
|
-
return stableStringify(model) === stableStringify(step)
|
|
572
|
-
? { ok: true }
|
|
573
|
-
: { ok: false, why: "it shares no argument with the step" };
|
|
574
|
-
}
|
|
575
|
-
const differs = shared.find((key) => !sameValue(model[key], step[key]));
|
|
576
|
-
return differs === undefined ? { ok: true } : { ok: false, why: `its ${differs} is not the step's` };
|
|
577
|
-
}
|
|
578
|
-
function sameValue(a, b) {
|
|
579
|
-
if (typeof a === "string" && typeof b === "string") {
|
|
580
|
-
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
581
|
-
}
|
|
582
|
-
return stableStringify(a) === stableStringify(b);
|
|
583
|
-
}
|
|
584
548
|
//# sourceMappingURL=pin-guard.js.map
|
package/dist/control/server.d.ts
CHANGED
|
@@ -1178,12 +1178,17 @@ export declare class ControlServer {
|
|
|
1178
1178
|
* hand-over itself is the question — the plan and the step — so the service
|
|
1179
1179
|
* names one recording and matches no words.
|
|
1180
1180
|
*
|
|
1181
|
-
* It arms
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
* the
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1181
|
+
* It arms on the model's first call after the hand-over, whatever that call
|
|
1182
|
+
* is (Eran, 2026-10-26). Runner 0.3.23 armed it only when the call was the
|
|
1183
|
+
* plan's first step, and lost it whenever the model first re-checked what
|
|
1184
|
+
* the plan before had already done. How the model is told depends on the
|
|
1185
|
+
* call. A call to one of the plan's own tools is answered in its place: the
|
|
1186
|
+
* plan makes that call itself, and letting it run would make it twice. Any
|
|
1187
|
+
* other call runs as the model wrote it, and the plan is told on its
|
|
1188
|
+
* PostToolUse, as an intent plan armed beside the model's own work is. The
|
|
1189
|
+
* plan before it stopped because the next step needed a judgement; the
|
|
1190
|
+
* continuation runs the half that was recorded, whatever the model would
|
|
1191
|
+
* have judged.
|
|
1187
1192
|
*
|
|
1188
1193
|
* Its values are read from the turn's own request — the rest of the task is
|
|
1189
1194
|
* the one the user asked for — with the model's words, the call it was about
|
|
@@ -1197,6 +1202,24 @@ export declare class ControlServer {
|
|
|
1197
1202
|
* not armed is reported for its ticket, with the reason.
|
|
1198
1203
|
*/
|
|
1199
1204
|
private tryContinuation;
|
|
1205
|
+
/** The body of {@link tryContinuation}, once the continuation is asked for. */
|
|
1206
|
+
private continuationAttempt;
|
|
1207
|
+
/**
|
|
1208
|
+
* A call meeting a continuation that armed beside another call of the same
|
|
1209
|
+
* message and still waits to be told (`pendingDirective`). A call to one of
|
|
1210
|
+
* the plan's own tools gets the plan's directive in its place: letting it
|
|
1211
|
+
* run would have the plan make the same call again — a duplicated write.
|
|
1212
|
+
* Any other call runs, and the plan is told on the first PostToolUse.
|
|
1213
|
+
*/
|
|
1214
|
+
private announceContinuationInstead;
|
|
1215
|
+
/**
|
|
1216
|
+
* Put back the fragment a continuation took the place of, when that
|
|
1217
|
+
* continuation retired having run no step and handed nothing over —
|
|
1218
|
+
* `run_scenario` refused, the plan vetoed, or a steer pin left by the
|
|
1219
|
+
* model. The model is doing the rest of the task itself, and that work is
|
|
1220
|
+
* recorded from its next call, as it would have been with no continuation.
|
|
1221
|
+
*/
|
|
1222
|
+
private restoreParkedFragment;
|
|
1200
1223
|
/**
|
|
1201
1224
|
* A continuation handed out and not armed: kept, retired, for its ticket's
|
|
1202
1225
|
* report at the seal (`not_steered`, with the reason, unmeasured). `code`
|
|
@@ -1211,12 +1234,6 @@ export declare class ControlServer {
|
|
|
1211
1234
|
* continuation request can never return it.
|
|
1212
1235
|
*/
|
|
1213
1236
|
private armedScenarioIds;
|
|
1214
|
-
/**
|
|
1215
|
-
* Whether the model's call is a continuation's first step: its tool, and
|
|
1216
|
-
* the step's own input in every argument the two share (pin-guard.ts
|
|
1217
|
-
* `sameFirstCall`). Anything that cannot be computed is "no".
|
|
1218
|
-
*/
|
|
1219
|
-
private isContinuationFirstCall;
|
|
1220
1237
|
/** Whether continuations are asked for: on unless `BIR_CONTINUATIONS=0`. */
|
|
1221
1238
|
private continuationsOn;
|
|
1222
1239
|
/**
|
package/dist/control/server.js
CHANGED
|
@@ -30,7 +30,7 @@ import { join, resolve as resolvePath } from "node:path";
|
|
|
30
30
|
import { BIR_CALL_ID, fingerprint, newCallId, parseQualifiedName, qualifyToolName, } from "./correlation.js";
|
|
31
31
|
import { StepIndexAllocator } from "./ordering.js";
|
|
32
32
|
import { pidAlive } from "./discovery.js";
|
|
33
|
-
import { pinCompatible
|
|
33
|
+
import { pinCompatible } from "./pin-guard.js";
|
|
34
34
|
import { agentDayOf, contextForToolUse, intentForToolUse, markTranscriptUsage, recentToolResults, settledLastAssistantText, settledTextAfterToolUse, thinkingForToolUse, usageSince, } from "./transcript.js";
|
|
35
35
|
import { isHousekeepingCall } from "../record/housekeeping.js";
|
|
36
36
|
import { asSent, userText } from "../record/prompt-text.js";
|
|
@@ -492,17 +492,6 @@ function reportedArgs(args) {
|
|
|
492
492
|
function score(value) {
|
|
493
493
|
return typeof value === "number" && Number.isFinite(value) ? value.toFixed(3) : undefined;
|
|
494
494
|
}
|
|
495
|
-
/**
|
|
496
|
-
* The tool a served scenario starts with, or undefined when the payload does
|
|
497
|
-
* not say. Untyped JSON from the service, read without trusting its shape.
|
|
498
|
-
*/
|
|
499
|
-
function firstStepTool(scenario) {
|
|
500
|
-
const steps = scenario?.steps;
|
|
501
|
-
if (!Array.isArray(steps) || steps.length === 0)
|
|
502
|
-
return undefined;
|
|
503
|
-
const name = steps[0]?.toolName;
|
|
504
|
-
return typeof name === "string" ? name : undefined;
|
|
505
|
-
}
|
|
506
495
|
function positiveEnv(name, fallback) {
|
|
507
496
|
const n = Number(process.env[name]);
|
|
508
497
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
@@ -3043,6 +3032,10 @@ export class ControlServer {
|
|
|
3043
3032
|
// Billed apart, the part after the hand-over would be saved twice.
|
|
3044
3033
|
const continuedBy = (state, from) => {
|
|
3045
3034
|
const chain = [];
|
|
3035
|
+
// A plan that ran no step reports `failed`, which books no saving: its
|
|
3036
|
+
// continuations then book their own, and it bills only its own window.
|
|
3037
|
+
if (state.outcome === "failed")
|
|
3038
|
+
return { chain, deriveUsd: 0 };
|
|
3046
3039
|
const ids = new Set(state.scenarioId ? [state.scenarioId] : []);
|
|
3047
3040
|
for (const s of states.slice(from + 1)) {
|
|
3048
3041
|
if (s.continuation && ids.has(s.continuation.ofScenarioId)) {
|
|
@@ -3057,12 +3050,15 @@ export class ControlServer {
|
|
|
3057
3050
|
const deriveUsd = [...chain, ...declined].reduce((usd, s) => usd + s.deriveCostUsd, 0);
|
|
3058
3051
|
return { chain, deriveUsd };
|
|
3059
3052
|
};
|
|
3053
|
+
const chains = states.map((state, i) => continuedBy(state, i));
|
|
3054
|
+
/** A continuation no plan of this turn books: its plan before it ran nothing. */
|
|
3055
|
+
const booksOwn = (state) => state.continuation !== undefined && !chains.some((c, j) => states[j] !== state && c.chain.includes(state));
|
|
3060
3056
|
// One report per plan armed this turn (fallbk.md §Runner 4). Each is billed
|
|
3061
3057
|
// for its own window of the transcript — from when it armed to when the next
|
|
3062
3058
|
// one did — so the turn's tokens are split between them, never counted twice.
|
|
3063
3059
|
// A turn with a single prompt-armed plan is exactly the report it always was.
|
|
3064
3060
|
states.forEach((state, i) => {
|
|
3065
|
-
const { chain, deriveUsd: chainDeriveUsd } =
|
|
3061
|
+
const { chain, deriveUsd: chainDeriveUsd } = chains[i];
|
|
3066
3062
|
// The plan whose arm ends this one's window: never one that continued it.
|
|
3067
3063
|
const next = states.slice(i + 1).find((s) => !chain.includes(s));
|
|
3068
3064
|
const nextMark = next?.armedBy === "intent" ? next.usageMark : undefined;
|
|
@@ -3098,6 +3094,7 @@ export class ControlServer {
|
|
|
3098
3094
|
cutShort: cutShort || undefined,
|
|
3099
3095
|
redoneBy: state === prompt && state.ranToEnd === true ? redoneBy : undefined,
|
|
3100
3096
|
chainDeriveUsd,
|
|
3097
|
+
booksOwnSaving: booksOwn(state),
|
|
3101
3098
|
});
|
|
3102
3099
|
if (!report)
|
|
3103
3100
|
return;
|
|
@@ -3564,6 +3561,21 @@ export class ControlServer {
|
|
|
3564
3561
|
if (stashed)
|
|
3565
3562
|
return this.preToolAnswer(this.replay.deliverInstead(stashed, toolName)) ?? {};
|
|
3566
3563
|
}
|
|
3564
|
+
// A continuation request of another call of this same message is still
|
|
3565
|
+
// out (the service's fallbk.md Part 3): calls of one message reach their
|
|
3566
|
+
// hooks at once. This one waits for it, so it meets whatever that request
|
|
3567
|
+
// armed — a direct plan whose first call is not `run_scenario` is vetoed
|
|
3568
|
+
// just below — and never opens the fragment beside it.
|
|
3569
|
+
const attempt = run.fragment?.attempt;
|
|
3570
|
+
if (attempt)
|
|
3571
|
+
await this.withinBudget(attempt, CONTINUATION_PROBE_MS + CONTINUATION_ARM_MS);
|
|
3572
|
+
// A continuation armed beside another call of this message is told on
|
|
3573
|
+
// that call's PostToolUse. A call to one of its own tools must not run
|
|
3574
|
+
// before then — the plan makes that call itself — so it is answered in
|
|
3575
|
+
// its place, and that answer tells the model of the plan.
|
|
3576
|
+
const told = planning ? undefined : this.announceContinuationInstead(run, toolName);
|
|
3577
|
+
if (told)
|
|
3578
|
+
return told;
|
|
3567
3579
|
const live = this.livePlan(run);
|
|
3568
3580
|
/** `run_scenario` for this session's own announced direct plan: allowed, see below. */
|
|
3569
3581
|
let allowDirect = false;
|
|
@@ -3766,8 +3778,7 @@ export class ControlServer {
|
|
|
3766
3778
|
// 2a. The model's first call after a plan handed over: the work recorded
|
|
3767
3779
|
// after an earlier hand-over at the same step may have a plan of its
|
|
3768
3780
|
// own (the service's fallbk.md Part 3). Asked with the hand-over, not
|
|
3769
|
-
// with words, and armed
|
|
3770
|
-
const dueFragment = run.fragment;
|
|
3781
|
+
// with words, and armed on this call, whatever the call is.
|
|
3771
3782
|
const before = run.replay;
|
|
3772
3783
|
const continued = planning
|
|
3773
3784
|
? undefined
|
|
@@ -3792,10 +3803,18 @@ export class ControlServer {
|
|
|
3792
3803
|
run.replays.splice(at, 1);
|
|
3793
3804
|
run.replay = before;
|
|
3794
3805
|
this.keepDeclinedContinuation(session, run, late, "arm_unannounced");
|
|
3795
|
-
run.
|
|
3806
|
+
if (run.parkedFragment?.by === late) {
|
|
3807
|
+
run.fragment = run.parkedFragment.fragment;
|
|
3808
|
+
run.parkedFragment = undefined;
|
|
3809
|
+
}
|
|
3796
3810
|
}
|
|
3797
3811
|
}
|
|
3798
|
-
|
|
3812
|
+
// A continuation that retired having run no step — refused, vetoed, or
|
|
3813
|
+
// left by the model — gives the model's work back to the fragment.
|
|
3814
|
+
this.restoreParkedFragment(run);
|
|
3815
|
+
// A continuation armed beside this call is this turn's plan, waiting for
|
|
3816
|
+
// the call's PostToolUse: no other plan is looked for in its place.
|
|
3817
|
+
const armed = planning || continued || run.replay !== before
|
|
3799
3818
|
? undefined
|
|
3800
3819
|
: await this.tryIntentMatch(session, run, toolName, intent, modelArgs);
|
|
3801
3820
|
if (armed) {
|
|
@@ -3816,8 +3835,9 @@ export class ControlServer {
|
|
|
3816
3835
|
}
|
|
3817
3836
|
// 3. No scenario took over: the model's own work after a hand-over is
|
|
3818
3837
|
// recorded as a fragment, starting with this call.
|
|
3819
|
-
// Not for the call an intent match just armed a plan
|
|
3820
|
-
// runs next, and the fragment opens on the model's
|
|
3838
|
+
// Not for the call a continuation or an intent match just armed a plan
|
|
3839
|
+
// beside: the plan runs next, and the fragment opens on the model's
|
|
3840
|
+
// own work after it.
|
|
3821
3841
|
if (run.fragment?.state === "due" && !run.pendingDirective) {
|
|
3822
3842
|
await this.openFragment(session, run, reasoning, toolName, modelArgs);
|
|
3823
3843
|
}
|
|
@@ -4572,12 +4592,17 @@ export class ControlServer {
|
|
|
4572
4592
|
* hand-over itself is the question — the plan and the step — so the service
|
|
4573
4593
|
* names one recording and matches no words.
|
|
4574
4594
|
*
|
|
4575
|
-
* It arms
|
|
4576
|
-
*
|
|
4577
|
-
*
|
|
4578
|
-
* the
|
|
4579
|
-
*
|
|
4580
|
-
*
|
|
4595
|
+
* It arms on the model's first call after the hand-over, whatever that call
|
|
4596
|
+
* is (Eran, 2026-10-26). Runner 0.3.23 armed it only when the call was the
|
|
4597
|
+
* plan's first step, and lost it whenever the model first re-checked what
|
|
4598
|
+
* the plan before had already done. How the model is told depends on the
|
|
4599
|
+
* call. A call to one of the plan's own tools is answered in its place: the
|
|
4600
|
+
* plan makes that call itself, and letting it run would make it twice. Any
|
|
4601
|
+
* other call runs as the model wrote it, and the plan is told on its
|
|
4602
|
+
* PostToolUse, as an intent plan armed beside the model's own work is. The
|
|
4603
|
+
* plan before it stopped because the next step needed a judgement; the
|
|
4604
|
+
* continuation runs the half that was recorded, whatever the model would
|
|
4605
|
+
* have judged.
|
|
4581
4606
|
*
|
|
4582
4607
|
* Its values are read from the turn's own request — the rest of the task is
|
|
4583
4608
|
* the one the user asked for — with the model's words, the call it was about
|
|
@@ -4610,13 +4635,43 @@ export class ControlServer {
|
|
|
4610
4635
|
return undefined;
|
|
4611
4636
|
}
|
|
4612
4637
|
f.asked = true;
|
|
4613
|
-
|
|
4638
|
+
// Calls of one message reach their hooks at once. The others wait for
|
|
4639
|
+
// this attempt (`onToolPre`), so they meet whatever it armed, and none
|
|
4640
|
+
// opens the fragment while it is still out.
|
|
4641
|
+
let done;
|
|
4642
|
+
f.attempt = new Promise((resolve) => {
|
|
4643
|
+
done = resolve;
|
|
4644
|
+
});
|
|
4645
|
+
try {
|
|
4646
|
+
return await this.continuationAttempt(session, run, f, toolName, intent, modelArgs);
|
|
4647
|
+
}
|
|
4648
|
+
finally {
|
|
4649
|
+
f.attempt = undefined;
|
|
4650
|
+
done();
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
/** The body of {@link tryContinuation}, once the continuation is asked for. */
|
|
4654
|
+
async continuationAttempt(session, run, f, toolName, intent, modelArgs) {
|
|
4655
|
+
const recorder = this.recorder;
|
|
4656
|
+
if (!isIntentMatcher(recorder))
|
|
4657
|
+
return undefined;
|
|
4614
4658
|
const framework = frameworkOf(session);
|
|
4615
|
-
const handover = {
|
|
4659
|
+
const handover = {
|
|
4660
|
+
scenarioId: f.scenarioId,
|
|
4661
|
+
stepIndex: f.stepIndex,
|
|
4662
|
+
...(f.kind ? { kind: f.kind } : {}),
|
|
4663
|
+
...(f.handoverId ? { id: f.handoverId } : {}),
|
|
4664
|
+
};
|
|
4665
|
+
// Whether this hand-over is still the turn's: its fragment due, nothing
|
|
4666
|
+
// recording, the turn open. Checked after every wait.
|
|
4667
|
+
const stillDue = () => run.fragment === f && f.state === "due" && !run.recording && !run.finished;
|
|
4616
4668
|
let landed = false;
|
|
4617
|
-
const probe =
|
|
4669
|
+
const probe = recorder
|
|
4618
4670
|
.matchIntent({
|
|
4619
|
-
|
|
4671
|
+
// Answered by the hand-over alone, so no words go: a service from
|
|
4672
|
+
// before continuations would match them against plan intents, and
|
|
4673
|
+
// the agent's reasoning is not sent for nothing.
|
|
4674
|
+
text: "",
|
|
4620
4675
|
currentToolName: toolName,
|
|
4621
4676
|
exclude: this.armedScenarioIds(run),
|
|
4622
4677
|
handover,
|
|
@@ -4629,13 +4684,11 @@ export class ControlServer {
|
|
|
4629
4684
|
});
|
|
4630
4685
|
const answer = await this.withinBudget(probe, CONTINUATION_PROBE_MS);
|
|
4631
4686
|
if (!answer) {
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
void probe.then((late) => this.noteLateContinuation(run, handover, late));
|
|
4638
|
-
}
|
|
4687
|
+
// A plan a late answer hands out is reported as late, for its ticket.
|
|
4688
|
+
// Whether the hand-over was counted is the service's to know: the
|
|
4689
|
+
// fragment's create call carries the same id.
|
|
4690
|
+
if (!landed)
|
|
4691
|
+
void probe.then((late) => this.noteLateContinuation(run, f, late));
|
|
4639
4692
|
logDetail("replay.continuation_miss", {
|
|
4640
4693
|
run: run.runId,
|
|
4641
4694
|
tool: toolName,
|
|
@@ -4655,7 +4708,7 @@ export class ControlServer {
|
|
|
4655
4708
|
continues: `${f.scenarioId}@${f.stepIndex}`,
|
|
4656
4709
|
recording: answer.continuation?.runId,
|
|
4657
4710
|
why: answer.continuation
|
|
4658
|
-
? "the work recorded after an earlier hand-over here has no plan
|
|
4711
|
+
? "the work recorded after an earlier hand-over here has no plan to hand out"
|
|
4659
4712
|
: "no hand-over at this step was recorded before",
|
|
4660
4713
|
});
|
|
4661
4714
|
return undefined;
|
|
@@ -4665,11 +4718,8 @@ export class ControlServer {
|
|
|
4665
4718
|
stepIndex: f.stepIndex,
|
|
4666
4719
|
runId: answer.continuation.runId,
|
|
4667
4720
|
};
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
const first = firstStepTool(match.scenario);
|
|
4671
|
-
if (match.scenario && first !== undefined && first !== toolName) {
|
|
4672
|
-
this.keepDeclinedContinuation(session, run, this.replay.declinedContinuation(match, "not_first_step", continuation));
|
|
4721
|
+
if (!stillDue()) {
|
|
4722
|
+
this.keepDeclinedContinuation(session, run, this.replay.declinedContinuation(match, "match_late", continuation));
|
|
4673
4723
|
return undefined;
|
|
4674
4724
|
}
|
|
4675
4725
|
const startedAt = Date.now();
|
|
@@ -4681,7 +4731,8 @@ export class ControlServer {
|
|
|
4681
4731
|
});
|
|
4682
4732
|
// The rest of the task the user asked for: its values are in the turn's
|
|
4683
4733
|
// request, and the model's words say what it is doing next.
|
|
4684
|
-
const
|
|
4734
|
+
const words = (intent?.text ?? "").trim().slice(0, 4_000);
|
|
4735
|
+
const prompt = [run.input, words].filter((s) => s.trim().length > 0).join("\n\n");
|
|
4685
4736
|
const arming = this.armOrLog(run, this.replay.arm(match, prompt, this.wrapped, "intent", {
|
|
4686
4737
|
liveCall: { toolName, toolInput },
|
|
4687
4738
|
recentResults: this.recentResultsFor(session, "call"),
|
|
@@ -4714,21 +4765,40 @@ export class ControlServer {
|
|
|
4714
4765
|
this.keepDeclinedContinuation(session, run, state);
|
|
4715
4766
|
return undefined;
|
|
4716
4767
|
}
|
|
4717
|
-
// Its values
|
|
4718
|
-
//
|
|
4719
|
-
//
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4768
|
+
// Its values, worked out before the model is told of it: a reading that
|
|
4769
|
+
// did not settle in time, or the plan's own code for its values failing,
|
|
4770
|
+
// is said as what it is, and the plan is never handed out.
|
|
4771
|
+
let paramsFailed = false;
|
|
4772
|
+
const settled = await this.withinBudget(state.plan.ready().then(() => true, () => {
|
|
4773
|
+
paramsFailed = true;
|
|
4774
|
+
return false;
|
|
4775
|
+
}), Math.max(0, CONTINUATION_ARM_MS - (Date.now() - startedAt)));
|
|
4776
|
+
if (!stillDue()) {
|
|
4777
|
+
this.keepDeclinedContinuation(session, run, state, "arm_unannounced");
|
|
4778
|
+
return undefined;
|
|
4779
|
+
}
|
|
4780
|
+
if (settled === null) {
|
|
4781
|
+
this.keepDeclinedContinuation(session, run, state, "arm_timeout");
|
|
4782
|
+
return undefined;
|
|
4783
|
+
}
|
|
4784
|
+
if (paramsFailed) {
|
|
4785
|
+
this.keepDeclinedContinuation(session, run, state, "params_failed");
|
|
4723
4786
|
return undefined;
|
|
4724
4787
|
}
|
|
4788
|
+
// Whatever the model's call is, the plan takes the rest of the task. A
|
|
4789
|
+
// call to one of its own tools is answered in its place: the plan makes
|
|
4790
|
+
// that call itself, and letting it run first would make it twice — a
|
|
4791
|
+
// duplicated write. Any other call (a re-check of what the plan before
|
|
4792
|
+
// did, say) runs as the model wrote it, and the plan is told after it.
|
|
4793
|
+
const inPlace = state.plan.usesTool(toolName);
|
|
4725
4794
|
state.usageMark = markTranscriptUsage(session.transcriptPath);
|
|
4726
4795
|
state.markUsage = () => markTranscriptUsage(session.transcriptPath);
|
|
4727
4796
|
run.replays.push(state);
|
|
4728
4797
|
run.replay = state;
|
|
4729
4798
|
// From here the continuation is this turn's plan. A hand-over of its own,
|
|
4730
|
-
// if it makes one, schedules its own fragment
|
|
4731
|
-
// left due is
|
|
4799
|
+
// if it makes one, schedules its own fragment. The one the plan before it
|
|
4800
|
+
// left due is parked, and put back should this plan run no step.
|
|
4801
|
+
run.parkedFragment = { by: state, fragment: f };
|
|
4732
4802
|
run.fragment = undefined;
|
|
4733
4803
|
const directive = this.replay.directiveFor(state) ?? "";
|
|
4734
4804
|
logLine("replay.continuation_armed", {
|
|
@@ -4737,11 +4807,67 @@ export class ControlServer {
|
|
|
4737
4807
|
continues: `${continuation.ofScenarioId}@${continuation.stepIndex}`,
|
|
4738
4808
|
recording: continuation.runId,
|
|
4739
4809
|
mode: state.mode,
|
|
4740
|
-
instead: toolName,
|
|
4810
|
+
instead: inPlace ? toolName : undefined,
|
|
4811
|
+
after: inPlace ? undefined : toolName,
|
|
4741
4812
|
ms: Date.now() - startedAt,
|
|
4742
|
-
why:
|
|
4813
|
+
why: inPlace
|
|
4814
|
+
? "the plan calculated from the work recorded after an earlier hand-over at this step takes the rest of the task, in place of the model's call to one of its own tools"
|
|
4815
|
+
: "the plan calculated from the work recorded after an earlier hand-over at this step takes the rest of the task after the model's call, which runs as written",
|
|
4816
|
+
});
|
|
4817
|
+
if (inPlace)
|
|
4818
|
+
return this.preToolAnswer(this.replay.deliverInstead(directive, toolName));
|
|
4819
|
+
run.pendingDirective = { state, text: directive };
|
|
4820
|
+
return undefined;
|
|
4821
|
+
}
|
|
4822
|
+
/**
|
|
4823
|
+
* A call meeting a continuation that armed beside another call of the same
|
|
4824
|
+
* message and still waits to be told (`pendingDirective`). A call to one of
|
|
4825
|
+
* the plan's own tools gets the plan's directive in its place: letting it
|
|
4826
|
+
* run would have the plan make the same call again — a duplicated write.
|
|
4827
|
+
* Any other call runs, and the plan is told on the first PostToolUse.
|
|
4828
|
+
*/
|
|
4829
|
+
announceContinuationInstead(run, toolName) {
|
|
4830
|
+
const pending = run.pendingDirective;
|
|
4831
|
+
const state = pending?.state;
|
|
4832
|
+
if (!pending || !state || state !== run.replay || !state.continuation)
|
|
4833
|
+
return undefined;
|
|
4834
|
+
if (!state.plan || state.retired || !pending.text || !state.plan.usesTool(toolName))
|
|
4835
|
+
return undefined;
|
|
4836
|
+
run.pendingDirective = undefined;
|
|
4837
|
+
logLine("replay.continuation_told", {
|
|
4838
|
+
run: run.runId,
|
|
4839
|
+
scenario: state.scenarioId ?? undefined,
|
|
4840
|
+
instead: toolName,
|
|
4841
|
+
why: "a call to one of the continuation's own tools, made beside the call it armed on — answered in its place, so the plan does not make it twice",
|
|
4842
|
+
});
|
|
4843
|
+
return this.preToolAnswer(this.replay.deliverInstead(pending.text, toolName));
|
|
4844
|
+
}
|
|
4845
|
+
/**
|
|
4846
|
+
* Put back the fragment a continuation took the place of, when that
|
|
4847
|
+
* continuation retired having run no step and handed nothing over —
|
|
4848
|
+
* `run_scenario` refused, the plan vetoed, or a steer pin left by the
|
|
4849
|
+
* model. The model is doing the rest of the task itself, and that work is
|
|
4850
|
+
* recorded from its next call, as it would have been with no continuation.
|
|
4851
|
+
*/
|
|
4852
|
+
restoreParkedFragment(run) {
|
|
4853
|
+
const parked = run.parkedFragment;
|
|
4854
|
+
if (!parked || run.fragment || run.recording || run.finished)
|
|
4855
|
+
return;
|
|
4856
|
+
const s = parked.by;
|
|
4857
|
+
if (!s.retired || s.stepsPinned > 0 || s.handover || s.ranToEnd || s.running || this.runningPlans.has(s))
|
|
4858
|
+
return;
|
|
4859
|
+
run.parkedFragment = undefined;
|
|
4860
|
+
run.fragment = parked.fragment;
|
|
4861
|
+
// Its report says why, for its ticket: told of the plan, the model never
|
|
4862
|
+
// asked for it — another call of the same message came first, or it chose
|
|
4863
|
+
// its own way after the directive.
|
|
4864
|
+
if (!s.declined && !s.directRequested)
|
|
4865
|
+
s.declined = "not_taken";
|
|
4866
|
+
logLine("replay.continuation_unused", {
|
|
4867
|
+
run: run.runId,
|
|
4868
|
+
scenario: s.scenarioId ?? undefined,
|
|
4869
|
+
why: "the continuation retired without running a step — the model's own work is recorded from this call, as before",
|
|
4743
4870
|
});
|
|
4744
|
-
return this.preToolAnswer(this.replay.deliverInstead(directive, toolName));
|
|
4745
4871
|
}
|
|
4746
4872
|
/**
|
|
4747
4873
|
* A continuation handed out and not armed: kept, retired, for its ticket's
|
|
@@ -4759,9 +4885,9 @@ export class ControlServer {
|
|
|
4759
4885
|
continues: state.continuation
|
|
4760
4886
|
? `${state.continuation.ofScenarioId}@${state.continuation.stepIndex}`
|
|
4761
4887
|
: undefined,
|
|
4762
|
-
why: code === "
|
|
4763
|
-
? "the
|
|
4764
|
-
: "the continuation was not armed in time",
|
|
4888
|
+
why: code === "params_failed"
|
|
4889
|
+
? "the continuation's own code for its values failed — never handed out, so never run"
|
|
4890
|
+
: "the continuation was not armed in time — never handed out, so never run",
|
|
4765
4891
|
});
|
|
4766
4892
|
}
|
|
4767
4893
|
state.sessionId = session.sessionId;
|
|
@@ -4797,27 +4923,6 @@ export class ControlServer {
|
|
|
4797
4923
|
.map((s) => s.scenarioId)
|
|
4798
4924
|
.filter((id) => typeof id === "string");
|
|
4799
4925
|
}
|
|
4800
|
-
/**
|
|
4801
|
-
* Whether the model's call is a continuation's first step: its tool, and
|
|
4802
|
-
* the step's own input in every argument the two share (pin-guard.ts
|
|
4803
|
-
* `sameFirstCall`). Anything that cannot be computed is "no".
|
|
4804
|
-
*/
|
|
4805
|
-
isContinuationFirstCall(state, toolName, modelArgs) {
|
|
4806
|
-
const plan = state.plan;
|
|
4807
|
-
if (!plan || plan.expectedTool() !== toolName)
|
|
4808
|
-
return false;
|
|
4809
|
-
try {
|
|
4810
|
-
const verdict = sameFirstCall(toolName, modelArgs, plan.toolInputForCurrentStep());
|
|
4811
|
-
if (!verdict.ok) {
|
|
4812
|
-
logDetail("replay.continuation_other_call", { tool: toolName, why: verdict.why });
|
|
4813
|
-
}
|
|
4814
|
-
return verdict.ok;
|
|
4815
|
-
}
|
|
4816
|
-
catch (err) {
|
|
4817
|
-
logDetail("replay.first_step_unknown", { tool: toolName, error: errText(err) });
|
|
4818
|
-
return false;
|
|
4819
|
-
}
|
|
4820
|
-
}
|
|
4821
4926
|
/** Whether continuations are asked for: on unless `BIR_CONTINUATIONS=0`. */
|
|
4822
4927
|
continuationsOn() {
|
|
4823
4928
|
if (this.opts.continuations !== undefined)
|
|
@@ -4884,7 +4989,15 @@ export class ControlServer {
|
|
|
4884
4989
|
const h = state?.handover;
|
|
4885
4990
|
if (!h || !state?.scenarioId)
|
|
4886
4991
|
return;
|
|
4887
|
-
run.fragment = {
|
|
4992
|
+
run.fragment = {
|
|
4993
|
+
scenarioId: state.scenarioId,
|
|
4994
|
+
stepIndex: h.stepIndex,
|
|
4995
|
+
state: "due",
|
|
4996
|
+
// Which half the model's work after it is (the service's fallbk.md
|
|
4997
|
+
// Part 3), and the id that makes the hand-over one hit.
|
|
4998
|
+
kind: h.kind,
|
|
4999
|
+
handoverId: `ho_${randomUUID()}`,
|
|
5000
|
+
};
|
|
4888
5001
|
}
|
|
4889
5002
|
/**
|
|
4890
5003
|
* Open the fragment run and record from this call on (fallbk.md D7).
|
|
@@ -4904,9 +5017,15 @@ export class ControlServer {
|
|
|
4904
5017
|
return;
|
|
4905
5018
|
const input = reasoning.trim() || `${toolName} ${serializeCapped(redact(args))}`.slice(0, 2_000);
|
|
4906
5019
|
const fallbackOf = { scenarioId: f.scenarioId, stepIndex: f.stepIndex };
|
|
4907
|
-
//
|
|
4908
|
-
// continuation
|
|
4909
|
-
|
|
5020
|
+
// Which half of a step's test this work is, and the hand-over's id, which
|
|
5021
|
+
// its continuation request carried too: the service counts the hand-over
|
|
5022
|
+
// once (Part 3). `counted` says the same to a service from before ids.
|
|
5023
|
+
const sent = {
|
|
5024
|
+
...fallbackOf,
|
|
5025
|
+
...(f.kind ? { kind: f.kind } : {}),
|
|
5026
|
+
...(f.handoverId ? { handoverId: f.handoverId } : {}),
|
|
5027
|
+
...(f.counted ? { counted: true } : {}),
|
|
5028
|
+
};
|
|
4910
5029
|
// The fragment is a run of the same host as the turn it continues.
|
|
4911
5030
|
const framework = frameworkOf(session);
|
|
4912
5031
|
const id = this.recorder.startRun(input, { ...this.runMetadata(session), fallbackOf }, framework ? { fallbackOf: sent, framework } : { fallbackOf: sent });
|
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
import type { ExecutionStage, ExecutionStepResult, FallbackKind } from "../replay/types.js";
|
|
9
9
|
export interface StartRunOptions {
|
|
10
10
|
/**
|
|
11
|
-
* Marks a *fragment* (fallbk.md D7). `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Marks a *fragment* (fallbk.md D7). `kind`: the kind of hand-over, so the
|
|
12
|
+
* other half of a step's test gets a fragment of its own (Part 3).
|
|
13
|
+
* `handoverId`: the hand-over's id, which its continuation request carried
|
|
14
|
+
* too, so the service counts the hand-over once. `counted`: the same for a
|
|
15
|
+
* service from before ids — it counted the hand-over when asked for its
|
|
16
|
+
* continuation.
|
|
14
17
|
*/
|
|
15
18
|
fallbackOf?: {
|
|
16
19
|
scenarioId: string;
|
|
17
20
|
stepIndex: number;
|
|
21
|
+
kind?: string;
|
|
22
|
+
handoverId?: string;
|
|
18
23
|
counted?: boolean;
|
|
19
24
|
};
|
|
20
25
|
/**
|
|
@@ -313,11 +318,16 @@ export interface IntentMatchRequest {
|
|
|
313
318
|
* A continuation request (the service's fallbk.md Part 3): the plan that
|
|
314
319
|
* just handed the task to the model, and the step it stopped in front of.
|
|
315
320
|
* The service answers with the plan of the work recorded after an earlier
|
|
316
|
-
* hand-over at that step, if there is one, and matches no words.
|
|
321
|
+
* hand-over at that step, if there is one, and matches no words. `kind`:
|
|
322
|
+
* the kind of hand-over (the other half of a step's test is continued only
|
|
323
|
+
* by the other half's recording); `id`: the hand-over's id, which its
|
|
324
|
+
* fragment's create call carries too, so the hand-over counts once.
|
|
317
325
|
*/
|
|
318
326
|
handover?: {
|
|
319
327
|
scenarioId: string;
|
|
320
328
|
stepIndex: number;
|
|
329
|
+
kind?: string;
|
|
330
|
+
id?: string;
|
|
321
331
|
};
|
|
322
332
|
}
|
|
323
333
|
/** The answer to a probe. */
|
|
@@ -608,13 +608,21 @@ export type DeclineCode = "replay_disabled" | "not_ready" | "known_bad_first_ste
|
|
|
608
608
|
*/
|
|
609
609
|
| "match_late"
|
|
610
610
|
/**
|
|
611
|
-
* A continuation (the service's fallbk.md Part 3)
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
611
|
+
* A continuation (the service's fallbk.md Part 3) armed after the hand-over
|
|
612
|
+
* and told to the model, which never ran it: `run_scenario` was never asked
|
|
613
|
+
* for — another call of the same message came first, the model did the
|
|
614
|
+
* rest its own way, or the call was refused. Set by the control server.
|
|
615
|
+
* (Runner 0.3.23 armed a continuation only when the model's call was its
|
|
616
|
+
* first step and declined it otherwise as `not_first_step`. From 0.3.24 it
|
|
617
|
+
* arms on the model's first call after the hand-over, whatever that is.)
|
|
618
|
+
*/
|
|
619
|
+
| "not_taken"
|
|
620
|
+
/**
|
|
621
|
+
* A continuation whose own code for working out its values (`paramsLogic`)
|
|
622
|
+
* failed on this request, so none of its steps could be computed. Set by
|
|
615
623
|
* the control server.
|
|
616
624
|
*/
|
|
617
|
-
| "
|
|
625
|
+
| "params_failed";
|
|
618
626
|
/**
|
|
619
627
|
* What a continuation continues (the service's fallbk.md Part 3): the plan
|
|
620
628
|
* that handed over, the step it stopped in front of, and the recording of the
|
|
@@ -712,10 +720,9 @@ export declare class ReplayController {
|
|
|
712
720
|
lateMatch(match: RunMatchLike, host?: HostContext): ReplayState;
|
|
713
721
|
/**
|
|
714
722
|
* The state of a continuation handed out and not armed (the service's
|
|
715
|
-
* fallbk.md Part 3): declined before any gate ran —
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
* ticket's report, with the reason.
|
|
723
|
+
* fallbk.md Part 3): declined before any gate ran — the service's answer
|
|
724
|
+
* came too late (`match_late`). Like {@link lateMatch}, it exists only for
|
|
725
|
+
* its ticket's report, with the reason.
|
|
719
726
|
*/
|
|
720
727
|
declinedContinuation(match: RunMatchLike, code: DeclineCode, continuation: ContinuationOf): ReplayState;
|
|
721
728
|
/**
|
|
@@ -1015,6 +1022,12 @@ export declare class ReplayController {
|
|
|
1015
1022
|
* them would be left out of the turn's saving.
|
|
1016
1023
|
*/
|
|
1017
1024
|
chainDeriveUsd?: number;
|
|
1025
|
+
/**
|
|
1026
|
+
* A continuation whose plan before it ran no step (its report is
|
|
1027
|
+
* `failed`, which books no saving): this one books its own, against its
|
|
1028
|
+
* own baseline, so the turn's saving is booked somewhere.
|
|
1029
|
+
*/
|
|
1030
|
+
booksOwnSaving?: boolean;
|
|
1018
1031
|
}): ExecutionReport | undefined;
|
|
1019
1032
|
/**
|
|
1020
1033
|
* The step a failed replay is fairly blamed on: the first whose own logic
|
|
@@ -376,10 +376,9 @@ export class ReplayController {
|
|
|
376
376
|
}
|
|
377
377
|
/**
|
|
378
378
|
* The state of a continuation handed out and not armed (the service's
|
|
379
|
-
* fallbk.md Part 3): declined before any gate ran —
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
* ticket's report, with the reason.
|
|
379
|
+
* fallbk.md Part 3): declined before any gate ran — the service's answer
|
|
380
|
+
* came too late (`match_late`). Like {@link lateMatch}, it exists only for
|
|
381
|
+
* its ticket's report, with the reason.
|
|
383
382
|
*/
|
|
384
383
|
declinedContinuation(match, code, continuation) {
|
|
385
384
|
const state = this.blankState(match, new Set(), "intent");
|
|
@@ -392,9 +391,7 @@ export class ReplayController {
|
|
|
392
391
|
run: match.runId,
|
|
393
392
|
scenario: match.scenarioId ?? undefined,
|
|
394
393
|
continues: `${continuation.ofScenarioId}@${continuation.stepIndex}`,
|
|
395
|
-
why:
|
|
396
|
-
? "a continuation starts only on its first step, and the model's call after the hand-over is another one"
|
|
397
|
-
: "the continuation came after the call it could have replaced had gone ahead",
|
|
394
|
+
why: "the continuation came after the call it could have replaced had gone ahead",
|
|
398
395
|
});
|
|
399
396
|
return state;
|
|
400
397
|
}
|
|
@@ -1998,8 +1995,11 @@ export class ReplayController {
|
|
|
1998
1995
|
// The agent did not take the plan's result and redid the task (AC1).
|
|
1999
1996
|
...(d.redoneBy ? { redoneBy: d.redoneBy } : {}),
|
|
2000
1997
|
// A continuation (the service's fallbk.md Part 3): the plan it continued
|
|
2001
|
-
// books the whole turn, so this report books no saving of its own
|
|
2002
|
-
|
|
1998
|
+
// books the whole turn, so this report books no saving of its own —
|
|
1999
|
+
// unless that plan ran nothing and books none.
|
|
2000
|
+
...(state.continuation && !d.booksOwnSaving
|
|
2001
|
+
? { continues: { scenarioId: state.continuation.ofScenarioId } }
|
|
2002
|
+
: {}),
|
|
2003
2003
|
// A turn in which another plan ran is not a measurement of what this task
|
|
2004
2004
|
// costs unaided, so it is no baseline sample (segmented.md R-MONEY-5).
|
|
2005
2005
|
// This covers two plans that both armed, and — the case that was silently
|
|
@@ -373,7 +373,7 @@ so there is exactly one switch and it cannot get out of step with itself.
|
|
|
373
373
|
| `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 |
|
|
374
374
|
| `BIR_PATH_ALLOW` | *(unset: any folder, from 0.3.21)* | Folders a runner-side `Read`, `Glob`, `Grep`, `Edit` or `Write` may touch (`*` for all). Unset means any path, as the agent's own tools reach it — a plan that reads the agent's memory file under `~/.claude` runs. `project` is the session's project folder alone (the rule up to 0.3.20); add folders after a comma, `~` for the home folder: `project,~/.claude`. Under a list, a path outside it, symlinks included, is refused and the plan hands over in front of it. `none` means no folder, and the file steps are steered in the session. `Bash` is `BIR_BASH_ALLOW`'s |
|
|
375
375
|
| `BIR_INTENT_ARM` | *(unset: off)* | `1` lets a whole plan whose intent matches the agent's reasoning mid-turn arm in the calling tool's place, which means waiting for it (`BIR_INTENT_ARM_BUDGET_MS`, 500). Unset or `0`: off — no probe is sent and nothing arms mid-turn, as up to 0.3.18 |
|
|
376
|
-
| `BIR_CONTINUATIONS` | *(unset: on, from 0.3.23)* | After a plan hands the task to the agent at a step, the agent's first call asks the service for the plan of the work recorded after an earlier hand-over at that step — once per hand-over, waiting 1.5 s at most for the answer and up to 7 s for its values — and
|
|
376
|
+
| `BIR_CONTINUATIONS` | *(unset: on, from 0.3.23)* | After a plan hands the task to the agent at a step, the agent's first call asks the service for the plan of the work recorded after an earlier hand-over at that step — once per hand-over, waiting 1.5 s at most for the answer and up to 7 s for its values — and that plan does the rest of the task, whatever the agent's call was (from 0.3.24; 0.3.23 ran it only when the call was its first step). A call to one of the plan's own tools is answered in its place, so the plan never makes it twice; any other call runs as the agent wrote it, and the agent is told of the plan on its result. Its saving is counted on the plan that handed over. `0`: off, and the agent's work is recorded as before. `BIR_INTENT_MATCH=0` turns it off too |
|
|
377
377
|
| `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
|
|
378
378
|
|
|
379
379
|
The `bir` server reads its own, from the environment Claude Code starts it with:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basein/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
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",
|