@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.
Files changed (54) hide show
  1. package/AGENTS.md +2 -2
  2. package/BACKLOG.md +1 -5
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +1 -1
  5. package/dist/lib/async-runs.d.ts +12 -0
  6. package/dist/lib/async-runs.js +53 -12
  7. package/dist/lib/command-templates.js +1 -1
  8. package/dist/lib/inspector-overlay.d.ts +5 -1
  9. package/dist/lib/inspector-overlay.js +104 -36
  10. package/dist/lib/inspector.js +1 -1
  11. package/dist/lib/observability.d.ts +12 -1
  12. package/dist/lib/observability.js +159 -80
  13. package/dist/lib/prompts.d.ts +1 -1
  14. package/dist/lib/prompts.js +1 -1
  15. package/dist/lib/runs-control.d.ts +2 -0
  16. package/dist/lib/runs-control.js +14 -1
  17. package/dist/lib/runs-ownership.js +17 -3
  18. package/dist/lib/runs-process.js +4 -3
  19. package/dist/lib/runs-start.js +1 -0
  20. package/dist/lib/runs-status.js +3 -0
  21. package/dist/lib/tools-inspect.js +2 -1
  22. package/dist/lib/tools-local.js +17 -2
  23. package/dist/lib/tools-spawn.js +10 -1
  24. package/dist/scripts/async-runner.mjs +24 -24
  25. package/dist/scripts/build-dist.mjs +6 -1
  26. package/dist/scripts/conformance.mjs +6 -1
  27. package/dist/scripts/recipe-utils.mjs +3 -3
  28. package/dist/skills/actors/SKILL.md +3 -3
  29. package/dist/skills/swarm/SKILL.md +1 -1
  30. package/docs/actor-inspector.md +3 -3
  31. package/docs/async-runs.md +10 -4
  32. package/docs/recipe-library.md +1 -1
  33. package/docs/tool-registry.md +2 -0
  34. package/lib/async-runs.ts +72 -12
  35. package/lib/command-templates.ts +1 -1
  36. package/lib/inspector-overlay.ts +129 -36
  37. package/lib/inspector.ts +1 -1
  38. package/lib/observability.ts +194 -76
  39. package/lib/prompts.ts +1 -1
  40. package/lib/runs-control.ts +20 -1
  41. package/lib/runs-ownership.ts +22 -3
  42. package/lib/runs-process.ts +4 -3
  43. package/lib/runs-start.ts +1 -0
  44. package/lib/runs-status.ts +5 -0
  45. package/lib/tools-inspect.ts +2 -1
  46. package/lib/tools-local.ts +21 -2
  47. package/lib/tools-spawn.ts +14 -1
  48. package/package.json +4 -3
  49. package/scripts/async-runner.mjs +24 -24
  50. package/scripts/build-dist.mjs +6 -1
  51. package/scripts/conformance.mjs +6 -1
  52. package/scripts/recipe-utils.mjs +3 -3
  53. package/skills/actors/SKILL.md +3 -3
  54. package/skills/swarm/SKILL.md +1 -1
@@ -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
- if (matchesKey(data, "escape") || data.toLowerCase() === "n") {
109
- this.feedback = { ok: false, message: "Kill cancelled." };
110
- this.killConfirmation = undefined;
111
- } else if (matchesKey(data, "return") || data.toLowerCase() === "y") {
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("border", `├${leadingBorder}`)}${selectorTop}${this.theme.fg("border", `${remainingBorder}┤`)}`,
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.killConfirmation
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.border("├", "", "┤", innerWidth));
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(16, overlayRows - 7));
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 renderKillConfirmation(): string[] {
420
- return [
421
- this.theme.fg(
422
- "error",
423
- ` Kill running actor run:${this.killConfirmation?.run}?`,
424
- ),
425
- this.theme.fg("muted", " This sends canonical control.kill and cannot be undone."),
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("dim", ` ${description}`)}`;
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 ` ${hint("y/enter", "confirm kill")} ${hint("n/esc", "cancel")}`;
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
- ? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
458
- : ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
508
+ ? hints(["↑↓", "option"], ["enter", "apply"], ["←/esc", "back"])
509
+ : hints(["↑↓", "option"], ["→/enter", "open"], ["←/esc", "back"]);
459
510
  if (this.focus === "recipe")
460
- return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("↑ at top", "tabs")} ${hint("←/esc", "tabs")}`;
511
+ return hints(
512
+ ["↑↓/pgup/pgdn", "scroll"],
513
+ ["↑ at top", "tabs"],
514
+ ["←/esc", "tabs"],
515
+ );
461
516
  if (this.focus === "detail")
462
- return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("←/esc", "back")}`;
517
+ return hints(["↑↓/pgup/pgdn", "scroll"], ["←/esc", "back"]);
463
518
  if (this.focus === "list")
464
- return ` ${hint("↑↓/pgup/pgdn", "row")} ${hint("→/enter", "open")} ${hint("←", "tabs")} ${hint("esc", "close")}`;
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
- const killHint = this.killRun && run?.status === "running" && run.runInstanceId
468
- ? ` ${hint("k", "kill")}`
469
- : "";
470
- return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")}${killHint} ${hint("esc", "close")}`;
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 ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
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
- "border",
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("border", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("border", "│")}`;
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("border", `${left}${titleText}${fill}${right}`);
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("border", "│")}${body}${this.theme.fg("border", "│")}`;
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" }]
@@ -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: status.status as RunObservedStatus,
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
- function getRunArtifacts(transition: RunTransition): string[] {
1184
- if (!transition.stateDir) return [];
1185
- return [
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 isUserRecipeFile(file: string | undefined): boolean {
1195
- if (!file) return false;
1196
- const recipeRoot = resolve(Paths.getRecipeRoot());
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
- export function shouldSuggestRecipePersistence(
1202
- transition: RunTransition,
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 formatRecipePersistenceSuggestion(transition: RunTransition): string {
1210
- if (!shouldSuggestRecipePersistence(transition)) return "";
1211
- if (transition.recipeFile) {
1212
- return `\nAgent note: this actor completed successfully from recipe ${transition.recipeFile}. If this recipe fits this machine's recurring workflow, ask the operator whether to copy or register it as a durable tool recipe under ~/.pi/agent/recipes. Do not auto-save without confirmation.`;
1213
- }
1214
- return `\nAgent note: this actor was spawned directly and completed successfully. If this pattern fits this machine's recurring workflow, ask the operator whether to save it as a durable recipe/tool under ~/.pi/agent/recipes with register_tool. Do not auto-save without confirmation.`;
1215
- }
1216
-
1217
- function formatTransitionPolicy(transition: RunTransition): string {
1218
- if (!transition.modelPolicy) return "";
1219
- const axis = (key: "model" | "thinking", label: string): string | undefined => {
1220
- const value = transition.modelPolicy?.[key];
1221
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1222
- const record = value as Record<string, unknown>;
1223
- const source = typeof record.source === "string" ? record.source : "unused";
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 artifacts = formatNamedArtifacts(transition.artifacts);
1250
- const runFiles = formatRunFileList(getRunArtifacts(transition));
1251
- const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
1252
- const policy = formatTransitionPolicy(transition);
1253
- const nextActions = formatTransitionNextActions(transition);
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
  }