@basein/runner 0.3.25 → 0.3.26
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 +27 -26
- package/dist/auth/client.d.ts +4 -1
- package/dist/auth/client.js +83 -14
- package/dist/bin/bir-hooks.js +4 -2
- package/dist/bin/bir.js +153 -37
- package/dist/bin/investigate.js +5 -0
- package/dist/bin/scenario-edit.d.ts +5 -0
- package/dist/bin/scenario-edit.js +17 -1
- package/dist/config/adapters/claude-code.d.ts +51 -7
- package/dist/config/adapters/claude-code.js +105 -18
- package/dist/config/generate.d.ts +61 -13
- package/dist/config/generate.js +108 -20
- package/dist/config/resolve.d.ts +5 -0
- package/dist/config/resolve.js +7 -0
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +28 -0
- package/dist/control/daemon.js +62 -0
- package/dist/control/paths.d.ts +19 -1
- package/dist/control/paths.js +20 -2
- package/dist/control/pin-guard.js +8 -2
- package/dist/control/recorder-main.js +17 -5
- package/dist/control/server.d.ts +63 -9
- package/dist/control/server.js +350 -62
- package/dist/control/transcript.d.ts +64 -3
- package/dist/control/transcript.js +318 -37
- package/dist/jsonrpc/framing.d.ts +18 -3
- package/dist/jsonrpc/framing.js +57 -4
- package/dist/proxy/intercept.d.ts +4 -0
- package/dist/proxy/intercept.js +4 -0
- package/dist/proxy/relay.d.ts +13 -0
- package/dist/proxy/relay.js +12 -1
- package/dist/proxy/session.d.ts +8 -1
- package/dist/proxy/session.js +67 -10
- package/dist/record/recorder.d.ts +6 -1
- package/dist/record/redact.d.ts +18 -4
- package/dist/record/redact.js +538 -79
- package/dist/record/remote-recorder.d.ts +26 -5
- package/dist/record/remote-recorder.js +174 -18
- package/dist/record/truncate.js +3 -1
- package/dist/replay/bash-channel.d.ts +7 -1
- package/dist/replay/bash-channel.js +9 -3
- package/dist/replay/builtins.d.ts +14 -0
- package/dist/replay/builtins.js +460 -95
- package/dist/replay/bundle.d.ts +15 -11
- package/dist/replay/bundle.js +167 -33
- package/dist/replay/controller.d.ts +62 -0
- package/dist/replay/controller.js +218 -32
- package/dist/replay/derive.js +46 -4
- package/dist/replay/handover.d.ts +13 -0
- package/dist/replay/handover.js +18 -0
- package/dist/replay/pricing.d.ts +61 -11
- package/dist/replay/pricing.js +135 -42
- package/dist/replay/safety.js +43 -5
- package/dist/upstream/http-client.d.ts +6 -3
- package/dist/upstream/http-client.js +54 -13
- package/dist/upstream/stdio-client.d.ts +7 -0
- package/dist/upstream/stdio-client.js +1 -1
- package/dist/util/atomic-write.d.ts +9 -1
- package/dist/util/atomic-write.js +9 -6
- package/dist/util/service-health.d.ts +7 -0
- package/dist/util/service-health.js +10 -3
- package/package.json +1 -1
- package/production.md +2 -2
- package/scripts/install.ps1 +14 -2
package/dist/replay/bundle.d.ts
CHANGED
|
@@ -104,13 +104,16 @@ export declare function assembleBundle(entries: readonly BundleEntry[], maxChars
|
|
|
104
104
|
*/
|
|
105
105
|
export declare function wentWell(entries: readonly BundleEntry[], opts: Pick<AssembleOptions, "handover" | "subTask">): boolean;
|
|
106
106
|
/**
|
|
107
|
-
* JSON text with every string longer than 40 characters cut to its
|
|
108
|
-
* and "…". Keys, numbers
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
107
|
+
* JSON text with every string of prose longer than 40 characters cut to its
|
|
108
|
+
* first 40 and "…". Keys, numbers, short strings and strings with no space in
|
|
109
|
+
* them ({@link BRIEF_ID_CHARS}: a link, a path, an id) stay as they are. A
|
|
110
|
+
* character is never split: an escape counts as one, `\n` and a Unicode
|
|
111
|
+
* escape (a backslash, a `u` and four hex digits) alike, and so does a pair of
|
|
112
|
+
* surrogates, written out (an emoji) or as two such escapes.
|
|
113
|
+
* Works on JSON a parser would refuse — a tool's `NaN`, a result cut short —
|
|
114
|
+
* because it walks the text's string literals from the start, so it always
|
|
115
|
+
* knows which quote opens one: the text between two strings, a list of numbers
|
|
116
|
+
* say, is never taken for one. An unterminated string is left as it is.
|
|
114
117
|
*/
|
|
115
118
|
export declare function cutLongText(json: string): string;
|
|
116
119
|
/**
|
|
@@ -120,10 +123,11 @@ export declare function cutLongText(json: string): string;
|
|
|
120
123
|
export declare function briefFields(fields: Record<string, unknown>, max: number): string;
|
|
121
124
|
/**
|
|
122
125
|
* A value of the plan's summary in a few words. Numbers, booleans and short
|
|
123
|
-
* strings whole;
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
126
|
+
* strings whole; long text cut, as a result's is ({@link cutLongText}: a link
|
|
127
|
+
* or an id is whole); a list or object whose JSON is short — a few IDs, names
|
|
128
|
+
* or addresses — whole; a longer list counted; a longer object by its first
|
|
129
|
+
* fields, one level down, below which it is counted too. A summary's long
|
|
130
|
+
* lists are mostly the records the steps above already show.
|
|
127
131
|
*/
|
|
128
132
|
export declare function briefValue(value: unknown, depth?: number): string;
|
|
129
133
|
/**
|
package/dist/replay/bundle.js
CHANGED
|
@@ -44,6 +44,14 @@ export const BASH_CHANNEL_MAX = 25_000;
|
|
|
44
44
|
const MAX_INPUT_CHARS = 2_000;
|
|
45
45
|
/** Fixed structural overhead reserved per step (labels, separators). */
|
|
46
46
|
const PER_STEP_OVERHEAD = 80;
|
|
47
|
+
/**
|
|
48
|
+
* The least each result is shown of. When the inputs leave the results less
|
|
49
|
+
* than this each, the results keep it and the inputs give up the difference:
|
|
50
|
+
* the results are what the agent answers from, and fourteen inputs at their cap
|
|
51
|
+
* once left them nothing at all, every "Result:" empty under a header that
|
|
52
|
+
* calls it the call's actual output.
|
|
53
|
+
*/
|
|
54
|
+
const MIN_RESULT_CHARS = 200;
|
|
47
55
|
/** The test's words, one line and short, for a bundle or a note. */
|
|
48
56
|
export function testWords(text) {
|
|
49
57
|
const line = (text ?? "").trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
@@ -134,7 +142,8 @@ const BRIEF_HEADER = "[BaseInstRunner calculated replay] The tool calls below ha
|
|
|
134
142
|
"for real, moments ago, against the live system, on the user's behalf. Every one " +
|
|
135
143
|
"of them succeeded, and every side effect is already in place. Do NOT repeat, redo " +
|
|
136
144
|
"or re-verify this work, and do NOT call any tools. In a long result, every record " +
|
|
137
|
-
"and field is shown, and text longer than 40 characters is cut short with \"…\"
|
|
145
|
+
"and field is shown, and text longer than 40 characters is cut short with \"…\"; " +
|
|
146
|
+
"a link, a path or an id (no spaces, up to 300 characters) is shown whole.";
|
|
138
147
|
/** The reply is output, the dearest tokens an agent spends: asked for briefly. */
|
|
139
148
|
const BRIEF_FOOTER = "\n\nThe work above is done. Answer the user's request from it in a few sentences: " +
|
|
140
149
|
"what was done, and the key results. Do not call any tools.";
|
|
@@ -144,6 +153,13 @@ const BRIEF_RESULT_CHARS = 300;
|
|
|
144
153
|
const BRIEF_SUMMARY_CHARS = 1_000;
|
|
145
154
|
/** A longer string — in a long result, or in the summary — is cut to this many characters. */
|
|
146
155
|
const BRIEF_STRING_CHARS = 40;
|
|
156
|
+
/**
|
|
157
|
+
* But a string with no space in it — a link, a path, an id, a hash, an e-mail
|
|
158
|
+
* address — is what the agent passes on to the user, and cut it opens nothing
|
|
159
|
+
* and names nothing: it is kept whole up to this many characters. Past them it
|
|
160
|
+
* is a blob (an encoded image, a minified line), and is cut like text.
|
|
161
|
+
*/
|
|
162
|
+
const BRIEF_ID_CHARS = 300;
|
|
147
163
|
/** A list or object in the summary whose JSON is up to this long is shown whole; a longer one is counted. */
|
|
148
164
|
const BRIEF_INLINE_CHARS = 160;
|
|
149
165
|
/** A longer object in the summary shows its first fields, this many. */
|
|
@@ -171,26 +187,40 @@ export function assembleBundle(entries, maxChars, opts = {}) {
|
|
|
171
187
|
? SUBTASK_FOOTER
|
|
172
188
|
: withReportException(brief ? BRIEF_FOOTER : anyRecorded ? MIXED_FOOTER : LIVE_FOOTER, opts.reportTool);
|
|
173
189
|
const summary = brief ? briefSummary(opts.brief?.summary) : "";
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
const labels = entries.reduce((sum, e) => sum +
|
|
191
|
+
PER_STEP_OVERHEAD +
|
|
192
|
+
e.toolName.length +
|
|
193
|
+
// A left-out step's line, which has no response to take from.
|
|
194
|
+
(e.notNeeded ? PER_STEP_OVERHEAD + (testWords(e.notNeeded.text)?.length ?? 0) : 0), 0);
|
|
195
|
+
const inputs = entries.reduce((sum, e) => sum + e.input.length, 0);
|
|
196
|
+
// What the frame and the labels leave is shared by the inputs and the
|
|
197
|
+
// results. The inputs go in whole and the results split what they leave —
|
|
198
|
+
// unless that is less than a floor each (or nothing at all, when the inputs
|
|
199
|
+
// alone pass the limit): then each result keeps the floor, and the inputs,
|
|
200
|
+
// cut to a share of the rest, say how much of themselves is left out.
|
|
201
|
+
const free = Math.max(0, maxChars - (header.length + footer.length + summary.length + labels));
|
|
202
|
+
let perResponse = n > 0 ? Math.floor(Math.max(0, free - inputs) / n) : Math.max(0, free - inputs);
|
|
203
|
+
let inputCap = Number.POSITIVE_INFINITY;
|
|
204
|
+
const floor = Math.min(MIN_RESULT_CHARS, Math.floor(free / (2 * Math.max(1, n))));
|
|
205
|
+
if (n > 0 && perResponse < floor) {
|
|
206
|
+
perResponse = floor;
|
|
207
|
+
inputCap = Math.floor((free - n * floor) / entries.length);
|
|
208
|
+
}
|
|
209
|
+
const shownEntries = Number.isFinite(inputCap)
|
|
210
|
+
? entries.map((e) => ({ ...e, input: truncate(e.input, inputCap) }))
|
|
211
|
+
: entries;
|
|
185
212
|
const show = (e) => {
|
|
186
213
|
const result = resultText(e.response);
|
|
214
|
+
// A built-in's result is shown as its own text, whole within its share.
|
|
215
|
+
const own = brief ? builtinText(e.toolName, e.response) : undefined;
|
|
216
|
+
const text = own ?? result.text;
|
|
187
217
|
return {
|
|
188
|
-
text: brief ? briefResult(
|
|
218
|
+
text: brief && own === undefined ? briefResult(text, perResponse) : truncate(text, perResponse),
|
|
189
219
|
marks: resultMarks(e.recorded, result.error),
|
|
190
220
|
};
|
|
191
221
|
};
|
|
192
222
|
const sections = [];
|
|
193
|
-
for (const run of consecutiveRuns(
|
|
223
|
+
for (const run of consecutiveRuns(shownEntries)) {
|
|
194
224
|
if (run.length === 1) {
|
|
195
225
|
const { entry: e, index: i } = run[0];
|
|
196
226
|
if (e.notNeeded) {
|
|
@@ -209,7 +239,14 @@ export function assembleBundle(entries, maxChars, opts = {}) {
|
|
|
209
239
|
}
|
|
210
240
|
sections.push(runSection(run, show));
|
|
211
241
|
}
|
|
212
|
-
|
|
242
|
+
// The labels are an estimate, and a bundle that still passes the limit loses
|
|
243
|
+
// its middle, never its footer: the last thing the agent is told. Where even
|
|
244
|
+
// that does not fit the limit, the end goes, as it always did.
|
|
245
|
+
const whole = header + sections.join("") + summary + footer;
|
|
246
|
+
if (whole.length <= maxChars)
|
|
247
|
+
return whole;
|
|
248
|
+
const keepFooter = truncate(header + sections.join("") + summary, Math.max(0, maxChars - footer.length)) + footer;
|
|
249
|
+
return keepFooter.length <= maxChars ? keepFooter : truncate(whole, maxChars);
|
|
213
250
|
}
|
|
214
251
|
/**
|
|
215
252
|
* Whether a run went entirely well and the agent answers now — the one case a
|
|
@@ -239,14 +276,67 @@ function briefResult(text, budget) {
|
|
|
239
276
|
return truncate(cutLongText(text), budget);
|
|
240
277
|
return truncate(text, budget);
|
|
241
278
|
}
|
|
279
|
+
/** The runner's built-ins whose results {@link builtinText} lays out as text. */
|
|
280
|
+
const BUILTIN_RESULT_TOOLS = new Set(["Bash", "Read", "Glob", "Grep"]);
|
|
281
|
+
/**
|
|
282
|
+
* A built-in tool's result as the text it is, or undefined when it is not one
|
|
283
|
+
* the brief layout can show so: what a command printed, a file's lines, a
|
|
284
|
+
* search's lines or file names.
|
|
285
|
+
*
|
|
286
|
+
* The runner's own tools answer in the hook's shape — `{stdout, stderr, …}`,
|
|
287
|
+
* `{type, file: {content, …}}`, `{mode, content | filenames, …}`, `{filenames,
|
|
288
|
+
* …}` — and not in MCP's `CallToolResult`, so {@link resultText} leaves them as
|
|
289
|
+
* JSON, and the brief layout took all of it for one long string: a command's
|
|
290
|
+
* output of 470 characters ending "TOTAL DIFFERENCE 100.00" was cut to its
|
|
291
|
+
* first 40, a Glob's file names every one, under a header that says to call no
|
|
292
|
+
* tools. The same words from an MCP tool were shown whole.
|
|
293
|
+
*/
|
|
294
|
+
function builtinText(toolName, response) {
|
|
295
|
+
if (!BUILTIN_RESULT_TOOLS.has(toolName))
|
|
296
|
+
return undefined;
|
|
297
|
+
let parsed;
|
|
298
|
+
try {
|
|
299
|
+
parsed = JSON.parse(response);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
305
|
+
return undefined;
|
|
306
|
+
const r = parsed;
|
|
307
|
+
const lines = (v) => Array.isArray(v) && v.every((x) => typeof x === "string") ? v.join("\n") : undefined;
|
|
308
|
+
let body;
|
|
309
|
+
if (toolName === "Bash") {
|
|
310
|
+
if (typeof r.stdout !== "string" || r.interrupted === true || r.isImage === true)
|
|
311
|
+
return undefined;
|
|
312
|
+
const stderr = typeof r.stderr === "string" && r.stderr !== "" ? `stderr: ${r.stderr}` : "";
|
|
313
|
+
body = stderr && r.stdout !== "" && !r.stdout.endsWith("\n") ? `${r.stdout}\n${stderr}` : r.stdout + stderr;
|
|
314
|
+
}
|
|
315
|
+
else if (toolName === "Read") {
|
|
316
|
+
const file = r.file;
|
|
317
|
+
body = r.type === "text" && typeof file?.content === "string" ? file.content : undefined;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
body = typeof r.content === "string" && toolName === "Grep" ? r.content : lines(r.filenames);
|
|
321
|
+
}
|
|
322
|
+
if (body === undefined)
|
|
323
|
+
return undefined;
|
|
324
|
+
if (body === "")
|
|
325
|
+
return toolName === "Bash" ? "(no output)" : toolName === "Read" ? "(empty file)" : "(nothing found)";
|
|
326
|
+
// A list the tool cut where it made it: its text alone would read as the whole answer.
|
|
327
|
+
return r.truncated === true ? `${body}\n(cut short by the tool: there may be more than is shown)` : body;
|
|
328
|
+
}
|
|
242
329
|
/**
|
|
243
|
-
* JSON text with every string longer than 40 characters cut to its
|
|
244
|
-
* and "…". Keys, numbers
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
330
|
+
* JSON text with every string of prose longer than 40 characters cut to its
|
|
331
|
+
* first 40 and "…". Keys, numbers, short strings and strings with no space in
|
|
332
|
+
* them ({@link BRIEF_ID_CHARS}: a link, a path, an id) stay as they are. A
|
|
333
|
+
* character is never split: an escape counts as one, `\n` and a Unicode
|
|
334
|
+
* escape (a backslash, a `u` and four hex digits) alike, and so does a pair of
|
|
335
|
+
* surrogates, written out (an emoji) or as two such escapes.
|
|
336
|
+
* Works on JSON a parser would refuse — a tool's `NaN`, a result cut short —
|
|
337
|
+
* because it walks the text's string literals from the start, so it always
|
|
338
|
+
* knows which quote opens one: the text between two strings, a list of numbers
|
|
339
|
+
* say, is never taken for one. An unterminated string is left as it is.
|
|
250
340
|
*/
|
|
251
341
|
export function cutLongText(json) {
|
|
252
342
|
let out = "";
|
|
@@ -256,24 +346,68 @@ export function cutLongText(json) {
|
|
|
256
346
|
if (open < 0)
|
|
257
347
|
return out + json.slice(at);
|
|
258
348
|
out += json.slice(at, open + 1);
|
|
259
|
-
// Walk the string a
|
|
260
|
-
//
|
|
349
|
+
// Walk the string a character at a time to its closing quote, noting where
|
|
350
|
+
// the 40th ends and whether the text has a space in it.
|
|
261
351
|
let end = open + 1;
|
|
262
352
|
let units = 0;
|
|
263
353
|
let cutAt = -1;
|
|
354
|
+
let spaced = false;
|
|
264
355
|
while (end < json.length && json[end] !== '"') {
|
|
265
|
-
|
|
356
|
+
const code = json.charCodeAt(end);
|
|
357
|
+
if (code === 92 || (code >= 0xd800 && code <= 0xdbff)) {
|
|
358
|
+
// An escape, or the first half of a surrogate pair: it may take more
|
|
359
|
+
// than one character of the text.
|
|
360
|
+
const unit = characterAt(json, end);
|
|
361
|
+
end += unit.length;
|
|
362
|
+
spaced ||= unit.space;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
// Nearly every character, and a result may be megabytes of them: one
|
|
366
|
+
// of the text, a space only when it says so.
|
|
367
|
+
spaced ||= code === 32 || ((code < 32 || code > 126) && /\s/.test(json[end]));
|
|
368
|
+
end += 1;
|
|
369
|
+
}
|
|
266
370
|
units += 1;
|
|
267
371
|
if (units === BRIEF_STRING_CHARS)
|
|
268
372
|
cutAt = end;
|
|
269
373
|
}
|
|
270
374
|
if (end >= json.length)
|
|
271
375
|
return out + json.slice(open + 1);
|
|
272
|
-
|
|
376
|
+
const cut = units > (spaced ? BRIEF_STRING_CHARS : BRIEF_ID_CHARS);
|
|
377
|
+
out += cut ? `${json.slice(open + 1, cutAt)}…"` : json.slice(open + 1, end + 1);
|
|
273
378
|
at = end + 1;
|
|
274
379
|
}
|
|
275
380
|
return out;
|
|
276
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* The character of a JSON string's text that starts at `i`: how many
|
|
384
|
+
* characters of the text it takes, and whether it is a space (a `\t`, or a
|
|
385
|
+
* Unicode escape of a space, is one as much as the character itself).
|
|
386
|
+
*/
|
|
387
|
+
function characterAt(text, i) {
|
|
388
|
+
const c = text[i];
|
|
389
|
+
if (c === "\\") {
|
|
390
|
+
const unit = unicodeEscape(text, i);
|
|
391
|
+
if (unit === undefined)
|
|
392
|
+
return { length: 2, space: /^[nrtf]$/.test(text[i + 1] ?? "") };
|
|
393
|
+
// Two escapes that are the halves of one character are one character.
|
|
394
|
+
const low = unit >= 0xd800 && unit <= 0xdbff ? unicodeEscape(text, i + 6) : undefined;
|
|
395
|
+
if (low !== undefined && low >= 0xdc00 && low <= 0xdfff)
|
|
396
|
+
return { length: 12, space: false };
|
|
397
|
+
return { length: 6, space: /\s/.test(String.fromCharCode(unit)) };
|
|
398
|
+
}
|
|
399
|
+
const code = c.charCodeAt(0);
|
|
400
|
+
const next = text.charCodeAt(i + 1);
|
|
401
|
+
if (code >= 0xd800 && code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff)
|
|
402
|
+
return { length: 2, space: false };
|
|
403
|
+
return { length: 1, space: /\s/.test(c) };
|
|
404
|
+
}
|
|
405
|
+
/** The code unit of a `\uXXXX` escape at `i`, or undefined when there is none there. */
|
|
406
|
+
function unicodeEscape(text, i) {
|
|
407
|
+
return text[i] === "\\" && text[i + 1] === "u" && /^[0-9a-fA-F]{4}$/.test(text.slice(i + 2, i + 6))
|
|
408
|
+
? parseInt(text.slice(i + 2, i + 6), 16)
|
|
409
|
+
: undefined;
|
|
410
|
+
}
|
|
277
411
|
/**
|
|
278
412
|
* The plan's summary (its response model) in a few words, or "" when it has
|
|
279
413
|
* nothing to add: none at all, or only the note that it was left out — which
|
|
@@ -304,15 +438,15 @@ export function briefFields(fields, max) {
|
|
|
304
438
|
}
|
|
305
439
|
/**
|
|
306
440
|
* A value of the plan's summary in a few words. Numbers, booleans and short
|
|
307
|
-
* strings whole;
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
441
|
+
* strings whole; long text cut, as a result's is ({@link cutLongText}: a link
|
|
442
|
+
* or an id is whole); a list or object whose JSON is short — a few IDs, names
|
|
443
|
+
* or addresses — whole; a longer list counted; a longer object by its first
|
|
444
|
+
* fields, one level down, below which it is counted too. A summary's long
|
|
445
|
+
* lists are mostly the records the steps above already show.
|
|
311
446
|
*/
|
|
312
447
|
export function briefValue(value, depth = 0) {
|
|
313
|
-
if (typeof value === "string")
|
|
314
|
-
return JSON.stringify(value
|
|
315
|
-
}
|
|
448
|
+
if (typeof value === "string")
|
|
449
|
+
return cutLongText(JSON.stringify(value));
|
|
316
450
|
if (value === null || typeof value !== "object")
|
|
317
451
|
return String(value);
|
|
318
452
|
let whole;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
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
|
+
import { type HandoverStopReason } from "./handover.js";
|
|
17
18
|
import { ProxyWorkQueue } from "./executor.js";
|
|
18
19
|
import { ScenarioReplayPlan } from "./plan.js";
|
|
19
20
|
import { type HostSinks } from "./safety.js";
|
|
@@ -310,6 +311,39 @@ export interface IntentMatchOptions {
|
|
|
310
311
|
maxRequestsPerTurn: number;
|
|
311
312
|
}
|
|
312
313
|
export declare const DEFAULT_INTENT_MATCH: IntentMatchOptions;
|
|
314
|
+
/**
|
|
315
|
+
* Where the stopped step stood when a plan handed over, in the words the
|
|
316
|
+
* service reads (its recordings/repo.ts `handoverFamily`): did the step
|
|
317
|
+
* happen. Its tool ran and only its output could not be read: `ran`. It was
|
|
318
|
+
* sent and nothing came back: `uncertain`. Stopped in front of it — the plan's
|
|
319
|
+
* time ran out (`budget`), its caller left (`cancelled`), its test could not
|
|
320
|
+
* say whether this request needs it (`test_failed`) — it was never sent:
|
|
321
|
+
* `not_run`. Otherwise the stop's own reason (`not_run`, `failed`,
|
|
322
|
+
* `concurrent`, `host_rejected`, `host_edited`), and a plain failure, which
|
|
323
|
+
* carries none, reads `failed`.
|
|
324
|
+
*
|
|
325
|
+
* The service continues a hand-over only with work that was recorded where the
|
|
326
|
+
* step had not happened (`not_run`, `failed`): the agent's work there starts
|
|
327
|
+
* with that very step. Continued after a step that ran, or may have, the plan
|
|
328
|
+
* made of it ran the step a second time. The three stops in front of a step
|
|
329
|
+
* went out under their own names, which the service reads as "may have run":
|
|
330
|
+
* a plan stopped by its time budget was never continued.
|
|
331
|
+
*/
|
|
332
|
+
export declare function stopOf(h: {
|
|
333
|
+
ranThisStep?: boolean;
|
|
334
|
+
inFlight?: unknown;
|
|
335
|
+
stopReason?: HandoverStopReason;
|
|
336
|
+
}): string;
|
|
337
|
+
/**
|
|
338
|
+
* Whether a run of the plan sent at least one of its calls: a step did its
|
|
339
|
+
* work, its tool answered with an error, its output could not be read after
|
|
340
|
+
* the tool ran, or it was sent and has not been seen through (a pin still out,
|
|
341
|
+
* a stop that may have happened). A step its test left out, one whose logic
|
|
342
|
+
* broke before its call, one that could not be sent and a recorded stand-in
|
|
343
|
+
* sent nothing — a plan that only did those has nothing a second run could
|
|
344
|
+
* repeat.
|
|
345
|
+
*/
|
|
346
|
+
export declare function sentACall(state: ReplayState): boolean;
|
|
313
347
|
/** The turn handed back to the model part-way (fallbk.md). */
|
|
314
348
|
export interface Handover {
|
|
315
349
|
kind: FallbackKind;
|
|
@@ -317,6 +351,8 @@ export interface Handover {
|
|
|
317
351
|
stepIndex: number;
|
|
318
352
|
toolName?: string;
|
|
319
353
|
error?: string;
|
|
354
|
+
/** For a `step_failed`: where the stopped step stood ({@link stopOf}). */
|
|
355
|
+
stop?: string;
|
|
320
356
|
/** What the model is told. Always built, even when delivery fails. */
|
|
321
357
|
note: string;
|
|
322
358
|
/** Set once the note reached the model, so it is never delivered twice. */
|
|
@@ -484,6 +520,16 @@ export interface ReplayState {
|
|
|
484
520
|
* the execution report says `measured: false`.
|
|
485
521
|
*/
|
|
486
522
|
callerCancelled?: boolean;
|
|
523
|
+
/**
|
|
524
|
+
* The plan ran to its end while its caller was gone, so its results reached
|
|
525
|
+
* nobody and no hand-over note exists: this says so, once, to the model
|
|
526
|
+
* ({@link ReplayController.takeNote}), so that it does not run the steps
|
|
527
|
+
* again.
|
|
528
|
+
*/
|
|
529
|
+
unseen?: {
|
|
530
|
+
note: string;
|
|
531
|
+
delivered: boolean;
|
|
532
|
+
};
|
|
487
533
|
/**
|
|
488
534
|
* The derivation is the service's to answer (no key here), so waiting on it
|
|
489
535
|
* is waiting on the service, and obeys its breaker (util/service-health.ts).
|
|
@@ -618,6 +664,14 @@ export type DeclineCode = "replay_disabled" | "not_ready" | "known_bad_first_ste
|
|
|
618
664
|
* arms on the model's first call after the hand-over, whatever that is.)
|
|
619
665
|
*/
|
|
620
666
|
| "not_taken"
|
|
667
|
+
/**
|
|
668
|
+
* The plan was cancelled by its caller (Esc on `run_scenario`, or a timeout)
|
|
669
|
+
* a few minutes ago, after at least one step had run, and the same plan came
|
|
670
|
+
* back: not armed again, so the steps whose side effects are in place do not
|
|
671
|
+
* run a second time. The agent does the task, told what ran. Set by the
|
|
672
|
+
* control server.
|
|
673
|
+
*/
|
|
674
|
+
| "after_cancel"
|
|
621
675
|
/**
|
|
622
676
|
* A continuation whose own code for working out its values (`paramsLogic`)
|
|
623
677
|
* failed on this request, so none of its steps could be computed. Set by
|
|
@@ -774,6 +828,12 @@ export declare class ReplayController {
|
|
|
774
828
|
* against the plan; the outcome is `fell_back` when a step ran and `failed`
|
|
775
829
|
* when none did, as for every hand-over. The note is the caller's to
|
|
776
830
|
* deliver ({@link takeNote}). Returns false when there was no live plan.
|
|
831
|
+
*
|
|
832
|
+
* A step the adapter was given and has not reported back on may be running:
|
|
833
|
+
* the plan stops after it, as for a call beside a pin ({@link concurrentCall}),
|
|
834
|
+
* and the hand-over says the step may have happened — never "did not run",
|
|
835
|
+
* which the service would continue with work that starts by running it
|
|
836
|
+
* again. Its result is still read when it lands.
|
|
777
837
|
*/
|
|
778
838
|
hostHandOver(state: ReplayState, why: "own_call" | "closed" | "late"): boolean;
|
|
779
839
|
/**
|
|
@@ -814,6 +874,8 @@ export declare class ReplayController {
|
|
|
814
874
|
* Returns it once and marks it delivered; undefined when there is none.
|
|
815
875
|
*/
|
|
816
876
|
takeNote(state: ReplayState | undefined): string | undefined;
|
|
877
|
+
/** Whether a note about this plan is still to be told to the model: a hand-over's, or the unseen results'. */
|
|
878
|
+
hasUndeliveredNote(state: ReplayState | undefined): boolean;
|
|
817
879
|
/**
|
|
818
880
|
* Deliver text to the model in place of the tool call it was about to make:
|
|
819
881
|
* as genuine command output for `Bash`, as the denial reason otherwise — the
|