@basein/runner 0.3.18 → 0.3.19
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/bin/bir-hooks.d.ts +11 -8
- package/dist/bin/bir-hooks.js +11 -8
- package/dist/bin/bir.js +20 -7
- package/dist/bin/investigate.d.ts +9 -0
- package/dist/bin/investigate.js +26 -12
- package/dist/bin/scenario-edit.js +10 -0
- package/dist/control/recorder-main.d.ts +15 -0
- package/dist/control/recorder-main.js +33 -8
- package/dist/control/server.d.ts +27 -11
- package/dist/control/server.js +100 -20
- package/dist/record/recorder.d.ts +10 -2
- package/dist/replay/condition-words.d.ts +28 -0
- package/dist/replay/condition-words.js +59 -0
- package/dist/replay/controller.d.ts +45 -6
- package/dist/replay/controller.js +181 -50
- package/dist/replay/handover.d.ts +7 -0
- package/dist/replay/handover.js +89 -51
- package/dist/replay/plan.d.ts +11 -4
- package/dist/replay/plan.js +28 -8
- package/dist/replay/types.d.ts +19 -3
- package/docs/calculatedReplay.md +45 -5
- package/docs/calculatedReplayGuide.md +13 -4
- package/package.json +1 -1
- package/production.md +21 -7
package/dist/replay/handover.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* and a tool's own payload is already in front of it where it ran.
|
|
12
12
|
*/
|
|
13
13
|
import { truncate } from "./bundle.js";
|
|
14
|
+
import { conditionSentences } from "./condition-words.js";
|
|
14
15
|
/** Hard ceiling on a note. Long enough to carry derived params, short enough to read. */
|
|
15
16
|
export const MAX_HANDOVER_NOTE = 4_000;
|
|
16
17
|
const MAX_ERROR_CHARS = 500;
|
|
@@ -26,6 +27,8 @@ const UNUSABLE_LINE = {
|
|
|
26
27
|
unsupported: "this runner cannot run",
|
|
27
28
|
};
|
|
28
29
|
export function buildHandoverNote(n) {
|
|
30
|
+
if (n.kind === "decision")
|
|
31
|
+
return decisionNote(n);
|
|
29
32
|
const intent = n.intent.length > 240 ? `${n.intent.slice(0, 240)}…` : n.intent;
|
|
30
33
|
const human = n.stepIndex + 1;
|
|
31
34
|
const tool = n.stepToolName ? ` (${n.stepToolName})` : "";
|
|
@@ -35,6 +38,91 @@ export function buildHandoverNote(n) {
|
|
|
35
38
|
];
|
|
36
39
|
if (intent)
|
|
37
40
|
lines.push(`Intent: ${intent}`);
|
|
41
|
+
lines.push(...ranLines(n));
|
|
42
|
+
if (n.kind === "unusable_call") {
|
|
43
|
+
// A call is a stretch of work, not a tool: the agent is told what the
|
|
44
|
+
// sub-task was, not which tool did not run.
|
|
45
|
+
const what = n.unusable?.callIntent ? ` ("${truncate(n.unusable.callIntent, 200)}")` : "";
|
|
46
|
+
const why = UNUSABLE_LINE[n.unusable?.reason ?? ""] ?? "that cannot run here";
|
|
47
|
+
lines.push(`Step ${human} of ${n.totalSteps} is a sub-task${what} ${why}.`);
|
|
48
|
+
}
|
|
49
|
+
else if (n.kind === "known_bad_step") {
|
|
50
|
+
const times = n.failureCount ? ` ${n.failureCount} times` : "";
|
|
51
|
+
// A parked step inside a called segment names the scenario that owns it:
|
|
52
|
+
// that is where the repair happens, and the agent's own step numbering has
|
|
53
|
+
// no row for it (R-CALL-30).
|
|
54
|
+
const owner = n.inSubTask
|
|
55
|
+
? `, in sub-task "${truncate(n.inSubTask, 120)}"${n.ownerScenarioId ? `, scenario ${n.ownerScenarioId}` : ""}`
|
|
56
|
+
: "";
|
|
57
|
+
lines.push(`Step ${human} of ${n.totalSteps}${tool.replace(/\)$/, `${owner})`)} did not run: it has failed${times} before, so the scenario stops in front of it.`);
|
|
58
|
+
}
|
|
59
|
+
else if (n.kind === "nondeterministic_step") {
|
|
60
|
+
// The calculation could not write code for this step's input: it is a
|
|
61
|
+
// judgement, and the agent is the one to make it (plan-services.md D8).
|
|
62
|
+
lines.push(`Step ${human} of ${n.totalSteps}${tool} did not run: its input needs a judgement the scenario cannot compute. Make that choice yourself and carry on.`);
|
|
63
|
+
}
|
|
64
|
+
else if (n.kind === "other_branch") {
|
|
65
|
+
// The plan holds one half of an "if … otherwise" and this request is the
|
|
66
|
+
// other (autocorrect.md AC5). The steps from here are not wrong, they are
|
|
67
|
+
// not this request's: the model is told what they are for, never to run
|
|
68
|
+
// them, and the list of their tools is left out below for that reason.
|
|
69
|
+
const why = n.whenText ? `they are only for when ${n.whenText}, and that is not so for this request` : "this request is the other case of the task, which they are not for";
|
|
70
|
+
lines.push(n.stepIndex >= n.totalSteps
|
|
71
|
+
? "None of the scenario's steps was needed for this request. Do the task as this request needs it."
|
|
72
|
+
: `Step ${human} of ${n.totalSteps}${tool} and the steps after it did not run: ${why}. Do the rest of the task yourself, as this request needs it.`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
lines.push(stepFailedLine(n, human, tool));
|
|
76
|
+
}
|
|
77
|
+
// Not for the other half: those tools are the ones this request must not
|
|
78
|
+
// run, and naming them invites exactly that.
|
|
79
|
+
if (n.remainingTools.length > 0 && n.kind !== "other_branch") {
|
|
80
|
+
lines.push(`The remaining planned steps were: ${n.remainingTools.join(", ")}.`);
|
|
81
|
+
}
|
|
82
|
+
if (n.params && Object.keys(n.params).length > 0) {
|
|
83
|
+
let json;
|
|
84
|
+
try {
|
|
85
|
+
json = JSON.stringify(n.params);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
json = "[unserializable]";
|
|
89
|
+
}
|
|
90
|
+
lines.push(`Parameters derived for this request: ${truncate(json, MAX_PARAMS_CHARS)}`);
|
|
91
|
+
}
|
|
92
|
+
return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The note of a plan stopped at its decision (the service's
|
|
96
|
+
* segmentsbreakdown.md SB-18): what ran, and the request's own words for the
|
|
97
|
+
* choice the agent now makes. Nothing that names the recorded half: no
|
|
98
|
+
* intent, no remaining steps, no parameters, and never the condition the
|
|
99
|
+
* service keeps (`stop.why`). Each would hint which way the recording went.
|
|
100
|
+
*/
|
|
101
|
+
function decisionNote(n) {
|
|
102
|
+
// Where the results of the steps that ran are, as the ran-steps line says.
|
|
103
|
+
const above = Math.max(0, Math.min(n.executedAbove ?? 0, n.executed));
|
|
104
|
+
const delivery = above >= n.executed ? "above" : n.delivery;
|
|
105
|
+
const from = n.executed === 0
|
|
106
|
+
? ""
|
|
107
|
+
: delivery === "above"
|
|
108
|
+
? " from the results above"
|
|
109
|
+
: delivery === "below"
|
|
110
|
+
? above > 0
|
|
111
|
+
? " from the results above and below"
|
|
112
|
+
: " from the results below"
|
|
113
|
+
: "";
|
|
114
|
+
const quote = conditionSentences(n.request);
|
|
115
|
+
const ask = quote ? `This request asks for a decision: “${quote}”.` : "This task has a decision to make here.";
|
|
116
|
+
const lines = [
|
|
117
|
+
"[BaseInstRunner calculated replay] The calculated scenario for this request ran the steps before a decision, and stopped there.",
|
|
118
|
+
...ranLines(n),
|
|
119
|
+
`${ask} Decide${from} which way applies, then continue the task, calling tools as needed.`,
|
|
120
|
+
];
|
|
121
|
+
return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
|
|
122
|
+
}
|
|
123
|
+
/** What ran before the stop, and what of it the model may assume: the lines every note carries. */
|
|
124
|
+
function ranLines(n) {
|
|
125
|
+
const lines = [];
|
|
38
126
|
// Steps left out by their tests sit among those before the stop (AC5): the
|
|
39
127
|
// ones that ran are then named, never "steps 1–N", which would say a step
|
|
40
128
|
// ran that did not.
|
|
@@ -100,57 +188,7 @@ export function buildHandoverNote(n) {
|
|
|
100
188
|
lines.push(`Step ${n.inFlight.stepIndex + 1} of ${n.totalSteps}${inTool} was started and has not reported back: ` +
|
|
101
189
|
"it may not have run, or may still complete. Check whether its work happened before you repeat it.");
|
|
102
190
|
}
|
|
103
|
-
|
|
104
|
-
// A call is a stretch of work, not a tool: the agent is told what the
|
|
105
|
-
// sub-task was, not which tool did not run.
|
|
106
|
-
const what = n.unusable?.callIntent ? ` ("${truncate(n.unusable.callIntent, 200)}")` : "";
|
|
107
|
-
const why = UNUSABLE_LINE[n.unusable?.reason ?? ""] ?? "that cannot run here";
|
|
108
|
-
lines.push(`Step ${human} of ${n.totalSteps} is a sub-task${what} ${why}.`);
|
|
109
|
-
}
|
|
110
|
-
else if (n.kind === "known_bad_step") {
|
|
111
|
-
const times = n.failureCount ? ` ${n.failureCount} times` : "";
|
|
112
|
-
// A parked step inside a called segment names the scenario that owns it:
|
|
113
|
-
// that is where the repair happens, and the agent's own step numbering has
|
|
114
|
-
// no row for it (R-CALL-30).
|
|
115
|
-
const owner = n.inSubTask
|
|
116
|
-
? `, in sub-task "${truncate(n.inSubTask, 120)}"${n.ownerScenarioId ? `, scenario ${n.ownerScenarioId}` : ""}`
|
|
117
|
-
: "";
|
|
118
|
-
lines.push(`Step ${human} of ${n.totalSteps}${tool.replace(/\)$/, `${owner})`)} did not run: it has failed${times} before, so the scenario stops in front of it.`);
|
|
119
|
-
}
|
|
120
|
-
else if (n.kind === "nondeterministic_step") {
|
|
121
|
-
// The calculation could not write code for this step's input: it is a
|
|
122
|
-
// judgement, and the agent is the one to make it (plan-services.md D8).
|
|
123
|
-
lines.push(`Step ${human} of ${n.totalSteps}${tool} did not run: its input needs a judgement the scenario cannot compute. Make that choice yourself and carry on.`);
|
|
124
|
-
}
|
|
125
|
-
else if (n.kind === "other_branch") {
|
|
126
|
-
// The plan holds one half of an "if … otherwise" and this request is the
|
|
127
|
-
// other (autocorrect.md AC5). The steps from here are not wrong, they are
|
|
128
|
-
// not this request's: the model is told what they are for, never to run
|
|
129
|
-
// them, and the list of their tools is left out below for that reason.
|
|
130
|
-
const why = n.whenText ? `they are only for when ${n.whenText}, and that is not so for this request` : "this request is the other case of the task, which they are not for";
|
|
131
|
-
lines.push(n.stepIndex >= n.totalSteps
|
|
132
|
-
? "None of the scenario's steps was needed for this request. Do the task as this request needs it."
|
|
133
|
-
: `Step ${human} of ${n.totalSteps}${tool} and the steps after it did not run: ${why}. Do the rest of the task yourself, as this request needs it.`);
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
lines.push(stepFailedLine(n, human, tool));
|
|
137
|
-
}
|
|
138
|
-
// Not for the other half: those tools are the ones this request must not
|
|
139
|
-
// run, and naming them invites exactly that.
|
|
140
|
-
if (n.remainingTools.length > 0 && n.kind !== "other_branch") {
|
|
141
|
-
lines.push(`The remaining planned steps were: ${n.remainingTools.join(", ")}.`);
|
|
142
|
-
}
|
|
143
|
-
if (n.params && Object.keys(n.params).length > 0) {
|
|
144
|
-
let json;
|
|
145
|
-
try {
|
|
146
|
-
json = JSON.stringify(n.params);
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
json = "[unserializable]";
|
|
150
|
-
}
|
|
151
|
-
lines.push(`Parameters derived for this request: ${truncate(json, MAX_PARAMS_CHARS)}`);
|
|
152
|
-
}
|
|
153
|
-
return truncate(lines.join("\n"), MAX_HANDOVER_NOTE);
|
|
191
|
+
return lines;
|
|
154
192
|
}
|
|
155
193
|
/** "Step 2", "Steps 1–3", "Steps 1, 3 and 5–6": 1-based numbers in order, runs joined. */
|
|
156
194
|
export function stepsPhrase(numbers) {
|
package/dist/replay/plan.d.ts
CHANGED
|
@@ -469,6 +469,12 @@ export declare class ScenarioReplayPlan {
|
|
|
469
469
|
stopStep(): SerializedScenarioStep | undefined;
|
|
470
470
|
/** The entry the plan stops in front of, with the frame that owns it. */
|
|
471
471
|
stopEntry(): FlatEntry | undefined;
|
|
472
|
+
/**
|
|
473
|
+
* The flat position of the chain's decision stop (the service's
|
|
474
|
+
* segmentsbreakdown.md SB-18), or -1 when it has none. The steps from there
|
|
475
|
+
* on are the half the recording took, whatever stops the plan first.
|
|
476
|
+
*/
|
|
477
|
+
decisionIndex(): number;
|
|
472
478
|
/** Every entry in order — for the report, and for the owning scenario ids. */
|
|
473
479
|
allEntries(): readonly FlatEntry[];
|
|
474
480
|
/** The entry awaiting execution, or undefined when the plan is done. */
|
|
@@ -539,11 +545,12 @@ export declare class ScenarioReplayPlan {
|
|
|
539
545
|
text: string | null;
|
|
540
546
|
} | undefined;
|
|
541
547
|
/**
|
|
542
|
-
* The tool names of the steps from flat position `from` to the end
|
|
543
|
-
* that runs only under a test marked so — for a
|
|
544
|
-
* planned steps", which must not read as steps
|
|
548
|
+
* The tool names of the steps from flat position `from` to the end (or to
|
|
549
|
+
* `until`, not included), each that runs only under a test marked so — for a
|
|
550
|
+
* hand-over note's "remaining planned steps", which must not read as steps
|
|
551
|
+
* this request needs.
|
|
545
552
|
*/
|
|
546
|
-
remainingFrom(from: number): string[];
|
|
553
|
+
remainingFrom(from: number, until?: number): string[];
|
|
547
554
|
/** The steps the plan's own cursor has left out so far, in order (AC5). */
|
|
548
555
|
leftOut(): readonly LeftOut[];
|
|
549
556
|
/** Whether any planned step runs only under a test. */
|
package/dist/replay/plan.js
CHANGED
|
@@ -700,6 +700,14 @@ export class ScenarioReplayPlan {
|
|
|
700
700
|
stopEntry() {
|
|
701
701
|
return this.stopsEarly() ? this.entries[this.stopAt] : undefined;
|
|
702
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* The flat position of the chain's decision stop (the service's
|
|
705
|
+
* segmentsbreakdown.md SB-18), or -1 when it has none. The steps from there
|
|
706
|
+
* on are the half the recording took, whatever stops the plan first.
|
|
707
|
+
*/
|
|
708
|
+
decisionIndex() {
|
|
709
|
+
return this.entries.findIndex((e) => e.step.stop?.kind === "decision");
|
|
710
|
+
}
|
|
703
711
|
/** Every entry in order — for the report, and for the owning scenario ids. */
|
|
704
712
|
allEntries() {
|
|
705
713
|
return this.entries;
|
|
@@ -853,12 +861,13 @@ export class ScenarioReplayPlan {
|
|
|
853
861
|
return undefined;
|
|
854
862
|
}
|
|
855
863
|
/**
|
|
856
|
-
* The tool names of the steps from flat position `from` to the end
|
|
857
|
-
* that runs only under a test marked so — for a
|
|
858
|
-
* planned steps", which must not read as steps
|
|
864
|
+
* The tool names of the steps from flat position `from` to the end (or to
|
|
865
|
+
* `until`, not included), each that runs only under a test marked so — for a
|
|
866
|
+
* hand-over note's "remaining planned steps", which must not read as steps
|
|
867
|
+
* this request needs.
|
|
859
868
|
*/
|
|
860
|
-
remainingFrom(from) {
|
|
861
|
-
return this.entries.slice(Math.max(0, from)).map((e) => {
|
|
869
|
+
remainingFrom(from, until) {
|
|
870
|
+
return this.entries.slice(Math.max(0, from), until).map((e) => {
|
|
862
871
|
const name = e.step.toolName ?? "";
|
|
863
872
|
const t = this.onlyWhen(e);
|
|
864
873
|
return t ? `${name} (only when ${t.text ?? "a request needs it"})` : name;
|
|
@@ -1279,10 +1288,21 @@ export class ScenarioReplayPlan {
|
|
|
1279
1288
|
break;
|
|
1280
1289
|
}
|
|
1281
1290
|
const anyArgs = args.slice(0, argsShown).some((a) => a !== undefined);
|
|
1291
|
+
// A plan whose chain holds a decision (the service's segmentsbreakdown.md
|
|
1292
|
+
// SB-18) never shows its intent, even when it stops earlier for another
|
|
1293
|
+
// reason: the intent names the recorded task, the half the recording took
|
|
1294
|
+
// included, and which way applies is the agent's choice.
|
|
1295
|
+
const atDecision = this.stopStep()?.stop?.kind === "decision";
|
|
1296
|
+
const shownIntent = this.decisionIndex() >= 0 ? "" : intent;
|
|
1282
1297
|
// A parked step (fallbk.md D3): the model is told up front that the plan is
|
|
1283
1298
|
// partial, so the hand-over that follows is expected rather than a surprise.
|
|
1284
1299
|
const tail = this.stopsEarly()
|
|
1285
|
-
? [
|
|
1300
|
+
? [
|
|
1301
|
+
"",
|
|
1302
|
+
atDecision
|
|
1303
|
+
? "The system will then hand the task back to you at a decision."
|
|
1304
|
+
: "The system will then hand the rest of the task back to you.",
|
|
1305
|
+
]
|
|
1286
1306
|
: [];
|
|
1287
1307
|
if (this.mode === "direct") {
|
|
1288
1308
|
// The sequence is listed even though the model does not call it itself:
|
|
@@ -1291,7 +1311,7 @@ export class ScenarioReplayPlan {
|
|
|
1291
1311
|
// place an operator watching the session can see what a replay is doing.
|
|
1292
1312
|
return [
|
|
1293
1313
|
"[BaseInstRunner calculated replay] A known-good tool sequence was recorded",
|
|
1294
|
-
`for this request${
|
|
1314
|
+
`for this request${shownIntent ? ` (intent: ${shownIntent})` : ""}.`,
|
|
1295
1315
|
"",
|
|
1296
1316
|
anyTest
|
|
1297
1317
|
? 'It runs these tools, in order, and returns their results. A step marked "only when" runs only when this request needs it, and the results say which ones did not:'
|
|
@@ -1313,7 +1333,7 @@ export class ScenarioReplayPlan {
|
|
|
1313
1333
|
"recorded for this request. Call exactly these tools, in order, one at a time:",
|
|
1314
1334
|
"",
|
|
1315
1335
|
...lines,
|
|
1316
|
-
...(
|
|
1336
|
+
...(shownIntent ? ["", `Context: ${shownIntent}`] : []),
|
|
1317
1337
|
...(anyTest
|
|
1318
1338
|
? [
|
|
1319
1339
|
"",
|
package/dist/replay/types.d.ts
CHANGED
|
@@ -47,9 +47,18 @@ export interface StepStop {
|
|
|
47
47
|
* `parked`: the step has failed too often and its retry is not due, or its
|
|
48
48
|
* grace has ended. `nondeterministic`: the calculation could not write code
|
|
49
49
|
* that computes this step's input — a judgement — so the agent makes it.
|
|
50
|
+
* `decision`: the task has an "if … otherwise" here (the service's
|
|
51
|
+
* segmentsbreakdown.md SB-6). The plan runs only the steps before it and
|
|
52
|
+
* hands the task back, and the agent decides which way applies. Not a
|
|
53
|
+
* failure: it counts nothing against the step. Runners up to 0.3.18 read it
|
|
54
|
+
* as an unknown kind and stop here too, as `known_bad_step`.
|
|
55
|
+
*/
|
|
56
|
+
kind: "parked" | "nondeterministic" | "decision";
|
|
57
|
+
/**
|
|
58
|
+
* For `nondeterministic`: which check the step failed at calculation. For
|
|
59
|
+
* `decision`: the condition in words. It holds the recorded run's values,
|
|
60
|
+
* so it is never shown to the agent (it would hint at the recorded half).
|
|
50
61
|
*/
|
|
51
|
-
kind: "parked" | "nondeterministic";
|
|
52
|
-
/** For `nondeterministic`: which check the step failed at calculation. */
|
|
53
62
|
why?: string;
|
|
54
63
|
/** For `parked` inside its grace: when the step may be tried again. */
|
|
55
64
|
retryAt?: string;
|
|
@@ -252,7 +261,14 @@ export type FallbackKind =
|
|
|
252
261
|
* (autocorrect.md AC5): this request is the other half of an "if …
|
|
253
262
|
* otherwise", and the agent does it. No fault of the plan; counts nothing.
|
|
254
263
|
*/
|
|
255
|
-
| "other_branch"
|
|
264
|
+
| "other_branch"
|
|
265
|
+
/**
|
|
266
|
+
* The plan ran the steps before its decision and stopped in front of it
|
|
267
|
+
* (the service's segmentsbreakdown.md SB-17): the agent decides which way
|
|
268
|
+
* applies and does the rest. No fault of the plan; it counts nothing, and
|
|
269
|
+
* the plan is billed only up to the stop (SB-26).
|
|
270
|
+
*/
|
|
271
|
+
| "decision";
|
|
256
272
|
/**
|
|
257
273
|
* Upgrade order. `not_steered` is the floor — the control group, and the safe
|
|
258
274
|
* answer if a turn dies mid-flight — and each later decision point can only move
|
package/docs/calculatedReplay.md
CHANGED
|
@@ -748,6 +748,30 @@ about it:
|
|
|
748
748
|
It ends with the remaining planned steps and the parameters derived for this request, within 4 000
|
|
749
749
|
characters.
|
|
750
750
|
|
|
751
|
+
**A plan's decision (0.3.19)** has a note of its own. The service serves a step with
|
|
752
|
+
`stop: {kind: "decision"}` where the task has an "if … otherwise" (its segmentsbreakdown.md). The
|
|
753
|
+
plan runs only the steps before it, in steer and direct mode and when a divergence's bundle runs up
|
|
754
|
+
to it, and hands the task back. The note opens *"[BaseInstRunner calculated replay] The calculated
|
|
755
|
+
scenario for this request ran the steps before a decision, and stopped there."*, says which steps
|
|
756
|
+
ran as above, and ends *"This request asks for a decision: “…”. Decide from the results above
|
|
757
|
+
which way applies, then continue the task, calling tools as needed."* (*below* in a direct run's
|
|
758
|
+
answer, *above and below* after a divergence; *"This task has a decision to make here."* when the
|
|
759
|
+
request has no such sentence). The quote is the live request's own sentences with a word for a
|
|
760
|
+
condition, found with the service's word list (`if`, `otherwise`, `else`, `unless`, `only when`,
|
|
761
|
+
`only if`, `in case`, `whether`, and the Hebrew ones), at most 600 characters
|
|
762
|
+
([`condition-words.ts`](../src/replay/condition-words.ts)). The note has no intent line, no
|
|
763
|
+
remaining planned steps and no parameters, and never the condition the service keeps (`stop.why`):
|
|
764
|
+
each names, or hints at, the half the recording took. The directive at the prompt leaves the intent
|
|
765
|
+
out for the same reason, and says the task comes back *"at a decision"*. The report is `fell_back`
|
|
766
|
+
(`failed` when no step ran) with `fallbackKind: "decision"`; the decision step has no verdict, so
|
|
767
|
+
nothing counts against it. A runner up to 0.3.18 reads the unknown kind as `known_bad_step`.
|
|
768
|
+
|
|
769
|
+
A plan whose chain has a decision keeps these protections when it stops *earlier* for another
|
|
770
|
+
reason — a parked or failed step, a judgement, the budget. Its note is the ordinary one above, but
|
|
771
|
+
with no intent line and no parameters, and its remaining planned steps end before the decision. Its
|
|
772
|
+
directive leaves the intent out too. An agent sent on to the recorded half's steps would start that
|
|
773
|
+
half's segment, and the plan would have decided after all.
|
|
774
|
+
|
|
751
775
|
With `BIR_REPLAY_RECORDED_OUTPUTS=1`, a step that could not be *sent* (a built-in, no proxy, off the
|
|
752
776
|
allowlist) is served its **recorded output** instead (§8.1), marked `(recorded)` in the bundle so the
|
|
753
777
|
model is never told a stale result is fresh. A timeout or an upstream error never is.
|
|
@@ -1010,10 +1034,16 @@ note replaces the call, in direct mode `run_scenario` answers with it). The deri
|
|
|
1010
1034
|
itself times out after max(`BIR_SERVICE_TIMEOUT_MS`, `BIR_DERIVE_BUDGET_MS`). None of these waits
|
|
1011
1035
|
happens while the service is cooling down after a failure: the turn simply runs normally.
|
|
1012
1036
|
|
|
1013
|
-
**Intent arming mid-turn is
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
arming. `
|
|
1037
|
+
**Intent arming mid-turn is on by default from 0.3.19.** An intent probe is sent from `PreToolUse`
|
|
1038
|
+
for hit counting. While arming is on — `BIR_SEGMENT_ARM` unset or `1`, the recorder's default from
|
|
1039
|
+
0.3.19, or `BIR_INTENT_ARM=1` — the probe may arm a plan in that call's place, and the hook waits at
|
|
1040
|
+
most `BIR_INTENT_ARM_BUDGET_MS` (**500**) for it and 1.5 s more for arming. With `BIR_SEGMENT_ARM=0`
|
|
1041
|
+
(and no `BIR_INTENT_ARM=1`), or `BIR_INTENT_ARM=0`, it is never waited on. Up to 0.3.18 arming was
|
|
1042
|
+
opt-in: a segment runs only mid-task, and a plan that stops at its decision hands the rest of the
|
|
1043
|
+
task to segments of its halves, so 0.3.19 turns it on. A probe asks the service to hand segments out
|
|
1044
|
+
(`acceptSegments`) only where one can arm: segment and intent arming on, in Claude Code. Any other
|
|
1045
|
+
probe — `BIR_INTENT_ARM=0`, or a session of another host — gets what would have armed instead, and
|
|
1046
|
+
it is logged (`replay.segment_would_arm`).
|
|
1017
1047
|
|
|
1018
1048
|
`UserPromptSubmit` must stay non-`async`: an async hook is fire-and-forget and cannot return
|
|
1019
1049
|
`additionalContext` at all.
|
|
@@ -1045,7 +1075,8 @@ stopped at a step that broke (a logic failure, a tool error, a call sent and nev
|
|
|
1045
1075
|
recorded stand-in (`BIR_REPLAY_RECORDED_OUTPUTS=1`) is never work done: it counts toward neither
|
|
1046
1076
|
`stepsPinned` nor the upgrade, and a direct run that used one is `diverged` at best. A call that ran
|
|
1047
1077
|
beside a pinned one hands over as `fell_back` or `failed` (§7), and so does a divergence whose
|
|
1048
|
-
bundle stops in front of the other half of an "if … otherwise" (§9.2)
|
|
1078
|
+
bundle stops in front of the other half of an "if … otherwise" (§9.2), or in front of the plan's
|
|
1079
|
+
decision (§8). A direct run cancelled by its caller
|
|
1049
1080
|
(Esc, or `bir-scenario`'s own timeout) books `fell_back` or `failed` with `measured: false`, never
|
|
1050
1081
|
`diverged`: that turn's cost says nothing about what the task costs.
|
|
1051
1082
|
|
|
@@ -1256,6 +1287,15 @@ tokens and every model in the list has a price; with no usage at all, the line s
|
|
|
1256
1287
|
`why="the host sent no usage"`. Such a host hands over one number for the whole run, so with more
|
|
1257
1288
|
than one plan in one run none of them is measured.
|
|
1258
1289
|
|
|
1290
|
+
**A plan stopped at its decision is billed up to the stop (0.3.19).** When a step of it ran, its
|
|
1291
|
+
hand-over takes a usage mark, and its report's window ends there: the agent's decision and the half
|
|
1292
|
+
it took are the agent's own, not the plan's (the service's segmentsbreakdown.md SB-26). This holds
|
|
1293
|
+
for a plan the prompt started, which gets a way to take a mark only when its stop is a decision, and
|
|
1294
|
+
for one an intent match started, whose window already ended at its own retire. Every other plan
|
|
1295
|
+
keeps its window — a plan of the prompt is billed for the whole turn up to the next plan, so the
|
|
1296
|
+
answer it was asked for stays on its bill — and every other hand-over still takes no mark. A
|
|
1297
|
+
decision reached before any step ran takes no mark either: its report is a sample of the whole turn.
|
|
1298
|
+
|
|
1259
1299
|
---
|
|
1260
1300
|
|
|
1261
1301
|
## 12. What the replayed turn records
|
|
@@ -327,7 +327,7 @@ Recording : yes
|
|
|
327
327
|
· the recorder runs in the background (pid 41820); its audit log is ~/.baseinstrunner/logs/<key>.log
|
|
328
328
|
· the BaseIn service last answered the recorder 12 s ago
|
|
329
329
|
· wrapped MCP calls are not approved by the runner: Claude Code asks as it would without it. For headless runs (`claude -p`) allow them in permissions.allow or --allowedTools, or set BIR_CORRELATION_DECISION=allow where the recorder starts
|
|
330
|
-
· recurring sub-tasks
|
|
330
|
+
· segments run mid-task — recurring sub-tasks, and the halves after a plan's decision — and so may whole plans (the default; BIR_SEGMENT_ARM=0 turns this off)
|
|
331
331
|
|
|
332
332
|
No problems found.
|
|
333
333
|
```
|
|
@@ -370,7 +370,8 @@ so there is exactly one switch and it cannot get out of step with itself.
|
|
|
370
370
|
| `BIR_REPLAY_RECORDED_OUTPUTS` | *(unset)* | `1` lets a step that could not be sent (a built-in off `BIR_BUILTIN_TOOLS`, no proxy, a server off the allowlist) stand on its recorded output instead of stopping the plan. Never for a timeout or an upstream error |
|
|
371
371
|
| `BIR_BUILTIN_TOOLS` | `Read,Glob,Grep,Edit,Write,Bash` | Built-ins the runner executes itself during a replay, in the session's own project directory and confined to it. A chain whose only non-MCP steps are on this list arms `direct`. A runner-side step never reaches the host's permission prompts, so leave off any tool you want the model to call itself; `Read,Glob,Grep` is the read-only set (the default up to 0.3.7). `none`: every built-in step is steered in the session. Claude Code sessions only: in a LangChain application a tool with one of these names is the application's own, and the runner runs none (§6.5) |
|
|
372
372
|
| `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 |
|
|
373
|
-
| `
|
|
373
|
+
| `BIR_SEGMENT_ARM` | *(unset: on, from 0.3.19)* | A segment the service hands out runs mid-task: a recurring sub-task, or a half of a task after a plan's decision. `0` is observe-only: what would have armed is logged (`replay.segment_would_arm`) and nothing is replaced. Up to 0.3.18 observe-only was the default. A recorder started before an update keeps its old default until `bir up --restart` |
|
|
374
|
+
| `BIR_INTENT_ARM` | *(unset)* | `1` lets a match found mid-turn arm a plan in the calling tool's place, which means waiting for it (`BIR_INTENT_ARM_BUDGET_MS`, 500); `0` never waits, and then nothing starts mid-task, segments included: what would have armed is logged; unset follows `BIR_SEGMENT_ARM`, so from 0.3.19 it is on unless `BIR_SEGMENT_ARM=0` |
|
|
374
375
|
| `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
|
|
375
376
|
|
|
376
377
|
The `bir` server reads its own, from the environment Claude Code starts it with:
|
|
@@ -493,7 +494,8 @@ Everything replay decides is one line. `grep` for these:
|
|
|
493
494
|
| `replay.step` | One direct step executed |
|
|
494
495
|
| `replay.step_not_taken` | A step's test said this request does not need it, and nothing was sent for it ([calculatedReplay.md](calculatedReplay.md) §9.2). `plan.armed … tests=` lists the tested steps by their place in the plan: `4:hand_over,11:skip` |
|
|
495
496
|
| `replay.diverge` / `replay.compose` | Off-script, and the recovery |
|
|
496
|
-
| `replay.handover` | The plan stopped and handed the rest of the task to the model; `kind=` and `step=` say where |
|
|
497
|
+
| `replay.handover` | The plan stopped and handed the rest of the task to the model; `kind=` and `step=` say where. `kind=decision` (0.3.19) is a plan that ran the steps before its decision and stopped there, as planned (`via=divergence` when a divergence's bundle ran up to it) |
|
|
498
|
+
| `run.fragment … decision=true` / `run.fragment_dropped` | After a plan's decision: the agent's own work from its next call is recorded as a fragment, from which the service makes the other half / a plan the intent check armed took that call instead, so nothing is recorded as the other half |
|
|
497
499
|
| `replay.vetoed` | A direct plan retired without running anything: `run_scenario` was not the first call, or never arrived |
|
|
498
500
|
| `replay.run_ambiguous` | Two sessions had `run_scenario` pending and the request named neither; nothing ran |
|
|
499
501
|
| `replay.cancelled` / `replay.note_kept` | A direct run's caller went away (Esc); no further step started, and the note is kept for the model's next call |
|
|
@@ -578,6 +580,11 @@ A recorded output that stood in for a step (`BIR_REPLAY_RECORDED_OUTPUTS=1`) is
|
|
|
578
580
|
work done. A direct run you cancel (Esc) is booked `fell_back` or `failed` with `measured: false`:
|
|
579
581
|
a turn cut short says nothing about what the task costs.
|
|
580
582
|
|
|
583
|
+
A plan that stops at its decision (runner 0.3.19 or newer) is booked `fell_back` with
|
|
584
|
+
`fallbackKind: decision`, or `failed` when no step ran. Nothing is counted against its steps, and
|
|
585
|
+
when a step ran its cost ends at the stop: the agent's decision and the half it took are not on the
|
|
586
|
+
plan's bill.
|
|
587
|
+
|
|
581
588
|
A turn's cost is **measured** only when its tokens were found and every model of the turn has a
|
|
582
589
|
price in the runner's table. A model with no price used to count as $0, and that zero was called
|
|
583
590
|
measured. Now such a turn is not measured, in Claude Code as in any host, and a `run.unmeasured`
|
|
@@ -656,6 +663,8 @@ Code's pool, as before.
|
|
|
656
663
|
| `run_scenario` answers "The prepared results are not available for this request. Do the task normally with your tools." | The plan could not be delivered — a recorder that does not answer, a timeout, an HTTP error — and the model does the task itself | The `scenario.answered` line on the `bir` server's stderr has the real reason. `BIR_SCENARIO_DEBUG=1` in that server's environment hands the raw reason to the model as an error, for debugging |
|
|
657
664
|
| A hand-over note says a step "was sent but did not answer … may or may not have completed" | The step timed out, or its upstream went away mid-call; nothing after it ran | Check whether its work happened before running it again — the model is told the same. Repeats: look at that upstream, or raise `BIR_STEP_TIMEOUT_MS` |
|
|
658
665
|
| A hand-over note says "a step never runs on another session's upstream" | Two Claude Code sessions are open in the project, and this one has not yet made a call of its own to that server, so bir cannot tell its proxy from the other session's. A direct plan is told apart by the process that started both the `bir` server and the session's proxies, so for one this means an older proxy or `bir` server (restart the session after an update), or a server started through a wrapper such as a shell or `npx` | Expected: the model calls the tool itself. Once the session's own call to the server has gone through, later replays use its proxy |
|
|
666
|
+
| A hand-over note says the plan "ran the steps before a decision, and stopped there" | The service split the plan at its "if … otherwise" (runner 0.3.19 or newer): the plan runs the steps both ways need, then the agent decides which way applies. The note quotes the request's own sentences with the condition | Expected. The agent's next step may start a segment of the way it chose (`replay.intent_armed … kind=segment`); with `BIR_SEGMENT_ARM=0` it does that part itself. `bir investigate` shows the run as `fell_back · decision` |
|
|
667
|
+
| A hand-over note says a step "did not run: it has failed before", and the step never failed | A runner up to 0.3.18 met a plan's decision, a stop it does not know. It stops there too, but its note names a failure and lists the remaining steps | Update the runner (`bir update`, then `bir up --restart`) |
|
|
659
668
|
| A hand-over note says a step "was not run by the host", or "ran with arguments a person chose, not the scenario's" | A LangChain application (§6.5). A person said no to the step, or answered in the tool's place, or a call limit blocked the call (`host_rejected`); or a person changed the call before it ran (`host_edited`) | Expected, and no fault of the plan: the step is booked `skipped`, the plan stops there and the model takes over. It is told not to run the step again — after a refusal, unless the user asks. §7 has what the report says |
|
|
660
669
|
| `recorder.send_failed … error="HTTP 409 … invalid_ticket"` | The execution report's ticket was already redeemed, or belongs to another scenario. Or the report came more than an hour after its plan was handed out | Harmless if the body says `duplicate: true`. Otherwise a rollover raced `SessionEnd`; the server books once. A report that came too late is not booked; the service as built (not deployed yet) takes a report for 24 hours |
|
|
661
670
|
| `recorder.send_failed … error="HTTP 429 …"` | The account is over one of the service's limits, and the answer is `429` with `Retry-After`. The limits are built on the service and not deployed yet; each is a setting there. By default: 120 runs a minute · 3,000 steps a minute (a tool call is two steps) · 10 title requests a minute · 20 requests in any one hour for Calculate, Prefer and Recalculate together (`bir scenario calc` then prints a line that begins `Could not start calculation (HTTP 429`) | What it costs: a create, a step or a report is sent once more, at most 2 s later, then dropped. A dropped step is a hole in its recording. A run whose create got the 429 runs without its plan. A 429 that the second try did not cure counts as the service being unwell: for 5 to 60 seconds no prompt of that recorder waits for a match. An intent probe over its own limit (1,500 a minute) is answered with no match, never with `429` |
|
|
@@ -1337,7 +1346,7 @@ Parameter database: target → setting (revision 4). Undo: bir scenario undo scn
|
|
|
1337
1346
|
| `revision_changed` | The plan changed since you read it: another edit, or a recalculation | `show` it again, check again, save again |
|
|
1338
1347
|
| `not_ready` or `scenario_calculating` | The scenario is not `ready`, or a recalculation is running | Wait for it to finish |
|
|
1339
1348
|
| `undo_refused` | The undo would stop a later step from giving its recorded call, or leave the final answer without its values | Fix that later step first. Or undo it on purpose: `--force --note "why"` |
|
|
1340
|
-
| `not_latest_edit` | The step has changed since the edit you named: a newer entry changed it. For a correction: the plan has changed since it | `bir` prints two commands: undo the newer one first, then yours |
|
|
1349
|
+
| `not_latest_edit` | The step has changed since the edit you named: a newer entry changed it. For a correction: the plan has changed since it. When the newer entry is an `ssplit_…` id, the plan was split at its decision after the correction, and a correction cannot be taken back while the split is in use | `bir` prints two commands: undo the newer one first, then yours. For a split, `bir` says to press Undo on the Split card of the plan's recording page first, and prints the command for yours |
|
|
1341
1350
|
| `already_undone` | That edit was undone already | To redo it, undo the undo. `bir` prints the command |
|
|
1342
1351
|
| `edit_replaced` | A recalculation replaced the plan after that edit | Nothing to undo; the edit is history now |
|
|
1343
1352
|
| `logic_sandbox_unavailable` | The service could not run step code just now (HTTP 503). Nothing was checked, saved or undone | Try again in a minute |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basein/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
4
4
|
"description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/production.md
CHANGED
|
@@ -594,6 +594,18 @@ Truncation is always marked with the original size.
|
|
|
594
594
|
- An Esc-interrupted turn is never booked as a saving (`measured:false`).
|
|
595
595
|
- A step that a host other than Claude Code (§3.2) stopped itself has two stop reasons of its own. `host_rejected`: a person said no, or answered in the tool's place, or a call limit blocked the call. The note says "Step N of M (tool) was not run by the host. The result above was written by the host, not by the tool. Do not run the step again unless the user asks." `host_edited`: a person changed the call. The note says "Step N of M (tool) ran with arguments a person chose, not the scenario's. Its result above is real and its side effects are in place. Do not run it again." The report's `fallbackKind` stays `step_failed`.
|
|
596
596
|
|
|
597
|
+
**The decision stop (0.3.19).** The service may serve a step with `stop: {kind: "decision"}`: the task has an "if … otherwise" there (the service's segmentsbreakdown.md). The plan never decides:
|
|
598
|
+
|
|
599
|
+
- It runs only the steps before the decision, then hands the task back, in steer and direct mode and after a divergence.
|
|
600
|
+
- The note says the plan "ran the steps before a decision, and stopped there", says which steps ran, and quotes the request's own sentences with a condition word (`if`, `otherwise`, `unless` and the others the service uses, Hebrew too; at most 600 characters). It ends "Decide from the results above which way applies, then continue the task". It has no intent line, no remaining steps and no parameters, and never the condition the service keeps (`stop.why`): each would hint at the half the recording took. The directive at the prompt leaves the intent out too, and says the task comes back "at a decision".
|
|
601
|
+
- A plan whose chain has a decision keeps these protections when it stops earlier for another reason (a parked or failed step, a judgement, the budget). Its note has no intent line and no parameters, and lists the remaining steps only up to the decision. Its directive leaves the intent out. An agent sent on to the recorded half's steps would start that half's segment, and the plan would have decided after all.
|
|
602
|
+
- The report is `fell_back` (or `failed` when no step ran) with `fallbackKind: "decision"` and `fallbackStepIndex` the decision step. The decision step gets no verdict, so nothing is counted against it.
|
|
603
|
+
- When a step ran, the hand-over takes the plan's usage mark: the plan is billed up to the stop, and the agent's decision and the half it took are not on its bill. This holds for a plan the prompt started and for one an intent match started. Every other hand-over still takes no mark.
|
|
604
|
+
- The agent's own next call opens the fragment, marked `metadata.decisionOf = {scenarioId, stepIndex}`: the service makes the other half of the task from it. When a plan the intent check armed takes that call instead (a segment of one half), the fragment is dropped (`run.fragment_dropped`): what follows is not the other half.
|
|
605
|
+
- `ranThrough` sends one entry per plan, with the plan's own position (the decision step itself for a decision). The service maps it onto the recording.
|
|
606
|
+
- `bir replay` stops in front of a decision and says so (`stopped before step N …: the plan's decision`).
|
|
607
|
+
- A runner up to 0.3.18 reads the unknown kind as `known_bad_step`: it stops there too, but its note and report name a failure.
|
|
608
|
+
|
|
597
609
|
**Measured cost.**
|
|
598
610
|
|
|
599
611
|
- A turn's cost is measured only when tokens were found and every model of the turn has a price in the runner's table. This holds for every host, Claude Code too. A model outside the table used to cost $0, and that zero was called measured.
|
|
@@ -710,7 +722,7 @@ Truncation is always marked with the original size.
|
|
|
710
722
|
| Remote servers without a literal credential header (OAuth, headersHelper, `${VAR}`) are left unwrapped. Earlier unasked wraps are undone. | The proxy has none of Claude Code's OAuth credentials; wrapped, these servers answered 401 to every call. | `--server <name>` (remembered). |
|
|
711
723
|
| A flag-less install from an installed package writes absolute-path entries and the self-starting SessionStart hook, for `bir wrap` too. | No npx registry round trip per server start (about 1 s), and it works offline. npx installs had nothing to start the recorder. | `--npx`. A checkout, or `bir` run through npx, still writes npx. |
|
|
712
724
|
| Hook timeouts: SessionStart command 15 s (was 30), HTTP 5 (10), UserPromptSubmit 10 (15), PreToolUse 15 (30), PostToolUse and PostToolUseFailure 5 (30), Subagent 3 (5), Stop 5 (10), SessionEnd 3 (30). | The timeout is the user's worst case during an incident. The server's deadlines sit inside it. | Not configurable; `bir setup` rewrites them. |
|
|
713
|
-
|
|
|
725
|
+
| From 0.3.19 segments run mid-task by default (`BIR_SEGMENT_ARM` unset means on), and so may whole plans by intent. An unsteered PreToolUse waits for the intent probe: at most `BIR_INTENT_ARM_BUDGET_MS` (500 ms), and up to 1.5 s more while an armed plan's values are read. From 0.3.0 to 0.3.18 arming was opt-in and the probe was never waited on. | A plan that stops at its decision hands the rest of the task to segments of its halves, and a segment runs only mid-task (the service's segmentsbreakdown.md SB-21). 0.2.12 held every PreToolUse up to 4 s for the probe. | `BIR_SEGMENT_ARM=0` (observe only: what would have armed is logged). `BIR_INTENT_ARM=0` arms nothing mid-task and never waits; its probes then ask for no segment, as in a session of another host, so what would have armed is logged too. |
|
|
714
726
|
| The prompt match budget is 3 s (was 2.5 s), and there is a match gate: over 2 s, prompts stop waiting for 5 min, doubling to 30 min. | A healthy service answers in 300-450 ms. A slower one would otherwise cost every prompt. A false trip costs one hold of replay, never latency. | `BIR_MATCH_BUDGET_MS=2500`. Raising `BIR_MATCH_SLOW_MS` above the budget effectively disables the gate. |
|
|
715
727
|
| Recorded outputs no longer stand in for a step that produced no result; the plan hands over at that step. | Last week's output presented as this turn's result is wrong data. | `BIR_REPLAY_RECORDED_OUTPUTS=1`, only for steps that could not be sent. `bir replay` keeps the stand-ins. |
|
|
716
728
|
| The direct plan budget is 100 s (was 120 s), and direct runs are clamped to 270 s. | Keeps a direct run under Claude Code's 2-minute auto-background of an MCP call, and inside `run_scenario`'s client timeout. | `BIR_REPLAY_PLAN_BUDGET_MS=120000` (or `BIR_REPLAY_BUDGET_MS`). |
|
|
@@ -845,7 +857,7 @@ Truncation is always marked with the original size.
|
|
|
845
857
|
- where recording goes, and as whom;
|
|
846
858
|
- when the service last answered;
|
|
847
859
|
- who approves wrapped MCP calls (the recorder's correlation mode);
|
|
848
|
-
- whether
|
|
860
|
+
- whether segments run mid-task (the default from 0.3.19) or are observed only, and whether whole plans may start mid-task too; a recorder older than this `bir` is named, since it keeps its old default until `bir up --restart`;
|
|
849
861
|
- superseded recorders;
|
|
850
862
|
- an npx-started scenario server;
|
|
851
863
|
- other installed projects.
|
|
@@ -894,8 +906,9 @@ In a folder that is set up for another host (§3.2), doctor prints `Host
|
|
|
894
906
|
| `registeredProxies[]`, `wrappedServers` | proxies that registered (`serverName`, `pid`, `cwd`, `version`) and the wrapped server names |
|
|
895
907
|
| `hooksVersion`, `pid`, `cwd`, `url` | the recorder's version and identity |
|
|
896
908
|
| `hostFields` | `2`: how much of another host's protocol (§3.2) this recorder understands. An adapter reads it before it relies on a field: with 1 it records, and from 2 it may also ask for a plan and step through it. A recorder that does not know another host answers no `hostFields`. |
|
|
897
|
-
| `segmentArm` | whether service
|
|
898
|
-
| `
|
|
909
|
+
| `segmentArm` | whether segments the service hands out may run mid-task (default from 0.3.19: yes; `BIR_SEGMENT_ARM=0`: observed only) |
|
|
910
|
+
| `intentArm` | whether a step's intent may start a plan in its own PreToolUse, which then waits for the probe. It follows `segmentArm` unless `BIR_INTENT_ARM` is set. Absent from a recorder older than 0.3.19. |
|
|
911
|
+
| `sessionCount`, `sessions[]` | at most 50, active ones first: `{sessionId, runId, active, idleMs, ended, steps, recording, replay, intent, fragment}`. `replay` includes `armed`, `unannounced`, `running`, `outcome` and `handover` (`{kind, stepIndex}`; `kind` may be `decision`). `fragment` is `{state}`, with `decision: true` for one scheduled at a plan's decision. |
|
|
899
912
|
|
|
900
913
|
### 8.4 Logs and audit lines
|
|
901
914
|
|
|
@@ -917,7 +930,7 @@ Lines read `[bir] <ISO time> <event> key=value …`, with each field clipped at
|
|
|
917
930
|
| Hooks | `control.listening` (port, `recorder=<state>`, `correlation=`). `control.correlation_allow` (auto-approval opted in). `control.hook_deadline`, `control.hook_unauthorized` (a token from another install), `control.hook_unknown`, `control.hook_oversize`, `control.hook_event_mismatch`, `control.route_failed`, `control.drain_abandoned`. |
|
|
918
931
|
| Recorder lifecycle | `daemon.restart` (replaced an older recorder), `daemon.restart_deferred` (mid-run: next start), `daemon.start_alongside`, `daemon.newer_kept`, `daemon.revived`. `control.port_busy` (holder and fix), `control.already_running`, `control.superseded`, `control.discovery_reclaimed`, `control.log_reopened`, `control.fatal`. `overrides.stale` (a local copy no longer matches its source: run `bir setup`). |
|
|
919
932
|
| Proxy (MCP log) | `proxy.upstream_gone` (exiting with the server), `proxy.recorder_revive`, `tier.downgrade`, `tier.rebound`, `relay.undeliverable`. `upstream.auth_required` (the remote refused the handshake: credentials), `upstream.needs_sign_in`, `upstream.headers_invalid`, `upstream.proxy_env_ignored` (this Node cannot use HTTPS_PROXY), `upstream.proxy_env_invalid`, `upstream.standing_stream_failed` (at most hourly). Verbose only: `proxy.started`, `tier.bound`, `tier.passthrough`, `run.start` / `run.finish`, `upstream.stream_*`. |
|
|
920
|
-
| Replay | `replay.enabled` (at start: servers, threshold, who derives). `plan.armed`. `replay.decision` (also `arm_timeout`, `arm_unannounced`, `no_directive`, `no_delivery_tool`, `missing_target`, `host_records_only`, `host_tool_missing`). `replay.pin`, `replay.pin_declined`, `replay.pin_abandoned`, `replay.pin_overran`, `replay.diverge`, `replay.diverge_overran`, `replay.handover`, `replay.vetoed`, `replay.unsafe_param`, `replay.derive_timeout`, `replay.budget_clamped`, `replay.concurrent_call`, `replay.no_session_proxy`, `replay.run_ambiguous`, `replay.run_abandoned`. |
|
|
933
|
+
| Replay | `replay.enabled` (at start: servers, threshold, who derives). `plan.armed`. `replay.decision` (also `arm_timeout`, `arm_unannounced`, `no_directive`, `no_delivery_tool`, `missing_target`, `host_records_only`, `host_tool_missing`). `replay.pin`, `replay.pin_declined`, `replay.pin_abandoned`, `replay.pin_overran`, `replay.diverge`, `replay.diverge_overran`, `replay.handover`, `replay.vetoed`, `replay.unsafe_param`, `replay.derive_timeout`, `replay.budget_clamped`, `replay.concurrent_call`, `replay.no_session_proxy`, `replay.run_ambiguous`, `replay.run_abandoned`. A plan's decision (0.3.19): `replay.handover kind=decision` (`via=divergence` when a divergence's bundle ran up to it), `run.fragment … decision=true` (the agent's own work after it is recorded), `run.fragment_dropped` (a plan the intent check armed took the agent's next call, so nothing is recorded as the other half), `replay.adhoc_decision` (`bir replay` stopped in front of it). `replay.enabled` also says `segments=run mid-task`, or `observed only` with `BIR_SEGMENT_ARM=0` or `BIR_INTENT_ARM=0`. |
|
|
921
934
|
| Run cost | `run.unmeasured` (the turn's cost is not measured: `why` gives the reason, and `models` the models that have no price in the runner's table). |
|
|
922
935
|
| A host other than Claude Code (§3.2) | `run.start … host=langchain` (`bir investigate` reads the host from it, and raises no `steer_mode` finding for such a turn: steer is the full mode there), `plan.armed … mode=steer host=langchain`. `run.finish … ended=<reason>` (the run did not end with an ordinary stop). `run.unmeasured` (`why="the host sent no usage"`, a model with no price, or a run that made no model call). `run.incomplete` (the host says the recording is not the whole run: its finish carries the `incomplete` mark). `run.host_failed` (the host says the run failed: its recording is closed with that error). The service's new build never picks a run with an `error` or that mark for automatic calculation, and such a run no longer catches its prompt, unless its owner preferred it or it already has a plan (built, not deployed yet). `replay.decision verdict=no-steer code=host_records_only` (the adapter asked for no plan with its prompt) and `code=host_tool_missing` (the plan names a tool that the agent which asked does not have). `replay.own_call` (the model made a call of its own while the plan was live: the plan is handed over). `replay.step_host_stopped` (`stop=rejected` or `stop=edited`). `session.host_refused` (a scope that is empty, longer than 64 characters or not a text: nothing of that session is recorded or replayed). |
|
|
923
936
|
| `bir-scenario` (MCP log) | `scenario.answered` (`ok` means results were delivered; also `partial`, `bound`), `scenario.recorder_revive`, `scenario.switched`, `scenario.hello_failed`, `scenario.tool_timeout`, `scenario.tool_truncated`. |
|
|
@@ -1053,9 +1066,9 @@ Defaults were checked in `src/`.
|
|
|
1053
1066
|
| `BIR_INTENT_MATCHES_PER_TURN` | recorder | 3 | Plans intent may arm per turn. |
|
|
1054
1067
|
| `BIR_INTENT_REQUESTS_PER_TURN` | recorder | 40 | Probes per turn. |
|
|
1055
1068
|
| `BIR_INTENT_SOURCES` | recorder | `text,thinking,tool` | Which text an intent may be read from. |
|
|
1056
|
-
| `BIR_INTENT_ARM` | recorder | unset (follows `BIR_SEGMENT_ARM
|
|
1069
|
+
| `BIR_INTENT_ARM` | recorder | unset (follows `BIR_SEGMENT_ARM`, so on from 0.3.19) | `1`: a probe may arm a plan in its own PreToolUse, and that hook waits for it. `0`: never. Never in a session of another host (§3.2), where a probe only counts. |
|
|
1057
1070
|
| `BIR_INTENT_ARM_BUDGET_MS` | recorder | 500 | How long that PreToolUse waits for the probe. |
|
|
1058
|
-
| `BIR_SEGMENT_ARM` | recorder |
|
|
1071
|
+
| `BIR_SEGMENT_ARM` | recorder | on (from 0.3.19; unset or `1`) | A segment the service hands out runs mid-task: one found by repeated work, or a half after a plan's decision. `0`: observe only, where what would have armed is logged (`replay.segment_would_arm`) and nothing is replaced. Up to 0.3.18 the default was observe only. Not in a session of another host. |
|
|
1059
1072
|
| `BIR_CONTROL_URL` | proxy, scenario | written into wrapped entries | The project's control server. Together with `BIR_CONTROL_TOKEN` it is the SDK channel and wins over discovery. |
|
|
1060
1073
|
| `BIR_CONTROL_TOKEN` | proxy, scenario | unset | Bearer token for the SDK channel. |
|
|
1061
1074
|
| `BIR_PROXY_STAY_UP` | proxy | unset | `1`: keep answering errors after the upstream dies (0.2.12). |
|
|
@@ -1113,6 +1126,7 @@ Defaults were checked in `src/`.
|
|
|
1113
1126
|
- `langChainGuide.md`, which is in the repository's root and not in the npm package: its "Where it stands" table and its part 5 describe the time before the release.
|
|
1114
1127
|
|
|
1115
1128
|
The install scripts cannot set a folder up for such a host, so there is no one-line install for it.
|
|
1129
|
+
8. **A plan's decision (0.3.19): the service first.** Deploy the service build that accepts `fallbackKind: "decision"` before 0.3.19 is published (the service's segmentsbreakdown.md section 8, step 1). An older service answers such a report 400, and the run's cost and saving are lost. Then test 0.3.18 against a plan with a decision (it stops there, reported as `known_bad_step`). The deployed service makes no split while its `SCENARIO_SPLIT` is false, so run this check on a sandbox or local service started with `SCENARIO_SPLIT=true`, with bir 0.3.18 pointed at it: set `BIR_AUTH_URL` to its address, run `bir login` (with an account on that service), then `bir up --restart` in the folder you test in. bir keeps one session, for the service that issued it: without that sign-in nothing is recorded on the local service, and while bir is signed in there, nothing on this PC is recorded on the deployed one. The service's segmentsbreakdown.md section 8, step 2, has the steps. Then point bir back: set `BIR_AUTH_URL` to the deployed service's address (removing it is not enough, since the local sign-in stored the local address), run `bir login` again, then `bir up --restart` in that folder. Then publish 0.3.19, and only then turn the deployed service's `SCENARIO_SPLIT` on.
|
|
1116
1130
|
|
|
1117
1131
|
**Manual end-to-end checks with the real Claude Code.** These are behaviours of Claude Code itself, which the automated tests simulate but cannot prove. Use the Claude Code version you will support; the approval rules and hook schema were read from 2.1.280.
|
|
1118
1132
|
|