@basein/runner 0.3.20 → 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/README.md +6 -5
- package/dist/bin/bir-hooks.d.ts +17 -19
- package/dist/bin/bir-hooks.js +17 -19
- 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 +15 -43
- package/dist/bin/scenario-edit.js +13 -44
- package/dist/control/recorder-main.d.ts +0 -15
- package/dist/control/recorder-main.js +22 -42
- package/dist/control/server.d.ts +24 -137
- package/dist/control/server.js +107 -1001
- 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/builtins.d.ts +45 -9
- package/dist/replay/builtins.js +131 -59
- package/dist/replay/bundle.d.ts +2 -2
- package/dist/replay/bundle.js +3 -3
- package/dist/replay/controller.d.ts +34 -125
- package/dist/replay/controller.js +171 -528
- 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 +30 -81
- package/docs/calculatedReplayGuide.md +17 -34
- package/package.json +1 -1
- package/production.md +7 -36
- 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/replay/builtins.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* A built-in lives inside the host session, not behind any MCP server, so there
|
|
6
6
|
* is no proxy to relay it to: historically every built-in step threw `not_run`
|
|
7
7
|
* and the plan handed the task back to the model. This module is the other
|
|
8
|
-
* option: execute the step here,
|
|
8
|
+
* option: execute the step here, from the *session's own project directory*,
|
|
9
9
|
* so a chain that starts with a `Read` no longer costs a hand-over.
|
|
10
10
|
*
|
|
11
11
|
* THE RULES, because this bypasses the host's permission prompts entirely:
|
|
@@ -17,10 +17,17 @@
|
|
|
17
17
|
* already has — its pinned call is answered `allow` too. What running it
|
|
18
18
|
* here changes is that no model sees the call before it runs, so an
|
|
19
19
|
* operator who wants one in the loop takes the tool off the list.
|
|
20
|
-
* - **
|
|
21
|
-
* session's project directory
|
|
22
|
-
* daemon's own cwd is never used: it is
|
|
23
|
-
* project (§5.1 of the change plan).
|
|
20
|
+
* - **Any path the agent could reach, unless `BIR_PATH_ALLOW` narrows it.**
|
|
21
|
+
* A relative path resolves against the session's project directory, as
|
|
22
|
+
* the host's tools resolve it; the daemon's own cwd is never used: it is
|
|
23
|
+
* `~/.baseinstrunner`, not the project (§5.1 of the change plan). An
|
|
24
|
+
* absolute path is taken as it is, inside the project or not: the agent's
|
|
25
|
+
* own tools reach any folder its session allows — the recorded call was
|
|
26
|
+
* one of them — and a runner-side `Bash` reaches any folder already. Up
|
|
27
|
+
* to 0.3.20 every path was confined to the project directory;
|
|
28
|
+
* `BIR_PATH_ALLOW=project` brings that back, and a list of folders
|
|
29
|
+
* ({@link parsePathAllow}) confines the file tools to those, symlinks
|
|
30
|
+
* included ({@link allowedPath}).
|
|
24
31
|
* - **Bash runs any command unless `BIR_BASH_ALLOW` narrows it.** Set, a
|
|
25
32
|
* command must match one of its prefixes ({@link bashAllowed}); `none`
|
|
26
33
|
* allows no command, and then `Bash` counts as off the list, so its steps
|
|
@@ -43,7 +50,8 @@ import { execFile } from "node:child_process";
|
|
|
43
50
|
import { randomBytes } from "node:crypto";
|
|
44
51
|
import { existsSync } from "node:fs";
|
|
45
52
|
import { readdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
46
|
-
import {
|
|
53
|
+
import { homedir } from "node:os";
|
|
54
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
47
55
|
// ── configuration ────────────────────────────────────────────────────────────
|
|
48
56
|
/** The built-ins {@link runBuiltin} actually implements. */
|
|
49
57
|
export const EXECUTABLE_BUILTINS = new Set([
|
|
@@ -61,8 +69,24 @@ export const EXECUTABLE_BUILTINS = new Set([
|
|
|
61
69
|
* set (`Read,Glob,Grep`).
|
|
62
70
|
*/
|
|
63
71
|
export const DEFAULT_BUILTIN_TOOLS = [...EXECUTABLE_BUILTINS];
|
|
72
|
+
/**
|
|
73
|
+
* The built-ins that reach files by a path, the ones `BIR_PATH_ALLOW`
|
|
74
|
+
* governs. `Bash` is not among them: what a command touches is
|
|
75
|
+
* `BIR_BASH_ALLOW`'s business.
|
|
76
|
+
*/
|
|
77
|
+
export const FILE_BUILTINS = new Set(["Read", "Glob", "Grep", "Edit", "Write"]);
|
|
64
78
|
/** `read` → `Read`: the vocabulary is small enough to forgive casing. */
|
|
65
79
|
const CANONICAL_BUILTIN = new Map([...EXECUTABLE_BUILTINS].map((name) => [name.toLowerCase(), name]));
|
|
80
|
+
/**
|
|
81
|
+
* A switch's list once it is set: comma-separated, trimmed, empty entries
|
|
82
|
+
* dropped, and `none` / `off` / `0` meaning nothing at all.
|
|
83
|
+
*/
|
|
84
|
+
function parseList(value) {
|
|
85
|
+
return value
|
|
86
|
+
.split(",")
|
|
87
|
+
.map((s) => s.trim())
|
|
88
|
+
.filter((s) => s.length > 0 && !["none", "off", "0"].includes(s.toLowerCase()));
|
|
89
|
+
}
|
|
66
90
|
/**
|
|
67
91
|
* Parse `BIR_BUILTIN_TOOLS`. Unset means {@link DEFAULT_BUILTIN_TOOLS}; set,
|
|
68
92
|
* it is the whole list — `none` (or an empty value) turns built-ins off
|
|
@@ -73,12 +97,7 @@ const CANONICAL_BUILTIN = new Map([...EXECUTABLE_BUILTINS].map((name) => [name.t
|
|
|
73
97
|
export function parseBuiltinTools(value) {
|
|
74
98
|
if (value === undefined)
|
|
75
99
|
return new Set(DEFAULT_BUILTIN_TOOLS);
|
|
76
|
-
|
|
77
|
-
.split(",")
|
|
78
|
-
.map((s) => s.trim())
|
|
79
|
-
.filter((s) => s.length > 0 && !["none", "off", "0"].includes(s.toLowerCase()))
|
|
80
|
-
.map((s) => CANONICAL_BUILTIN.get(s.toLowerCase()) ?? s);
|
|
81
|
-
return new Set(names);
|
|
100
|
+
return new Set(parseList(value).map((s) => CANONICAL_BUILTIN.get(s.toLowerCase()) ?? s));
|
|
82
101
|
}
|
|
83
102
|
/**
|
|
84
103
|
* Parse `BIR_BASH_ALLOW`: comma-separated command prefixes, `*` for
|
|
@@ -89,10 +108,24 @@ export function parseBuiltinTools(value) {
|
|
|
89
108
|
export function parseBashAllow(value) {
|
|
90
109
|
if (value === undefined)
|
|
91
110
|
return ["*"];
|
|
92
|
-
return value
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
return parseList(value);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse `BIR_PATH_ALLOW`: the folders a runner-side `Read`, `Glob`, `Grep`,
|
|
115
|
+
* `Edit` or `Write` may touch, comma-separated, `*` for anywhere. Unset means
|
|
116
|
+
* anywhere, as the agent's own tools reach any folder its session allows (up
|
|
117
|
+
* to 0.3.20 it was the project directory alone). In a list, `project` is the
|
|
118
|
+
* session's project directory, `~` the home folder (`~/.claude` a folder in
|
|
119
|
+
* it), and any other relative folder resolves against the project directory.
|
|
120
|
+
* Set, it is the whole list, as `BIR_BASH_ALLOW` is: `none` (or an empty
|
|
121
|
+
* value) allows no folder, and the file tools then count as off
|
|
122
|
+
* `BIR_BUILTIN_TOOLS`, so their steps are steered in the session
|
|
123
|
+
* (controller.ts).
|
|
124
|
+
*/
|
|
125
|
+
export function parsePathAllow(value) {
|
|
126
|
+
if (value === undefined)
|
|
127
|
+
return ["*"];
|
|
128
|
+
return parseList(value);
|
|
96
129
|
}
|
|
97
130
|
/**
|
|
98
131
|
* Whether `command` matches the `BIR_BASH_ALLOW` prefixes. A prefix that ends
|
|
@@ -117,8 +150,9 @@ export function bashAllowed(command, allow) {
|
|
|
117
150
|
// ── the failure contract ─────────────────────────────────────────────────────
|
|
118
151
|
/**
|
|
119
152
|
* The step cannot run here — policy, not breakage: the tool is not
|
|
120
|
-
* implemented, its path
|
|
121
|
-
*
|
|
153
|
+
* implemented, its path is outside the folders `BIR_PATH_ALLOW` names, the
|
|
154
|
+
* command is not one `BIR_BASH_ALLOW` allows, or the input is not even the
|
|
155
|
+
* right shape. Maps to `not_run`: nothing happened, the
|
|
122
156
|
* session may still do it, a recorded output may stand in.
|
|
123
157
|
*/
|
|
124
158
|
export class BuiltinNotRunnable extends Error {
|
|
@@ -186,7 +220,9 @@ export async function runBuiltin(toolName, input, ctx) {
|
|
|
186
220
|
throw new BuiltinNotRunnable(`no runner-side implementation for ${toolName}`);
|
|
187
221
|
}
|
|
188
222
|
}
|
|
189
|
-
// ──
|
|
223
|
+
// ── which paths a file step may touch ────────────────────────────────────────
|
|
224
|
+
/** The word in `BIR_PATH_ALLOW` that stands for the session's project directory. */
|
|
225
|
+
const PROJECT_WORD = "project";
|
|
190
226
|
/** Case-blind on Windows, where `C:\Repo` and `c:\repo` are one directory. */
|
|
191
227
|
function samePath(a, b) {
|
|
192
228
|
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
@@ -199,54 +235,87 @@ function underRoot(root, abs) {
|
|
|
199
235
|
: abs.startsWith(prefix)));
|
|
200
236
|
}
|
|
201
237
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* gets resolved, so a `Write` to a new file is judged by its real directory.
|
|
238
|
+
* The folders the context's `BIR_PATH_ALLOW` list names, as absolute paths,
|
|
239
|
+
* or `anywhere` for `*`. `project` is the session's project directory, `~`
|
|
240
|
+
* the home folder, and any other relative entry resolves against the project.
|
|
206
241
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
function allowedFolders(ctx) {
|
|
243
|
+
const allow = ctx.pathAllow ?? [PROJECT_WORD];
|
|
244
|
+
if (allow.includes("*"))
|
|
245
|
+
return "anywhere";
|
|
246
|
+
const project = resolve(ctx.cwd);
|
|
247
|
+
return allow.map((entry) => {
|
|
248
|
+
if (entry.toLowerCase() === PROJECT_WORD)
|
|
249
|
+
return project;
|
|
250
|
+
if (entry === "~")
|
|
251
|
+
return homedir();
|
|
252
|
+
if (entry.startsWith("~/") || entry.startsWith("~\\"))
|
|
253
|
+
return resolve(homedir(), entry.slice(2));
|
|
254
|
+
return resolve(project, entry);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/** Why a path was refused, naming the switch that refused it. */
|
|
258
|
+
function outsideMessage(p, allow) {
|
|
259
|
+
return allow.length === 1 && allow[0].toLowerCase() === PROJECT_WORD
|
|
260
|
+
? `${p} is outside the session's project directory, and BIR_PATH_ALLOW=project keeps bir inside it`
|
|
261
|
+
: `${p} is outside the folders BIR_PATH_ALLOW names (${allow.join(", ")}), so bir does not touch it unattended`;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* `abs` as it is on disk: its longest existing ancestor replaced by that
|
|
265
|
+
* ancestor's realpath (symlinks resolved, and on macOS `/var` read as
|
|
266
|
+
* `/private/var`), the rest kept as written. A path none of which exists
|
|
267
|
+
* comes back as it is.
|
|
268
|
+
*/
|
|
269
|
+
async function physical(abs) {
|
|
220
270
|
let probe = abs;
|
|
221
|
-
|
|
271
|
+
const rest = [];
|
|
222
272
|
for (;;) {
|
|
223
273
|
try {
|
|
224
|
-
real = await realpath(probe);
|
|
225
|
-
|
|
274
|
+
const real = await realpath(probe);
|
|
275
|
+
return rest.length > 0 ? join(real, ...rest.reverse()) : real;
|
|
226
276
|
}
|
|
227
277
|
catch {
|
|
228
278
|
const parent = dirname(probe);
|
|
229
279
|
if (samePath(parent, probe))
|
|
230
|
-
|
|
280
|
+
return abs; // the filesystem root: nothing exists
|
|
281
|
+
rest.push(basename(probe));
|
|
231
282
|
probe = parent;
|
|
232
283
|
}
|
|
233
284
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Resolve `p` as the host does — against the session's project directory
|
|
288
|
+
* when it is relative — and allow it anywhere, as the agent's own tools
|
|
289
|
+
* would, unless `BIR_PATH_ALLOW` names folders. Then anything outside them is
|
|
290
|
+
* refused: first as written, then physically ({@link physical}, both sides),
|
|
291
|
+
* so a symlink inside an allowed folder cannot point the step at a file
|
|
292
|
+
* beyond it, and a `Write` to a new file is judged by its real folder.
|
|
293
|
+
*/
|
|
294
|
+
async function allowedPath(ctx, p, what = "file_path") {
|
|
295
|
+
if (typeof p !== "string" || p.trim() === "") {
|
|
296
|
+
throw new BuiltinNotRunnable(`${what} is required`);
|
|
244
297
|
}
|
|
298
|
+
const abs = resolve(ctx.cwd, p);
|
|
299
|
+
const folders = allowedFolders(ctx);
|
|
300
|
+
if (folders === "anywhere")
|
|
301
|
+
return abs;
|
|
302
|
+
if (folders.length === 0) {
|
|
303
|
+
throw new BuiltinNotRunnable("BIR_PATH_ALLOW allows no folder, so bir touches no file unattended");
|
|
304
|
+
}
|
|
305
|
+
const outside = () => {
|
|
306
|
+
throw new BuiltinNotRunnable(outsideMessage(p, ctx.pathAllow ?? [PROJECT_WORD]));
|
|
307
|
+
};
|
|
308
|
+
if (!folders.some((folder) => underRoot(folder, abs)))
|
|
309
|
+
outside();
|
|
310
|
+
const real = await physical(abs);
|
|
311
|
+
const realFolders = await Promise.all(folders.map(physical));
|
|
312
|
+
if (!realFolders.some((folder) => underRoot(folder, real)))
|
|
313
|
+
outside();
|
|
245
314
|
return abs;
|
|
246
315
|
}
|
|
247
316
|
// ── Read ─────────────────────────────────────────────────────────────────────
|
|
248
317
|
async function runRead(input, ctx) {
|
|
249
|
-
const filePath = await
|
|
318
|
+
const filePath = await allowedPath(ctx, input.file_path);
|
|
250
319
|
const size = (await stat(filePath)).size;
|
|
251
320
|
if (size > MAX_FILE_BYTES) {
|
|
252
321
|
throw new Error(`file is ${size} bytes, more than the ${MAX_FILE_BYTES} this runner reads`);
|
|
@@ -276,7 +345,7 @@ async function runGlob(input, ctx, deadline) {
|
|
|
276
345
|
if (typeof pattern !== "string" || pattern.trim() === "") {
|
|
277
346
|
throw new BuiltinNotRunnable("pattern is required");
|
|
278
347
|
}
|
|
279
|
-
const root = await
|
|
348
|
+
const root = await allowedPath(ctx, typeof input.path === "string" ? input.path : ".", "path");
|
|
280
349
|
const startedAt = Date.now();
|
|
281
350
|
const re = globToRegExp(pattern);
|
|
282
351
|
const found = [];
|
|
@@ -324,7 +393,7 @@ async function runGrep(input, ctx, deadline) {
|
|
|
324
393
|
catch (err) {
|
|
325
394
|
throw new BuiltinNotRunnable(`the pattern is not a regular expression this runner can run: ${err.message}`);
|
|
326
395
|
}
|
|
327
|
-
const target = await
|
|
396
|
+
const target = await allowedPath(ctx, typeof input.path === "string" ? input.path : ".", "path");
|
|
328
397
|
// ripgrep's `-g '*.ts'` matches the basename at any depth; only a pattern
|
|
329
398
|
// with a `/` in it speaks about the path.
|
|
330
399
|
const globRe = typeof input.glob === "string" && input.glob
|
|
@@ -417,7 +486,7 @@ async function matchFile(path, re, firstOnly) {
|
|
|
417
486
|
}
|
|
418
487
|
// ── Edit / Write ─────────────────────────────────────────────────────────────
|
|
419
488
|
async function runEdit(input, ctx) {
|
|
420
|
-
const filePath = await
|
|
489
|
+
const filePath = await allowedPath(ctx, input.file_path);
|
|
421
490
|
const oldString = input.old_string;
|
|
422
491
|
const newString = input.new_string;
|
|
423
492
|
if (typeof oldString !== "string" || oldString === "") {
|
|
@@ -452,7 +521,7 @@ async function runEdit(input, ctx) {
|
|
|
452
521
|
};
|
|
453
522
|
}
|
|
454
523
|
async function runWrite(input, ctx) {
|
|
455
|
-
const filePath = await
|
|
524
|
+
const filePath = await allowedPath(ctx, input.file_path);
|
|
456
525
|
const content = input.content;
|
|
457
526
|
if (typeof content !== "string")
|
|
458
527
|
throw new BuiltinNotRunnable("content is required");
|
|
@@ -579,9 +648,10 @@ function takePwdMarker(stderr, mark) {
|
|
|
579
648
|
const SKIP_DIRS = new Set([".git", "node_modules", ".hg", ".svn"]);
|
|
580
649
|
/**
|
|
581
650
|
* Walk `root`, calling `visit(relativePath, absolutePath, mtimeMs)` for every
|
|
582
|
-
* regular file. Never follows directory symlinks — the
|
|
583
|
-
* the root, and a link out of it must not
|
|
584
|
-
* the
|
|
651
|
+
* regular file. Never follows directory symlinks — the folder was checked at
|
|
652
|
+
* the root, and under a `BIR_PATH_ALLOW` list a link out of it must not
|
|
653
|
+
* re-open the door (nor may a link loop the walk). Returns whether the walk
|
|
654
|
+
* was cut short (entry cap, deadline, or `visit` saying stop).
|
|
585
655
|
*/
|
|
586
656
|
async function walk(root, deadline, visit) {
|
|
587
657
|
let seen = 0;
|
|
@@ -617,7 +687,9 @@ async function walk(root, deadline, visit) {
|
|
|
617
687
|
catch {
|
|
618
688
|
continue;
|
|
619
689
|
}
|
|
620
|
-
|
|
690
|
+
// `relative`, not a slice: a walk may now start at a drive's or the
|
|
691
|
+
// disk's root (`C:\`, `/`), which already ends in a separator.
|
|
692
|
+
const rel = relative(root, abs).split(sep).join("/");
|
|
621
693
|
if (!visit(rel, abs, mtime))
|
|
622
694
|
return true;
|
|
623
695
|
}
|
package/dist/replay/bundle.d.ts
CHANGED
|
@@ -51,8 +51,8 @@ export interface AssembleOptions {
|
|
|
51
51
|
/** The plan stopped part-way and the model is to carry on (fallbk.md). */
|
|
52
52
|
handover?: boolean;
|
|
53
53
|
/**
|
|
54
|
-
* The plan was armed mid-task by intent
|
|
55
|
-
*
|
|
54
|
+
* The plan was armed mid-task by intent. Its results feed the agent's own
|
|
55
|
+
* task, which continues. `handover` wins over this.
|
|
56
56
|
*/
|
|
57
57
|
subTask?: boolean;
|
|
58
58
|
/**
|
package/dist/replay/bundle.js
CHANGED
|
@@ -100,8 +100,8 @@ const HANDOVER_HEADER = "[BaseInstRunner calculated replay] The tool calls below
|
|
|
100
100
|
"continue the task yourself from where they end.";
|
|
101
101
|
const HANDOVER_FOOTER = "\n\nThe calls above are done. Continue the rest of the task yourself, calling tools as needed.";
|
|
102
102
|
/**
|
|
103
|
-
* The plan ran inside the agent's own task (
|
|
104
|
-
* but the task did not, so the model must not be told to answer without tools.
|
|
103
|
+
* The plan ran inside the agent's own task (armed mid-task by intent): it
|
|
104
|
+
* finished, but the task did not, so the model must not be told to answer without tools.
|
|
105
105
|
* This is {@link HANDOVER_HEADER} without "The scenario stopped before the task
|
|
106
106
|
* was finished" — nothing stopped, the sub-task is simply done.
|
|
107
107
|
*/
|
|
@@ -121,7 +121,7 @@ export function assembleBundle(entries, maxChars, opts = {}) {
|
|
|
121
121
|
const anyRecorded = entries.some((e) => e.recorded);
|
|
122
122
|
// A hand-over outranks a sub-task: it says "continue" too, and it also has to
|
|
123
123
|
// say that the plan stopped short. A sub-task outranks the live and mixed
|
|
124
|
-
// headers, whose wording forbids further tool calls
|
|
124
|
+
// headers, whose wording forbids further tool calls.
|
|
125
125
|
const header = opts.handover
|
|
126
126
|
? HANDOVER_HEADER
|
|
127
127
|
: opts.subTask
|
|
@@ -11,14 +11,13 @@
|
|
|
11
11
|
* degrades to "run the turn normally" or returns something the session can
|
|
12
12
|
* ignore. Nothing throws at a hook.
|
|
13
13
|
*/
|
|
14
|
-
import type { ExecutionReport
|
|
14
|
+
import type { ExecutionReport } from "../record/recorder.js";
|
|
15
15
|
import { type ReplayMode, type StepReach } from "./coverage.js";
|
|
16
16
|
import { deriveParameters, type AgentDay, type DeriveContext, type DeriveResult } from "./derive.js";
|
|
17
17
|
import { ProxyWorkQueue } from "./executor.js";
|
|
18
18
|
import { ScenarioReplayPlan } from "./plan.js";
|
|
19
19
|
import { type HostSinks } from "./safety.js";
|
|
20
20
|
import { SourceRunOutputs } from "./source-run.js";
|
|
21
|
-
import { type FlatStop } from "./flatten.js";
|
|
22
21
|
import { type ExecutionOutcome, type ExecutionStepResult, type FallbackKind, type SerializedScenario, type SerializedScenarioStep } from "./types.js";
|
|
23
22
|
/** The first-party tool a `direct` plan is delivered through (§6.3). */
|
|
24
23
|
export declare const DIRECT_TOOL_NAME = "mcp__bir__run_scenario";
|
|
@@ -239,14 +238,6 @@ export declare const RUN_NO_PLAN = "no_plan";
|
|
|
239
238
|
export interface ReplayOptions {
|
|
240
239
|
/** On unless `BIR_REPLAY=0`. Everything here is inert when false. */
|
|
241
240
|
enabled: boolean;
|
|
242
|
-
/**
|
|
243
|
-
* Whether a handed-out segment may run mid-task. The recorder sets it from
|
|
244
|
-
* `BIR_SEGMENT_ARM`: on unless that is `0`, from runner 0.3.19 (the
|
|
245
|
-
* service's segmentsbreakdown.md SB-21). Omitted, as by a test or an
|
|
246
|
-
* embedder, it is off: observe-only, where the runner asks for no segment,
|
|
247
|
-
* arms none, and only logs what would have armed (R-OUT-10).
|
|
248
|
-
*/
|
|
249
|
-
segmentArm?: boolean;
|
|
250
241
|
/** `BIR_REPLAY_ALLOW_SERVERS`; undefined means every wrapped server. */
|
|
251
242
|
allowServers?: ReadonlySet<string>;
|
|
252
243
|
/**
|
|
@@ -266,6 +257,16 @@ export interface ReplayOptions {
|
|
|
266
257
|
* rather than armed `direct` and stopped at the first one.
|
|
267
258
|
*/
|
|
268
259
|
bashAllow?: readonly string[];
|
|
260
|
+
/**
|
|
261
|
+
* `BIR_PATH_ALLOW` — the folders a runner-side `Read`, `Glob`, `Grep`,
|
|
262
|
+
* `Edit` or `Write` may touch (`*` for any; `project` is the session's
|
|
263
|
+
* project directory). Omitted, the environment decides; the default is
|
|
264
|
+
* `*`, any path the agent's own tools could reach (up to 0.3.20 the
|
|
265
|
+
* project directory alone). Empty allows no folder, and then the file tools
|
|
266
|
+
* count as off {@link ReplayOptions.builtinTools}, as `Bash` does under an
|
|
267
|
+
* empty {@link ReplayOptions.bashAllow}.
|
|
268
|
+
*/
|
|
269
|
+
pathAllow?: readonly string[];
|
|
269
270
|
budgets?: Partial<ReplayBudgets>;
|
|
270
271
|
/**
|
|
271
272
|
* The service: the lazy source-run fetch, and the parameter derivation this
|
|
@@ -297,21 +298,13 @@ export interface ReplayOptions {
|
|
|
297
298
|
export interface IntentMatchOptions {
|
|
298
299
|
/** `BIR_INTENT_MATCH`. */
|
|
299
300
|
enabled: boolean;
|
|
300
|
-
/**
|
|
301
|
-
* How long a `PreToolUse` waits for the service. A timeout is a miss.
|
|
302
|
-
*
|
|
303
|
-
* 4 000 ms, not the 1 500 of Part 1: a request whose best segment lands in
|
|
304
|
-
* the *not clear* band has one live question to ask under the server's own
|
|
305
|
-
* 2 500 ms attempt, and must still answer well inside the hook's 30 s
|
|
306
|
-
* (segmented.md R-HIT-11). A clear match or a miss answers as fast as before.
|
|
307
|
-
*/
|
|
301
|
+
/** How long a probe's request may take. A timeout is a miss. */
|
|
308
302
|
budgetMs: number;
|
|
309
303
|
/** Plans intent matching may arm in one turn. */
|
|
310
304
|
maxPerTurn: number;
|
|
311
305
|
/**
|
|
312
|
-
* Probes one turn may *send*. Separate from `maxPerTurn`, and
|
|
313
|
-
*
|
|
314
|
-
* bookkeeping, and it must not stop at the third arm (R-HIT-5).
|
|
306
|
+
* Probes one turn may *send*. Separate from `maxPerTurn`, and larger: most
|
|
307
|
+
* probes find nothing, and a turn that armed nothing yet may still match.
|
|
315
308
|
*/
|
|
316
309
|
maxRequestsPerTurn: number;
|
|
317
310
|
}
|
|
@@ -391,24 +384,10 @@ export interface ReplayState {
|
|
|
391
384
|
armedAt: number;
|
|
392
385
|
reported: boolean;
|
|
393
386
|
sourceRun?: SourceRunOutputs;
|
|
394
|
-
/**
|
|
395
|
-
* Why the flattener stopped the plan, when it is what stopped it: a call this
|
|
396
|
-
* chain cannot run right now (segmented.md R-CALL-29). The note names the
|
|
397
|
-
* sub-task and the reason, and the report carries `unusable_call`.
|
|
398
|
-
*/
|
|
399
|
-
flatStop?: FlatStop;
|
|
400
387
|
/** Set once a plan has been retired, so nothing re-arms mid-turn. */
|
|
401
388
|
retired: boolean;
|
|
402
389
|
/** What armed it: the prompt, or a ReAct iteration's intent (fallbk.md). */
|
|
403
390
|
armedBy: "prompt" | "intent";
|
|
404
|
-
/**
|
|
405
|
-
* The user's request this plan serves, in their own words: the prompt for a
|
|
406
|
-
* plan the prompt armed (set in `arm`), the turn's prompt for one an intent
|
|
407
|
-
* match armed (set by the control server). Read only for the note of a plan
|
|
408
|
-
* stopped at its decision, which quotes the request's condition sentences
|
|
409
|
-
* (the service's segmentsbreakdown.md SB-18). Never sent anywhere.
|
|
410
|
-
*/
|
|
411
|
-
request?: string;
|
|
412
391
|
/**
|
|
413
392
|
* The Claude Code session this plan serves, set by the control server. Its
|
|
414
393
|
* steps run on that session's own proxies (executor.ts). Absent for `bir
|
|
@@ -418,8 +397,9 @@ export interface ReplayState {
|
|
|
418
397
|
/**
|
|
419
398
|
* That session's project directory, set by the control server from the
|
|
420
399
|
* hook payloads — never the daemon's own cwd, which is `~/.baseinstrunner`.
|
|
421
|
-
* It is where an allow-listed built-in step executes and
|
|
422
|
-
*
|
|
400
|
+
* It is where an allow-listed built-in step executes and what its relative
|
|
401
|
+
* paths resolve against (replay/builtins.ts; `BIR_PATH_ALLOW=project`
|
|
402
|
+
* confines the file tools to it). Absent — `bir replay`, an older server —
|
|
423
403
|
* no built-in executes: the step is `not_run`, exactly as before.
|
|
424
404
|
*/
|
|
425
405
|
sessionCwd?: string;
|
|
@@ -432,25 +412,6 @@ export interface ReplayState {
|
|
|
432
412
|
* paths, which resolve against the project directory as the host's do.
|
|
433
413
|
*/
|
|
434
414
|
bashCwd?: string;
|
|
435
|
-
/**
|
|
436
|
-
* Which kind of row was handed out (segmented.md R-OUT-7). A `segment` is a
|
|
437
|
-
* named sub-task of a recording rather than a whole task, and it is judged
|
|
438
|
-
* against its own similarity gate. Absent from an older server, and then read
|
|
439
|
-
* as `scenario` (R-COMPAT-2).
|
|
440
|
-
*/
|
|
441
|
-
kind: "scenario" | "segment";
|
|
442
|
-
/** Where a handed-out segment sits in its recording (R-OUT-8). */
|
|
443
|
-
segment?: MatchSegment;
|
|
444
|
-
/** Which of the segment's two keys scored (R-OUT-3). For logs. */
|
|
445
|
-
key?: MatchKey;
|
|
446
|
-
/** The cheap model's answer, when the score sat in the not-clear band. */
|
|
447
|
-
verified?: MatchVerdict;
|
|
448
|
-
/**
|
|
449
|
-
* Segments of this plan's own recording that armed later in the same turn
|
|
450
|
-
* (R-MONEY-4). Their share comes off this plan's baseline, so one recording's
|
|
451
|
-
* steps are never counted in two baselines of one turn.
|
|
452
|
-
*/
|
|
453
|
-
sharedWith: string[];
|
|
454
415
|
/** Set when the plan handed the task to the model part-way (fallbk.md). */
|
|
455
416
|
handover?: Handover;
|
|
456
417
|
/**
|
|
@@ -472,11 +433,9 @@ export interface ReplayState {
|
|
|
472
433
|
*/
|
|
473
434
|
diverging?: boolean;
|
|
474
435
|
/**
|
|
475
|
-
* Which gate declined, as a fixed code
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
* and reading prose for that would break the first time a sentence was
|
|
479
|
-
* reworded.
|
|
436
|
+
* Which gate declined, as a fixed code. The prose `why` that `decline()`
|
|
437
|
+
* logs stays free text; this is what code and the report read, and reading
|
|
438
|
+
* prose would break the first time a sentence was reworded.
|
|
480
439
|
*/
|
|
481
440
|
declined?: DeclineCode;
|
|
482
441
|
/**
|
|
@@ -496,10 +455,7 @@ export interface ReplayState {
|
|
|
496
455
|
* A plan that finished its work stops costing at that moment: the agent's
|
|
497
456
|
* later work in the same turn is the agent's, not this plan's. A plan that
|
|
498
457
|
* handed over or diverged takes no mark, because the follow-on work is the
|
|
499
|
-
* direct consequence of its stopping (R-MONEY-3, R-FALL-5)
|
|
500
|
-
* stopped at its decision after a step ran: it did all it was for, and the
|
|
501
|
-
* agent's decision and the half it chose are the agent's own (the service's
|
|
502
|
-
* segmentsbreakdown.md SB-26).
|
|
458
|
+
* direct consequence of its stopping (R-MONEY-3, R-FALL-5).
|
|
503
459
|
*/
|
|
504
460
|
retiredMark?: unknown;
|
|
505
461
|
retiredAt?: number;
|
|
@@ -567,10 +523,7 @@ export interface ReplayState {
|
|
|
567
523
|
* copies are cut to 3,800 bytes, and the agent's work is built on them.
|
|
568
524
|
*/
|
|
569
525
|
keepCalls?: boolean;
|
|
570
|
-
/**
|
|
571
|
-
* Those calls, in the order the plan made them, keyed by frame and step —
|
|
572
|
-
* a sub-task called twice keeps both calls of each step.
|
|
573
|
-
*/
|
|
526
|
+
/** Those calls, in the order the plan made them, keyed by step. */
|
|
574
527
|
turnCalls?: Map<string, TurnCall>;
|
|
575
528
|
/** What they hold, in characters: bounded by {@link TURN_CALLS_MAX_CHARS}. */
|
|
576
529
|
turnCallChars?: number;
|
|
@@ -613,7 +566,7 @@ export interface TurnCall {
|
|
|
613
566
|
* Why a match did not arm. One code per gate of the ladder, so the caller can
|
|
614
567
|
* act on a decline without parsing the sentence that explains it.
|
|
615
568
|
*/
|
|
616
|
-
export type DeclineCode = "replay_disabled" | "not_ready" | "
|
|
569
|
+
export type DeclineCode = "replay_disabled" | "not_ready" | "known_bad_first_step" | "nondeterministic_first_step" | "coverage" | "missing_target" | "no_derive_key"
|
|
617
570
|
/** The service handed over no plan: this runner is too old for it. */
|
|
618
571
|
| "runner_update_required"
|
|
619
572
|
/**
|
|
@@ -689,6 +642,8 @@ export declare class ReplayController {
|
|
|
689
642
|
private readonly builtinTools;
|
|
690
643
|
/** See {@link ReplayOptions.bashAllow}. */
|
|
691
644
|
private readonly bashAllow;
|
|
645
|
+
/** See {@link ReplayOptions.pathAllow}. */
|
|
646
|
+
private readonly pathAllow;
|
|
692
647
|
/** Calls waiting for a plan's pins to change (see `awaitPins`). */
|
|
693
648
|
private readonly pinWaiters;
|
|
694
649
|
/**
|
|
@@ -698,16 +653,6 @@ export declare class ReplayController {
|
|
|
698
653
|
*/
|
|
699
654
|
private hostSignals;
|
|
700
655
|
constructor(opts: ReplayOptions);
|
|
701
|
-
/**
|
|
702
|
-
* Whether a handed-out segment may actually arm (segmented.md R-OUT-10).
|
|
703
|
-
*
|
|
704
|
-
* Only when the option says so. The recorder passes it on unless
|
|
705
|
-
* `BIR_SEGMENT_ARM=0` (from 0.3.19; the service's segmentsbreakdown.md
|
|
706
|
-
* SB-21); a controller built without it stays observe-only, asks for no
|
|
707
|
-
* segment and arms none. The server enforces the same thing from its side
|
|
708
|
-
* (R-OUT-9).
|
|
709
|
-
*/
|
|
710
|
-
get segmentArm(): boolean;
|
|
711
656
|
/** The plan's own delivery vehicle is never a scenario step. */
|
|
712
657
|
isDirectTool(toolName: string): boolean;
|
|
713
658
|
/**
|
|
@@ -745,16 +690,9 @@ export declare class ReplayController {
|
|
|
745
690
|
* stops when that is before its end. For a host that steps through the plan
|
|
746
691
|
* itself and must not have to pick the tool names out of a directive
|
|
747
692
|
* written for a model — a reworded sentence would break every adapter in
|
|
748
|
-
* the field. Built from the same
|
|
693
|
+
* the field. Built from the same steps the directive lists.
|
|
749
694
|
*/
|
|
750
695
|
planData(state: ReplayState): PlanData | undefined;
|
|
751
|
-
/**
|
|
752
|
-
* Whether the plan's steps end in front of a decision (the service's
|
|
753
|
-
* segmentsbreakdown.md SB-6): it runs the steps before it, then hands the
|
|
754
|
-
* task back there. The control server gives such a plan a usage mark, so
|
|
755
|
-
* that it is billed only up to the stop (SB-26).
|
|
756
|
-
*/
|
|
757
|
-
stopsAtDecision(state: ReplayState): boolean;
|
|
758
696
|
/**
|
|
759
697
|
* Take back a plan that armed and has run nothing, as a decline.
|
|
760
698
|
*
|
|
@@ -901,9 +839,9 @@ export declare class ReplayController {
|
|
|
901
839
|
*/
|
|
902
840
|
postTool(state: ReplayState, toolUseId: string, output: string, failed?: string, how?: PostToolOptions): boolean;
|
|
903
841
|
/**
|
|
904
|
-
* A steer plan whose cursor reached its end: in front of a parked step
|
|
905
|
-
*
|
|
906
|
-
*
|
|
842
|
+
* A steer plan whose cursor reached its end: in front of a parked step,
|
|
843
|
+
* which hands over; otherwise it ran to its end — and from here every call
|
|
844
|
+
* is the model's own.
|
|
907
845
|
*/
|
|
908
846
|
private finishSteer;
|
|
909
847
|
/**
|
|
@@ -1044,10 +982,6 @@ export declare class ReplayController {
|
|
|
1044
982
|
* there is nothing to be similar *to* and nothing to decline. It books no
|
|
1045
983
|
* execution either — there is no ticket, and inventing a saving for a manual
|
|
1046
984
|
* invocation is exactly the kind of number a ledger must never contain.
|
|
1047
|
-
*
|
|
1048
|
-
* One served stop is kept: a plan's decision (the service's
|
|
1049
|
-
* segmentsbreakdown.md SB-6). The run stops in front of it, and `stopped`
|
|
1050
|
-
* says so with the reason `decision`.
|
|
1051
985
|
*/
|
|
1052
986
|
runAdHoc(scenario: SerializedScenario, prompt: string, wrapped: ReadonlySet<string>): Promise<{
|
|
1053
987
|
ok: boolean;
|
|
@@ -1117,23 +1051,10 @@ export declare class ReplayController {
|
|
|
1117
1051
|
*/
|
|
1118
1052
|
private handOver;
|
|
1119
1053
|
/**
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
* step.
|
|
1124
|
-
*/
|
|
1125
|
-
private stopKindOf;
|
|
1126
|
-
/**
|
|
1127
|
-
* A divergence's bundle that ended at the plan's decision (SB-17): logged
|
|
1128
|
-
* as `handOver` logs every other hand-over, and its usage mark taken there
|
|
1129
|
-
* when a step of the plan ran (SB-26).
|
|
1130
|
-
*/
|
|
1131
|
-
private decidedInDivergence;
|
|
1132
|
-
/**
|
|
1133
|
-
* The steps before flat position `bound` that ran, 1-based, and the ones
|
|
1134
|
-
* their tests left out (AC5) — the plan cursor's own, and a divergence
|
|
1135
|
-
* bundle's (`extra`). `left` is empty when no step was left out; `ran` is
|
|
1136
|
-
* then not filled, since every step before `bound` ran.
|
|
1054
|
+
* The steps before position `bound` that ran, 1-based, and the ones their
|
|
1055
|
+
* tests left out (AC5) — the plan cursor's own, and a divergence bundle's
|
|
1056
|
+
* (`extra`). `left` is empty when no step was left out; `ran` is then not
|
|
1057
|
+
* filled, since every step before `bound` ran.
|
|
1137
1058
|
*/
|
|
1138
1059
|
private ranBefore;
|
|
1139
1060
|
private noteFor;
|
|
@@ -1242,12 +1163,6 @@ export declare class ReplayController {
|
|
|
1242
1163
|
/** Outcomes only move forward; `not_steered` is the floor (§11.1). */
|
|
1243
1164
|
private upgrade;
|
|
1244
1165
|
private retire;
|
|
1245
|
-
/**
|
|
1246
|
-
* Where the plan's work ended, for its bill: now, and the transcript's
|
|
1247
|
-
* watermark now. Taken at a completing retire, and at a decision hand-over
|
|
1248
|
-
* after a step ran (the service's segmentsbreakdown.md SB-26).
|
|
1249
|
-
*/
|
|
1250
|
-
private markRetired;
|
|
1251
1166
|
}
|
|
1252
1167
|
/** The subset of `RunMatch` replay consumes. Structural, so tests need no recorder. */
|
|
1253
1168
|
export interface RunMatchLike {
|
|
@@ -1259,11 +1174,6 @@ export interface RunMatchLike {
|
|
|
1259
1174
|
fallback?: {
|
|
1260
1175
|
maxStepFailures: number;
|
|
1261
1176
|
};
|
|
1262
|
-
/** All absent from an older server (segmented.md R-COMPAT-2). */
|
|
1263
|
-
kind?: "scenario" | "segment";
|
|
1264
|
-
segment?: MatchSegment;
|
|
1265
|
-
key?: MatchKey;
|
|
1266
|
-
verified?: MatchVerdict;
|
|
1267
1177
|
/** The frozen name, beside the runtime intent the plan runs on (R-INTENT-12). */
|
|
1268
1178
|
intentName?: string;
|
|
1269
1179
|
/** Why the service handed over no plan, when it says. Read for `runner_update_required`. */
|
|
@@ -1282,8 +1192,7 @@ export declare function knownBadStepIndex(steps: readonly SerializedScenarioStep
|
|
|
1282
1192
|
*
|
|
1283
1193
|
* A step that carries `stop` was decided by the service — `null` means run it,
|
|
1284
1194
|
* even when its count is past the limit: its retry is due. A step without the
|
|
1285
|
-
* field came from an older service, and the old counter rule decides it.
|
|
1286
|
-
* can sit in one chain, since a called segment is served on its own terms.
|
|
1195
|
+
* field came from an older service, and the old counter rule decides it.
|
|
1287
1196
|
*/
|
|
1288
1197
|
export declare function stopIndex(steps: readonly SerializedScenarioStep[], maxStepFailures: number | undefined): number | undefined;
|
|
1289
1198
|
export {};
|