pi-subagents 0.48.0 → 0.49.0
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/CHANGELOG.md +47 -19
- package/docs/agents.md +1 -0
- package/docs/configuration.md +28 -5
- package/docs/missions.md +4 -2
- package/docs/observability.md +26 -2
- package/package.json +1 -1
- package/src/agents/agents.ts +27 -12
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +20 -0
- package/src/extension/index.ts +18 -8
- package/src/extension/public-execution.ts +30 -4
- package/src/extension/schemas.ts +8 -7
- package/src/extension/tool-description.ts +10 -8
- package/src/inspectors/herdr/actions.ts +11 -2
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/intercom/intercom-bridge.ts +3 -1
- package/src/missions/store.ts +8 -3
- package/src/runs/background/active-async-capacity.ts +82 -25
- package/src/runs/background/async-execution.ts +24 -36
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +11 -1
- package/src/runs/background/fleet-view.ts +18 -5
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +14 -6
- package/src/runs/background/run-status.ts +96 -4
- package/src/runs/background/subagent-runner.ts +4 -1
- package/src/runs/foreground/prompt-audit.ts +4 -3
- package/src/runs/foreground/subagent-executor.ts +219 -20
- package/src/runs/shared/completion-guard.ts +17 -1
- package/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +23 -4
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +3 -1
- package/src/tui/render.ts +86 -39
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +8 -2
- package/src/workflows/scripted-workflow.ts +46 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { safeTerminalText } from "../../shared/display-text.ts";
|
|
4
5
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
5
6
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
6
7
|
import {
|
|
@@ -196,7 +197,7 @@ function readSessionTranscriptTail(sessionFile: string, maxLines: number, truste
|
|
|
196
197
|
try {
|
|
197
198
|
const parsed = JSON.parse(line) as unknown;
|
|
198
199
|
const messageLine = sessionMessageLine(parsed);
|
|
199
|
-
if (messageLine) lines.push(messageLine);
|
|
200
|
+
if (messageLine) lines.push(...messageLine.split(/\r?\n/));
|
|
200
201
|
} catch {
|
|
201
202
|
malformed++;
|
|
202
203
|
}
|
|
@@ -410,6 +411,18 @@ function appendKnownArtifacts(lines: string[], input: { outputPaths: string[]; s
|
|
|
410
411
|
for (const artifact of artifacts) lines.push(` ${artifact}`);
|
|
411
412
|
}
|
|
412
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Sanitizes each assembled line independently.
|
|
416
|
+
*
|
|
417
|
+
* Sanitizing the joined response would let a single binary fragment in child
|
|
418
|
+
* output collapse the whole transcript, including run state, artifact paths, and
|
|
419
|
+
* warnings, into the binary placeholder. Per-line sanitization keeps that damage
|
|
420
|
+
* to the offending line.
|
|
421
|
+
*/
|
|
422
|
+
function safeTranscriptLines(lines: string[]): string {
|
|
423
|
+
return lines.map((line) => safeTerminalText(line)).join("\n");
|
|
424
|
+
}
|
|
425
|
+
|
|
413
426
|
function appendTranscriptBody(lines: string[], sourceLabel: string, sourceLines: string[], truncated: boolean): void {
|
|
414
427
|
lines.push(`${sourceLabel}${truncated ? " (tail truncated)" : ""}:`);
|
|
415
428
|
if (sourceLines.length === 0) {
|
|
@@ -470,7 +483,7 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
|
|
|
470
483
|
for (const warning of warnings) lines.push(` ${warning}`);
|
|
471
484
|
}
|
|
472
485
|
appendTranscriptBody(lines, transcriptSource, transcriptLines, truncated);
|
|
473
|
-
return lines
|
|
486
|
+
return safeTranscriptLines(lines);
|
|
474
487
|
}
|
|
475
488
|
|
|
476
489
|
export function formatNestedRunTranscript(run: NestedRunSummary, options: TranscriptOptions = {}): string {
|
|
@@ -488,7 +501,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
|
|
|
488
501
|
appendKnownArtifacts(lines, { outputPaths: [], sessionFile: run.sessionFile });
|
|
489
502
|
if (!run.sessionFile) {
|
|
490
503
|
appendTranscriptBody(lines, "Transcript tail", [], false);
|
|
491
|
-
return lines
|
|
504
|
+
return safeTranscriptLines(lines);
|
|
492
505
|
}
|
|
493
506
|
const sessionTail = readSessionTranscriptTail(run.sessionFile, lineLimit, options.sessionRoots ?? []);
|
|
494
507
|
if (sessionTail.warnings.length) {
|
|
@@ -496,7 +509,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
|
|
|
496
509
|
for (const warning of sessionTail.warnings) lines.push(` ${warning}`);
|
|
497
510
|
}
|
|
498
511
|
appendTranscriptBody(lines, `Session transcript tail from ${run.sessionFile}`, sessionTail.lines, false);
|
|
499
|
-
return lines
|
|
512
|
+
return safeTranscriptLines(lines);
|
|
500
513
|
}
|
|
501
514
|
|
|
502
515
|
export function formatAsyncResultTranscript(data: {
|
|
@@ -536,5 +549,5 @@ export function formatAsyncResultTranscript(data: {
|
|
|
536
549
|
].filter((line): line is string => Boolean(line));
|
|
537
550
|
appendKnownArtifacts(lines, { outputPaths: [], sessionFile, resultPath });
|
|
538
551
|
appendTranscriptBody(lines, "Result transcript tail", transcriptLines.filter((line) => line.trim()), output.split(/\r?\n/).length > lineLimit);
|
|
539
|
-
return lines
|
|
552
|
+
return safeTranscriptLines(lines);
|
|
540
553
|
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AsyncRunSummary } from "./async-status.ts";
|
|
3
3
|
|
|
4
|
+
const INTERCOM_DETACH_ERROR = "detached for intercom coordination";
|
|
5
|
+
|
|
6
|
+
function isIntercomDetached(run: AsyncRunSummary): boolean {
|
|
7
|
+
return run.steps.some((step) => step.execution?.status === "detached"
|
|
8
|
+
|| step.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true)
|
|
9
|
+
|| run.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatIntercomDetachGuidance(run: AsyncRunSummary): string | undefined {
|
|
13
|
+
if (!isIntercomDetached(run)) return undefined;
|
|
14
|
+
return `Run "${run.id}" detached for intercom coordination. Reply to the supervisor request first, then wait with subagent_wait({ id: "${run.id}" }). Use subagent({ action: "status", id: "${run.id}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefined {
|
|
5
18
|
const step = run.steps.find((candidate) => candidate.status === "failed" && candidate.sessionFile && fs.existsSync(candidate.sessionFile));
|
|
6
19
|
if (!step) {
|
|
@@ -15,19 +28,26 @@ export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefin
|
|
|
15
28
|
|
|
16
29
|
export function formatResumeFirstFailedRunDetail(run: AsyncRunSummary): string | undefined {
|
|
17
30
|
if (run.state !== "failed") return undefined;
|
|
31
|
+
const detachGuidance = formatIntercomDetachGuidance(run);
|
|
32
|
+
if (detachGuidance) return detachGuidance;
|
|
18
33
|
const command = formatAsyncReviveCommand(run);
|
|
19
34
|
if (!command) return undefined;
|
|
20
35
|
return `Resume-first: failed run "${run.id}" has a persisted child session. Revive the original run with ${command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
export function formatResumeFirstFailedRunsNote(runs: AsyncRunSummary[]): string {
|
|
24
|
-
const
|
|
25
|
-
|
|
39
|
+
const failedRuns = runs.filter((run) => run.state === "failed");
|
|
40
|
+
const detachGuidance = failedRuns
|
|
41
|
+
.map(formatIntercomDetachGuidance)
|
|
42
|
+
.filter((guidance): guidance is string => Boolean(guidance));
|
|
43
|
+
const resumable = failedRuns
|
|
44
|
+
.filter((run) => !isIntercomDetached(run))
|
|
26
45
|
.map((run) => ({ run, command: formatAsyncReviveCommand(run) }))
|
|
27
46
|
.filter((entry): entry is { run: AsyncRunSummary; command: string } => Boolean(entry.command));
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
const resumeGuidance = resumable.length === 0
|
|
48
|
+
? ""
|
|
49
|
+
: resumable.length === 1
|
|
50
|
+
? ` Resume-first: failed run "${resumable[0]!.run.id}" has a persisted child session. Revive the original run with ${resumable[0]!.command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`
|
|
51
|
+
: ` Resume-first: ${resumable.length} failed runs have persisted child sessions. Inspect status and revive each original run before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
|
|
52
|
+
return `${detachGuidance.length > 0 ? ` ${detachGuidance.join(" ")}` : ""}${resumeGuidance}`;
|
|
33
53
|
}
|
|
@@ -8,6 +8,8 @@ const MAX_TASK_SUMMARY_LENGTH = 120;
|
|
|
8
8
|
export interface RetainedChild {
|
|
9
9
|
runId: string;
|
|
10
10
|
parentRunId?: string;
|
|
11
|
+
workflowKey?: string;
|
|
12
|
+
state: "complete" | "failed" | "paused";
|
|
11
13
|
agent: string;
|
|
12
14
|
taskSummary: string;
|
|
13
15
|
completedAt: number;
|
|
@@ -33,16 +35,20 @@ function boundedTaskSummary(value: string | undefined): string {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export function listRetainedChildren(asyncDirRoot: string, sessionId: string): RetainedChild[] {
|
|
36
|
-
return listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete"], reconcile: false })
|
|
38
|
+
return listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete", "failed", "paused"], reconcile: false })
|
|
37
39
|
.flatMap((run) => {
|
|
38
40
|
if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
|
|
41
|
+
if (run.state !== "complete" && run.state !== "failed" && run.state !== "paused") return [];
|
|
39
42
|
const step = run.steps[0]!;
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
const terminalStep = step.status === "complete" || step.status === "completed" || step.status === "failed" || step.status === "paused";
|
|
44
|
+
if (!terminalStep || !retainedSessionFile(step.sessionFile ?? run.sessionFile)) return [];
|
|
45
|
+
const completedAt = run.endedAt ?? run.lastUpdate;
|
|
42
46
|
if (completedAt === undefined) return [];
|
|
43
47
|
return [{
|
|
44
48
|
runId: run.id,
|
|
45
49
|
...(run.parentWorkflowRunId ? { parentRunId: run.parentWorkflowRunId } : {}),
|
|
50
|
+
...(run.workflowKey ? { workflowKey: run.workflowKey } : {}),
|
|
51
|
+
state: run.state,
|
|
46
52
|
agent: step.agent,
|
|
47
53
|
taskSummary: boundedTaskSummary(step.description),
|
|
48
54
|
completedAt,
|
|
@@ -55,13 +61,15 @@ export function listRetainedChildren(asyncDirRoot: string, sessionId: string): R
|
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export function formatRetainedChildren(children: RetainedChild[]): string {
|
|
58
|
-
if (children.length === 0) return "No
|
|
64
|
+
if (children.length === 0) return "No recoverable retained children in the active parent session.";
|
|
59
65
|
return [
|
|
60
|
-
`
|
|
66
|
+
`Recoverable retained children (newest first, last ${MAX_RETAINED_CHILDREN}):`,
|
|
61
67
|
...children.flatMap((child) => [
|
|
62
|
-
`- ${child.runId} | ${child.agent} | ${new Date(child.completedAt).toISOString()}`,
|
|
68
|
+
`- ${child.runId} | ${child.agent} | ${child.state} | ${new Date(child.completedAt).toISOString()}`,
|
|
69
|
+
...(child.parentRunId ? [` workflow: ${child.parentRunId}${child.workflowKey ? ` (${child.workflowKey})` : ""}`] : []),
|
|
63
70
|
` task: ${child.taskSummary || "(no task summary)"}`,
|
|
64
71
|
` session: ${child.sessionPath}`,
|
|
72
|
+
` resume: subagent({ action: "resume", id: "${child.runId}", message: "..." })`,
|
|
65
73
|
...(child.tokenTotals ? [` tokens: input ${child.tokenTotals.input}, output ${child.tokenTotals.output}, total ${child.tokenTotals.total}`] : []),
|
|
66
74
|
]),
|
|
67
75
|
].join("\n");
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { safeTerminalText } from "../../shared/display-text.ts";
|
|
4
5
|
import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
|
|
5
6
|
import { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
|
|
6
7
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
7
8
|
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
8
9
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
9
10
|
import { DIRS, type AsyncStatus, type Details, type ForegroundResumeRun, type NestedRunSummary, type SteeringStatus, type SubagentState } from "../../shared/types.ts";
|
|
11
|
+
import { inspectActiveAsyncCapacityOwner, type ActiveAsyncCapacityInspection } from "./active-async-capacity.ts";
|
|
10
12
|
import { readStatus } from "../../shared/utils.ts";
|
|
11
13
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
12
14
|
import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
|
|
@@ -31,6 +33,66 @@ interface RunStatusParams {
|
|
|
31
33
|
lines?: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function formatProcessTerminal(value: AsyncStatus["processTerminal"] | undefined): string {
|
|
37
|
+
if (!value) return "missing";
|
|
38
|
+
return `${value.state}${value.reason ? ` (${value.reason})` : ""}${value.runnerProcessInstanceId ? ` · runner ${value.runnerProcessInstanceId}` : ""}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function debugProcessTerminal(asyncDir: string, status: AsyncStatus): { sidecar?: AsyncStatus["processTerminal"]; overlay?: AsyncStatus["processTerminal"] } {
|
|
42
|
+
const expected = { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId };
|
|
43
|
+
return {
|
|
44
|
+
sidecar: readProcessTerminal(asyncDir, expected),
|
|
45
|
+
overlay: sanitizeProcessTerminal(status.processTerminal, expected, path.join(asyncDir, "status.json")),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatCapacityOwner(inspect: ActiveAsyncCapacityInspection): string[] {
|
|
50
|
+
if (!inspect.owner) return [`Active capacity: ${inspect.release.state} — ${inspect.release.reason}`];
|
|
51
|
+
const owner = inspect.owner;
|
|
52
|
+
return [
|
|
53
|
+
`Active capacity: ${inspect.release.state} — ${inspect.release.reason}`,
|
|
54
|
+
`Capacity owner: ${inspect.relation} slot ${owner.slot}, ${owner.kind}, generation ${owner.generation}`,
|
|
55
|
+
`Capacity session: ${owner.ownerSessionId}`,
|
|
56
|
+
owner.sourceRunId ? `Capacity source run: ${owner.sourceRunId}` : undefined,
|
|
57
|
+
`Capacity async dir: ${owner.asyncDir}`,
|
|
58
|
+
owner.runnerProcessInstanceId ? `Capacity runner: ${owner.runnerProcessInstanceId}` : undefined,
|
|
59
|
+
owner.runnerStartedAt !== undefined ? `Capacity runner started: ${new Date(owner.runnerStartedAt).toISOString()}` : undefined,
|
|
60
|
+
].filter((line): line is string => line !== undefined);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatWorkflowDebug(status: AsyncStatus): string[] {
|
|
64
|
+
if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey) return [];
|
|
65
|
+
const lines = [
|
|
66
|
+
status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
|
|
67
|
+
status.mode === "workflow" ? `Workflow children: ${(status.steps ?? []).length}` : undefined,
|
|
68
|
+
].filter((line): line is string => line !== undefined);
|
|
69
|
+
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
70
|
+
lines.push(` ${index + 1}. key ${step.workflowKey ?? "n/a"} · ${step.agent} · ${step.status} · async ${step.async === undefined ? "unknown" : step.async ? "yes" : "no"}${step.runId ? ` · run ${step.runId}` : ""}`);
|
|
71
|
+
}
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatRunLifecycleDebug(input: { status: AsyncStatus; asyncDir: string; sidecarProcessTerminal: AsyncStatus["processTerminal"] | undefined; overlayProcessTerminal: AsyncStatus["processTerminal"] | undefined; capacity: ActiveAsyncCapacityInspection }): string {
|
|
76
|
+
const { status, asyncDir, sidecarProcessTerminal, overlayProcessTerminal, capacity } = input;
|
|
77
|
+
const lines = [
|
|
78
|
+
"Run lifecycle debug",
|
|
79
|
+
`Run: ${status.runId}`,
|
|
80
|
+
`Dir: ${asyncDir}`,
|
|
81
|
+
`Status file: ${path.join(asyncDir, "status.json")}`,
|
|
82
|
+
`Process terminal file: ${path.join(asyncDir, "process-terminal.json")}`,
|
|
83
|
+
`Session: ${status.sessionId ?? "unknown"}`,
|
|
84
|
+
`State: ${status.state}`,
|
|
85
|
+
`Mode: ${status.mode}`,
|
|
86
|
+
status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}` : undefined,
|
|
87
|
+
status.workflowKey ? `Workflow key: ${status.workflowKey}` : undefined,
|
|
88
|
+
`Status process terminal: ${formatProcessTerminal(overlayProcessTerminal)}`,
|
|
89
|
+
`Sidecar process terminal: ${formatProcessTerminal(sidecarProcessTerminal)}`,
|
|
90
|
+
...formatCapacityOwner(capacity),
|
|
91
|
+
...formatWorkflowDebug(status),
|
|
92
|
+
].filter((line): line is string => line !== undefined);
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
34
96
|
interface RunStatusDeps {
|
|
35
97
|
asyncDirRoot?: string;
|
|
36
98
|
resultsDir?: string;
|
|
@@ -39,6 +101,7 @@ interface RunStatusDeps {
|
|
|
39
101
|
state?: SubagentState;
|
|
40
102
|
nested?: NestedRunResolutionScope;
|
|
41
103
|
sessionRoots?: string[];
|
|
104
|
+
activeCapacityRoot?: string;
|
|
42
105
|
}
|
|
43
106
|
|
|
44
107
|
function hasExistingSessionFile(value: unknown): value is string {
|
|
@@ -50,12 +113,16 @@ function formatCheckpointGuidance(runId: string | undefined, checkpoint: AsyncSt
|
|
|
50
113
|
return `Checkpoint: ${checkpoint.name}${checkpoint.message ? ` — ${checkpoint.message}` : ""}\nApprove: subagent({ action: "approve-checkpoint", id: "${runId}" })\nReject: subagent({ action: "reject-checkpoint", id: "${runId}" })`;
|
|
51
114
|
}
|
|
52
115
|
|
|
53
|
-
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
116
|
+
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown; runId?: unknown; workflowKey?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
54
117
|
if (options.stopped) return "Resume: unavailable; stopped runs are not resumable. Start a new run instead.";
|
|
55
118
|
const knownChildren = children
|
|
56
119
|
.map((child, index) => ({ child, index }))
|
|
57
120
|
.filter(({ child }) => typeof child.agent === "string");
|
|
58
121
|
if (!runId || knownChildren.length === 0) return "Resume: unavailable; no child session file was persisted.";
|
|
122
|
+
const workflowChildren = knownChildren.filter(({ child }) => typeof child.runId === "string" && child.runId.trim() && hasExistingSessionFile(child.sessionFile));
|
|
123
|
+
if (workflowChildren.length > 0) {
|
|
124
|
+
return workflowChildren.map(({ child }) => `Revive workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: subagent({ action: "resume", id: "${child.runId}", message: "..." })`).join("\n");
|
|
125
|
+
}
|
|
59
126
|
const singleSessionFile = knownChildren[0]?.child.sessionFile ?? fallbackSessionFile;
|
|
60
127
|
if (children.length === 1 && knownChildren.length === 1 && hasExistingSessionFile(singleSessionFile)) {
|
|
61
128
|
return `Revive: subagent({ action: "resume", id: "${runId}", message: "..." })`;
|
|
@@ -178,7 +245,7 @@ function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options:
|
|
|
178
245
|
lines.push("Result transcript tail:");
|
|
179
246
|
if (outputLines.length === 0) lines.push(" (no recovered final output available yet)");
|
|
180
247
|
else for (const line of outputLines) lines.push(` ${line}`);
|
|
181
|
-
return lines.join("\n");
|
|
248
|
+
return lines.map((line) => safeTerminalText(line)).join("\n");
|
|
182
249
|
}
|
|
183
250
|
|
|
184
251
|
function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): string {
|
|
@@ -263,7 +330,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
263
330
|
let location;
|
|
264
331
|
try {
|
|
265
332
|
const requestedId = params.id ?? params.runId;
|
|
266
|
-
if (
|
|
333
|
+
if (params.action === "debug.run") {
|
|
334
|
+
location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
|
|
335
|
+
} else if (!params.dir && requestedId) {
|
|
267
336
|
const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
|
|
268
337
|
if (resolved?.kind === "foreground") {
|
|
269
338
|
const run = deps.state?.foregroundRuns?.get(resolved.id);
|
|
@@ -331,6 +400,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
331
400
|
}
|
|
332
401
|
const status = reconciliation.status;
|
|
333
402
|
if (!status && diskStatus?.displayDismissedAt !== undefined) {
|
|
403
|
+
if (params.action === "debug.run") {
|
|
404
|
+
const { sidecar, overlay } = debugProcessTerminal(asyncDir, diskStatus);
|
|
405
|
+
const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
|
|
406
|
+
return {
|
|
407
|
+
content: [{ type: "text", text: formatRunLifecycleDebug({ status: diskStatus, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
|
|
408
|
+
details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
|
|
409
|
+
};
|
|
410
|
+
}
|
|
334
411
|
if (params.view === "transcript") {
|
|
335
412
|
if (currentSessionId && diskStatus.sessionId !== currentSessionId) {
|
|
336
413
|
return {
|
|
@@ -350,6 +427,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
350
427
|
const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
|
|
351
428
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
352
429
|
if (status) {
|
|
430
|
+
if (params.action === "debug.run") {
|
|
431
|
+
const { sidecar, overlay } = debugProcessTerminal(asyncDir, status);
|
|
432
|
+
const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: "text", text: formatRunLifecycleDebug({ status, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
|
|
435
|
+
details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
|
|
436
|
+
};
|
|
437
|
+
}
|
|
353
438
|
if (params.view === "transcript") {
|
|
354
439
|
if (currentSessionId && status.sessionId !== currentSessionId) {
|
|
355
440
|
return {
|
|
@@ -475,9 +560,16 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
475
560
|
}
|
|
476
561
|
|
|
477
562
|
if (resultPath) {
|
|
563
|
+
if (params.action === "debug.run") {
|
|
564
|
+
return {
|
|
565
|
+
content: [{ type: "text", text: "Run lifecycle debug needs an async run directory with status.json." }],
|
|
566
|
+
isError: true,
|
|
567
|
+
details: { mode: "single", results: [] },
|
|
568
|
+
};
|
|
569
|
+
}
|
|
478
570
|
try {
|
|
479
571
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
480
|
-
const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
|
|
572
|
+
const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
|
|
481
573
|
if (params.view === "transcript") {
|
|
482
574
|
try {
|
|
483
575
|
return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
|
|
@@ -143,6 +143,8 @@ import {
|
|
|
143
143
|
type ChildWatchdogStateSnapshot,
|
|
144
144
|
} from "../../watchdog/child-status.ts";
|
|
145
145
|
|
|
146
|
+
const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task completion.";
|
|
147
|
+
|
|
146
148
|
interface SubagentRunConfig {
|
|
147
149
|
id: string;
|
|
148
150
|
steps: RunnerStep[];
|
|
@@ -1693,6 +1695,7 @@ async function runSingleStep(
|
|
|
1693
1695
|
const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
|
|
1694
1696
|
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
|
|
1695
1697
|
const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
1698
|
+
const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
|
|
1696
1699
|
const effectiveFinalError = stoppedAfterAcceptance
|
|
1697
1700
|
? ctx.stopMessage ?? "Subagent stopped by user."
|
|
1698
1701
|
: timedOutAfterAcceptance
|
|
@@ -1701,7 +1704,7 @@ async function runSingleStep(
|
|
|
1701
1704
|
? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
|
|
1702
1705
|
: acceptanceCanFailRun
|
|
1703
1706
|
? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
|
|
1704
|
-
: finalResult?.error;
|
|
1707
|
+
: finalResult?.error ?? (intercomDetachReceipt ? INTERCOM_DETACH_RECEIPT : undefined);
|
|
1705
1708
|
|
|
1706
1709
|
const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
|
|
1707
1710
|
? persistStepArtifacts({
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Agent, type StreamFn, type ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
4
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
5
|
+
import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
|
|
5
6
|
import type { ForegroundRunControl } from "../../shared/types.ts";
|
|
6
7
|
export type PromptAuditView = "authored" | "runtime" | "effective";
|
|
7
8
|
|
|
@@ -42,7 +43,7 @@ function finalAssistantText(agent: Agent): string {
|
|
|
42
43
|
return "";
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
async function resolveRewriteAuth(ctx: ExtensionContext, model: RegistryModel): Promise<{ apiKey?: string; headers?:
|
|
46
|
+
async function resolveRewriteAuth(ctx: ExtensionContext, model: RegistryModel): Promise<{ apiKey?: string; headers?: ProviderHeaders; env?: Record<string, string> }> {
|
|
46
47
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
47
48
|
if (auth.ok === false) throw new Error(`Prompt redo model auth failed for ${fullModelId(model)}: ${auth.error}`);
|
|
48
49
|
return {
|
|
@@ -91,7 +92,7 @@ export async function rewritePromptWithGuidance(input: {
|
|
|
91
92
|
tools: [],
|
|
92
93
|
},
|
|
93
94
|
convertToLlm,
|
|
94
|
-
|
|
95
|
+
...agentStreamOptions(streamFn),
|
|
95
96
|
getApiKey: (providerName) => providerName === model.provider ? auth.apiKey : undefined,
|
|
96
97
|
toolExecution: "sequential",
|
|
97
98
|
});
|