@llblab/pi-actors 0.42.0 → 0.42.1
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/BACKLOG.md +1 -5
- package/CHANGELOG.md +10 -0
- package/README.md +2 -0
- package/dist/lib/async-runs.d.ts +12 -0
- package/dist/lib/async-runs.js +46 -5
- package/dist/lib/command-templates.js +1 -1
- package/dist/lib/inspector-overlay.d.ts +4 -1
- package/dist/lib/inspector-overlay.js +71 -15
- package/dist/lib/inspector.js +1 -1
- package/dist/lib/observability.d.ts +12 -0
- package/dist/lib/observability.js +149 -5
- 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 +9 -9
- 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 +1 -1
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +1 -1
- package/docs/async-runs.md +7 -1
- package/docs/tool-registry.md +2 -0
- package/lib/async-runs.ts +65 -5
- package/lib/command-templates.ts +1 -1
- package/lib/inspector-overlay.ts +67 -15
- package/lib/inspector.ts +1 -1
- package/lib/observability.ts +180 -4
- 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 +9 -9
- 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 +1 -1
- 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));
|
|
@@ -261,9 +274,7 @@ export class ActorInspectorOverlay {
|
|
|
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)
|
|
@@ -392,12 +403,21 @@ export class ActorInspectorOverlay {
|
|
|
392
403
|
this.killConfirmation = {
|
|
393
404
|
run: run.run,
|
|
394
405
|
runInstanceId: run.runInstanceId,
|
|
406
|
+
status: run.status,
|
|
395
407
|
};
|
|
408
|
+
this.killDialogChoice = "cancel";
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private cancelKill(): void {
|
|
412
|
+
this.feedback = { ok: false, message: "Kill cancelled." };
|
|
413
|
+
this.killConfirmation = undefined;
|
|
414
|
+
this.killDialogChoice = "cancel";
|
|
396
415
|
}
|
|
397
416
|
|
|
398
417
|
private confirmKill(): void {
|
|
399
418
|
const confirmation = this.killConfirmation;
|
|
400
419
|
this.killConfirmation = undefined;
|
|
420
|
+
this.killDialogChoice = "cancel";
|
|
401
421
|
if (!confirmation || !this.killRun) {
|
|
402
422
|
this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
|
|
403
423
|
return;
|
|
@@ -416,14 +436,40 @@ export class ActorInspectorOverlay {
|
|
|
416
436
|
}
|
|
417
437
|
}
|
|
418
438
|
|
|
419
|
-
private
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
)
|
|
425
|
-
|
|
439
|
+
private renderKillDialog(_width: number, innerWidth: number): string[] {
|
|
440
|
+
const confirmation = this.killConfirmation!;
|
|
441
|
+
const totalRows = this.contentViewportRows() + 7;
|
|
442
|
+
const cancel = this.killDialogChoice === "cancel"
|
|
443
|
+
? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
|
|
444
|
+
: this.theme.fg("muted", " Cancel ");
|
|
445
|
+
const kill = this.killDialogChoice === "kill"
|
|
446
|
+
? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
|
|
447
|
+
: this.theme.fg("error", " Kill actor ");
|
|
448
|
+
const body = [
|
|
449
|
+
"",
|
|
450
|
+
this.theme.fg("error", "Kill this running actor?"),
|
|
451
|
+
"",
|
|
452
|
+
`${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
|
|
453
|
+
`${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
|
|
454
|
+
"",
|
|
455
|
+
this.theme.fg("text", "This sends canonical control.kill."),
|
|
456
|
+
this.theme.fg("error", "The action is destructive and cannot be undone."),
|
|
457
|
+
"",
|
|
458
|
+
`${cancel} ${kill}`,
|
|
459
|
+
"",
|
|
426
460
|
];
|
|
461
|
+
const lines = [this.border("╭", " Confirm Actor Kill ", "╮", innerWidth)];
|
|
462
|
+
const availableRows = Math.max(1, totalRows - 2);
|
|
463
|
+
const topPadding = Math.max(0, Math.floor((availableRows - body.length) / 2));
|
|
464
|
+
const dialogRows = [
|
|
465
|
+
...Array.from({ length: topPadding }, () => ""),
|
|
466
|
+
...body,
|
|
467
|
+
].slice(0, availableRows);
|
|
468
|
+
while (dialogRows.length < availableRows) dialogRows.push("");
|
|
469
|
+
for (const line of dialogRows)
|
|
470
|
+
lines.push(this.row(this.center(line, innerWidth), innerWidth));
|
|
471
|
+
lines.push(this.border("╰", " ←→/tab choose · enter activate · esc cancel ", "╯", innerWidth));
|
|
472
|
+
return lines;
|
|
427
473
|
}
|
|
428
474
|
|
|
429
475
|
private listItemCount(): number {
|
|
@@ -1293,6 +1339,12 @@ export class ActorInspectorOverlay {
|
|
|
1293
1339
|
return plain.slice(index);
|
|
1294
1340
|
}
|
|
1295
1341
|
|
|
1342
|
+
private center(content: string, width: number): string {
|
|
1343
|
+
const contentWidth = visibleWidth(content);
|
|
1344
|
+
const leading = Math.max(0, Math.floor((width - contentWidth) / 2));
|
|
1345
|
+
return `${" ".repeat(leading)}${content}`;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1296
1348
|
private fit(content: string, width: number): string {
|
|
1297
1349
|
// A Component render entry must remain exactly one terminal row. Persisted
|
|
1298
1350
|
// 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";
|
|
@@ -41,6 +45,7 @@ export interface RunObservation {
|
|
|
41
45
|
failures?: number;
|
|
42
46
|
ownerId?: string;
|
|
43
47
|
artifacts?: Record<string, string>;
|
|
48
|
+
launchCorrelation?: Record<string, string>;
|
|
44
49
|
launchSource?: AsyncRuns.AsyncRunLaunchSource;
|
|
45
50
|
modelPolicy?: Record<string, unknown>;
|
|
46
51
|
notificationPolicy?: "normal" | "silent";
|
|
@@ -48,6 +53,7 @@ export interface RunObservation {
|
|
|
48
53
|
terminalHandled?: boolean;
|
|
49
54
|
retireWhen?: string;
|
|
50
55
|
run: string;
|
|
56
|
+
semanticResult?: RunTerminalSemanticResult;
|
|
51
57
|
tool?: string;
|
|
52
58
|
stateDir?: string;
|
|
53
59
|
status: RunObservedStatus;
|
|
@@ -162,6 +168,19 @@ export function deliverRunTransitionNotifications(
|
|
|
162
168
|
transition.to,
|
|
163
169
|
);
|
|
164
170
|
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (transition.stateDir) {
|
|
173
|
+
AsyncRuns.recordRunTerminalDeliveryFailure(
|
|
174
|
+
transition.stateDir,
|
|
175
|
+
transition.to,
|
|
176
|
+
error,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
sink.notify(
|
|
181
|
+
`Actor terminal delivery failed for run:${transition.run}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`,
|
|
182
|
+
"error",
|
|
183
|
+
);
|
|
165
184
|
} finally {
|
|
166
185
|
inFlight.delete(key);
|
|
167
186
|
}
|
|
@@ -420,12 +439,23 @@ export interface RunTransition {
|
|
|
420
439
|
run: string;
|
|
421
440
|
stateDir?: string;
|
|
422
441
|
artifacts?: Record<string, string>;
|
|
442
|
+
launchCorrelation?: Record<string, string>;
|
|
423
443
|
launchSource?: AsyncRuns.AsyncRunLaunchSource;
|
|
424
444
|
modelPolicy?: Record<string, unknown>;
|
|
425
445
|
recipeFile?: string;
|
|
426
446
|
terminalHandled?: boolean;
|
|
427
447
|
to: RunObservedStatus;
|
|
428
448
|
tool?: string;
|
|
449
|
+
semanticResult?: RunTerminalSemanticResult;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export interface RunTerminalSemanticResult {
|
|
453
|
+
body?: string;
|
|
454
|
+
correlationId?: string;
|
|
455
|
+
metadata: Record<string, unknown>;
|
|
456
|
+
summary: string;
|
|
457
|
+
synthesized: boolean;
|
|
458
|
+
type: string;
|
|
429
459
|
}
|
|
430
460
|
|
|
431
461
|
export interface RunOutboxEvent {
|
|
@@ -504,15 +534,145 @@ function scanRunStateDirs(
|
|
|
504
534
|
return result;
|
|
505
535
|
}
|
|
506
536
|
|
|
537
|
+
const TERMINAL_RESULT_BYTES = 8 * 1024;
|
|
538
|
+
const TERMINAL_RESULT_CHARS = 4_000;
|
|
539
|
+
|
|
540
|
+
function readBoundedStart(path: string): string {
|
|
541
|
+
if (!existsSync(path)) return "";
|
|
542
|
+
const fd = openSync(path, "r");
|
|
543
|
+
try {
|
|
544
|
+
const size = Math.min(fstatSync(fd).size, TERMINAL_RESULT_BYTES);
|
|
545
|
+
const buffer = Buffer.alloc(size);
|
|
546
|
+
const bytes = readSync(fd, buffer, 0, size, 0);
|
|
547
|
+
const text = buffer.subarray(0, bytes).toString("utf8").trim();
|
|
548
|
+
return text.length > TERMINAL_RESULT_CHARS
|
|
549
|
+
? `${text.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
|
|
550
|
+
: text;
|
|
551
|
+
} finally {
|
|
552
|
+
closeSync(fd);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function semanticBodyFromStdout(stateDir: string): string | undefined {
|
|
557
|
+
const text = readBoundedStart(join(stateDir, "stdout.log"));
|
|
558
|
+
if (!text) return undefined;
|
|
559
|
+
const withoutMarker = text.replace(/^ACTOR_REVIEW_RESULT\s*(?:\r?\n)?/, "").trim();
|
|
560
|
+
return withoutMarker || undefined;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function terminalSemanticResult(
|
|
564
|
+
status: Record<string, unknown>,
|
|
565
|
+
stateDir: string,
|
|
566
|
+
observedStatus: RunObservedStatus,
|
|
567
|
+
): RunTerminalSemanticResult {
|
|
568
|
+
const correlation = status.launch_correlation &&
|
|
569
|
+
typeof status.launch_correlation === "object" &&
|
|
570
|
+
!Array.isArray(status.launch_correlation)
|
|
571
|
+
? status.launch_correlation as Record<string, unknown>
|
|
572
|
+
: {};
|
|
573
|
+
const correlationId =
|
|
574
|
+
typeof correlation.correlation_id === "string"
|
|
575
|
+
? correlation.correlation_id
|
|
576
|
+
: typeof correlation.tool_call_id === "string"
|
|
577
|
+
? correlation.tool_call_id
|
|
578
|
+
: undefined;
|
|
579
|
+
const mailbox = status.mailbox &&
|
|
580
|
+
typeof status.mailbox === "object" &&
|
|
581
|
+
!Array.isArray(status.mailbox)
|
|
582
|
+
? status.mailbox as Record<string, unknown>
|
|
583
|
+
: {};
|
|
584
|
+
const emits = Array.isArray(mailbox.emits)
|
|
585
|
+
? mailbox.emits.filter((item): item is string => typeof item === "string")
|
|
586
|
+
: [];
|
|
587
|
+
const outbox = readJsonlFileResilient<Record<string, unknown>>(
|
|
588
|
+
join(stateDir, "outbox.jsonl"),
|
|
589
|
+
).records;
|
|
590
|
+
const explicit = outbox.findLast((record) => {
|
|
591
|
+
const type = String(record.type ?? record.event ?? "");
|
|
592
|
+
return emits.includes(type) && !["command.done", "run.done", "run.failed"].includes(type);
|
|
593
|
+
});
|
|
594
|
+
if (explicit) {
|
|
595
|
+
const type = String(explicit.type ?? explicit.event);
|
|
596
|
+
return {
|
|
597
|
+
...(explicit.body === undefined
|
|
598
|
+
? {} : { body: formatSemanticBody(explicit.body) }),
|
|
599
|
+
...(typeof explicit.correlation_id === "string"
|
|
600
|
+
? { correlationId: explicit.correlation_id } : correlationId ? { correlationId } : {}),
|
|
601
|
+
metadata: {
|
|
602
|
+
...(explicit.metadata &&
|
|
603
|
+
typeof explicit.metadata === "object" &&
|
|
604
|
+
!Array.isArray(explicit.metadata)
|
|
605
|
+
? explicit.metadata as Record<string, unknown> : {}),
|
|
606
|
+
...(status.transport_context &&
|
|
607
|
+
typeof status.transport_context === "object" &&
|
|
608
|
+
!Array.isArray(status.transport_context)
|
|
609
|
+
? {
|
|
610
|
+
transport_context: status.transport_context as Record<string, unknown>,
|
|
611
|
+
} : {}),
|
|
612
|
+
run: String(status.run ?? ""),
|
|
613
|
+
status: observedStatus,
|
|
614
|
+
},
|
|
615
|
+
summary: String(explicit.summary ?? type),
|
|
616
|
+
synthesized: false,
|
|
617
|
+
type,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const reviewCompleted = observedStatus === "done" && emits.includes("review.completed");
|
|
621
|
+
const result = status.result &&
|
|
622
|
+
typeof status.result === "object" &&
|
|
623
|
+
!Array.isArray(status.result)
|
|
624
|
+
? status.result as Record<string, unknown>
|
|
625
|
+
: {};
|
|
626
|
+
const type = reviewCompleted
|
|
627
|
+
? "review.completed"
|
|
628
|
+
: observedStatus === "done" ? "run.done" : "run.failed";
|
|
629
|
+
const stdoutBody = observedStatus === "done"
|
|
630
|
+
? semanticBodyFromStdout(stateDir) : undefined;
|
|
631
|
+
return {
|
|
632
|
+
...(stdoutBody
|
|
633
|
+
? { body: stdoutBody }
|
|
634
|
+
: typeof result.error === "string" ? { body: result.error } : {}),
|
|
635
|
+
...(correlationId ? { correlationId } : {}),
|
|
636
|
+
metadata: {
|
|
637
|
+
...(status.transport_context &&
|
|
638
|
+
typeof status.transport_context === "object" &&
|
|
639
|
+
!Array.isArray(status.transport_context)
|
|
640
|
+
? {
|
|
641
|
+
transport_context: status.transport_context as Record<string, unknown>,
|
|
642
|
+
} : {}),
|
|
643
|
+
run: String(status.run ?? ""), status: observedStatus,
|
|
644
|
+
},
|
|
645
|
+
summary: reviewCompleted ? "Review completed." :
|
|
646
|
+
observedStatus === "done" ? "Run completed." : `Run ${observedStatus}.`,
|
|
647
|
+
synthesized: true,
|
|
648
|
+
type,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function formatSemanticBody(body: unknown): string {
|
|
653
|
+
const rendered = typeof body === "string" ? body : JSON.stringify(body);
|
|
654
|
+
const text = typeof rendered === "string" ? rendered : String(body);
|
|
655
|
+
const compact = text.trim();
|
|
656
|
+
return compact.length > TERMINAL_RESULT_CHARS
|
|
657
|
+
? `${compact.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
|
|
658
|
+
: compact;
|
|
659
|
+
}
|
|
660
|
+
|
|
507
661
|
function observeRun(stateDir: string): RunObservation | undefined {
|
|
508
662
|
try {
|
|
509
663
|
const status = AsyncRuns.getRunStatus(stateDir);
|
|
510
664
|
const progress = getProgress(status);
|
|
511
665
|
const run = typeof status.run === "string" ? status.run : undefined;
|
|
512
666
|
if (!run) return undefined;
|
|
667
|
+
const observedStatus = status.status as RunObservedStatus;
|
|
513
668
|
return {
|
|
514
669
|
activeSubagents: toNumber(progress.activeSubagents),
|
|
515
670
|
completed: toNumber(progress.completed),
|
|
671
|
+
...(status.launch_correlation &&
|
|
672
|
+
typeof status.launch_correlation === "object" &&
|
|
673
|
+
!Array.isArray(status.launch_correlation)
|
|
674
|
+
? { launchCorrelation: status.launch_correlation as Record<string, string> }
|
|
675
|
+
: {}),
|
|
516
676
|
failures: Array.isArray(progress.failures)
|
|
517
677
|
? progress.failures.length
|
|
518
678
|
: undefined,
|
|
@@ -542,9 +702,12 @@ function observeRun(stateDir: string): RunObservation | undefined {
|
|
|
542
702
|
...(typeof status.retire_when === "string"
|
|
543
703
|
? { retireWhen: status.retire_when }
|
|
544
704
|
: {}),
|
|
705
|
+
...(TERMINAL.has(observedStatus)
|
|
706
|
+
? { semanticResult: terminalSemanticResult(status, stateDir, observedStatus) }
|
|
707
|
+
: {}),
|
|
545
708
|
run,
|
|
546
709
|
stateDir,
|
|
547
|
-
status:
|
|
710
|
+
status: observedStatus,
|
|
548
711
|
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
549
712
|
updatedAt: getUpdatedAt(status),
|
|
550
713
|
};
|
|
@@ -933,9 +1096,11 @@ export function detectRunTransitions(
|
|
|
933
1096
|
run: run.run,
|
|
934
1097
|
...(run.stateDir ? { stateDir: run.stateDir } : {}),
|
|
935
1098
|
...(run.artifacts ? { artifacts: run.artifacts } : {}),
|
|
1099
|
+
...(run.launchCorrelation ? { launchCorrelation: run.launchCorrelation } : {}),
|
|
936
1100
|
...(run.launchSource ? { launchSource: run.launchSource } : {}),
|
|
937
1101
|
...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
|
|
938
1102
|
...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
|
|
1103
|
+
...(run.semanticResult ? { semanticResult: run.semanticResult } : {}),
|
|
939
1104
|
...(run.terminalHandled ? { terminalHandled: true } : {}),
|
|
940
1105
|
to: run.status,
|
|
941
1106
|
...(run.tool ? { tool: run.tool } : {}),
|
|
@@ -1195,7 +1360,8 @@ function isUserRecipeFile(file: string | undefined): boolean {
|
|
|
1195
1360
|
if (!file) return false;
|
|
1196
1361
|
const recipeRoot = resolve(Paths.getRecipeRoot());
|
|
1197
1362
|
const path = resolve(file);
|
|
1198
|
-
|
|
1363
|
+
const relation = relative(recipeRoot, path);
|
|
1364
|
+
return relation === "" || (!relation.startsWith("..") && !isAbsolute(relation));
|
|
1199
1365
|
}
|
|
1200
1366
|
|
|
1201
1367
|
export function shouldSuggestRecipePersistence(
|
|
@@ -1245,16 +1411,26 @@ function formatTransitionNextActions(transition: RunTransition): string {
|
|
|
1245
1411
|
return `\nNext actions: ${actions.join(" | ")}`;
|
|
1246
1412
|
}
|
|
1247
1413
|
|
|
1414
|
+
function formatTransitionSemanticResult(transition: RunTransition): string {
|
|
1415
|
+
const result = transition.semanticResult;
|
|
1416
|
+
if (!result) return "";
|
|
1417
|
+
const correlation = result.correlationId
|
|
1418
|
+
? `\nCorrelation: ${result.correlationId}` : "";
|
|
1419
|
+
const body = result.body ? `\nResult:\n${result.body}` : "";
|
|
1420
|
+
return `\nSemantic result: ${result.type} — ${result.summary}${correlation}${body}`;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1248
1423
|
export function formatRunTransitionMessage(transition: RunTransition): string {
|
|
1249
1424
|
const artifacts = formatNamedArtifacts(transition.artifacts);
|
|
1250
1425
|
const runFiles = formatRunFileList(getRunArtifacts(transition));
|
|
1251
1426
|
const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
|
|
1252
1427
|
const policy = formatTransitionPolicy(transition);
|
|
1253
1428
|
const nextActions = formatTransitionNextActions(transition);
|
|
1429
|
+
const semanticResult = formatTransitionSemanticResult(transition);
|
|
1254
1430
|
if (transition.to === "done")
|
|
1255
|
-
return `Run ${transition.run} completed successfully.${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
|
|
1431
|
+
return `Run ${transition.run} completed successfully.${semanticResult}${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
|
|
1256
1432
|
if (transition.to === "failed")
|
|
1257
|
-
return `Run ${transition.run} failed.${policy}${artifacts}${runFiles}${nextActions}`;
|
|
1433
|
+
return `Run ${transition.run} failed.${semanticResult}${policy}${artifacts}${runFiles}${nextActions}`;
|
|
1258
1434
|
if (transition.to === "cancelled")
|
|
1259
1435
|
return `Run ${transition.run} was cancelled.${policy}${nextActions}`;
|
|
1260
1436
|
if (transition.to === "killed")
|
package/lib/runs-control.ts
CHANGED
|
@@ -42,6 +42,7 @@ export function getRunProcessSignalPlan(
|
|
|
42
42
|
export interface RunProcessSignalDeps {
|
|
43
43
|
killProcess?: typeof process.kill;
|
|
44
44
|
runtimePlatform?: NodeJS.Platform;
|
|
45
|
+
spawnProcess?: typeof spawnSync;
|
|
45
46
|
verifyIdentity?: (
|
|
46
47
|
pid: number,
|
|
47
48
|
expected: RunProcessIdentity,
|
|
@@ -70,8 +71,26 @@ export function signalOwnedRunProcess(
|
|
|
70
71
|
}
|
|
71
72
|
const plan = getRunProcessSignalPlan(pid, signal, runtimePlatform);
|
|
72
73
|
if (plan.command && plan.args) {
|
|
73
|
-
const
|
|
74
|
+
const spawnProcess = deps.spawnProcess ?? spawnSync;
|
|
75
|
+
let result = spawnProcess(plan.command, plan.args, { encoding: "utf8" });
|
|
76
|
+
if (
|
|
77
|
+
runtimePlatform === "win32" &&
|
|
78
|
+
result.status !== 0 &&
|
|
79
|
+
!plan.args.includes("/F")
|
|
80
|
+
) {
|
|
81
|
+
result = spawnProcess(plan.command, [...plan.args, "/F"], {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
74
85
|
if (result.status !== 0) {
|
|
86
|
+
if (expectedIdentity) {
|
|
87
|
+
const finalProof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(
|
|
88
|
+
pid,
|
|
89
|
+
expectedIdentity,
|
|
90
|
+
runtimePlatform,
|
|
91
|
+
);
|
|
92
|
+
if (finalProof.status === "dead_pid") return plan;
|
|
93
|
+
}
|
|
75
94
|
throw new Error(
|
|
76
95
|
result.stderr?.trim() ||
|
|
77
96
|
result.stdout?.trim() ||
|
package/lib/runs-ownership.ts
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
realpathSync,
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
|
-
import {
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
16
17
|
|
|
17
18
|
import { writeJsonAtomic } from "./file-state.ts";
|
|
18
19
|
|
|
@@ -35,13 +36,31 @@ function comparablePath(path: string): string {
|
|
|
35
36
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function isSystemTempRootAlias(resolved: string, canonical: string): boolean {
|
|
40
|
+
const tempRoot = resolve(tmpdir());
|
|
41
|
+
const relativeStateDir = relative(tempRoot, resolved);
|
|
42
|
+
if (
|
|
43
|
+
relativeStateDir === ".." ||
|
|
44
|
+
relativeStateDir.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) ||
|
|
45
|
+
isAbsolute(relativeStateDir)
|
|
46
|
+
) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const canonicalTempRoot = realpathSync.native(tempRoot);
|
|
50
|
+
const expectedCanonical = resolve(canonicalTempRoot, relativeStateDir);
|
|
51
|
+
return comparablePath(canonical) === comparablePath(expectedCanonical);
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
function assertCanonicalDirectory(stateDir: string): string {
|
|
39
55
|
const resolved = resolve(stateDir);
|
|
40
56
|
if (lstatSync(resolved).isSymbolicLink()) {
|
|
41
57
|
throw new Error(`Run state directory cannot be a symlink: ${resolved}`);
|
|
42
58
|
}
|
|
43
|
-
const canonical = realpathSync(resolved);
|
|
44
|
-
if (
|
|
59
|
+
const canonical = realpathSync.native(resolved);
|
|
60
|
+
if (
|
|
61
|
+
comparablePath(canonical) !== comparablePath(resolved) &&
|
|
62
|
+
!isSystemTempRootAlias(resolved, canonical)
|
|
63
|
+
) {
|
|
45
64
|
throw new Error(`Run state directory has an ambiguous symlink alias: ${resolved}`);
|
|
46
65
|
}
|
|
47
66
|
return resolved;
|
package/lib/runs-process.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
8
8
|
import { platform } from "node:os";
|
|
9
|
-
import {
|
|
9
|
+
import { posix, win32 } from "node:path";
|
|
10
10
|
|
|
11
11
|
export interface RunProcessIdentity {
|
|
12
12
|
command: string;
|
|
@@ -125,8 +125,9 @@ export function captureRunProcessIdentity(
|
|
|
125
125
|
if (!identity.command.includes(runnerPath) || !identity.command.includes(stateDir)) {
|
|
126
126
|
return undefined;
|
|
127
127
|
}
|
|
128
|
-
const
|
|
129
|
-
const
|
|
128
|
+
const pathApi = runtimePlatform === "win32" ? win32 : posix;
|
|
129
|
+
const resolvedCwd = pathApi.resolve(cwd);
|
|
130
|
+
const canonicalCwd = runtimePlatform === platform() && existsSync(resolvedCwd)
|
|
130
131
|
? realpathSync.native(resolvedCwd)
|
|
131
132
|
: resolvedCwd;
|
|
132
133
|
const expectedCwd =
|
package/lib/runs-start.ts
CHANGED
package/lib/runs-status.ts
CHANGED
|
@@ -61,6 +61,9 @@ export function buildRunStatus(
|
|
|
61
61
|
? "running"
|
|
62
62
|
: (getInterruptedRunStatus(stateDir) ?? "exited");
|
|
63
63
|
const terminalHandled = readJson(join(stateDir, "terminal-handled.json"));
|
|
64
|
+
const terminalDeliveryFailure = readJson(
|
|
65
|
+
join(stateDir, "terminal-delivery-failure.json"),
|
|
66
|
+
);
|
|
64
67
|
return {
|
|
65
68
|
...meta,
|
|
66
69
|
eventsFile: join(stateDir, "events.jsonl"),
|
|
@@ -70,6 +73,8 @@ export function buildRunStatus(
|
|
|
70
73
|
process_identity_status: processIdentity.status,
|
|
71
74
|
progress: readJson(join(stateDir, "progress.json")) || null,
|
|
72
75
|
result: result || null,
|
|
76
|
+
...(terminalDeliveryFailure
|
|
77
|
+
? { terminal_delivery_failure: terminalDeliveryFailure } : {}),
|
|
73
78
|
...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
|
|
74
79
|
state_dir: String(meta.state_dir ?? stateDir),
|
|
75
80
|
stderrLog: join(stateDir, "stderr.log"),
|
package/lib/tools-inspect.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
10
11
|
|
|
11
12
|
import * as AsyncRuns from "./async-runs.ts";
|
|
12
13
|
import * as Limits from "./limits.ts";
|
|
@@ -269,7 +270,7 @@ function getPiActorsRuntimeStatus(): Record<string, unknown> {
|
|
|
269
270
|
} catch {
|
|
270
271
|
git_commit = undefined;
|
|
271
272
|
}
|
|
272
|
-
const entrypoint =
|
|
273
|
+
const entrypoint = fileURLToPath(import.meta.url);
|
|
273
274
|
return {
|
|
274
275
|
automatic_recipe_review: Paths.isAutomaticRecipeReviewEnabled(),
|
|
275
276
|
entrypoint,
|
package/lib/tools-local.ts
CHANGED
|
@@ -143,6 +143,14 @@ export function createRuntimeToolDefinition(
|
|
|
143
143
|
paramSchema.run_id = Schema.stringSchema(
|
|
144
144
|
"Optional run id override for this async template recipe invocation.",
|
|
145
145
|
);
|
|
146
|
+
if (isAsyncRecipe) {
|
|
147
|
+
paramSchema.correlation_id = Schema.stringSchema(
|
|
148
|
+
"Optional workflow correlation id preserved in terminal follow-up delivery.",
|
|
149
|
+
);
|
|
150
|
+
paramSchema.transport_context = Schema.looseObjectSchema(
|
|
151
|
+
"Optional originating transport route preserved for detached terminal follow-up.",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
146
154
|
return {
|
|
147
155
|
name: cfg.name,
|
|
148
156
|
label: cfg.name,
|
|
@@ -155,7 +163,7 @@ export function createRuntimeToolDefinition(
|
|
|
155
163
|
)
|
|
156
164
|
: Prompts.formatRegisteredToolPromptSnippet(cfg.template),
|
|
157
165
|
async execute(
|
|
158
|
-
|
|
166
|
+
toolCallId: string,
|
|
159
167
|
params: unknown,
|
|
160
168
|
signal: AbortSignal | undefined,
|
|
161
169
|
_onUpdate: unknown,
|
|
@@ -172,7 +180,7 @@ export function createRuntimeToolDefinition(
|
|
|
172
180
|
}
|
|
173
181
|
if (isAsyncRecipe) {
|
|
174
182
|
const input = params as Record<string, unknown>;
|
|
175
|
-
const { run_id, ...values } = input;
|
|
183
|
+
const { correlation_id, run_id, transport_context, ...values } = input;
|
|
176
184
|
const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
|
|
177
185
|
const runId =
|
|
178
186
|
typeof run_id === "string" && run_id.trim()
|
|
@@ -182,6 +190,17 @@ export function createRuntimeToolDefinition(
|
|
|
182
190
|
{
|
|
183
191
|
...base,
|
|
184
192
|
launch_source: "tool",
|
|
193
|
+
launch_correlation: {
|
|
194
|
+
...(typeof correlation_id === "string"
|
|
195
|
+
? { correlation_id } : {}),
|
|
196
|
+
tool_call_id: toolCallId,
|
|
197
|
+
},
|
|
198
|
+
...(transport_context &&
|
|
199
|
+
typeof transport_context === "object" &&
|
|
200
|
+
!Array.isArray(transport_context)
|
|
201
|
+
? {
|
|
202
|
+
transport_context: transport_context as Record<string, unknown>,
|
|
203
|
+
} : {}),
|
|
185
204
|
ownerId: getRunOwnerId(ctx),
|
|
186
205
|
run_id: runId,
|
|
187
206
|
tool: cfg.name,
|
package/lib/tools-spawn.ts
CHANGED
|
@@ -144,6 +144,9 @@ export function createSpawnToolDefinition<
|
|
|
144
144
|
as: Schema.stringSchema(
|
|
145
145
|
"Optional actor address for the spawned run, e.g. run:<id>.",
|
|
146
146
|
),
|
|
147
|
+
correlation_id: Schema.stringSchema(
|
|
148
|
+
"Optional workflow correlation id preserved in terminal follow-up delivery.",
|
|
149
|
+
),
|
|
147
150
|
file: Schema.stringSchema(
|
|
148
151
|
"Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
|
|
149
152
|
),
|
|
@@ -162,6 +165,9 @@ export function createSpawnToolDefinition<
|
|
|
162
165
|
values: Schema.looseObjectSchema(
|
|
163
166
|
"Runtime placeholder values passed to the actor.",
|
|
164
167
|
),
|
|
168
|
+
transport_context: Schema.looseObjectSchema(
|
|
169
|
+
"Optional originating transport route preserved for detached terminal follow-up, e.g. Telegram chat_id and thread_id.",
|
|
170
|
+
),
|
|
165
171
|
verbose: Schema.booleanSchema(
|
|
166
172
|
"Return full JSON instead of compact text.",
|
|
167
173
|
),
|
|
@@ -169,7 +175,7 @@ export function createSpawnToolDefinition<
|
|
|
169
175
|
[],
|
|
170
176
|
),
|
|
171
177
|
async execute(
|
|
172
|
-
|
|
178
|
+
toolCallId: string,
|
|
173
179
|
params: unknown,
|
|
174
180
|
_signal: AbortSignal | undefined,
|
|
175
181
|
_onUpdate: unknown,
|
|
@@ -196,7 +202,14 @@ export function createSpawnToolDefinition<
|
|
|
196
202
|
{
|
|
197
203
|
file: recipe,
|
|
198
204
|
launch_source: "spawn",
|
|
205
|
+
launch_correlation: {
|
|
206
|
+
...(typeof input.correlation_id === "string"
|
|
207
|
+
? { correlation_id: input.correlation_id } : {}),
|
|
208
|
+
tool_call_id: toolCallId,
|
|
209
|
+
},
|
|
199
210
|
ownerId: getRunOwnerId(ctx),
|
|
211
|
+
...(input.transport_context
|
|
212
|
+
? { transport_context: asRecord(input.transport_context) } : {}),
|
|
200
213
|
run_id: runId,
|
|
201
214
|
...(input.template !== undefined
|
|
202
215
|
? {
|