@basein/runner 0.3.21 → 0.3.22
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 +7 -16
- package/dist/bin/bir-hooks.js +7 -16
- package/dist/bin/bir-scenario.js +1 -1
- package/dist/bin/bir.js +1 -21
- package/dist/bin/investigate.d.ts +0 -9
- package/dist/bin/investigate.js +14 -42
- package/dist/bin/scenario-edit.js +13 -44
- package/dist/control/recorder-main.d.ts +0 -15
- package/dist/control/recorder-main.js +7 -39
- package/dist/control/server.d.ts +24 -137
- package/dist/control/server.js +101 -995
- package/dist/record/recorder.d.ts +11 -142
- package/dist/record/recorder.js +0 -3
- package/dist/record/remote-recorder.d.ts +1 -9
- package/dist/record/remote-recorder.js +3 -49
- package/dist/record/switchable-recorder.js +0 -2
- package/dist/replay/bundle.d.ts +2 -2
- package/dist/replay/bundle.js +3 -3
- package/dist/replay/controller.d.ts +19 -123
- package/dist/replay/controller.js +151 -518
- package/dist/replay/derive.d.ts +0 -10
- package/dist/replay/derive.js +0 -3
- package/dist/replay/handover.d.ts +2 -37
- package/dist/replay/handover.js +38 -106
- package/dist/replay/logic.d.ts +0 -11
- package/dist/replay/logic.js +0 -24
- package/dist/replay/plan.d.ts +43 -163
- package/dist/replay/plan.js +144 -556
- package/dist/replay/safety.d.ts +4 -5
- package/dist/replay/safety.js +7 -7
- package/dist/replay/source-run.d.ts +15 -24
- package/dist/replay/source-run.js +25 -58
- package/dist/replay/targets.d.ts +11 -21
- package/dist/replay/targets.js +30 -69
- package/dist/replay/types.d.ts +9 -96
- package/dist/replay/types.js +3 -16
- package/dist/util/service-headers.d.ts +1 -2
- package/dist/util/service-headers.js +1 -2
- package/docs/calculatedReplay.md +17 -74
- package/docs/calculatedReplayGuide.md +9 -30
- package/package.json +1 -1
- package/production.md +5 -35
- package/dist/replay/condition-words.d.ts +0 -28
- package/dist/replay/condition-words.js +0 -59
- package/dist/replay/flatten.d.ts +0 -161
- package/dist/replay/flatten.js +0 -222
- package/dist/replay/reads-only.d.ts +0 -29
- package/dist/replay/reads-only.js +0 -261
package/dist/bin/bir-hooks.d.ts
CHANGED
|
@@ -104,22 +104,13 @@
|
|
|
104
104
|
* found (default 5; 0 off)
|
|
105
105
|
* BIR_INTENT_SOURCES which text a step's intent may come from
|
|
106
106
|
* (default text,thinking,tool)
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* BIR_INTENT_ARM=0), and then at most
|
|
115
|
-
* BIR_INTENT_ARM_BUDGET_MS (default 500)
|
|
116
|
-
* BIR_SEGMENT_ARM unset or 1 (the default from 0.3.19): a segment
|
|
117
|
-
* the service hands out runs mid-task — one found
|
|
118
|
-
* by repeated work, or a half after a plan's
|
|
119
|
-
* decision — and so may a whole plan whose intent
|
|
120
|
-
* a step matches (BIR_INTENT_ARM follows it unless
|
|
121
|
-
* set). 0 is observe-only: what would have armed
|
|
122
|
-
* is logged and nothing is replaced
|
|
107
|
+
* BIR_INTENT_ARM 1 lets a whole plan whose intent matches the
|
|
108
|
+
* agent's reasoning mid-turn arm in the place of the
|
|
109
|
+
* call. Unset (or 0) is off: no probe is sent and
|
|
110
|
+
* nothing arms mid-turn
|
|
111
|
+
* BIR_INTENT_ARM_BUDGET_MS how long a call waits for that probe (default 500)
|
|
112
|
+
* BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40)
|
|
113
|
+
* BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 1500)
|
|
123
114
|
* BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (unset: the
|
|
124
115
|
* controller's 100000, BIR_REPLAY_PLAN_BUDGET_MS)
|
|
125
116
|
* BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
|
package/dist/bin/bir-hooks.js
CHANGED
|
@@ -104,22 +104,13 @@
|
|
|
104
104
|
* found (default 5; 0 off)
|
|
105
105
|
* BIR_INTENT_SOURCES which text a step's intent may come from
|
|
106
106
|
* (default text,thinking,tool)
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* BIR_INTENT_ARM=0), and then at most
|
|
115
|
-
* BIR_INTENT_ARM_BUDGET_MS (default 500)
|
|
116
|
-
* BIR_SEGMENT_ARM unset or 1 (the default from 0.3.19): a segment
|
|
117
|
-
* the service hands out runs mid-task — one found
|
|
118
|
-
* by repeated work, or a half after a plan's
|
|
119
|
-
* decision — and so may a whole plan whose intent
|
|
120
|
-
* a step matches (BIR_INTENT_ARM follows it unless
|
|
121
|
-
* set). 0 is observe-only: what would have armed
|
|
122
|
-
* is logged and nothing is replaced
|
|
107
|
+
* BIR_INTENT_ARM 1 lets a whole plan whose intent matches the
|
|
108
|
+
* agent's reasoning mid-turn arm in the place of the
|
|
109
|
+
* call. Unset (or 0) is off: no probe is sent and
|
|
110
|
+
* nothing arms mid-turn
|
|
111
|
+
* BIR_INTENT_ARM_BUDGET_MS how long a call waits for that probe (default 500)
|
|
112
|
+
* BIR_INTENT_REQUESTS_PER_TURN probes one turn may send (default 40)
|
|
113
|
+
* BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 1500)
|
|
123
114
|
* BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (unset: the
|
|
124
115
|
* controller's 100000, BIR_REPLAY_PLAN_BUDGET_MS)
|
|
125
116
|
* BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
|
package/dist/bin/bir-scenario.js
CHANGED
|
@@ -488,7 +488,7 @@ class BadArguments extends Error {
|
|
|
488
488
|
}
|
|
489
489
|
const TARGET = {
|
|
490
490
|
type: "string",
|
|
491
|
-
description: "A scn_ id, or the run_ id of
|
|
491
|
+
description: "A scn_ id, or the run_ id of the recording it was calculated from — as investigate and scenario_show print them.",
|
|
492
492
|
};
|
|
493
493
|
const STEP = {
|
|
494
494
|
type: "integer",
|
package/dist/bin/bir.js
CHANGED
|
@@ -114,7 +114,7 @@ Commands:
|
|
|
114
114
|
a scenario: intent, params, steps (--step: just that one)
|
|
115
115
|
scenario calc <runId|scnId> [--force [--discard-edits]]
|
|
116
116
|
calculate (or re-derive) a run's scenario; a scn_ id
|
|
117
|
-
|
|
117
|
+
is recalculated in place, with --force
|
|
118
118
|
scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze]
|
|
119
119
|
try a change to one step against the recording; saves nothing
|
|
120
120
|
scenario edit <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->]
|
|
@@ -3118,20 +3118,6 @@ async function doctor(args) {
|
|
|
3118
3118
|
"For headless runs (`claude -p`) allow them in permissions.allow or --allowedTools, or set " +
|
|
3119
3119
|
"BIR_CORRELATION_DECISION=allow where the recorder starts");
|
|
3120
3120
|
}
|
|
3121
|
-
// Whether a segment the service hands out may run mid-task (segmented.md
|
|
3122
|
-
// R-LIFE-8). On by default from 0.3.19 (the service's segmentsbreakdown.md
|
|
3123
|
-
// SB-21); either state must be visible without reading a log, and an older
|
|
3124
|
-
// recorder still running here keeps the old default until it is replaced.
|
|
3125
|
-
// A segment starts only where the probe is waited on, so `intentArm: false`
|
|
3126
|
-
// (BIR_INTENT_ARM=0) starts nothing mid-task, segments included; a
|
|
3127
|
-
// recorder older than that field says nothing of it.
|
|
3128
|
-
notes.push(health.segmentArm === true && health.intentArm !== false
|
|
3129
|
-
? "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)"
|
|
3130
|
-
: health.segmentArm === true
|
|
3131
|
-
? "segments are observed only — nothing starts mid-task (BIR_INTENT_ARM=0 where the recorder started)"
|
|
3132
|
-
: theirs && theirs !== VERSION
|
|
3133
|
-
? `segments are observed only — this recorder (${theirs}) is older than this bir, which runs them by default: \`bir up --restart\``
|
|
3134
|
-
: `segments are observed only — nothing is replaced (BIR_SEGMENT_ARM=0 where the recorder started)${health.intentArm === true ? "; whole plans may still start mid-task (BIR_INTENT_ARM=1)" : ""}`);
|
|
3135
3121
|
const sessions = health.sessions ?? [];
|
|
3136
3122
|
const probes = sessions.reduce((n, s) => n + (s.intent?.probes ?? 0), 0);
|
|
3137
3123
|
const armed = sessions.reduce((n, s) => n + (s.intent?.armed ?? 0), 0);
|
|
@@ -3721,12 +3707,6 @@ async function replayCommand(args) {
|
|
|
3721
3707
|
}
|
|
3722
3708
|
out(`response ${JSON.stringify(result.responseModel ?? {})}`);
|
|
3723
3709
|
out(`steps ${result.steps ?? 0}${result.partial ? " (partial — budget reached)" : ""}`);
|
|
3724
|
-
// A plan's decision (the service's segmentsbreakdown.md SB-6): the steps
|
|
3725
|
-
// from there are the half the recording took, so the run stops before them.
|
|
3726
|
-
if (result.stopped?.reason === "decision") {
|
|
3727
|
-
out(`stopped before step ${result.stopped.step}${result.stopped.tool ? ` (${result.stopped.tool})` : ""}: ` +
|
|
3728
|
-
"the plan's decision, which the agent makes — the steps from there did not run");
|
|
3729
|
-
}
|
|
3730
3710
|
return 0;
|
|
3731
3711
|
}
|
|
3732
3712
|
// ── wrap ───────────────────────────────────────────────────────────────────
|
|
@@ -245,13 +245,6 @@ interface ServiceInvestigation {
|
|
|
245
245
|
runId?: string | null;
|
|
246
246
|
} | null;
|
|
247
247
|
corrections?: ServiceCorrection[];
|
|
248
|
-
/**
|
|
249
|
-
* The step the plan stops in front of so that the agent decides, and the
|
|
250
|
-
* condition in words (the service's segmentsbreakdown.md SB-6). Null when
|
|
251
|
-
* the plan has none; absent from an older service.
|
|
252
|
-
*/
|
|
253
|
-
decisionStep?: number | null;
|
|
254
|
-
decisionText?: string | null;
|
|
255
248
|
} | null;
|
|
256
249
|
executions?: Array<{
|
|
257
250
|
id: string;
|
|
@@ -272,8 +265,6 @@ interface ServiceInvestigation {
|
|
|
272
265
|
status: string;
|
|
273
266
|
error?: string;
|
|
274
267
|
}> | null;
|
|
275
|
-
/** Where a `fell_back` run handed over, and why (`decision`, …): null for any other run. */
|
|
276
|
-
fallbackKind?: string | null;
|
|
277
268
|
/** The repeat the agent redid the task in, and its own calls there (AC1). */
|
|
278
269
|
redoneByRunId?: string | null;
|
|
279
270
|
redoneOwnCalls?: number | null;
|
package/dist/bin/investigate.js
CHANGED
|
@@ -35,7 +35,6 @@ const INCIDENTS = new Set([
|
|
|
35
35
|
"replay.arm_failed",
|
|
36
36
|
"replay.derive_failed",
|
|
37
37
|
"replay.compose_failed",
|
|
38
|
-
"replay.flatten_failed",
|
|
39
38
|
"replay.thread_failed",
|
|
40
39
|
"replay.step_failed",
|
|
41
40
|
"replay.failed",
|
|
@@ -203,8 +202,8 @@ export function findTurn(turns, id) {
|
|
|
203
202
|
/** The runner's own gate vocabulary, with the cause and the fix (guide §9). */
|
|
204
203
|
export const DECLINES = {
|
|
205
204
|
replay_disabled: {
|
|
206
|
-
cause: "calculated replay was off in this bir-hooks (BIR_REPLAY=0)
|
|
207
|
-
fix: "start bir-hooks with BIR_REPLAY=1 (the default)
|
|
205
|
+
cause: "calculated replay was off in this bir-hooks (BIR_REPLAY=0).",
|
|
206
|
+
fix: "start bir-hooks with BIR_REPLAY=1 (the default).",
|
|
208
207
|
},
|
|
209
208
|
not_ready: {
|
|
210
209
|
cause: "the matched recording had no ready scenario at that moment.",
|
|
@@ -222,16 +221,12 @@ export const DECLINES = {
|
|
|
222
221
|
},
|
|
223
222
|
known_bad_first_step: {
|
|
224
223
|
cause: "the chain's first step is parked after repeated failures.",
|
|
225
|
-
fix: "wait: the service retries it once an hour for a day, then repairs the plan from the next run of the prompt — unless the plan has steps edited by hand,
|
|
224
|
+
fix: "wait: the service retries it once an hour for a day, then repairs the plan from the next run of the prompt — unless the plan has steps edited by hand, which it does not repair on its own. To repair now: `bir scenario calc <scnId> --force` (add --discard-edits to drop hand edits), or fix the step: `bir scenario edit <scnId> --step 0 --input-logic <file>`. `bir investigate <scnId>` names the scenario id.",
|
|
226
225
|
},
|
|
227
226
|
nondeterministic_first_step: {
|
|
228
227
|
cause: "the chain's first step needs a judgement the calculation could not write code for, so the plan cannot start.",
|
|
229
228
|
fix: "if its code only has a slip, fix the step yourself: read it with `bir scenario show <scnId> --step 0`, write the input logic to a file, and run `bir scenario edit <scnId> --step 0 --input-logic <file>`; the service checks it against the recording before it saves it. A real judgement — a choice with no fixed rule — stays with the agent until a model step exists (plan-services.md W3.1).",
|
|
230
229
|
},
|
|
231
|
-
unusable_first_step: {
|
|
232
|
-
cause: "the first step calls a sub-task that is gone, switched off or stale.",
|
|
233
|
-
fix: "enable or recalculate that sub-task, or recalculate this scenario.",
|
|
234
|
-
},
|
|
235
230
|
missing_target: {
|
|
236
231
|
cause: "the plan needs a target — a value a prompt must name, such as a file, a table or a region — and the prompt did not name it. A replay never takes a target from the recording.",
|
|
237
232
|
fix: 'name it in the prompt. If the value is the same every time — a database, a host, a working folder — it is a setting, not a target: `bir scenario edit <scnId> --param <name> --kind setting --note "why"` lets the runner use its recorded value when a prompt names none (`bir scenario show <scnId>` lists the parameters and their kinds).',
|
|
@@ -246,11 +241,6 @@ export const DECLINES = {
|
|
|
246
241
|
cause: "the plan was not ready before the hook that would have announced it had to answer: the prompt hook waits 7.5 s in all, match included, and a tool call that starts a plan mid-task waits 1.5 s. What arming waits for is the reading of the prompt's values (the derivation). A plan that was not announced is never run.",
|
|
247
242
|
fix: "run the prompt again. If it keeps happening the reading is slow: the recorder's log (`bir status` names the file) shows when its `replay.derived` line landed, and a dry replay shows what the service's reading took, under `reading` (`bir scenario replay <scnId> --prompt \"…\" --dry --json`). BIR_HOOK_DERIVE_BUDGET_MS does not help here, because the hook's own limit was reached first. To read values on this machine instead of asking the service, set BIR_DERIVE_API_KEY where Claude Code starts.",
|
|
248
243
|
},
|
|
249
|
-
// Nor this one: the control server's own rule (the service's offer1.md O8).
|
|
250
|
-
would_repeat: {
|
|
251
|
-
cause: "a plan the intent check matched would have started after the agent's call, not in its place, and its steps call that same tool: it would have made the call a second time — an e-mail sent twice (issue fx2). Never twice: it was not started, and the agent went on.",
|
|
252
|
-
fix: "nothing to fix in the plan. A scenario starts in the call's place only when the agent's call is its first step, value for value; one recorded in the order the agent works in (the agent's own run after a decision becomes one) is what starts there.",
|
|
253
|
-
},
|
|
254
244
|
// Nor is this one: the match itself was late, so nothing was armed at all.
|
|
255
245
|
match_late: {
|
|
256
246
|
cause: "the service's answer to the match landed too late: after the prompt hook had stopped waiting for it (3 s, BIR_MATCH_BUDGET_MS); while prompts were not waiting on the match at all — after one slow answer they stop waiting for a few minutes (`replay.match_gate state=holding`); or with the hook not waiting from the start, because a failure to reach the service was still on record (a `service.degraded` line, perhaps hours before, and no `service.recovered` until after this prompt) or the match request's first attempt failed. The agent did the task itself.",
|
|
@@ -264,10 +254,6 @@ export const DECLINES = {
|
|
|
264
254
|
cause: "the service matched the prompt and handed over no plan: this runner is older than its plans need.",
|
|
265
255
|
fix: "update the runner (`bir update`), then run `bir setup` in each project that uses it; `bir doctor` says when the service is satisfied.",
|
|
266
256
|
},
|
|
267
|
-
flatten_failed: {
|
|
268
|
-
cause: "the chain could not be flattened — a sub-task it calls no longer resolves.",
|
|
269
|
-
fix: "recalculate the scenario.",
|
|
270
|
-
},
|
|
271
257
|
// The gates of a host that is not Claude Code (a LangChain application).
|
|
272
258
|
host_records_only: {
|
|
273
259
|
cause: "the runner in that application's folder is older than replay for LangChain needs, so the LangChain package only records.",
|
|
@@ -439,11 +425,9 @@ export function localFindings(turn) {
|
|
|
439
425
|
}
|
|
440
426
|
for (const i of turn.incidents) {
|
|
441
427
|
const fields = Object.fromEntries(Object.entries(i).filter(([k]) => k !== "at" && k !== "event"));
|
|
442
|
-
// A stop at the plan's decision is the plan working as split: no fault.
|
|
443
|
-
const decisionStop = i.event === "replay.handover" && i.kind === "decision";
|
|
444
428
|
out.push({
|
|
445
429
|
code: i.event.replace(/\./g, "_"),
|
|
446
|
-
severity: i.event.endsWith("_failed") || i.event === "replay.failed" ? "problem" :
|
|
430
|
+
severity: i.event.endsWith("_failed") || i.event === "replay.failed" ? "problem" : "warn",
|
|
447
431
|
title: `${i.event}${i.error ? `: ${String(i.error)}` : ""}`,
|
|
448
432
|
cause: Object.entries(fields)
|
|
449
433
|
.map(([k, v]) => `${k}=${String(v)}`)
|
|
@@ -454,15 +438,13 @@ export function localFindings(turn) {
|
|
|
454
438
|
: "the turn's cost could not be read from the transcript, or the runner has no price for the model: update it. Its report is unmeasured; run bir-hooks where the Claude Code transcript is readable."
|
|
455
439
|
: i.event === "replay.handover" && i.kind === "other_branch"
|
|
456
440
|
? "this request was the other half of the task the plan holds (a step's test said so), and the agent did it; that run is how the service learns the other half."
|
|
457
|
-
:
|
|
458
|
-
? "
|
|
459
|
-
: i.event === "
|
|
460
|
-
? "the
|
|
461
|
-
: i.event === "run.
|
|
462
|
-
? "the
|
|
463
|
-
:
|
|
464
|
-
? "the application's run failed; its recording is closed with that error and never becomes a plan."
|
|
465
|
-
: "see the guide's troubleshooting table for this line.",
|
|
441
|
+
: i.event === "replay.diverge" || i.event === "replay.handover"
|
|
442
|
+
? "the model left the script or the runner handed the turn back; the service section shows what the fallback cost."
|
|
443
|
+
: i.event === "run.incomplete"
|
|
444
|
+
? "the recording is not the whole run, so it is never calculated and no longer catches its prompt; run the task again to record it whole."
|
|
445
|
+
: i.event === "run.host_failed"
|
|
446
|
+
? "the application's run failed; its recording is closed with that error and never becomes a plan."
|
|
447
|
+
: "see the guide's troubleshooting table for this line.",
|
|
466
448
|
});
|
|
467
449
|
}
|
|
468
450
|
const r = turn.reported;
|
|
@@ -545,12 +527,6 @@ export function savedPct(turn) {
|
|
|
545
527
|
return baseline > 0 ? r.savedUsd / baseline : null;
|
|
546
528
|
}
|
|
547
529
|
const short = (id) => (id ? id.slice(0, 13) : "—");
|
|
548
|
-
/**
|
|
549
|
-
* An execution's outcome, and where it handed the task over when the service
|
|
550
|
-
* says (`fell_back · decision`). The service sends `fallbackKind: null` for a
|
|
551
|
-
* run that did not hand over, and an older one sends nothing.
|
|
552
|
-
*/
|
|
553
|
-
const outcomeOf = (e) => typeof e.fallbackKind === "string" && e.fallbackKind !== "" ? `${e.outcome} · ${e.fallbackKind}` : e.outcome;
|
|
554
530
|
/** 4624 → "4,624", whatever the machine's locale. */
|
|
555
531
|
const count = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
556
532
|
const money = (v) => (v == null ? "—" : usd(v));
|
|
@@ -653,10 +629,6 @@ export function renderService(out, s, viewerIsOwner) {
|
|
|
653
629
|
if (sc.held) {
|
|
654
630
|
out(` HELD since ${when(sc.held.at ?? undefined)}${sc.held.reason ? ` (${sc.held.reason})` : ""}: not handed out — the agent does the task until the plan is corrected or let go${sc.held.runId ? `; the redo is ${sc.held.runId}` : ""}`);
|
|
655
631
|
}
|
|
656
|
-
if (typeof sc.decisionStep === "number") {
|
|
657
|
-
const text = typeof sc.decisionText === "string" ? sc.decisionText.replace(/\s+/g, " ").trim() : "";
|
|
658
|
-
out(` decision at step ${sc.decisionStep}: the plan stops in front of it, and the agent decides${text ? ` (${text.length > 160 ? `${text.slice(0, 160)}…` : text})` : ""}`);
|
|
659
|
-
}
|
|
660
632
|
for (const line of stepLines(sc.steps))
|
|
661
633
|
out(` ${line}`);
|
|
662
634
|
const correction = newestCorrection(sc.corrections);
|
|
@@ -674,7 +646,7 @@ export function renderService(out, s, viewerIsOwner) {
|
|
|
674
646
|
if (ex.length > 0) {
|
|
675
647
|
out(` executions (${ex.length}, newest first)`);
|
|
676
648
|
for (const e of ex.slice(0, 10)) {
|
|
677
|
-
out(` ${when(e.createdAt)} ${short(e.id)} ${
|
|
649
|
+
out(` ${when(e.createdAt)} ${short(e.id)} ${e.outcome.padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
|
|
678
650
|
const failed = e.steps?.filter((st) => st.status === "failed") ?? [];
|
|
679
651
|
for (const st of failed)
|
|
680
652
|
out(` step ${st.stepIndex} ${st.toolName ?? ""} failed${st.error ? `: ${st.error.slice(0, 120)}` : ""}`);
|
|
@@ -702,7 +674,7 @@ function stepLines(steps) {
|
|
|
702
674
|
if (st.correctionId)
|
|
703
675
|
parts.push(`${st.correctionKind ?? "changed"} by correction ${st.correctionId}`);
|
|
704
676
|
if (parts.length > 0)
|
|
705
|
-
lines.push(`step ${st.stepIndex} ${st.toolName ?? "
|
|
677
|
+
lines.push(`step ${st.stepIndex} ${st.toolName ?? "?"}: ${parts.join("; ")}`);
|
|
706
678
|
}
|
|
707
679
|
return lines;
|
|
708
680
|
}
|
|
@@ -809,7 +781,7 @@ export async function investigateCommand(args, deps) {
|
|
|
809
781
|
if (rows.length === 0)
|
|
810
782
|
out(" none yet");
|
|
811
783
|
for (const e of rows) {
|
|
812
|
-
out(` ${when(e.createdAt)} ${e.id} ${
|
|
784
|
+
out(` ${when(e.createdAt)} ${e.id} ${e.outcome.padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
|
|
813
785
|
out(` scenario ${e.scenarioId}${e.runId ? ` run ${e.runId}` : ""}${e.intent ? ` "${e.intent.slice(0, 60)}"` : ""}`);
|
|
814
786
|
}
|
|
815
787
|
out();
|
|
@@ -100,9 +100,8 @@ export async function scenarioEditCommand(args, deps) {
|
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* The raw JSON, as it always was — scripts and the guide read it. A `scn_` id
|
|
103
|
-
* goes to `GET /scenarios/:id
|
|
104
|
-
*
|
|
105
|
-
* with the scenario's id and `chainRevision` put in front of it. The revision
|
|
103
|
+
* goes to `GET /scenarios/:id`; `--step` narrows the print to that one step
|
|
104
|
+
* object, with the scenario's id and `chainRevision` put in front of it. The revision
|
|
106
105
|
* is a scenario's, not a step's, and it is what `edit --revision` (and the MCP
|
|
107
106
|
* `scenario_edit`'s `revision`) takes: a step printed without it sent the model
|
|
108
107
|
* that had just read the step off to guess one (C20).
|
|
@@ -130,7 +129,7 @@ async function show(args, deps) {
|
|
|
130
129
|
const served = reply.body;
|
|
131
130
|
const scenario = served?.scenario;
|
|
132
131
|
if (!scenario)
|
|
133
|
-
return
|
|
132
|
+
return noScenario(target, args, deps);
|
|
134
133
|
if (!Array.isArray(scenario.steps)) {
|
|
135
134
|
out(`${scenario.id ?? target} is ${scenario.state ?? "not ready"}, so it has no steps to show yet.`);
|
|
136
135
|
return 1;
|
|
@@ -510,7 +509,7 @@ function inputPhrase(status, verifiable) {
|
|
|
510
509
|
case "not_checked":
|
|
511
510
|
return verifiable
|
|
512
511
|
? "not checked: the recorded call is plain text, not an object"
|
|
513
|
-
: "not checked: the
|
|
512
|
+
: "not checked: the recording holds no call to check this step against";
|
|
514
513
|
case "differs":
|
|
515
514
|
return "differs from the recorded call";
|
|
516
515
|
case "throws":
|
|
@@ -955,12 +954,10 @@ function paramUndone(answer, editId, target, args, deps) {
|
|
|
955
954
|
* step from the recording, so the service refuses `--force` on a plan with
|
|
956
955
|
* hand edits (D4) unless `--discard-edits` says to throw them away.
|
|
957
956
|
*
|
|
958
|
-
* A `run_` id posts `/recordings/runs/:id/calculate
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
* A scenario id always names one that exists, so it is always a
|
|
963
|
-
* recalculation in place, and asks for `--force` as the run form does.
|
|
957
|
+
* A `run_` id posts `/recordings/runs/:id/calculate`. A `scn_` id posts
|
|
958
|
+
* `/scenarios/:id/recalculate`. A scenario id always names one that exists,
|
|
959
|
+
* so it is always a recalculation in place, and asks for `--force` as the run
|
|
960
|
+
* form does.
|
|
964
961
|
*/
|
|
965
962
|
async function calc(args, deps) {
|
|
966
963
|
const { out } = deps;
|
|
@@ -1106,28 +1103,13 @@ function localFailure(args, deps, code, text, extra = {}) {
|
|
|
1106
1103
|
deps.out(args.json ? jsonText({ error: code, details: text, ...extra }) : text);
|
|
1107
1104
|
return 1;
|
|
1108
1105
|
}
|
|
1109
|
-
/**
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
* they have — instead of suggesting a calculation, which would start a new
|
|
1113
|
-
* whole-run plan beside them rather than reach the one the person meant (C21).
|
|
1114
|
-
*/
|
|
1115
|
-
function noWholeRunScenario(target, segments, args, deps) {
|
|
1116
|
-
const listed = (Array.isArray(segments) ? segments : []).filter((s) => typeof s?.id === "string");
|
|
1117
|
-
if (listed.length === 0) {
|
|
1118
|
-
return localFailure(args, deps, "no_scenario", `${target} has no calculated scenario yet — \`bir scenario calc ${target}\` first.`);
|
|
1119
|
-
}
|
|
1120
|
-
const range = (s) => typeof s.stepFrom === "number" && typeof s.stepTo === "number" ? ` steps ${s.stepFrom}–${s.stepTo} of the recording` : "";
|
|
1121
|
-
const text = [
|
|
1122
|
-
`${target} has no whole-run scenario, only sub-task scenarios — and a sub-task scenario is addressed by its own scn_ id:`,
|
|
1123
|
-
...listed.map((s) => ` ${s.id}${range(s)}`),
|
|
1124
|
-
].join("\n");
|
|
1125
|
-
return localFailure(args, deps, "no_whole_run_scenario", text, { segments: listed.map((s) => s.id) });
|
|
1106
|
+
/** A run with no calculated scenario yet. */
|
|
1107
|
+
function noScenario(target, args, deps) {
|
|
1108
|
+
return localFailure(args, deps, "no_scenario", `${target} has no calculated scenario yet — \`bir scenario calc ${target}\` first.`);
|
|
1126
1109
|
}
|
|
1127
1110
|
/**
|
|
1128
1111
|
* A `run_` id is the recording's; the routes that change a scenario take its
|
|
1129
|
-
* `scn_` id (D7)
|
|
1130
|
-
* `scn_` id is used as it is — sub-task scenarios have nothing else.
|
|
1112
|
+
* `scn_` id (D7), and a `scn_` id is used as it is.
|
|
1131
1113
|
*/
|
|
1132
1114
|
async function resolveTarget(target, args, deps, clock = new ReadClock()) {
|
|
1133
1115
|
if (target.startsWith("scn_"))
|
|
@@ -1145,7 +1127,7 @@ async function resolveTarget(target, args, deps, clock = new ReadClock()) {
|
|
|
1145
1127
|
const served = reply.body;
|
|
1146
1128
|
const id = served?.scenario?.id;
|
|
1147
1129
|
if (typeof id !== "string")
|
|
1148
|
-
return { code:
|
|
1130
|
+
return { code: noScenario(target, args, deps) };
|
|
1149
1131
|
return { id };
|
|
1150
1132
|
}
|
|
1151
1133
|
/**
|
|
@@ -1199,9 +1181,6 @@ function explain(reply, args, deps, e) {
|
|
|
1199
1181
|
out(`${e.verb} (invalid_input): the service found the request invalid${details ? ` — ${details}` : ""}.`);
|
|
1200
1182
|
return 1;
|
|
1201
1183
|
}
|
|
1202
|
-
case "not_a_tool_step":
|
|
1203
|
-
out(`${e.verb} (not_a_tool_step): step ${e.step ?? "?"} is a sub-task call row, not a tool step. Call rows belong to the service; only tool steps can be edited.`);
|
|
1204
|
-
return 1;
|
|
1205
1184
|
case "nothing_to_change":
|
|
1206
1185
|
// The empty command is refused on this side before any request, so a
|
|
1207
1186
|
// service `nothing_to_change` always means the change is already the
|
|
@@ -1231,16 +1210,6 @@ function explain(reply, args, deps, e) {
|
|
|
1231
1210
|
// newer entry puts the step back as this one left it, and then this one
|
|
1232
1211
|
// can be undone. It used to loop, when undo only toggled (C23).
|
|
1233
1212
|
const latest = typeof body.latestEditId === "string" ? body.latestEditId : "<the newer edit>";
|
|
1234
|
-
if (latest.startsWith("ssplit_")) {
|
|
1235
|
-
// The plan was split at its decision since (the service's
|
|
1236
|
-
// segmentsbreakdown.md). No `--edit` takes a split back: its Undo is
|
|
1237
|
-
// on the recording page's Split card, and only then can this go.
|
|
1238
|
-
const what = aboutCorrection ? "correction" : "edit";
|
|
1239
|
-
out(`${e.verb} (not_latest_edit): the plan was split at its decision after that ${what} (${latest}). ` +
|
|
1240
|
-
`It cannot be taken back while the split is in use: undo the split first (Undo on the Split card of the plan's recording page), then this ${what}:`);
|
|
1241
|
-
out(` bir scenario undo ${target} --edit ${e.editId ?? "<this edit>"}`);
|
|
1242
|
-
return 1;
|
|
1243
|
-
}
|
|
1244
1213
|
out(aboutCorrection
|
|
1245
1214
|
? `${e.verb} (not_latest_edit): the plan has changed since that correction — ${latest} changed it after. Undo the newer one first, then this one:`
|
|
1246
1215
|
: `${e.verb} (not_latest_edit): the ${aboutParam ? "parameter" : "step"} has changed since that edit — ${latest} changed it after. Undo the newer one first, then this one:`);
|
|
@@ -52,21 +52,6 @@ export declare function derivationKey(daemon?: boolean): {
|
|
|
52
52
|
apiKey?: string;
|
|
53
53
|
source?: string;
|
|
54
54
|
};
|
|
55
|
-
/**
|
|
56
|
-
* `BIR_SEGMENT_ARM`: whether a segment the service hands out may run mid-task.
|
|
57
|
-
* On unless it is `0`, from runner 0.3.19 (the service's segmentsbreakdown.md
|
|
58
|
-
* SB-21): segments found by repeated work, and the halves after a plan's
|
|
59
|
-
* decision. `0` is observe-only: what would have armed is logged, and nothing
|
|
60
|
-
* is replaced. While it is on, whole plans may start mid-task by intent too:
|
|
61
|
-
* `BIR_INTENT_ARM` follows it unless that is set.
|
|
62
|
-
*/
|
|
63
|
-
export declare function segmentArmFromEnv(env?: NodeJS.ProcessEnv): boolean;
|
|
64
|
-
/**
|
|
65
|
-
* What the `replay.enabled` startup line says segments do. A segment starts
|
|
66
|
-
* only where the probe is waited on, so `BIR_INTENT_ARM=0` starts nothing
|
|
67
|
-
* mid-task, segments included — the rule `bir doctor` words from /health.
|
|
68
|
-
*/
|
|
69
|
-
export declare function segmentsAtStartup(segmentArm: boolean, env?: NodeJS.ProcessEnv): string;
|
|
70
55
|
/** Where the two replay switches came from, for `/health` and `bir doctor`. */
|
|
71
56
|
export type ReplaySource = "env" | "sidecar" | "default";
|
|
72
57
|
/** What a recorder still serves (K4, `ControlServer.activity()`). */
|
|
@@ -85,9 +85,8 @@ function wrappedServersFor(cwd) {
|
|
|
85
85
|
* `BIR_INTENT_SOURCES` — which text a step's intent may be read from
|
|
86
86
|
* (segmented.md R-INTENT-3). Unset or unparseable means all three.
|
|
87
87
|
*
|
|
88
|
-
* It governs only what is *sent*.
|
|
89
|
-
*
|
|
90
|
-
* line from the tool name and arguments (R-HIT-4).
|
|
88
|
+
* It governs only what is *sent*. With every source off, a step goes out with
|
|
89
|
+
* no text, and no intent probe is sent for it.
|
|
91
90
|
*/
|
|
92
91
|
function parseIntentSources(value) {
|
|
93
92
|
if (value === undefined)
|
|
@@ -152,33 +151,12 @@ export function derivationKey(daemon = process.env[DAEMON_ENV] === "1") {
|
|
|
152
151
|
}
|
|
153
152
|
return {};
|
|
154
153
|
}
|
|
155
|
-
/**
|
|
156
|
-
* `BIR_SEGMENT_ARM`: whether a segment the service hands out may run mid-task.
|
|
157
|
-
* On unless it is `0`, from runner 0.3.19 (the service's segmentsbreakdown.md
|
|
158
|
-
* SB-21): segments found by repeated work, and the halves after a plan's
|
|
159
|
-
* decision. `0` is observe-only: what would have armed is logged, and nothing
|
|
160
|
-
* is replaced. While it is on, whole plans may start mid-task by intent too:
|
|
161
|
-
* `BIR_INTENT_ARM` follows it unless that is set.
|
|
162
|
-
*/
|
|
163
|
-
export function segmentArmFromEnv(env = process.env) {
|
|
164
|
-
return env.BIR_SEGMENT_ARM !== "0";
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* What the `replay.enabled` startup line says segments do. A segment starts
|
|
168
|
-
* only where the probe is waited on, so `BIR_INTENT_ARM=0` starts nothing
|
|
169
|
-
* mid-task, segments included — the rule `bir doctor` words from /health.
|
|
170
|
-
*/
|
|
171
|
-
export function segmentsAtStartup(segmentArm, env = process.env) {
|
|
172
|
-
if (!segmentArm)
|
|
173
|
-
return "observed only (BIR_SEGMENT_ARM=0)";
|
|
174
|
-
return env.BIR_INTENT_ARM === "0" ? "observed only (BIR_INTENT_ARM=0)" : "run mid-task";
|
|
175
|
-
}
|
|
176
154
|
/**
|
|
177
155
|
* Assemble the replay configuration (docs/calculatedReplay.md §5.3 of the guide).
|
|
178
156
|
*
|
|
179
157
|
* There is no steer threshold: a match the service hands out is steered
|
|
180
158
|
* (controller.ts, gate 4). A recorder started with `BIR_MIN_STEER_SIMILARITY`
|
|
181
|
-
*
|
|
159
|
+
* set ignores it.
|
|
182
160
|
*/
|
|
183
161
|
function buildReplayOptions(keeper, policy, key) {
|
|
184
162
|
// The environment still wins — an operator at a terminal is the most
|
|
@@ -227,22 +205,14 @@ function buildReplayOptions(keeper, policy, key) {
|
|
|
227
205
|
// BIR_INTENT_MATCH=0 turns it off on its own.
|
|
228
206
|
intentMatch: {
|
|
229
207
|
enabled: process.env.BIR_INTENT_MATCH !== "0",
|
|
230
|
-
// The probe's own ceiling, not a wait on the hook's path:
|
|
231
|
-
//
|
|
232
|
-
// BIR_SEGMENT_ARM=0 or BIR_INTENT_ARM=0 — and then at most
|
|
208
|
+
// The probe's own ceiling, not a wait on the hook's path: a probe is sent
|
|
209
|
+
// only with BIR_INTENT_ARM=1, and `PreToolUse` waits for it at most
|
|
233
210
|
// BIR_INTENT_ARM_BUDGET_MS (500 ms), never while the service is failing
|
|
234
|
-
// (control/server.ts)
|
|
235
|
-
|
|
236
|
-
// not-clear band its one live question under the service's own 2 500 ms
|
|
237
|
-
// attempt (segmented.md R-HIT-11).
|
|
238
|
-
budgetMs: positiveInt(process.env.BIR_INTENT_MATCH_BUDGET_MS, 4_000),
|
|
211
|
+
// (control/server.ts).
|
|
212
|
+
budgetMs: positiveInt(process.env.BIR_INTENT_MATCH_BUDGET_MS, 1_500),
|
|
239
213
|
maxPerTurn: positiveInt(process.env.BIR_INTENT_MATCHES_PER_TURN, 3),
|
|
240
214
|
maxRequestsPerTurn: positiveInt(process.env.BIR_INTENT_REQUESTS_PER_TURN, 40),
|
|
241
215
|
},
|
|
242
|
-
// On by default from 0.3.19 (the service's segmentsbreakdown.md SB-21): a
|
|
243
|
-
// plan that stops at its decision hands the rest to segments of each half,
|
|
244
|
-
// and they run only mid-task. BIR_SEGMENT_ARM=0 brings back observe-only.
|
|
245
|
-
segmentArm: segmentArmFromEnv(),
|
|
246
216
|
// Both read live, never captured (C5). The recorder signs in after it
|
|
247
217
|
// listens, refreshes the token as the session outlives it, and may sign
|
|
248
218
|
// in or out while it runs; replay reads whatever holds *now*. The old
|
|
@@ -282,8 +252,6 @@ function buildReplayOptions(keeper, policy, key) {
|
|
|
282
252
|
? "the service"
|
|
283
253
|
: "recorded sample values — sign in, or a scenario with a target will not run"),
|
|
284
254
|
intentMatch: opts.intentMatch?.enabled ? "on" : "off",
|
|
285
|
-
// Whether a segment the service hands out runs, or is only logged.
|
|
286
|
-
segments: segmentsAtStartup(opts.segmentArm === true),
|
|
287
255
|
source,
|
|
288
256
|
why: "a prompt that matches a calculated scenario runs it: the scenario's own steps run without a " +
|
|
289
257
|
"permission prompt of their own (never in plan mode); every other tool call is asked about as " +
|