@basein/runner 0.3.17 → 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-scenario.d.ts +5 -1
- package/dist/bin/bir-scenario.js +155 -5
- package/dist/bin/bir.js +22 -9
- package/dist/bin/investigate.d.ts +9 -0
- package/dist/bin/investigate.js +26 -12
- package/dist/bin/scenario-edit.d.ts +7 -0
- package/dist/bin/scenario-edit.js +19 -2
- package/dist/bin/scenario-liveness.d.ts +12 -1
- package/dist/bin/scenario-liveness.js +8 -4
- package/dist/bin/setup.js +3 -3
- package/dist/control/client.d.ts +5 -0
- package/dist/control/client.js +7 -0
- package/dist/control/ensure-hook.d.ts +14 -3
- package/dist/control/ensure-hook.js +18 -5
- package/dist/control/recorder-main.d.ts +15 -0
- package/dist/control/recorder-main.js +33 -8
- package/dist/control/server.d.ts +192 -13
- package/dist/control/server.js +819 -41
- package/dist/control/transcript.d.ts +19 -0
- package/dist/control/transcript.js +57 -0
- package/dist/proxy/intercept.js +13 -0
- package/dist/proxy/session.d.ts +23 -0
- package/dist/proxy/session.js +71 -3
- package/dist/record/recorder.d.ts +156 -2
- package/dist/record/recorder.js +9 -0
- package/dist/record/remote-recorder.d.ts +59 -1
- package/dist/record/remote-recorder.js +297 -20
- package/dist/record/switchable-recorder.js +6 -0
- package/dist/replay/bundle.d.ts +8 -0
- package/dist/replay/bundle.js +20 -6
- package/dist/replay/condition-words.d.ts +28 -0
- package/dist/replay/condition-words.js +59 -0
- package/dist/replay/controller.d.ts +78 -7
- package/dist/replay/controller.js +318 -62
- package/dist/replay/derive.d.ts +15 -0
- package/dist/replay/derive.js +5 -0
- package/dist/replay/handover.d.ts +7 -0
- package/dist/replay/handover.js +89 -51
- package/dist/replay/plan.d.ts +101 -5
- package/dist/replay/plan.js +260 -18
- package/dist/replay/source-run.d.ts +2 -2
- package/dist/replay/source-run.js +3 -3
- package/dist/replay/targets.d.ts +40 -1
- package/dist/replay/targets.js +105 -3
- package/dist/replay/types.d.ts +25 -8
- package/docs/calculatedReplay.md +45 -5
- package/docs/calculatedReplayGuide.md +13 -4
- package/package.json +1 -1
- package/production.md +21 -7
package/dist/bin/bir-hooks.d.ts
CHANGED
|
@@ -101,15 +101,18 @@
|
|
|
101
101
|
* Separate from BIR_INTENT_MATCHES_PER_TURN: arming
|
|
102
102
|
* is a claim on the turn, counting hits is not
|
|
103
103
|
* BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 4000).
|
|
104
|
-
* Not a wait: PreToolUse
|
|
105
|
-
*
|
|
106
|
-
*
|
|
104
|
+
* Not a wait of its own: PreToolUse waits for a
|
|
105
|
+
* probe only while arming is on (the default from
|
|
106
|
+
* 0.3.19; off with BIR_SEGMENT_ARM=0 or
|
|
107
|
+
* BIR_INTENT_ARM=0), and then at most
|
|
107
108
|
* BIR_INTENT_ARM_BUDGET_MS (default 500)
|
|
108
|
-
* BIR_SEGMENT_ARM 1
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* (
|
|
109
|
+
* BIR_SEGMENT_ARM unset or 1 (the default from 0.3.19): a segment
|
|
110
|
+
* the service hands out runs mid-task — one found
|
|
111
|
+
* by repeated work, or a half after a plan's
|
|
112
|
+
* decision — and so may a whole plan whose intent
|
|
113
|
+
* a step matches (BIR_INTENT_ARM follows it unless
|
|
114
|
+
* set). 0 is observe-only: what would have armed
|
|
115
|
+
* is logged and nothing is replaced
|
|
113
116
|
* BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (unset: the
|
|
114
117
|
* controller's 100000, BIR_REPLAY_PLAN_BUDGET_MS)
|
|
115
118
|
* BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
|
package/dist/bin/bir-hooks.js
CHANGED
|
@@ -101,15 +101,18 @@
|
|
|
101
101
|
* Separate from BIR_INTENT_MATCHES_PER_TURN: arming
|
|
102
102
|
* is a claim on the turn, counting hits is not
|
|
103
103
|
* BIR_INTENT_MATCH_BUDGET_MS one intent probe's own ceiling (default 4000).
|
|
104
|
-
* Not a wait: PreToolUse
|
|
105
|
-
*
|
|
106
|
-
*
|
|
104
|
+
* Not a wait of its own: PreToolUse waits for a
|
|
105
|
+
* probe only while arming is on (the default from
|
|
106
|
+
* 0.3.19; off with BIR_SEGMENT_ARM=0 or
|
|
107
|
+
* BIR_INTENT_ARM=0), and then at most
|
|
107
108
|
* BIR_INTENT_ARM_BUDGET_MS (default 500)
|
|
108
|
-
* BIR_SEGMENT_ARM 1
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* (
|
|
109
|
+
* BIR_SEGMENT_ARM unset or 1 (the default from 0.3.19): a segment
|
|
110
|
+
* the service hands out runs mid-task — one found
|
|
111
|
+
* by repeated work, or a half after a plan's
|
|
112
|
+
* decision — and so may a whole plan whose intent
|
|
113
|
+
* a step matches (BIR_INTENT_ARM follows it unless
|
|
114
|
+
* set). 0 is observe-only: what would have armed
|
|
115
|
+
* is logged and nothing is replaced
|
|
113
116
|
* BIR_REPLAY_BUDGET_MS whole-plan ceiling, direct mode (unset: the
|
|
114
117
|
* controller's 100000, BIR_REPLAY_PLAN_BUDGET_MS)
|
|
115
118
|
* BIR_STEP_TIMEOUT_MS one direct tools/call (default 60000)
|
|
@@ -68,7 +68,11 @@
|
|
|
68
68
|
* WHICH TOOLS ARE OFFERED. `run_scenario`, always — unless the runner is
|
|
69
69
|
* switched off (`bir off`), when nothing is: an inert runner adds nothing to
|
|
70
70
|
* the model's context, and a call from a list taken before the switch starts
|
|
71
|
-
* nothing and asks nobody.
|
|
71
|
+
* nothing and asks nobody. `report_wrong_result` comes with it: the agent that
|
|
72
|
+
* reads a replay's results is the one who can tell they do not answer the
|
|
73
|
+
* request, and says so in one call that holds the plan and undoes nothing
|
|
74
|
+
* (the service's review of calcscenarioautocorrect.md, item 19); the control
|
|
75
|
+
* server knows which session asks, and so which plan. The rest come with scenario editing, which
|
|
72
76
|
* `bir setup` turns on (a person's `bir scenario editing off` is kept): the
|
|
73
77
|
* reading tools — also with `BIR_SCENARIO_TOOLS=read` in this server's
|
|
74
78
|
* environment — and the tools that CHANGE a scenario (D2). A project wired by
|
package/dist/bin/bir-scenario.js
CHANGED
|
@@ -68,7 +68,11 @@
|
|
|
68
68
|
* WHICH TOOLS ARE OFFERED. `run_scenario`, always — unless the runner is
|
|
69
69
|
* switched off (`bir off`), when nothing is: an inert runner adds nothing to
|
|
70
70
|
* the model's context, and a call from a list taken before the switch starts
|
|
71
|
-
* nothing and asks nobody.
|
|
71
|
+
* nothing and asks nobody. `report_wrong_result` comes with it: the agent that
|
|
72
|
+
* reads a replay's results is the one who can tell they do not answer the
|
|
73
|
+
* request, and says so in one call that holds the plan and undoes nothing
|
|
74
|
+
* (the service's review of calcscenarioautocorrect.md, item 19); the control
|
|
75
|
+
* server knows which session asks, and so which plan. The rest come with scenario editing, which
|
|
72
76
|
* `bir setup` turns on (a person's `bir scenario editing off` is kept): the
|
|
73
77
|
* reading tools — also with `BIR_SCENARIO_TOOLS=read` in this server's
|
|
74
78
|
* environment — and the tools that CHANGE a scenario (D2). A project wired by
|
|
@@ -184,6 +188,39 @@ const TOOL = {
|
|
|
184
188
|
"never otherwise.",
|
|
185
189
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
186
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* The agent's way to say a replay was wrong (the service's review of
|
|
193
|
+
* calcscenarioautocorrect.md, item 19). A plan that took the wrong half of an
|
|
194
|
+
* "if … otherwise" was caught only when the agent said so in its answer, or
|
|
195
|
+
* did the task again — and the results it was handed told it not to check.
|
|
196
|
+
* Offered wherever `run_scenario` is: the agent that reads a replay's results
|
|
197
|
+
* is the one who can tell they are wrong. It holds the plan on the service and
|
|
198
|
+
* undoes nothing. Must match `REPORT_TOOL_NAME`'s suffix in the replay
|
|
199
|
+
* controller.
|
|
200
|
+
*/
|
|
201
|
+
const REPORT_TOOL_NAME = "report_wrong_result";
|
|
202
|
+
/** What the agent may say, in characters; the control server and the service cut it there too. */
|
|
203
|
+
const REPORT_REASON_MAX = 2_000;
|
|
204
|
+
/** The whole report's request: the control server waits up to 10 s for the service. */
|
|
205
|
+
const REPORT_TIMEOUT_MS = 15_000;
|
|
206
|
+
const REPORT_TOOL = {
|
|
207
|
+
name: REPORT_TOOL_NAME,
|
|
208
|
+
description: "Call this when the results of a replayed scenario (run_scenario, or a plan bir ran for you) do not answer " +
|
|
209
|
+
"the user's request — for example, the wrong branch of an if/otherwise was taken, or the wrong item was acted " +
|
|
210
|
+
"on. Say why in one or two sentences. It does not undo anything.",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: "object",
|
|
213
|
+
properties: {
|
|
214
|
+
reason: {
|
|
215
|
+
type: "string",
|
|
216
|
+
maxLength: REPORT_REASON_MAX,
|
|
217
|
+
description: "Why the results do not answer the request, in one or two sentences.",
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
required: ["reason"],
|
|
221
|
+
additionalProperties: false,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
187
224
|
/**
|
|
188
225
|
* Ask the control server to run the armed plan. Never throws. The reasons it
|
|
189
226
|
* returns are for the audit log; {@link toolResult} decides what the model reads.
|
|
@@ -213,8 +250,14 @@ async function runArmedPlan(cwd, flight, toolUseId) {
|
|
|
213
250
|
"content-type": "application/json",
|
|
214
251
|
authorization: `Bearer ${found.token}`,
|
|
215
252
|
},
|
|
216
|
-
// `ppid`: the host that started this server and the session's proxies
|
|
217
|
-
|
|
253
|
+
// `ppid`: the host that started this server and the session's proxies
|
|
254
|
+
// (WHOSE PROXIES above). `reports`: this server offers
|
|
255
|
+
// `report_wrong_result`, so the results may say the agent can use it.
|
|
256
|
+
body: JSON.stringify({
|
|
257
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
258
|
+
ppid: process.ppid,
|
|
259
|
+
reports: listTools(cwd).some((t) => t.name === REPORT_TOOL_NAME),
|
|
260
|
+
}),
|
|
218
261
|
signal: controller.signal,
|
|
219
262
|
});
|
|
220
263
|
const raw = await res.text();
|
|
@@ -333,6 +376,60 @@ function toolResult(result) {
|
|
|
333
376
|
return said(ALREADY_RUNNING_TEXT);
|
|
334
377
|
return said(UNAVAILABLE_TEXT);
|
|
335
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Tell the control server the agent said a replay was wrong (review item 19):
|
|
381
|
+
* it knows the session — by the host's id for this call, as for
|
|
382
|
+
* `run_scenario` — and so the plan, and it reports it to the service. Never
|
|
383
|
+
* throws; every failure is an answer.
|
|
384
|
+
*/
|
|
385
|
+
async function reportWrongResult(cwd, flight, reason, toolUseId) {
|
|
386
|
+
const found = await resolveControl(cwd, DISCOVERY_GRACE_MS, 50);
|
|
387
|
+
if (!found?.url || !found.token)
|
|
388
|
+
return { ok: false, why: "no_plan" };
|
|
389
|
+
if (flight.cancelled)
|
|
390
|
+
return { ok: false, why: "cancelled" };
|
|
391
|
+
const controller = new AbortController();
|
|
392
|
+
flight.abort = controller;
|
|
393
|
+
const timer = setTimeout(() => controller.abort(), REPORT_TIMEOUT_MS);
|
|
394
|
+
timer.unref?.();
|
|
395
|
+
try {
|
|
396
|
+
const res = await fetch(`${found.url.replace(/\/+$/, "")}/scenario/dispute`, {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${found.token}` },
|
|
399
|
+
body: JSON.stringify({ reason, ...(toolUseId ? { toolUseId } : {}), ppid: process.ppid }),
|
|
400
|
+
signal: controller.signal,
|
|
401
|
+
});
|
|
402
|
+
const raw = await res.text();
|
|
403
|
+
// An older recorder has no such route (404): it cannot take the report.
|
|
404
|
+
if (!res.ok)
|
|
405
|
+
return { ok: false, why: res.status === 404 ? "unsupported" : `HTTP ${res.status}` };
|
|
406
|
+
const body = JSON.parse(raw);
|
|
407
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : { ok: false };
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
return { ok: false, why: flight.cancelled ? "cancelled" : errText(err) };
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
clearTimeout(timer);
|
|
414
|
+
flight.abort = undefined;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The one sentence the agent reads back. Never an error: the report is a side
|
|
419
|
+
* note to its answer, and whatever happened to it, the agent's next move is to
|
|
420
|
+
* tell the user.
|
|
421
|
+
*/
|
|
422
|
+
function reportAnswer(result) {
|
|
423
|
+
if (result.ok === true) {
|
|
424
|
+
return said(result.held === true
|
|
425
|
+
? "Reported. bir will not use this plan again until it is checked."
|
|
426
|
+
: "Reported. Now tell the user what is wrong with these results.");
|
|
427
|
+
}
|
|
428
|
+
if (result.why === "no_plan") {
|
|
429
|
+
return said("Nothing was reported: no scenario was replayed for this session's latest request.");
|
|
430
|
+
}
|
|
431
|
+
return said("The report could not be sent right now. Tell the user what is wrong with these results.");
|
|
432
|
+
}
|
|
336
433
|
// ── the scenario tools: `bir scenario …`, spawned ─────────────────────────────
|
|
337
434
|
/** How long a `bir` that may change a scenario may take. A check replays the chain on the service. */
|
|
338
435
|
const BIR_TIMEOUT_MS = 120_000;
|
|
@@ -720,7 +817,11 @@ function readToolsOptedIn() {
|
|
|
720
817
|
const value = (process.env.BIR_SCENARIO_TOOLS ?? "").trim().toLowerCase();
|
|
721
818
|
return value === "read" || value === "1" || value === "on" || value === "true";
|
|
722
819
|
}
|
|
723
|
-
/**
|
|
820
|
+
/**
|
|
821
|
+
* The tools to list for `cwd`, read now: the switches may have moved since the
|
|
822
|
+
* last list. `report_wrong_result` comes with `run_scenario`, editing on or off:
|
|
823
|
+
* whoever reads a replay's results may need to say they are wrong.
|
|
824
|
+
*/
|
|
724
825
|
function listTools(cwd) {
|
|
725
826
|
if (runnerOff(cwd))
|
|
726
827
|
return [];
|
|
@@ -728,6 +829,7 @@ function listTools(cwd) {
|
|
|
728
829
|
const reading = editing || readToolsOptedIn();
|
|
729
830
|
return [
|
|
730
831
|
TOOL,
|
|
832
|
+
REPORT_TOOL,
|
|
731
833
|
...SCENARIO_TOOLS.filter((t) => (t.changes ? editing : reading)).map(({ name, description, inputSchema }) => ({
|
|
732
834
|
name,
|
|
733
835
|
description,
|
|
@@ -968,7 +1070,8 @@ function main() {
|
|
|
968
1070
|
cwd,
|
|
969
1071
|
state: () => {
|
|
970
1072
|
const off = runnerOff(cwd);
|
|
971
|
-
|
|
1073
|
+
const tools = off ? [] : listTools(cwd);
|
|
1074
|
+
return { tools: tools.length, off, report: tools.some((t) => t.name === REPORT_TOOL_NAME) };
|
|
972
1075
|
},
|
|
973
1076
|
});
|
|
974
1077
|
/**
|
|
@@ -1035,6 +1138,27 @@ function main() {
|
|
|
1035
1138
|
}
|
|
1036
1139
|
reply({ jsonrpc: "2.0", id, result: answer });
|
|
1037
1140
|
};
|
|
1141
|
+
/** `report_wrong_result`: tell the control server, and answer exactly once — unless cancelled. */
|
|
1142
|
+
const answerReport = async (id, flight, reason, toolUseId) => {
|
|
1143
|
+
let result;
|
|
1144
|
+
try {
|
|
1145
|
+
result = await reportWrongResult(cwd, flight, reason, toolUseId);
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
result = { ok: false, why: errText(err) };
|
|
1149
|
+
}
|
|
1150
|
+
if (inFlight.get(id) === flight)
|
|
1151
|
+
inFlight.delete(id);
|
|
1152
|
+
if (flight.cancelled || closing)
|
|
1153
|
+
return;
|
|
1154
|
+
logLine("scenario.reported", {
|
|
1155
|
+
ok: result.ok === true,
|
|
1156
|
+
held: result.held === true || undefined,
|
|
1157
|
+
why: typeof result.why === "string" ? result.why.slice(0, 200) : undefined,
|
|
1158
|
+
bound: toolUseId !== undefined || undefined,
|
|
1159
|
+
});
|
|
1160
|
+
reply({ jsonrpc: "2.0", id, result: reportAnswer(result) });
|
|
1161
|
+
};
|
|
1038
1162
|
/** A scenario tool: run `bir`, and answer exactly once — unless cancelled. */
|
|
1039
1163
|
const answerTool = async (id, tool, args, flight) => {
|
|
1040
1164
|
let result;
|
|
@@ -1136,6 +1260,32 @@ function main() {
|
|
|
1136
1260
|
void answerTool(id, scenarioTool, paramsObject(params.arguments), flight);
|
|
1137
1261
|
return;
|
|
1138
1262
|
}
|
|
1263
|
+
if (name === REPORT_TOOL_NAME) {
|
|
1264
|
+
// Off: nothing ran a plan, so there is nothing to report — said
|
|
1265
|
+
// plainly, as the reading tools say it.
|
|
1266
|
+
if (runnerOff(cwd)) {
|
|
1267
|
+
logLine("scenario.reported", { ok: false, why: "runner is off" });
|
|
1268
|
+
reply({
|
|
1269
|
+
jsonrpc: "2.0",
|
|
1270
|
+
id,
|
|
1271
|
+
result: said("BaseInstRunner is switched off here, so there is nothing to report."),
|
|
1272
|
+
});
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const reason = paramsObject(params.arguments).reason;
|
|
1276
|
+
if (typeof reason !== "string" || reason.trim() === "") {
|
|
1277
|
+
reply({
|
|
1278
|
+
jsonrpc: "2.0",
|
|
1279
|
+
id,
|
|
1280
|
+
result: { content: [{ type: "text", text: "`reason` is required: say in a sentence or two why the results are wrong." }], isError: true },
|
|
1281
|
+
});
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const flight = { tool: REPORT_TOOL_NAME, cancelled: false };
|
|
1285
|
+
inFlight.set(id, flight);
|
|
1286
|
+
void answerReport(id, flight, reason.trim().slice(0, REPORT_REASON_MAX), toolUseIdOf(params));
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1139
1289
|
if (name !== TOOL_NAME) {
|
|
1140
1290
|
reply(errorResponse(id, JsonRpcErrorCode.MethodNotFound, `unknown tool: ${params.name}`));
|
|
1141
1291
|
return;
|
package/dist/bin/bir.js
CHANGED
|
@@ -3118,13 +3118,20 @@ 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
|
|
3122
|
-
//
|
|
3123
|
-
//
|
|
3124
|
-
//
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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)" : ""}`);
|
|
3128
3135
|
const sessions = health.sessions ?? [];
|
|
3129
3136
|
const probes = sessions.reduce((n, s) => n + (s.intent?.probes ?? 0), 0);
|
|
3130
3137
|
const armed = sessions.reduce((n, s) => n + (s.intent?.armed ?? 0), 0);
|
|
@@ -3269,8 +3276,8 @@ async function doctor(args) {
|
|
|
3269
3276
|
// lets Claude Code run them without asking; said only when that is not so.
|
|
3270
3277
|
if (scenarioEntry && isScenarioServer(scenarioEntry.config)) {
|
|
3271
3278
|
if (!editingEnabled(sidecar, cwd)) {
|
|
3272
|
-
notes.push("the bir MCP server offers run_scenario only: scenario editing is off here —
|
|
3273
|
-
"adds investigate and the scenario tools (restart Claude Code after)");
|
|
3279
|
+
notes.push("the bir MCP server offers run_scenario and report_wrong_result only: scenario editing is off here — " +
|
|
3280
|
+
"`bir scenario editing on` adds investigate and the scenario tools (restart Claude Code after)");
|
|
3274
3281
|
}
|
|
3275
3282
|
else if (![claudeUserSettingsPath(), paths.settings, paths.settingsLocal].some((file) => allowRules(file).some((rule) => rule === BIR_TOOLS_RULE || rule === `${BIR_TOOLS_RULE}__*`))) {
|
|
3276
3283
|
notes.push("Claude Code asks before investigate and the scenario tools run here — `bir setup` allows every bir tool " +
|
|
@@ -3714,6 +3721,12 @@ async function replayCommand(args) {
|
|
|
3714
3721
|
}
|
|
3715
3722
|
out(`response ${JSON.stringify(result.responseModel ?? {})}`);
|
|
3716
3723
|
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
|
+
}
|
|
3717
3730
|
return 0;
|
|
3718
3731
|
}
|
|
3719
3732
|
// ── wrap ───────────────────────────────────────────────────────────────────
|
|
@@ -245,6 +245,13 @@ 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;
|
|
248
255
|
} | null;
|
|
249
256
|
executions?: Array<{
|
|
250
257
|
id: string;
|
|
@@ -265,6 +272,8 @@ interface ServiceInvestigation {
|
|
|
265
272
|
status: string;
|
|
266
273
|
error?: string;
|
|
267
274
|
}> | null;
|
|
275
|
+
/** Where a `fell_back` run handed over, and why (`decision`, …): null for any other run. */
|
|
276
|
+
fallbackKind?: string | null;
|
|
268
277
|
/** The repeat the agent redid the task in, and its own calls there (AC1). */
|
|
269
278
|
redoneByRunId?: string | null;
|
|
270
279
|
redoneOwnCalls?: number | null;
|
package/dist/bin/investigate.js
CHANGED
|
@@ -203,8 +203,8 @@ export function findTurn(turns, id) {
|
|
|
203
203
|
/** The runner's own gate vocabulary, with the cause and the fix (guide §9). */
|
|
204
204
|
export const DECLINES = {
|
|
205
205
|
replay_disabled: {
|
|
206
|
-
cause: "calculated replay was off in this bir-hooks (BIR_REPLAY=0), or a sub-task was handed out
|
|
207
|
-
fix: "start bir-hooks with BIR_REPLAY=1 (the default); for sub-tasks
|
|
206
|
+
cause: "calculated replay was off in this bir-hooks (BIR_REPLAY=0), or a sub-task was handed out to a recorder that only observes sub-tasks (BIR_SEGMENT_ARM=0, or a recorder older than 0.3.19).",
|
|
207
|
+
fix: "start bir-hooks with BIR_REPLAY=1 (the default); for sub-tasks leave BIR_SEGMENT_ARM unset (on from 0.3.19), then `bir up --restart`.",
|
|
208
208
|
},
|
|
209
209
|
not_ready: {
|
|
210
210
|
cause: "the matched recording had no ready scenario at that moment.",
|
|
@@ -434,9 +434,11 @@ export function localFindings(turn) {
|
|
|
434
434
|
}
|
|
435
435
|
for (const i of turn.incidents) {
|
|
436
436
|
const fields = Object.fromEntries(Object.entries(i).filter(([k]) => k !== "at" && k !== "event"));
|
|
437
|
+
// A stop at the plan's decision is the plan working as split: no fault.
|
|
438
|
+
const decisionStop = i.event === "replay.handover" && i.kind === "decision";
|
|
437
439
|
out.push({
|
|
438
440
|
code: i.event.replace(/\./g, "_"),
|
|
439
|
-
severity: i.event.endsWith("_failed") || i.event === "replay.failed" ? "problem" : "warn",
|
|
441
|
+
severity: i.event.endsWith("_failed") || i.event === "replay.failed" ? "problem" : decisionStop ? "info" : "warn",
|
|
440
442
|
title: `${i.event}${i.error ? `: ${String(i.error)}` : ""}`,
|
|
441
443
|
cause: Object.entries(fields)
|
|
442
444
|
.map(([k, v]) => `${k}=${String(v)}`)
|
|
@@ -447,13 +449,15 @@ export function localFindings(turn) {
|
|
|
447
449
|
: "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."
|
|
448
450
|
: i.event === "replay.handover" && i.kind === "other_branch"
|
|
449
451
|
? "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."
|
|
450
|
-
:
|
|
451
|
-
? "the
|
|
452
|
-
: i.event === "
|
|
453
|
-
? "the
|
|
454
|
-
: i.event === "run.
|
|
455
|
-
? "the
|
|
456
|
-
:
|
|
452
|
+
: decisionStop
|
|
453
|
+
? "nothing to fix: the plan stopped at its decision, as planned. It ran the steps before it, and the agent chose the way. A segment of the half it chose may have run next (a `replay.intent_armed` line with kind segment); a `run.fragment` line with decision=true is the agent's own work, from which the service makes the other half."
|
|
454
|
+
: i.event === "replay.diverge" || i.event === "replay.handover"
|
|
455
|
+
? "the model left the script or the runner handed the turn back; the service section shows what the fallback cost."
|
|
456
|
+
: i.event === "run.incomplete"
|
|
457
|
+
? "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."
|
|
458
|
+
: i.event === "run.host_failed"
|
|
459
|
+
? "the application's run failed; its recording is closed with that error and never becomes a plan."
|
|
460
|
+
: "see the guide's troubleshooting table for this line.",
|
|
457
461
|
});
|
|
458
462
|
}
|
|
459
463
|
const r = turn.reported;
|
|
@@ -536,6 +540,12 @@ export function savedPct(turn) {
|
|
|
536
540
|
return baseline > 0 ? r.savedUsd / baseline : null;
|
|
537
541
|
}
|
|
538
542
|
const short = (id) => (id ? id.slice(0, 13) : "—");
|
|
543
|
+
/**
|
|
544
|
+
* An execution's outcome, and where it handed the task over when the service
|
|
545
|
+
* says (`fell_back · decision`). The service sends `fallbackKind: null` for a
|
|
546
|
+
* run that did not hand over, and an older one sends nothing.
|
|
547
|
+
*/
|
|
548
|
+
const outcomeOf = (e) => typeof e.fallbackKind === "string" && e.fallbackKind !== "" ? `${e.outcome} · ${e.fallbackKind}` : e.outcome;
|
|
539
549
|
/** 4624 → "4,624", whatever the machine's locale. */
|
|
540
550
|
const count = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
541
551
|
const money = (v) => (v == null ? "—" : usd(v));
|
|
@@ -638,6 +648,10 @@ export function renderService(out, s, viewerIsOwner) {
|
|
|
638
648
|
if (sc.held) {
|
|
639
649
|
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}` : ""}`);
|
|
640
650
|
}
|
|
651
|
+
if (typeof sc.decisionStep === "number") {
|
|
652
|
+
const text = typeof sc.decisionText === "string" ? sc.decisionText.replace(/\s+/g, " ").trim() : "";
|
|
653
|
+
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})` : ""}`);
|
|
654
|
+
}
|
|
641
655
|
for (const line of stepLines(sc.steps))
|
|
642
656
|
out(` ${line}`);
|
|
643
657
|
const correction = newestCorrection(sc.corrections);
|
|
@@ -655,7 +669,7 @@ export function renderService(out, s, viewerIsOwner) {
|
|
|
655
669
|
if (ex.length > 0) {
|
|
656
670
|
out(` executions (${ex.length}, newest first)`);
|
|
657
671
|
for (const e of ex.slice(0, 10)) {
|
|
658
|
-
out(` ${when(e.createdAt)} ${short(e.id)} ${e.
|
|
672
|
+
out(` ${when(e.createdAt)} ${short(e.id)} ${outcomeOf(e).padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
|
|
659
673
|
const failed = e.steps?.filter((st) => st.status === "failed") ?? [];
|
|
660
674
|
for (const st of failed)
|
|
661
675
|
out(` step ${st.stepIndex} ${st.toolName ?? ""} failed${st.error ? `: ${st.error.slice(0, 120)}` : ""}`);
|
|
@@ -790,7 +804,7 @@ export async function investigateCommand(args, deps) {
|
|
|
790
804
|
if (rows.length === 0)
|
|
791
805
|
out(" none yet");
|
|
792
806
|
for (const e of rows) {
|
|
793
|
-
out(` ${when(e.createdAt)} ${e.id} ${e.
|
|
807
|
+
out(` ${when(e.createdAt)} ${e.id} ${outcomeOf(e).padEnd(13)} cost ${usd(e.costUsd)} saved ${money(e.savedUsd)}${e.savedPct != null ? ` (${pct(e.savedPct)})` : ""}${e.measured ? "" : " unmeasured"}`);
|
|
794
808
|
out(` scenario ${e.scenarioId}${e.runId ? ` run ${e.runId}` : ""}${e.intent ? ` "${e.intent.slice(0, 60)}"` : ""}`);
|
|
795
809
|
}
|
|
796
810
|
out();
|
|
@@ -222,6 +222,13 @@ export declare const SCENARIO_EDIT_SUBCOMMANDS: ReadonlySet<string>;
|
|
|
222
222
|
export declare const READ_TOOLS: readonly ["scenario_show", "scenario_edits", "investigate"];
|
|
223
223
|
/** The ones that change a scenario: offered only after `bir scenario editing on` (D2). */
|
|
224
224
|
export declare const EDIT_TOOLS: readonly ["scenario_check", "scenario_edit", "scenario_undo"];
|
|
225
|
+
/**
|
|
226
|
+
* The ones every session with the `bir` server has, editing on or off: the
|
|
227
|
+
* delivery of a direct plan, and the agent's way to say a replay's results do
|
|
228
|
+
* not answer the request (the service's review of calcscenarioautocorrect.md,
|
|
229
|
+
* item 19). `bir setup`'s allow rule, `mcp__bir`, covers every one.
|
|
230
|
+
*/
|
|
231
|
+
export declare const BASE_TOOLS: readonly ["run_scenario", "report_wrong_result"];
|
|
225
232
|
export declare function scenarioEditCommand(args: ScenarioEditArgs, deps: ScenarioEditDeps): Promise<number>;
|
|
226
233
|
/**
|
|
227
234
|
* A logic file's text. UTF-8, with a byte-order mark dropped — and UTF-16 with
|
|
@@ -59,6 +59,13 @@ export const SCENARIO_EDIT_SUBCOMMANDS = new Set([
|
|
|
59
59
|
export const READ_TOOLS = ["scenario_show", "scenario_edits", "investigate"];
|
|
60
60
|
/** The ones that change a scenario: offered only after `bir scenario editing on` (D2). */
|
|
61
61
|
export const EDIT_TOOLS = ["scenario_check", "scenario_edit", "scenario_undo"];
|
|
62
|
+
/**
|
|
63
|
+
* The ones every session with the `bir` server has, editing on or off: the
|
|
64
|
+
* delivery of a direct plan, and the agent's way to say a replay's results do
|
|
65
|
+
* not answer the request (the service's review of calcscenarioautocorrect.md,
|
|
66
|
+
* item 19). `bir setup`'s allow rule, `mcp__bir`, covers every one.
|
|
67
|
+
*/
|
|
68
|
+
export const BASE_TOOLS = ["run_scenario", "report_wrong_result"];
|
|
62
69
|
const USAGE = {
|
|
63
70
|
show: "usage: bir scenario show <runId|scnId> [--step <n>]",
|
|
64
71
|
check: "usage: bir scenario check <runId|scnId> --step <n> [--input-logic <file|->] [--output-logic <file|->] [--unfreeze] [--json]",
|
|
@@ -1067,11 +1074,11 @@ function describeEditing(out, sidecar, cwd) {
|
|
|
1067
1074
|
// working on a scenario, and every other session would pay for their
|
|
1068
1075
|
// definitions on every request without ever calling them.
|
|
1069
1076
|
if (on) {
|
|
1070
|
-
out(` the bir MCP server offers
|
|
1077
|
+
out(` the bir MCP server offers ${[...BASE_TOOLS, ...READ_TOOLS].join(", ")} (they read)`);
|
|
1071
1078
|
out(` and ${EDIT_TOOLS.join(", ")} (they change a scenario)`);
|
|
1072
1079
|
}
|
|
1073
1080
|
else {
|
|
1074
|
-
out(
|
|
1081
|
+
out(` the bir MCP server offers ${BASE_TOOLS.join(", ")}`);
|
|
1075
1082
|
out(` \`bir scenario editing on\` adds ${EDIT_TOOLS.join(", ")} (they change a scenario)`);
|
|
1076
1083
|
out(` and ${READ_TOOLS.join(", ")} (they read)`);
|
|
1077
1084
|
out(" (BIR_SCENARIO_TOOLS=read in the server's environment offers the reading ones alone)");
|
|
@@ -1224,6 +1231,16 @@ function explain(reply, args, deps, e) {
|
|
|
1224
1231
|
// newer entry puts the step back as this one left it, and then this one
|
|
1225
1232
|
// can be undone. It used to loop, when undo only toggled (C23).
|
|
1226
1233
|
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
|
+
}
|
|
1227
1244
|
out(aboutCorrection
|
|
1228
1245
|
? `${e.verb} (not_latest_edit): the plan has changed since that correction — ${latest} changed it after. Undo the newer one first, then this one:`
|
|
1229
1246
|
: `${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:`);
|
|
@@ -75,6 +75,13 @@ export interface HelloBody {
|
|
|
75
75
|
/** How many tools this server lists now (0 when off). */
|
|
76
76
|
tools: number;
|
|
77
77
|
off: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* It lists `report_wrong_result` (runner 0.3.18 on; the service's review of
|
|
80
|
+
* calcscenarioautocorrect.md, item 19): a replay's results may then tell the
|
|
81
|
+
* agent it can say they are wrong. Sent only when true; an older recorder
|
|
82
|
+
* ignores it.
|
|
83
|
+
*/
|
|
84
|
+
report?: boolean;
|
|
78
85
|
}
|
|
79
86
|
/** A discovery record as written, its pid not yet checked. */
|
|
80
87
|
export interface RecorderRecord {
|
|
@@ -100,10 +107,14 @@ export declare function launchRevive(cwd: string): Promise<void>;
|
|
|
100
107
|
export interface LivenessOptions {
|
|
101
108
|
/** The project this server runs for. */
|
|
102
109
|
cwd: string;
|
|
103
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* What this server lists now. Called for every hello. `report`: the list
|
|
112
|
+
* holds `report_wrong_result` (review item 19).
|
|
113
|
+
*/
|
|
104
114
|
state: () => {
|
|
105
115
|
tools: number;
|
|
106
116
|
off: boolean;
|
|
117
|
+
report?: boolean;
|
|
107
118
|
};
|
|
108
119
|
now?: () => number;
|
|
109
120
|
random?: () => number;
|
|
@@ -314,10 +314,14 @@ export class ScenarioLiveness {
|
|
|
314
314
|
state() {
|
|
315
315
|
try {
|
|
316
316
|
const s = this.opts.state();
|
|
317
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
tools: Number.isInteger(s.tools) && s.tools >= 0 ? s.tools : 0,
|
|
319
|
+
off: s.off === true,
|
|
320
|
+
report: s.report === true && s.off !== true,
|
|
321
|
+
};
|
|
318
322
|
}
|
|
319
323
|
catch {
|
|
320
|
-
return { tools: 0, off: false };
|
|
324
|
+
return { tools: 0, off: false, report: false };
|
|
321
325
|
}
|
|
322
326
|
}
|
|
323
327
|
async sendHello(why) {
|
|
@@ -330,8 +334,8 @@ export class ScenarioLiveness {
|
|
|
330
334
|
const target = this.target();
|
|
331
335
|
if (!target)
|
|
332
336
|
return;
|
|
333
|
-
const { tools, off } = this.state();
|
|
334
|
-
const body = { pid: process.pid, version: packageVersion(), tools, off };
|
|
337
|
+
const { tools, off, report } = this.state();
|
|
338
|
+
const body = { pid: process.pid, version: packageVersion(), tools, off, ...(report ? { report } : {}) };
|
|
335
339
|
this.tried = target.identity;
|
|
336
340
|
this.lastHelloAt = this.now();
|
|
337
341
|
let reach;
|
package/dist/bin/setup.js
CHANGED
|
@@ -43,7 +43,7 @@ import { normalizePath } from "../control/paths.js";
|
|
|
43
43
|
import { allowRules, BIR_TOOLS_RULE, claudeCodePaths, claudeCodeSetUp } from "../config/adapters/claude-code.js";
|
|
44
44
|
import { allocateReplacementPort, editingEnabled, isScenarioServer, isWrapped, LOOPBACK_NO_PROXY, probeFreePort, proxyConfigured, projectRecord, readSidecar, SCENARIO_SERVER_KEY, setProjectRecord, settleReplaySwitch, writeSidecar, allocateFreeProjectPort, } from "../config/generate.js";
|
|
45
45
|
import { resolveServers } from "../config/resolve.js";
|
|
46
|
-
import { EDIT_TOOLS, READ_TOOLS } from "./scenario-edit.js";
|
|
46
|
+
import { BASE_TOOLS, EDIT_TOOLS, READ_TOOLS } from "./scenario-edit.js";
|
|
47
47
|
import { errText } from "../util/log.js";
|
|
48
48
|
import { offSource } from "../util/off-switch.js";
|
|
49
49
|
import { packageVersion } from "../util/version.js";
|
|
@@ -508,8 +508,8 @@ export async function runSetup(args, deps) {
|
|
|
508
508
|
if (resolveServers(project).some((s) => s.name === SCENARIO_SERVER_KEY && isScenarioServer(s.config))) {
|
|
509
509
|
const asks = !allowRules(claudeCodePaths(project).settingsLocal).includes(BIR_TOOLS_RULE);
|
|
510
510
|
out(editingEnabled(readSidecar(), project)
|
|
511
|
-
? ` tools ${[
|
|
512
|
-
:
|
|
511
|
+
? ` tools ${[...BASE_TOOLS, ...READ_TOOLS, ...EDIT_TOOLS].join(", ")} — ${asks ? "Claude Code asks before each" : "Claude Code runs them without asking, except a forced change"}`
|
|
512
|
+
: ` tools ${BASE_TOOLS.join(" and ")} only — scenario editing is off here (\`bir scenario editing on\` adds investigate and the scenario tools)`);
|
|
513
513
|
}
|
|
514
514
|
if (ensured) {
|
|
515
515
|
const { info, health: h } = ensured.status;
|
package/dist/control/client.d.ts
CHANGED
|
@@ -68,6 +68,11 @@ export declare class ControlClient {
|
|
|
68
68
|
}): Promise<RegisterResult | undefined>;
|
|
69
69
|
/** Report one completed MCP call. Resolves false when the send was dropped. */
|
|
70
70
|
report(step: ProxyStepReport): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* The tool list of the server this proxy wraps, as it answered `tools/list`
|
|
73
|
+
* (the service's autocorrect.md AC12). Resolves false when the send was dropped.
|
|
74
|
+
*/
|
|
75
|
+
tools(serverName: string, tools: readonly unknown[]): Promise<boolean>;
|
|
71
76
|
health(): Promise<Record<string, unknown> | undefined>;
|
|
72
77
|
/**
|
|
73
78
|
* Ask the control server to shut down. Resolves true when it agreed; the
|
package/dist/control/client.js
CHANGED
|
@@ -26,6 +26,13 @@ export class ControlClient {
|
|
|
26
26
|
const body = this.identity ? { ...step, proxyId: this.identity.proxyId } : step;
|
|
27
27
|
return (await this.post("/proxy/step", body)) !== undefined;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The tool list of the server this proxy wraps, as it answered `tools/list`
|
|
31
|
+
* (the service's autocorrect.md AC12). Resolves false when the send was dropped.
|
|
32
|
+
*/
|
|
33
|
+
async tools(serverName, tools) {
|
|
34
|
+
return (await this.post("/proxy/tools", { ...this.identity, serverName, tools })) !== undefined;
|
|
35
|
+
}
|
|
29
36
|
async health() {
|
|
30
37
|
return (await this.request("GET", "/health"));
|
|
31
38
|
}
|