@llblab/pi-actors 0.42.0 → 0.42.2
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/AGENTS.md +2 -2
- package/BACKLOG.md +1 -5
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/dist/lib/async-runs.d.ts +12 -0
- package/dist/lib/async-runs.js +53 -12
- package/dist/lib/command-templates.js +1 -1
- package/dist/lib/inspector-overlay.d.ts +5 -1
- package/dist/lib/inspector-overlay.js +104 -36
- package/dist/lib/inspector.js +1 -1
- package/dist/lib/observability.d.ts +12 -1
- package/dist/lib/observability.js +159 -80
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/runs-control.d.ts +2 -0
- package/dist/lib/runs-control.js +14 -1
- package/dist/lib/runs-ownership.js +17 -3
- package/dist/lib/runs-process.js +4 -3
- package/dist/lib/runs-start.js +1 -0
- package/dist/lib/runs-status.js +3 -0
- package/dist/lib/tools-inspect.js +2 -1
- package/dist/lib/tools-local.js +17 -2
- package/dist/lib/tools-spawn.js +10 -1
- package/dist/scripts/async-runner.mjs +24 -24
- package/dist/scripts/build-dist.mjs +6 -1
- package/dist/scripts/conformance.mjs +6 -1
- package/dist/scripts/recipe-utils.mjs +3 -3
- package/dist/skills/actors/SKILL.md +3 -3
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +3 -3
- package/docs/async-runs.md +10 -4
- package/docs/recipe-library.md +1 -1
- package/docs/tool-registry.md +2 -0
- package/lib/async-runs.ts +72 -12
- package/lib/command-templates.ts +1 -1
- package/lib/inspector-overlay.ts +129 -36
- package/lib/inspector.ts +1 -1
- package/lib/observability.ts +194 -76
- package/lib/prompts.ts +1 -1
- package/lib/runs-control.ts +20 -1
- package/lib/runs-ownership.ts +22 -3
- package/lib/runs-process.ts +4 -3
- package/lib/runs-start.ts +1 -0
- package/lib/runs-status.ts +5 -0
- package/lib/tools-inspect.ts +2 -1
- package/lib/tools-local.ts +21 -2
- package/lib/tools-spawn.ts +14 -1
- package/package.json +4 -3
- package/scripts/async-runner.mjs +24 -24
- package/scripts/build-dist.mjs +6 -1
- package/scripts/conformance.mjs +6 -1
- package/scripts/recipe-utils.mjs +3 -3
- package/skills/actors/SKILL.md +3 -3
- package/skills/swarm/SKILL.md +1 -1
package/lib/inspector-overlay.ts
CHANGED
|
@@ -69,7 +69,8 @@ export class ActorInspectorOverlay {
|
|
|
69
69
|
private detailScroll = 0;
|
|
70
70
|
private detailTurn?: Inspector.ActorInspectorTurnItem;
|
|
71
71
|
private feedback?: ActorInspectorOverlayActionResult;
|
|
72
|
-
private killConfirmation?: { run: string; runInstanceId: string };
|
|
72
|
+
private killConfirmation?: { run: string; runInstanceId: string; status: string };
|
|
73
|
+
private killDialogChoice: "cancel" | "kill" = "cancel";
|
|
73
74
|
private readonly readKeys = new Set<string>();
|
|
74
75
|
private focus: "runs" | "tabs" | "recipe" | "list" | "detail" | "select" = "tabs";
|
|
75
76
|
private filterControlIndex = 0;
|
|
@@ -105,11 +106,21 @@ export class ActorInspectorOverlay {
|
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
107
108
|
if (this.killConfirmation) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this.
|
|
111
|
-
} else if (
|
|
109
|
+
const key = data.toLowerCase();
|
|
110
|
+
if (matchesKey(data, "escape") || key === "n") {
|
|
111
|
+
this.cancelKill();
|
|
112
|
+
} else if (
|
|
113
|
+
matchesKey(data, "left") ||
|
|
114
|
+
matchesKey(data, "right") ||
|
|
115
|
+
matchesKey(data, "tab")
|
|
116
|
+
) {
|
|
117
|
+
this.killDialogChoice =
|
|
118
|
+
this.killDialogChoice === "cancel" ? "kill" : "cancel";
|
|
119
|
+
} else if (key === "y") {
|
|
112
120
|
this.confirmKill();
|
|
121
|
+
} else if (matchesKey(data, "return")) {
|
|
122
|
+
if (this.killDialogChoice === "kill") this.confirmKill();
|
|
123
|
+
else this.cancelKill();
|
|
113
124
|
}
|
|
114
125
|
this.tui.requestRender();
|
|
115
126
|
return;
|
|
@@ -215,6 +226,8 @@ export class ActorInspectorOverlay {
|
|
|
215
226
|
this.ensureSelectedRun(this.runs());
|
|
216
227
|
const safeWidth = Math.max(24, width);
|
|
217
228
|
const innerWidth = Math.max(1, safeWidth - 2);
|
|
229
|
+
if (this.killConfirmation)
|
|
230
|
+
return this.renderKillDialog(safeWidth, innerWidth);
|
|
218
231
|
const lines: string[] = [];
|
|
219
232
|
lines.push(this.border("╭", " Actor Inspector ", "╮", innerWidth));
|
|
220
233
|
lines.push(this.row(this.renderRunControl(), innerWidth));
|
|
@@ -257,13 +270,11 @@ export class ActorInspectorOverlay {
|
|
|
257
270
|
Math.max(0, innerWidth - visibleWidth(selectorTop) - selectorAnchor),
|
|
258
271
|
);
|
|
259
272
|
lines.push(
|
|
260
|
-
`${this.theme.fg("
|
|
273
|
+
`${this.theme.fg("borderAccent", `├${leadingBorder}`)}${selectorTop}${this.theme.fg("borderAccent", `${remainingBorder}┤`)}`,
|
|
261
274
|
);
|
|
262
275
|
} else lines.push(this.border("├", "", "┤", innerWidth));
|
|
263
276
|
this.contentStripeIndices = [];
|
|
264
|
-
const baseContent = this.
|
|
265
|
-
? this.renderKillConfirmation()
|
|
266
|
-
: this.detailOpen
|
|
277
|
+
const baseContent = this.detailOpen
|
|
267
278
|
? this.renderDetail(innerWidth)
|
|
268
279
|
: this.selectedRun
|
|
269
280
|
? this.renderTimeline(innerWidth)
|
|
@@ -311,9 +322,7 @@ export class ActorInspectorOverlay {
|
|
|
311
322
|
);
|
|
312
323
|
lines.push(this.row(`${leadingBase}${visiblePopup}${preservedBase}`, innerWidth, true));
|
|
313
324
|
}
|
|
314
|
-
lines.push(this.
|
|
315
|
-
lines.push(this.row(this.renderKeyHints(), innerWidth));
|
|
316
|
-
lines.push(this.border("╰", "", "╯", innerWidth));
|
|
325
|
+
lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
|
|
317
326
|
return lines;
|
|
318
327
|
}
|
|
319
328
|
|
|
@@ -333,7 +342,7 @@ export class ActorInspectorOverlay {
|
|
|
333
342
|
|
|
334
343
|
private contentViewportRows(): number {
|
|
335
344
|
const overlayRows = Math.floor(this.tui.terminal.rows * 0.94);
|
|
336
|
-
return Math.max(4, Math.min(
|
|
345
|
+
return Math.max(4, Math.min(24, overlayRows - 5));
|
|
337
346
|
}
|
|
338
347
|
|
|
339
348
|
private selectRun(run: string, index: number): void {
|
|
@@ -392,12 +401,21 @@ export class ActorInspectorOverlay {
|
|
|
392
401
|
this.killConfirmation = {
|
|
393
402
|
run: run.run,
|
|
394
403
|
runInstanceId: run.runInstanceId,
|
|
404
|
+
status: run.status,
|
|
395
405
|
};
|
|
406
|
+
this.killDialogChoice = "cancel";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private cancelKill(): void {
|
|
410
|
+
this.feedback = { ok: false, message: "Kill cancelled." };
|
|
411
|
+
this.killConfirmation = undefined;
|
|
412
|
+
this.killDialogChoice = "cancel";
|
|
396
413
|
}
|
|
397
414
|
|
|
398
415
|
private confirmKill(): void {
|
|
399
416
|
const confirmation = this.killConfirmation;
|
|
400
417
|
this.killConfirmation = undefined;
|
|
418
|
+
this.killDialogChoice = "cancel";
|
|
401
419
|
if (!confirmation || !this.killRun) {
|
|
402
420
|
this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
|
|
403
421
|
return;
|
|
@@ -416,14 +434,40 @@ export class ActorInspectorOverlay {
|
|
|
416
434
|
}
|
|
417
435
|
}
|
|
418
436
|
|
|
419
|
-
private
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
)
|
|
425
|
-
|
|
437
|
+
private renderKillDialog(_width: number, innerWidth: number): string[] {
|
|
438
|
+
const confirmation = this.killConfirmation!;
|
|
439
|
+
const totalRows = this.contentViewportRows() + 5;
|
|
440
|
+
const cancel = this.killDialogChoice === "cancel"
|
|
441
|
+
? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
|
|
442
|
+
: this.theme.fg("muted", " Cancel ");
|
|
443
|
+
const kill = this.killDialogChoice === "kill"
|
|
444
|
+
? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
|
|
445
|
+
: this.theme.fg("error", " Kill actor ");
|
|
446
|
+
const body = [
|
|
447
|
+
"",
|
|
448
|
+
this.theme.fg("error", "Kill this running actor?"),
|
|
449
|
+
"",
|
|
450
|
+
`${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
|
|
451
|
+
`${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
|
|
452
|
+
"",
|
|
453
|
+
this.theme.fg("text", "This sends canonical control.kill."),
|
|
454
|
+
this.theme.fg("error", "The action is destructive and cannot be undone."),
|
|
455
|
+
"",
|
|
456
|
+
`${cancel} ${kill}`,
|
|
457
|
+
"",
|
|
426
458
|
];
|
|
459
|
+
const lines = [this.border("╭", " Confirm Actor Kill ", "╮", innerWidth)];
|
|
460
|
+
const availableRows = Math.max(1, totalRows - 2);
|
|
461
|
+
const topPadding = Math.max(0, Math.floor((availableRows - body.length) / 2));
|
|
462
|
+
const dialogRows = [
|
|
463
|
+
...Array.from({ length: topPadding }, () => ""),
|
|
464
|
+
...body,
|
|
465
|
+
].slice(0, availableRows);
|
|
466
|
+
while (dialogRows.length < availableRows) dialogRows.push("");
|
|
467
|
+
for (const line of dialogRows)
|
|
468
|
+
lines.push(this.row(this.center(line, innerWidth), innerWidth));
|
|
469
|
+
lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
|
|
470
|
+
return lines;
|
|
427
471
|
}
|
|
428
472
|
|
|
429
473
|
private listItemCount(): number {
|
|
@@ -449,27 +493,53 @@ export class ActorInspectorOverlay {
|
|
|
449
493
|
|
|
450
494
|
private renderKeyHints(): string {
|
|
451
495
|
const hint = (keys: string, description: string) =>
|
|
452
|
-
`${this.theme.fg("accent", keys)}${this.theme.fg("
|
|
496
|
+
`${this.theme.fg("accent", keys)}${this.theme.fg("borderAccent", ` ${description}`)}`;
|
|
497
|
+
const divider = this.theme.fg("borderAccent", " ─ ");
|
|
498
|
+
const hints = (...items: Array<[string, string]>) =>
|
|
499
|
+
items.map(([keys, description]) => hint(keys, description)).join(divider);
|
|
453
500
|
if (this.killConfirmation)
|
|
454
|
-
return
|
|
501
|
+
return hints(
|
|
502
|
+
["←→/tab", "choose"],
|
|
503
|
+
["enter/y", "confirm"],
|
|
504
|
+
["esc/n", "cancel"],
|
|
505
|
+
);
|
|
455
506
|
if (this.focus === "select")
|
|
456
507
|
return this.menuLevel === "value"
|
|
457
|
-
?
|
|
458
|
-
:
|
|
508
|
+
? hints(["↑↓", "option"], ["enter", "apply"], ["←/esc", "back"])
|
|
509
|
+
: hints(["↑↓", "option"], ["→/enter", "open"], ["←/esc", "back"]);
|
|
459
510
|
if (this.focus === "recipe")
|
|
460
|
-
return
|
|
511
|
+
return hints(
|
|
512
|
+
["↑↓/pgup/pgdn", "scroll"],
|
|
513
|
+
["↑ at top", "tabs"],
|
|
514
|
+
["←/esc", "tabs"],
|
|
515
|
+
);
|
|
461
516
|
if (this.focus === "detail")
|
|
462
|
-
return
|
|
517
|
+
return hints(["↑↓/pgup/pgdn", "scroll"], ["←/esc", "back"]);
|
|
463
518
|
if (this.focus === "list")
|
|
464
|
-
return
|
|
519
|
+
return hints(
|
|
520
|
+
["↑↓/pgup/pgdn", "row"],
|
|
521
|
+
["→/enter", "open"],
|
|
522
|
+
["←", "tabs"],
|
|
523
|
+
["esc", "close"],
|
|
524
|
+
);
|
|
465
525
|
if (this.focus === "runs") {
|
|
526
|
+
const items: Array<[string, string]> = [
|
|
527
|
+
["←→", "run"],
|
|
528
|
+
["↓", "tabs"],
|
|
529
|
+
["enter", "list"],
|
|
530
|
+
];
|
|
466
531
|
const run = this.runs().find((item) => item.run === this.selectedRun);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
return
|
|
532
|
+
if (this.killRun && run?.status === "running" && run.runInstanceId)
|
|
533
|
+
items.push(["k", "kill"]);
|
|
534
|
+
items.push(["esc", "close"]);
|
|
535
|
+
return hints(...items);
|
|
471
536
|
}
|
|
472
|
-
return
|
|
537
|
+
return hints(
|
|
538
|
+
["←→", "navigate"],
|
|
539
|
+
["↑↓", "change row"],
|
|
540
|
+
["enter", "select"],
|
|
541
|
+
["esc", "close"],
|
|
542
|
+
);
|
|
473
543
|
}
|
|
474
544
|
|
|
475
545
|
private renderRunControl(): string {
|
|
@@ -1154,7 +1224,7 @@ export class ActorInspectorOverlay {
|
|
|
1154
1224
|
);
|
|
1155
1225
|
const border = (left: string, right: string, marker = "") =>
|
|
1156
1226
|
this.theme.fg(
|
|
1157
|
-
"
|
|
1227
|
+
"borderAccent",
|
|
1158
1228
|
`${omitLeftBorder ? "" : left}${marker}${"─".repeat(Math.max(0, contentWidth - visibleWidth(marker)))}${omitRightBorder ? "" : right}`,
|
|
1159
1229
|
);
|
|
1160
1230
|
return [
|
|
@@ -1169,7 +1239,7 @@ export class ActorInspectorOverlay {
|
|
|
1169
1239
|
const styled = focused || index === parentIndex
|
|
1170
1240
|
? this.theme.bg("selectedBg", colored)
|
|
1171
1241
|
: colored;
|
|
1172
|
-
return `${omitLeftBorder ? "" : this.theme.fg("
|
|
1242
|
+
return `${omitLeftBorder ? "" : this.theme.fg("borderAccent", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("borderAccent", "│")}`;
|
|
1173
1243
|
}),
|
|
1174
1244
|
border("╰", "╯", hiddenBelow ? "↓" : ""),
|
|
1175
1245
|
];
|
|
@@ -1239,10 +1309,27 @@ export class ActorInspectorOverlay {
|
|
|
1239
1309
|
this.focus = "list";
|
|
1240
1310
|
}
|
|
1241
1311
|
|
|
1312
|
+
private footerBorder(hints: string, width: number): string {
|
|
1313
|
+
const prefix = " ";
|
|
1314
|
+
const suffix = " ─";
|
|
1315
|
+
const content = truncateToWidth(
|
|
1316
|
+
hints,
|
|
1317
|
+
Math.max(0, width - visibleWidth(prefix) - visibleWidth(suffix)),
|
|
1318
|
+
"",
|
|
1319
|
+
);
|
|
1320
|
+
const fill = "─".repeat(
|
|
1321
|
+
Math.max(
|
|
1322
|
+
0,
|
|
1323
|
+
width - visibleWidth(prefix) - visibleWidth(suffix) - visibleWidth(content),
|
|
1324
|
+
),
|
|
1325
|
+
);
|
|
1326
|
+
return `${this.theme.fg("borderAccent", `╰${prefix}`)}${content}${this.theme.fg("borderAccent", `${suffix}${fill}╯`)}`;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1242
1329
|
private border(left: string, title: string, right: string, width: number): string {
|
|
1243
1330
|
const titleText = truncateToWidth(title, width, "");
|
|
1244
1331
|
const fill = "─".repeat(Math.max(0, width - visibleWidth(titleText)));
|
|
1245
|
-
return this.theme.fg("
|
|
1332
|
+
return this.theme.fg("borderAccent", `${left}${titleText}${fill}${right}`);
|
|
1246
1333
|
}
|
|
1247
1334
|
|
|
1248
1335
|
private stripeBackground(content: string, index: number): string {
|
|
@@ -1265,7 +1352,7 @@ export class ActorInspectorOverlay {
|
|
|
1265
1352
|
|
|
1266
1353
|
private row(content: string, width: number, fitted = false): string {
|
|
1267
1354
|
const body = fitted ? content : this.fit(content, width);
|
|
1268
|
-
return `${this.theme.fg("
|
|
1355
|
+
return `${this.theme.fg("borderAccent", "│")}${body}${this.theme.fg("borderAccent", "│")}`;
|
|
1269
1356
|
}
|
|
1270
1357
|
|
|
1271
1358
|
private takeVisiblePrefix(content: string, width: number): string {
|
|
@@ -1293,6 +1380,12 @@ export class ActorInspectorOverlay {
|
|
|
1293
1380
|
return plain.slice(index);
|
|
1294
1381
|
}
|
|
1295
1382
|
|
|
1383
|
+
private center(content: string, width: number): string {
|
|
1384
|
+
const contentWidth = visibleWidth(content);
|
|
1385
|
+
const leading = Math.max(0, Math.floor((width - contentWidth) / 2));
|
|
1386
|
+
return `${" ".repeat(leading)}${content}`;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1296
1389
|
private fit(content: string, width: number): string {
|
|
1297
1390
|
// A Component render entry must remain exactly one terminal row. Persisted
|
|
1298
1391
|
// prompts, messages, and tool results may contain line breaks or tabs;
|
package/lib/inspector.ts
CHANGED
|
@@ -248,7 +248,7 @@ export function readActorInspectorTurns(stateDir: string): ActorInspectorTurnIte
|
|
|
248
248
|
withFileTypes: true,
|
|
249
249
|
})
|
|
250
250
|
.filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
|
|
251
|
-
.map((file) => path.join("sessions", entry.name, file.name))
|
|
251
|
+
.map((file) => path.posix.join("sessions", entry.name, file.name))
|
|
252
252
|
.filter((file) => !recordedFiles.has(file));
|
|
253
253
|
return files.length > 0
|
|
254
254
|
? [{ id: entry.name, session_files: files, stage: "subagent" }]
|
package/lib/observability.ts
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
closeSync,
|
|
8
9
|
existsSync,
|
|
10
|
+
fstatSync,
|
|
11
|
+
openSync,
|
|
9
12
|
readdirSync,
|
|
10
13
|
readFileSync,
|
|
14
|
+
readSync,
|
|
11
15
|
watch,
|
|
12
16
|
type FSWatcher,
|
|
13
17
|
} from "node:fs";
|
|
@@ -17,7 +21,6 @@ import {
|
|
|
17
21
|
isAbsolute,
|
|
18
22
|
join,
|
|
19
23
|
relative,
|
|
20
|
-
resolve,
|
|
21
24
|
} from "node:path";
|
|
22
25
|
|
|
23
26
|
import * as AsyncRuns from "./async-runs.ts";
|
|
@@ -41,6 +44,7 @@ export interface RunObservation {
|
|
|
41
44
|
failures?: number;
|
|
42
45
|
ownerId?: string;
|
|
43
46
|
artifacts?: Record<string, string>;
|
|
47
|
+
launchCorrelation?: Record<string, string>;
|
|
44
48
|
launchSource?: AsyncRuns.AsyncRunLaunchSource;
|
|
45
49
|
modelPolicy?: Record<string, unknown>;
|
|
46
50
|
notificationPolicy?: "normal" | "silent";
|
|
@@ -48,6 +52,7 @@ export interface RunObservation {
|
|
|
48
52
|
terminalHandled?: boolean;
|
|
49
53
|
retireWhen?: string;
|
|
50
54
|
run: string;
|
|
55
|
+
semanticResult?: RunTerminalSemanticResult;
|
|
51
56
|
tool?: string;
|
|
52
57
|
stateDir?: string;
|
|
53
58
|
status: RunObservedStatus;
|
|
@@ -162,6 +167,19 @@ export function deliverRunTransitionNotifications(
|
|
|
162
167
|
transition.to,
|
|
163
168
|
);
|
|
164
169
|
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (transition.stateDir) {
|
|
172
|
+
AsyncRuns.recordRunTerminalDeliveryFailure(
|
|
173
|
+
transition.stateDir,
|
|
174
|
+
transition.to,
|
|
175
|
+
error,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
179
|
+
sink.notify(
|
|
180
|
+
`Actor terminal delivery failed for run:${transition.run}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`,
|
|
181
|
+
"error",
|
|
182
|
+
);
|
|
165
183
|
} finally {
|
|
166
184
|
inFlight.delete(key);
|
|
167
185
|
}
|
|
@@ -420,12 +438,23 @@ export interface RunTransition {
|
|
|
420
438
|
run: string;
|
|
421
439
|
stateDir?: string;
|
|
422
440
|
artifacts?: Record<string, string>;
|
|
441
|
+
launchCorrelation?: Record<string, string>;
|
|
423
442
|
launchSource?: AsyncRuns.AsyncRunLaunchSource;
|
|
424
443
|
modelPolicy?: Record<string, unknown>;
|
|
425
444
|
recipeFile?: string;
|
|
426
445
|
terminalHandled?: boolean;
|
|
427
446
|
to: RunObservedStatus;
|
|
428
447
|
tool?: string;
|
|
448
|
+
semanticResult?: RunTerminalSemanticResult;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export interface RunTerminalSemanticResult {
|
|
452
|
+
body?: string;
|
|
453
|
+
correlationId?: string;
|
|
454
|
+
metadata: Record<string, unknown>;
|
|
455
|
+
summary: string;
|
|
456
|
+
synthesized: boolean;
|
|
457
|
+
type: string;
|
|
429
458
|
}
|
|
430
459
|
|
|
431
460
|
export interface RunOutboxEvent {
|
|
@@ -504,15 +533,145 @@ function scanRunStateDirs(
|
|
|
504
533
|
return result;
|
|
505
534
|
}
|
|
506
535
|
|
|
536
|
+
const TERMINAL_RESULT_BYTES = 8 * 1024;
|
|
537
|
+
const TERMINAL_RESULT_CHARS = 4_000;
|
|
538
|
+
|
|
539
|
+
function readBoundedStart(path: string): string {
|
|
540
|
+
if (!existsSync(path)) return "";
|
|
541
|
+
const fd = openSync(path, "r");
|
|
542
|
+
try {
|
|
543
|
+
const size = Math.min(fstatSync(fd).size, TERMINAL_RESULT_BYTES);
|
|
544
|
+
const buffer = Buffer.alloc(size);
|
|
545
|
+
const bytes = readSync(fd, buffer, 0, size, 0);
|
|
546
|
+
const text = buffer.subarray(0, bytes).toString("utf8").trim();
|
|
547
|
+
return text.length > TERMINAL_RESULT_CHARS
|
|
548
|
+
? `${text.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
|
|
549
|
+
: text;
|
|
550
|
+
} finally {
|
|
551
|
+
closeSync(fd);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function semanticBodyFromStdout(stateDir: string): string | undefined {
|
|
556
|
+
const text = readBoundedStart(join(stateDir, "stdout.log"));
|
|
557
|
+
if (!text) return undefined;
|
|
558
|
+
const withoutMarker = text.replace(/^ACTOR_REVIEW_RESULT\s*(?:\r?\n)?/, "").trim();
|
|
559
|
+
return withoutMarker || undefined;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function terminalSemanticResult(
|
|
563
|
+
status: Record<string, unknown>,
|
|
564
|
+
stateDir: string,
|
|
565
|
+
observedStatus: RunObservedStatus,
|
|
566
|
+
): RunTerminalSemanticResult {
|
|
567
|
+
const correlation = status.launch_correlation &&
|
|
568
|
+
typeof status.launch_correlation === "object" &&
|
|
569
|
+
!Array.isArray(status.launch_correlation)
|
|
570
|
+
? status.launch_correlation as Record<string, unknown>
|
|
571
|
+
: {};
|
|
572
|
+
const correlationId =
|
|
573
|
+
typeof correlation.correlation_id === "string"
|
|
574
|
+
? correlation.correlation_id
|
|
575
|
+
: typeof correlation.tool_call_id === "string"
|
|
576
|
+
? correlation.tool_call_id
|
|
577
|
+
: undefined;
|
|
578
|
+
const mailbox = status.mailbox &&
|
|
579
|
+
typeof status.mailbox === "object" &&
|
|
580
|
+
!Array.isArray(status.mailbox)
|
|
581
|
+
? status.mailbox as Record<string, unknown>
|
|
582
|
+
: {};
|
|
583
|
+
const emits = Array.isArray(mailbox.emits)
|
|
584
|
+
? mailbox.emits.filter((item): item is string => typeof item === "string")
|
|
585
|
+
: [];
|
|
586
|
+
const outbox = readJsonlFileResilient<Record<string, unknown>>(
|
|
587
|
+
join(stateDir, "outbox.jsonl"),
|
|
588
|
+
).records;
|
|
589
|
+
const explicit = outbox.findLast((record) => {
|
|
590
|
+
const type = String(record.type ?? record.event ?? "");
|
|
591
|
+
return emits.includes(type) && !["command.done", "run.done", "run.failed"].includes(type);
|
|
592
|
+
});
|
|
593
|
+
if (explicit) {
|
|
594
|
+
const type = String(explicit.type ?? explicit.event);
|
|
595
|
+
return {
|
|
596
|
+
...(explicit.body === undefined
|
|
597
|
+
? {} : { body: formatSemanticBody(explicit.body) }),
|
|
598
|
+
...(typeof explicit.correlation_id === "string"
|
|
599
|
+
? { correlationId: explicit.correlation_id } : correlationId ? { correlationId } : {}),
|
|
600
|
+
metadata: {
|
|
601
|
+
...(explicit.metadata &&
|
|
602
|
+
typeof explicit.metadata === "object" &&
|
|
603
|
+
!Array.isArray(explicit.metadata)
|
|
604
|
+
? explicit.metadata as Record<string, unknown> : {}),
|
|
605
|
+
...(status.transport_context &&
|
|
606
|
+
typeof status.transport_context === "object" &&
|
|
607
|
+
!Array.isArray(status.transport_context)
|
|
608
|
+
? {
|
|
609
|
+
transport_context: status.transport_context as Record<string, unknown>,
|
|
610
|
+
} : {}),
|
|
611
|
+
run: String(status.run ?? ""),
|
|
612
|
+
status: observedStatus,
|
|
613
|
+
},
|
|
614
|
+
summary: String(explicit.summary ?? type),
|
|
615
|
+
synthesized: false,
|
|
616
|
+
type,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const reviewCompleted = observedStatus === "done" && emits.includes("review.completed");
|
|
620
|
+
const result = status.result &&
|
|
621
|
+
typeof status.result === "object" &&
|
|
622
|
+
!Array.isArray(status.result)
|
|
623
|
+
? status.result as Record<string, unknown>
|
|
624
|
+
: {};
|
|
625
|
+
const type = reviewCompleted
|
|
626
|
+
? "review.completed"
|
|
627
|
+
: observedStatus === "done" ? "run.done" : "run.failed";
|
|
628
|
+
const stdoutBody = observedStatus === "done"
|
|
629
|
+
? semanticBodyFromStdout(stateDir) : undefined;
|
|
630
|
+
return {
|
|
631
|
+
...(stdoutBody
|
|
632
|
+
? { body: stdoutBody }
|
|
633
|
+
: typeof result.error === "string" ? { body: result.error } : {}),
|
|
634
|
+
...(correlationId ? { correlationId } : {}),
|
|
635
|
+
metadata: {
|
|
636
|
+
...(status.transport_context &&
|
|
637
|
+
typeof status.transport_context === "object" &&
|
|
638
|
+
!Array.isArray(status.transport_context)
|
|
639
|
+
? {
|
|
640
|
+
transport_context: status.transport_context as Record<string, unknown>,
|
|
641
|
+
} : {}),
|
|
642
|
+
run: String(status.run ?? ""), status: observedStatus,
|
|
643
|
+
},
|
|
644
|
+
summary: reviewCompleted ? "Review completed." :
|
|
645
|
+
observedStatus === "done" ? "Run completed." : `Run ${observedStatus}.`,
|
|
646
|
+
synthesized: true,
|
|
647
|
+
type,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function formatSemanticBody(body: unknown): string {
|
|
652
|
+
const rendered = typeof body === "string" ? body : JSON.stringify(body);
|
|
653
|
+
const text = typeof rendered === "string" ? rendered : String(body);
|
|
654
|
+
const compact = text.trim();
|
|
655
|
+
return compact.length > TERMINAL_RESULT_CHARS
|
|
656
|
+
? `${compact.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
|
|
657
|
+
: compact;
|
|
658
|
+
}
|
|
659
|
+
|
|
507
660
|
function observeRun(stateDir: string): RunObservation | undefined {
|
|
508
661
|
try {
|
|
509
662
|
const status = AsyncRuns.getRunStatus(stateDir);
|
|
510
663
|
const progress = getProgress(status);
|
|
511
664
|
const run = typeof status.run === "string" ? status.run : undefined;
|
|
512
665
|
if (!run) return undefined;
|
|
666
|
+
const observedStatus = status.status as RunObservedStatus;
|
|
513
667
|
return {
|
|
514
668
|
activeSubagents: toNumber(progress.activeSubagents),
|
|
515
669
|
completed: toNumber(progress.completed),
|
|
670
|
+
...(status.launch_correlation &&
|
|
671
|
+
typeof status.launch_correlation === "object" &&
|
|
672
|
+
!Array.isArray(status.launch_correlation)
|
|
673
|
+
? { launchCorrelation: status.launch_correlation as Record<string, string> }
|
|
674
|
+
: {}),
|
|
516
675
|
failures: Array.isArray(progress.failures)
|
|
517
676
|
? progress.failures.length
|
|
518
677
|
: undefined,
|
|
@@ -542,9 +701,12 @@ function observeRun(stateDir: string): RunObservation | undefined {
|
|
|
542
701
|
...(typeof status.retire_when === "string"
|
|
543
702
|
? { retireWhen: status.retire_when }
|
|
544
703
|
: {}),
|
|
704
|
+
...(TERMINAL.has(observedStatus)
|
|
705
|
+
? { semanticResult: terminalSemanticResult(status, stateDir, observedStatus) }
|
|
706
|
+
: {}),
|
|
545
707
|
run,
|
|
546
708
|
stateDir,
|
|
547
|
-
status:
|
|
709
|
+
status: observedStatus,
|
|
548
710
|
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
549
711
|
updatedAt: getUpdatedAt(status),
|
|
550
712
|
};
|
|
@@ -933,9 +1095,11 @@ export function detectRunTransitions(
|
|
|
933
1095
|
run: run.run,
|
|
934
1096
|
...(run.stateDir ? { stateDir: run.stateDir } : {}),
|
|
935
1097
|
...(run.artifacts ? { artifacts: run.artifacts } : {}),
|
|
1098
|
+
...(run.launchCorrelation ? { launchCorrelation: run.launchCorrelation } : {}),
|
|
936
1099
|
...(run.launchSource ? { launchSource: run.launchSource } : {}),
|
|
937
1100
|
...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
|
|
938
1101
|
...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
|
|
1102
|
+
...(run.semanticResult ? { semanticResult: run.semanticResult } : {}),
|
|
939
1103
|
...(run.terminalHandled ? { terminalHandled: true } : {}),
|
|
940
1104
|
to: run.status,
|
|
941
1105
|
...(run.tool ? { tool: run.tool } : {}),
|
|
@@ -1180,86 +1344,40 @@ export function shouldSendRunTransitionFollowUp(
|
|
|
1180
1344
|
return shouldNotifyRunTransition(transition);
|
|
1181
1345
|
}
|
|
1182
1346
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
join(transition.stateDir, "stdout.log"),
|
|
1187
|
-
join(transition.stateDir, "stderr.log"),
|
|
1188
|
-
join(transition.stateDir, "result.json"),
|
|
1189
|
-
join(transition.stateDir, "events.jsonl"),
|
|
1190
|
-
join(transition.stateDir, "outbox.jsonl"),
|
|
1191
|
-
];
|
|
1192
|
-
}
|
|
1347
|
+
const TERMINAL_FOLLOW_UP_ARTIFACT_LIMIT = 4;
|
|
1348
|
+
const TERMINAL_FOLLOW_UP_IDENTIFIER_CHARS = 120;
|
|
1349
|
+
const TERMINAL_FOLLOW_UP_PATH_CHARS = 320;
|
|
1193
1350
|
|
|
1194
|
-
function
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
const path = resolve(file);
|
|
1198
|
-
return path === recipeRoot || path.startsWith(`${recipeRoot}/`);
|
|
1351
|
+
function compactTerminalText(value: string, limit: number): string {
|
|
1352
|
+
const compact = value.replaceAll(/\s+/g, " ").trim();
|
|
1353
|
+
return compact.length > limit ? `${compact.slice(0, limit - 1)}…` : compact;
|
|
1199
1354
|
}
|
|
1200
1355
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
): boolean {
|
|
1204
|
-
if (transition.to !== "done") return false;
|
|
1205
|
-
if (isUserRecipeFile(transition.recipeFile)) return false;
|
|
1206
|
-
return Boolean(transition.recipeFile) || transition.launchSource === "spawn";
|
|
1356
|
+
function formatTerminalPath(path: string): string {
|
|
1357
|
+
return `\`${compactTerminalText(path, TERMINAL_FOLLOW_UP_PATH_CHARS)}\``;
|
|
1207
1358
|
}
|
|
1208
1359
|
|
|
1209
|
-
function
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
if (source === "unused") return undefined;
|
|
1225
|
-
const renderedValue =
|
|
1226
|
-
typeof record.value === "string" && record.value.trim()
|
|
1227
|
-
? ` (${record.value.trim()})`
|
|
1228
|
-
: "";
|
|
1229
|
-
return `${label}: ${source}${renderedValue}`;
|
|
1230
|
-
};
|
|
1231
|
-
const lines = [axis("model", "Model"), axis("thinking", "Thinking")].filter(
|
|
1232
|
-
(line): line is string => Boolean(line),
|
|
1233
|
-
);
|
|
1234
|
-
return lines.length ? `\nPolicy:\n- ${lines.join("\n- ")}` : "";
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
function formatTransitionNextActions(transition: RunTransition): string {
|
|
1238
|
-
const actions = [
|
|
1239
|
-
`inspect target=run:${transition.run} view=status`,
|
|
1240
|
-
transition.to === "done" && Object.keys(transition.artifacts ?? {}).length > 0
|
|
1241
|
-
? `inspect target=run:${transition.run} view=artifacts`
|
|
1242
|
-
: `inspect target=run:${transition.run} view=tail`,
|
|
1243
|
-
`inspect target=run:${transition.run} view=messages`,
|
|
1244
|
-
].filter(Boolean);
|
|
1245
|
-
return `\nNext actions: ${actions.join(" | ")}`;
|
|
1360
|
+
function formatTerminalResultLocations(transition: RunTransition): string {
|
|
1361
|
+
const artifactPaths = [...new Set(
|
|
1362
|
+
Object.values(transition.artifacts ?? {}).filter(
|
|
1363
|
+
(path): path is string => typeof path === "string" && path.length > 0,
|
|
1364
|
+
),
|
|
1365
|
+
)];
|
|
1366
|
+
if (artifactPaths.length === 0)
|
|
1367
|
+
return transition.stateDir ? `\nBase: ${formatTerminalPath(transition.stateDir)}` : "";
|
|
1368
|
+
const base = commonDirectory(artifactPaths);
|
|
1369
|
+
const artifactPreview = artifactPaths
|
|
1370
|
+
.slice(0, TERMINAL_FOLLOW_UP_ARTIFACT_LIMIT)
|
|
1371
|
+
.map((path) => formatTerminalPath(base ? relativeName(base, path) : path));
|
|
1372
|
+
const omitted = artifactPaths.length - artifactPreview.length;
|
|
1373
|
+
const artifacts = `${artifactPreview.join(", ")}${omitted > 0 ? ` (+${omitted} more)` : ""}`;
|
|
1374
|
+
return `${base ? `\nBase: ${formatTerminalPath(base)}` : ""}\nArtifacts: ${artifacts}`;
|
|
1246
1375
|
}
|
|
1247
1376
|
|
|
1248
1377
|
export function formatRunTransitionMessage(transition: RunTransition): string {
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
if (transition.to === "done")
|
|
1255
|
-
return `Run ${transition.run} completed successfully.${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
|
|
1256
|
-
if (transition.to === "failed")
|
|
1257
|
-
return `Run ${transition.run} failed.${policy}${artifacts}${runFiles}${nextActions}`;
|
|
1258
|
-
if (transition.to === "cancelled")
|
|
1259
|
-
return `Run ${transition.run} was cancelled.${policy}${nextActions}`;
|
|
1260
|
-
if (transition.to === "killed")
|
|
1261
|
-
return `Run ${transition.run} was force-killed.${policy}${nextActions}`;
|
|
1262
|
-
if (transition.to === "exited")
|
|
1263
|
-
return `Run ${transition.run} exited before writing a result.${policy}${nextActions}`;
|
|
1264
|
-
return `Run ${transition.run} finished with status ${transition.to}.${policy}${nextActions}`;
|
|
1378
|
+
const run = compactTerminalText(
|
|
1379
|
+
transition.run,
|
|
1380
|
+
TERMINAL_FOLLOW_UP_IDENTIFIER_CHARS,
|
|
1381
|
+
);
|
|
1382
|
+
return `Run: \`${run}\`\nStatus: \`${transition.to}\`${formatTerminalResultLocations(transition)}`;
|
|
1265
1383
|
}
|