pi-subagents 0.53.0 → 0.55.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 +66 -5
- package/README.md +1 -1
- package/agents/reviewer.md +12 -2
- package/docs/agents.md +4 -4
- package/docs/configuration.md +6 -6
- package/docs/extension-api.md +14 -4
- package/docs/models.md +41 -6
- package/docs/observability.md +14 -3
- package/docs/tool-reference.md +21 -6
- package/docs/workflows.md +7 -1
- package/index.ts +10 -1
- package/package.json +1 -1
- package/prompts/council.md +19 -7
- package/prompts/parallel-review.md +5 -1
- package/prompts/review-loop.md +9 -5
- package/skills/council-mode/SKILL.md +50 -26
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
- package/skills/pi-subagents/references/execution-controls.md +13 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
- package/src/agents/agent-management.ts +115 -14
- package/src/agents/agent-serializer.ts +2 -2
- package/src/agents/agents.ts +195 -47
- package/src/api/external-job-provider.ts +10 -1
- package/src/api/preflight.ts +33 -19
- package/src/api/project-panes.ts +2 -0
- package/src/extension/doctor.ts +10 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +24 -4
- package/src/extension/public-execution.ts +12 -9
- package/src/extension/rpc.ts +77 -3
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -6
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/inspectors/herdr/focus.ts +55 -0
- package/src/inspectors/herdr/project-panes.ts +228 -44
- package/src/integrations/herdr-status.ts +26 -4
- package/src/runs/background/async-execution.ts +112 -13
- package/src/runs/background/async-job-tracker.ts +33 -14
- package/src/runs/background/async-resume.ts +20 -2
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/async-status.ts +10 -0
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/control-channel.ts +98 -10
- package/src/runs/background/notify.ts +62 -1
- package/src/runs/background/result-watcher.ts +4 -3
- package/src/runs/background/run-status.ts +15 -1
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +251 -38
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +4 -3
- package/src/runs/foreground/async-stop-action.ts +23 -2
- package/src/runs/foreground/execution.ts +110 -8
- package/src/runs/foreground/subagent-executor.ts +361 -45
- package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
- package/src/runs/shared/acceptance.ts +1 -0
- package/src/runs/shared/child-identity.ts +36 -0
- package/src/runs/shared/completion-guard.ts +50 -1
- package/src/runs/shared/external-job-bridge.ts +53 -37
- package/src/runs/shared/external-job-runner.ts +126 -23
- package/src/runs/shared/model-fallback.ts +46 -15
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/orca-progress-tabs.ts +71 -9
- package/src/runs/shared/parallel-utils.ts +12 -0
- package/src/runs/shared/pi-args.ts +31 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
- package/src/runs/shared/tool-availability.ts +1 -3
- package/src/shared/launch-contract.ts +6 -5
- package/src/shared/thinking-ceiling.ts +52 -0
- package/src/shared/types.ts +72 -5
- package/src/tui/fleet-status.ts +66 -13
- package/src/tui/render.ts +5 -4
- package/src/watchdog/permission-arbiter.ts +59 -51
- package/src/workflows/scripted-workflow.ts +107 -10
|
@@ -55,6 +55,8 @@ export interface StopRequest {
|
|
|
55
55
|
ts?: number;
|
|
56
56
|
source?: string;
|
|
57
57
|
reason?: string;
|
|
58
|
+
targetIndex?: number;
|
|
59
|
+
childId?: string;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export type SteerDeliveryMode = "steer" | "follow_up" | "auto";
|
|
@@ -92,6 +94,7 @@ export interface SteerAck {
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
const STEER_REQUESTS_DIR = "steer-requests";
|
|
97
|
+
const STOP_REQUESTS_DIR = "stop-requests";
|
|
95
98
|
const REVIVAL_BRIEFS_DIR = "revival-briefs";
|
|
96
99
|
export const MAX_STEER_QUEUE_SIZE = 20;
|
|
97
100
|
const STEER_TARGETS_DIR = "steer-targets";
|
|
@@ -121,6 +124,11 @@ export function stopRequestPath(asyncDir: string): string {
|
|
|
121
124
|
return path.join(controlInboxDir(asyncDir), "stop.json");
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
/** Directory of parent-to-runner stop requests. */
|
|
128
|
+
export function stopRequestsDir(asyncDir: string): string {
|
|
129
|
+
return path.join(controlInboxDir(asyncDir), STOP_REQUESTS_DIR);
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
/** Directory of parent-to-runner steering requests. */
|
|
125
133
|
export function steerRequestsDir(asyncDir: string): string {
|
|
126
134
|
return path.join(controlInboxDir(asyncDir), STEER_REQUESTS_DIR);
|
|
@@ -164,13 +172,24 @@ export function steerAckPathFromDir(dir: string, requestId: string): string {
|
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
function assertChildIndex(index: number): void {
|
|
167
|
-
if (!Number.isInteger(index) || index < 0 || index > 1_000_000) throw new Error("
|
|
175
|
+
if (!Number.isInteger(index) || index < 0 || index > 1_000_000) throw new Error("child index must be a non-negative integer.");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validStopChildId(childId: unknown): childId is string {
|
|
179
|
+
return typeof childId === "string"
|
|
180
|
+
&& Boolean(childId.trim())
|
|
181
|
+
&& childId.length <= 256
|
|
182
|
+
&& !/[\r\n]/.test(childId);
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
function steerRequestFileName(request: SteerRequest): string {
|
|
171
186
|
return `${String(request.ts).padStart(13, "0")}-${Buffer.from(request.id).toString("base64url")}.json`;
|
|
172
187
|
}
|
|
173
188
|
|
|
189
|
+
function stopRequestFileName(request: StopRequest): string {
|
|
190
|
+
return `${String(request.ts ?? 0).padStart(13, "0")}-${randomUUID()}.json`;
|
|
191
|
+
}
|
|
192
|
+
|
|
174
193
|
function validSteerRequest(request: Partial<SteerRequest>): request is SteerRequest {
|
|
175
194
|
return request.type === "steer"
|
|
176
195
|
&& typeof request.id === "string"
|
|
@@ -279,8 +298,12 @@ export function requestAsyncStop(
|
|
|
279
298
|
payload: Omit<StopRequest, "type"> = {},
|
|
280
299
|
deps: { now?: () => number } = {},
|
|
281
300
|
): string {
|
|
282
|
-
|
|
301
|
+
if (payload.targetIndex !== undefined) assertChildIndex(payload.targetIndex);
|
|
302
|
+
if (payload.childId !== undefined && !validStopChildId(payload.childId)) {
|
|
303
|
+
throw new Error("stop childId must be a non-empty string without newlines and at most 256 characters.");
|
|
304
|
+
}
|
|
283
305
|
const request: StopRequest = { ...payload, ts: payload.ts ?? deps.now?.() ?? Date.now(), type: "stop" };
|
|
306
|
+
const requestPath = path.join(stopRequestsDir(asyncDir), stopRequestFileName(request));
|
|
284
307
|
writeAtomicJson(requestPath, request);
|
|
285
308
|
return requestPath;
|
|
286
309
|
}
|
|
@@ -521,16 +544,78 @@ export function consumeTimeoutRequest(
|
|
|
521
544
|
|
|
522
545
|
export function consumeStopRequest(
|
|
523
546
|
asyncDir: string,
|
|
524
|
-
fsImpl: Pick<typeof fs, "existsSync" | "rmSync"> = fs,
|
|
547
|
+
fsImpl: Pick<typeof fs, "existsSync" | "rmSync" | "readdirSync" | "readFileSync"> = fs,
|
|
525
548
|
): boolean {
|
|
526
|
-
|
|
527
|
-
|
|
549
|
+
return consumeStopRequestPayload(asyncDir, fsImpl) !== undefined;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function parseStopRequest(raw: unknown): StopRequest | undefined {
|
|
553
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
554
|
+
const parsed = raw as Partial<StopRequest>;
|
|
555
|
+
if (parsed.type !== "stop") return undefined;
|
|
556
|
+
if (Object.hasOwn(parsed, "targetIndex") && !(Number.isInteger(parsed.targetIndex) && parsed.targetIndex! >= 0 && parsed.targetIndex! <= 1_000_000)) return undefined;
|
|
557
|
+
if (Object.hasOwn(parsed, "childId") && !validStopChildId(parsed.childId)) return undefined;
|
|
558
|
+
return {
|
|
559
|
+
type: "stop",
|
|
560
|
+
...(typeof parsed.ts === "number" ? { ts: parsed.ts } : {}),
|
|
561
|
+
...(typeof parsed.source === "string" ? { source: parsed.source } : {}),
|
|
562
|
+
...(typeof parsed.reason === "string" ? { reason: parsed.reason } : {}),
|
|
563
|
+
...(parsed.targetIndex !== undefined ? { targetIndex: parsed.targetIndex } : {}),
|
|
564
|
+
...(validStopChildId(parsed.childId) ? { childId: parsed.childId } : {}),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function consumeStopRequestFile(
|
|
569
|
+
requestPath: string,
|
|
570
|
+
fsImpl: Pick<typeof fs, "rmSync" | "readFileSync">,
|
|
571
|
+
): StopRequest | undefined {
|
|
572
|
+
let request: StopRequest | undefined;
|
|
573
|
+
try {
|
|
574
|
+
request = parseStopRequest(JSON.parse(fsImpl.readFileSync(requestPath, "utf-8")));
|
|
575
|
+
} catch {
|
|
576
|
+
request = undefined;
|
|
577
|
+
}
|
|
528
578
|
try {
|
|
529
579
|
fsImpl.rmSync(requestPath, { force: true, recursive: true });
|
|
530
580
|
} catch {
|
|
531
|
-
// Already removed by a concurrent check —
|
|
581
|
+
// Already removed by a concurrent check — do not execute it twice.
|
|
582
|
+
return undefined;
|
|
532
583
|
}
|
|
533
|
-
return
|
|
584
|
+
return request;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function consumeStopRequestPayloads(
|
|
588
|
+
asyncDir: string,
|
|
589
|
+
fsImpl: Pick<typeof fs, "existsSync" | "rmSync" | "readdirSync" | "readFileSync"> = fs,
|
|
590
|
+
): StopRequest[] {
|
|
591
|
+
const dir = stopRequestsDir(asyncDir);
|
|
592
|
+
const requests: StopRequest[] = [];
|
|
593
|
+
if (fsImpl.existsSync(dir)) {
|
|
594
|
+
let entries: string[];
|
|
595
|
+
try {
|
|
596
|
+
entries = fsImpl.readdirSync(dir).filter((name) => name.endsWith(".json")).sort();
|
|
597
|
+
} catch {
|
|
598
|
+
entries = [];
|
|
599
|
+
}
|
|
600
|
+
for (const entry of entries) {
|
|
601
|
+
const request = consumeStopRequestFile(path.join(dir, entry), fsImpl);
|
|
602
|
+
if (request) requests.push(request);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const legacyPath = stopRequestPath(asyncDir);
|
|
607
|
+
if (fsImpl.existsSync(legacyPath)) {
|
|
608
|
+
const request = consumeStopRequestFile(legacyPath, fsImpl);
|
|
609
|
+
if (request) requests.push(request);
|
|
610
|
+
}
|
|
611
|
+
return requests.sort((left, right) => (left.ts ?? 0) - (right.ts ?? 0));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function consumeStopRequestPayload(
|
|
615
|
+
asyncDir: string,
|
|
616
|
+
fsImpl: Pick<typeof fs, "existsSync" | "rmSync" | "readdirSync" | "readFileSync"> = fs,
|
|
617
|
+
): StopRequest | undefined {
|
|
618
|
+
return consumeStopRequestPayloads(asyncDir, fsImpl)[0];
|
|
534
619
|
}
|
|
535
620
|
|
|
536
621
|
/** Parent side: write the authoritative portable interrupt request. */
|
|
@@ -560,8 +645,10 @@ export function deliverStopRequest(input: {
|
|
|
560
645
|
signal?: NodeJS.Signals;
|
|
561
646
|
now?: () => number;
|
|
562
647
|
source?: string;
|
|
648
|
+
targetIndex?: number;
|
|
649
|
+
childId?: string;
|
|
563
650
|
}): void {
|
|
564
|
-
requestAsyncStop(input.asyncDir, input.source ? { source: input.source } : {}, { now: input.now });
|
|
651
|
+
requestAsyncStop(input.asyncDir, { ...(input.source ? { source: input.source } : {}), ...(input.targetIndex !== undefined ? { targetIndex: input.targetIndex } : {}), ...(input.childId ? { childId: input.childId } : {}) }, { now: input.now });
|
|
565
652
|
}
|
|
566
653
|
|
|
567
654
|
/**
|
|
@@ -575,7 +662,7 @@ export function watchAsyncControlInbox(
|
|
|
575
662
|
opts: {
|
|
576
663
|
onInterrupt: () => void;
|
|
577
664
|
onTimeout?: () => void;
|
|
578
|
-
onStop?: () => void;
|
|
665
|
+
onStop?: (request: StopRequest) => void;
|
|
579
666
|
onSteer?: (request: SteerRequest) => void;
|
|
580
667
|
onSteerCapability?: (capability: SteerCapability) => void;
|
|
581
668
|
onSteerAck?: (ack: SteerAck) => void;
|
|
@@ -599,7 +686,7 @@ export function watchAsyncControlInbox(
|
|
|
599
686
|
const check = (): void => {
|
|
600
687
|
if (disposed) return;
|
|
601
688
|
try {
|
|
602
|
-
|
|
689
|
+
for (const stopRequest of consumeStopRequestPayloads(asyncDir, fsImpl)) opts.onStop?.(stopRequest);
|
|
603
690
|
if (consumeTimeoutRequest(asyncDir, fsImpl)) opts.onTimeout?.();
|
|
604
691
|
if (consumeInterruptRequest(asyncDir, fsImpl)) opts.onInterrupt();
|
|
605
692
|
for (const request of consumeSteerRequests(asyncDir, fsImpl)) opts.onSteer?.(request);
|
|
@@ -651,6 +738,7 @@ export function watchAsyncControlInbox(
|
|
|
651
738
|
try {
|
|
652
739
|
if (shouldUseNativeFsWatch("runner-control-inbox", opts.platform)) {
|
|
653
740
|
watchDir(dir);
|
|
741
|
+
watchDir(stopRequestsDir(asyncDir), true);
|
|
654
742
|
watchDir(steerRequestsDir(asyncDir), true);
|
|
655
743
|
watchDir(steerCapabilitiesDir(asyncDir), true);
|
|
656
744
|
watchDir(path.join(dir, STEER_ACKS_DIR), true);
|
|
@@ -24,6 +24,9 @@ export interface SubagentNotifyDetails {
|
|
|
24
24
|
taskInfo?: string;
|
|
25
25
|
resultPreview: string;
|
|
26
26
|
durationMs?: number;
|
|
27
|
+
workflowRunId?: string;
|
|
28
|
+
childRuns?: Array<{ runId: string; workflowKey?: string; agent?: string; status?: string }>;
|
|
29
|
+
reconciledFromDetachedChild?: string;
|
|
27
30
|
sessionLabel?: string;
|
|
28
31
|
sessionValue?: string;
|
|
29
32
|
handoffPath?: string;
|
|
@@ -40,12 +43,18 @@ export interface CompletionNotification {
|
|
|
40
43
|
summary?: string;
|
|
41
44
|
exitCode?: number;
|
|
42
45
|
state?: string;
|
|
46
|
+
mode?: string;
|
|
47
|
+
runId?: string | null;
|
|
48
|
+
reconciledFromDetachedChild?: string;
|
|
43
49
|
processSignal?: string | null;
|
|
44
50
|
interrupted?: boolean;
|
|
45
51
|
timedOut?: boolean;
|
|
46
52
|
stopped?: boolean;
|
|
47
53
|
turnBudgetExceeded?: boolean;
|
|
48
54
|
results?: Array<{
|
|
55
|
+
runId?: string;
|
|
56
|
+
workflowKey?: string;
|
|
57
|
+
agent?: string;
|
|
49
58
|
status?: string;
|
|
50
59
|
success?: boolean;
|
|
51
60
|
exitCode?: number | null;
|
|
@@ -94,8 +103,23 @@ function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
|
|
|
94
103
|
return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
function formatChildRun(child: { runId: string; workflowKey?: string; agent?: string; status?: string }): string {
|
|
107
|
+
const label = child.workflowKey ?? child.agent;
|
|
108
|
+
const status = child.status ? ` (${child.status})` : "";
|
|
109
|
+
return `${label ? `${label}=` : ""}${child.runId}${status}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatCorrelationLines(details: SubagentNotifyDetails): string[] {
|
|
113
|
+
return [
|
|
114
|
+
details.workflowRunId ? `Workflow run: ${details.workflowRunId}` : undefined,
|
|
115
|
+
details.childRuns?.length ? `Child runs: ${details.childRuns.map(formatChildRun).join(", ")}` : undefined,
|
|
116
|
+
details.reconciledFromDetachedChild ? `Reconciled detached child: ${details.reconciledFromDetachedChild}` : undefined,
|
|
117
|
+
].filter((line): line is string => line !== undefined);
|
|
118
|
+
}
|
|
119
|
+
|
|
97
120
|
export function formatSingleCompletion(details: SubagentNotifyDetails): string {
|
|
98
121
|
const sessionLine = formatSessionLine(details);
|
|
122
|
+
const correlationLines = formatCorrelationLines(details);
|
|
99
123
|
const taskKind = details.source === "foreground" ? "Detached foreground task" : "Background task";
|
|
100
124
|
const scheduleLine = details.scheduleOrigin
|
|
101
125
|
? `Scheduled run from **${details.scheduleOrigin.name ?? details.scheduleOrigin.id}** (schedule ${details.scheduleOrigin.id}).`
|
|
@@ -108,6 +132,8 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
|
|
|
108
132
|
details.resultPreview.trim() ? details.resultPreview : "(no output)",
|
|
109
133
|
details.handoffPath ? "" : undefined,
|
|
110
134
|
details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,
|
|
135
|
+
correlationLines.length && !details.handoffPath ? "" : undefined,
|
|
136
|
+
...correlationLines,
|
|
111
137
|
sessionLine ? "" : undefined,
|
|
112
138
|
sessionLine,
|
|
113
139
|
]
|
|
@@ -139,11 +165,27 @@ export function parseSubagentNotifyContent(content: string): SubagentNotifyDetai
|
|
|
139
165
|
}
|
|
140
166
|
const sessionLine = sessionIndex >= 0 ? body[sessionIndex] : undefined;
|
|
141
167
|
const handoffIndex = body.findIndex((line) => line.startsWith("Parallel handoff: "));
|
|
142
|
-
const
|
|
168
|
+
const workflowRunIndex = body.findIndex((line) => line.startsWith("Workflow run: "));
|
|
169
|
+
const childRunsIndex = body.findIndex((line) => line.startsWith("Child runs: "));
|
|
170
|
+
const reconciledIndex = body.findIndex((line) => line.startsWith("Reconciled detached child: "));
|
|
171
|
+
const metadataIndexes = [sessionIndex, handoffIndex, workflowRunIndex, childRunsIndex, reconciledIndex].filter((index) => index >= 0);
|
|
143
172
|
const firstMetadataIndex = metadataIndexes.length ? Math.min(...metadataIndexes) : body.length;
|
|
144
173
|
const resultEnd = firstMetadataIndex > 0 && body[firstMetadataIndex - 1]?.trim() === "" ? firstMetadataIndex - 1 : firstMetadataIndex;
|
|
145
174
|
const resultPreview = body.slice(0, resultEnd).join("\n").trim() || "(no output)";
|
|
146
175
|
const handoffPath = handoffIndex >= 0 ? body[handoffIndex]!.slice("Parallel handoff: ".length).trim() : undefined;
|
|
176
|
+
const workflowRunId = workflowRunIndex >= 0 ? body[workflowRunIndex]!.slice("Workflow run: ".length).trim() : undefined;
|
|
177
|
+
const childRuns = childRunsIndex >= 0
|
|
178
|
+
? body[childRunsIndex]!.slice("Child runs: ".length).split(", ").map((part) => {
|
|
179
|
+
const trimmed = part.trim();
|
|
180
|
+
const statusMatch = trimmed.match(/^(.*?)(?: \(([^)]*)\))?$/);
|
|
181
|
+
const raw = statusMatch?.[1] ?? trimmed;
|
|
182
|
+
const separator = raw.indexOf("=");
|
|
183
|
+
return separator >= 0
|
|
184
|
+
? { workflowKey: raw.slice(0, separator), runId: raw.slice(separator + 1), ...(statusMatch?.[2] ? { status: statusMatch[2] } : {}) }
|
|
185
|
+
: { runId: raw, ...(statusMatch?.[2] ? { status: statusMatch[2] } : {}) };
|
|
186
|
+
}).filter((child) => child.runId)
|
|
187
|
+
: undefined;
|
|
188
|
+
const reconciledFromDetachedChild = reconciledIndex >= 0 ? body[reconciledIndex]!.slice("Reconciled detached child: ".length).trim() : undefined;
|
|
147
189
|
let sessionLabel: string | undefined;
|
|
148
190
|
let sessionValue: string | undefined;
|
|
149
191
|
if (sessionLine) {
|
|
@@ -159,6 +201,9 @@ export function parseSubagentNotifyContent(content: string): SubagentNotifyDetai
|
|
|
159
201
|
...(parsedScheduleOrigin ? { scheduleOrigin: parsedScheduleOrigin } : {}),
|
|
160
202
|
resultPreview,
|
|
161
203
|
...(handoffPath ? { handoffPath } : {}),
|
|
204
|
+
...(workflowRunId ? { workflowRunId } : {}),
|
|
205
|
+
...(childRuns?.length ? { childRuns } : {}),
|
|
206
|
+
...(reconciledFromDetachedChild ? { reconciledFromDetachedChild } : {}),
|
|
162
207
|
...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),
|
|
163
208
|
};
|
|
164
209
|
}
|
|
@@ -173,6 +218,7 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
|
|
|
173
218
|
blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ""}${detail.scheduleOrigin ? ` — scheduled run from ${detail.scheduleOrigin.name ?? detail.scheduleOrigin.id} (schedule ${detail.scheduleOrigin.id})` : ""}`);
|
|
174
219
|
blocks.push(detail.resultPreview.trim() ? detail.resultPreview : "(no output)");
|
|
175
220
|
if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);
|
|
221
|
+
blocks.push(...formatCorrelationLines(detail));
|
|
176
222
|
if (sessionLine) blocks.push(sessionLine);
|
|
177
223
|
blocks.push("");
|
|
178
224
|
}
|
|
@@ -237,6 +283,18 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
|
|
|
237
283
|
? result.parallelHandoff as { path?: unknown }
|
|
238
284
|
: undefined;
|
|
239
285
|
const handoffPath = typeof parallelHandoff?.path === "string" ? parallelHandoff.path : undefined;
|
|
286
|
+
const rawRunId = typeof result.runId === "string" ? result.runId : typeof result.id === "string" ? result.id : undefined;
|
|
287
|
+
const workflowRunId = (result.mode === "workflow" || agent === "workflow") && rawRunId ? rawRunId : undefined;
|
|
288
|
+
const childRuns = result.results?.flatMap((child) => {
|
|
289
|
+
if (typeof child.runId !== "string" || !child.runId.trim()) return [];
|
|
290
|
+
return [{
|
|
291
|
+
runId: child.runId,
|
|
292
|
+
...(typeof child.workflowKey === "string" ? { workflowKey: child.workflowKey } : {}),
|
|
293
|
+
...(typeof child.agent === "string" ? { agent: child.agent } : {}),
|
|
294
|
+
...(typeof child.status === "string" ? { status: child.status } : {}),
|
|
295
|
+
}];
|
|
296
|
+
}) ?? [];
|
|
297
|
+
const reconciledFromDetachedChild = typeof result.reconciledFromDetachedChild === "string" ? result.reconciledFromDetachedChild : undefined;
|
|
240
298
|
const session =
|
|
241
299
|
result.shareUrl
|
|
242
300
|
? { label: "Session", value: result.shareUrl }
|
|
@@ -258,6 +316,9 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
|
|
|
258
316
|
resultPreview: summary,
|
|
259
317
|
...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
|
|
260
318
|
...(handoffPath ? { handoffPath } : {}),
|
|
319
|
+
...(workflowRunId ? { workflowRunId } : {}),
|
|
320
|
+
...(childRuns.length ? { childRuns } : {}),
|
|
321
|
+
...(reconciledFromDetachedChild ? { reconciledFromDetachedChild } : {}),
|
|
261
322
|
...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
|
|
262
323
|
};
|
|
263
324
|
}
|
|
@@ -57,7 +57,7 @@ type ResultWatcherDeps = {
|
|
|
57
57
|
coalesceDelayMs?: number;
|
|
58
58
|
/** Returns true while a durable completion source needs periodic delivery checks. */
|
|
59
59
|
hasDeliveryDemand?: () => boolean;
|
|
60
|
-
/** Control how slow result-index scans are logged. Defaults to \"
|
|
60
|
+
/** Control how slow result-index scans are logged. Defaults to \"activity\". */
|
|
61
61
|
resultScanLogging?: "all" | "activity" | "off";
|
|
62
62
|
platform?: NodeJS.Platform;
|
|
63
63
|
};
|
|
@@ -606,12 +606,13 @@ export function createResultWatcher(
|
|
|
606
606
|
const logScanStats = (stats: ResultScanStats) => {
|
|
607
607
|
const elapsed = Date.now() - stats.startedAt;
|
|
608
608
|
if (elapsed < SLOW_RESULT_SCAN_MS) return;
|
|
609
|
-
|
|
609
|
+
const resultScanLogging = deps.resultScanLogging ?? "activity";
|
|
610
|
+
if (resultScanLogging === "off") return;
|
|
610
611
|
// A scan that inspected and scheduled nothing is a quiet no-op (e.g. the
|
|
611
612
|
// healthy periodic rescan while no async runs are pending). Under
|
|
612
613
|
// "activity", skip it so empty scans do not burn context tokens in the
|
|
613
614
|
// session transcript.
|
|
614
|
-
if (
|
|
615
|
+
if (resultScanLogging === "activity" && stats.files === 0 && stats.scheduled === 0) return;
|
|
615
616
|
console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
|
|
616
617
|
};
|
|
617
618
|
const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
|
|
@@ -22,6 +22,7 @@ import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegis
|
|
|
22
22
|
import { readMissionBinding } from "../../missions/lifecycle.ts";
|
|
23
23
|
import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts";
|
|
24
24
|
import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
25
|
+
import { getExternalJobProvider } from "../../api/external-job-provider.ts";
|
|
25
26
|
|
|
26
27
|
interface RunStatusParams {
|
|
27
28
|
action?: string;
|
|
@@ -161,6 +162,14 @@ function formatSteeringSummary(input: { steering?: SteeringStatus }): string | u
|
|
|
161
162
|
return `${steering.requested} requested, ${steering.scheduled} scheduled, ${steering.pending} pending, ${steering.delivered} delivered, ${steering.failed} failed, ${steering.recovered} recovered${lateAcknowledgments ? `, ${lateAcknowledgments} late acknowledged` : ""}`;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
function externalJobFollowUpSupported(provider: string): boolean {
|
|
166
|
+
try {
|
|
167
|
+
return typeof getExternalJobProvider(provider)?.followUp === "function";
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
164
173
|
function rememberedForegroundChildOutput(child: ForegroundResumeRun["children"][number]): string {
|
|
165
174
|
const outputPath = child.artifactPaths?.outputPath ?? child.savedOutputPath;
|
|
166
175
|
if (outputPath && fs.existsSync(outputPath)) {
|
|
@@ -513,6 +522,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
513
522
|
&& Boolean(control.workflowSteeringDir && fs.existsSync(control.workflowSteeringDir))
|
|
514
523
|
&& (control.activeChildren?.size ?? 0) > 0)
|
|
515
524
|
: [];
|
|
525
|
+
let hasExternalJobFollowUpHint = false;
|
|
516
526
|
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
517
527
|
const stepActivityText = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
|
|
518
528
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
@@ -535,6 +545,10 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
535
545
|
if (step.externalJob?.state) lines.push(` Provider state: ${step.externalJob.state}`);
|
|
536
546
|
if (step.externalJob?.conversationUrl) lines.push(` Conversation: ${step.externalJob.conversationUrl}`);
|
|
537
547
|
if (step.externalJob?.resultArtifactPath) lines.push(` Result artifact: ${step.externalJob.resultArtifactPath}`);
|
|
548
|
+
if ((step.status === "complete" || step.status === "completed") && step.externalJob?.state === "completed" && step.externalJob.providerJobId && externalJobFollowUpSupported(step.runner.provider)) {
|
|
549
|
+
hasExternalJobFollowUpHint = true;
|
|
550
|
+
lines.push(` Follow-up: subagent({ action: "resume", id: "${status.runId}", index: ${index}, message: "..." })`);
|
|
551
|
+
}
|
|
538
552
|
}
|
|
539
553
|
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
|
|
540
554
|
const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
|
|
@@ -563,7 +577,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
563
577
|
if (status.state === "running" && !allExternal && status.mode !== "workflow") lines.push(`Steer running child: subagent({ action: "steer", id: "${status.runId}", message: "..." })`);
|
|
564
578
|
if (status.state !== "running") {
|
|
565
579
|
lines.push(allExternal
|
|
566
|
-
? "Resume: unavailable; external runners do not persist Pi sessions."
|
|
580
|
+
? hasExternalJobFollowUpHint ? "Resume: use the external-job follow-up hint above." : "Resume: unavailable; external runners do not persist Pi sessions."
|
|
567
581
|
: formatResumeGuidance(status.runId, status.steps ?? [], status.sessionFile, { stopped: status.state === "stopped" || status.stopped === true }));
|
|
568
582
|
}
|
|
569
583
|
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
@@ -97,6 +97,7 @@ interface ResultChildOutcome {
|
|
|
97
97
|
thinking?: string;
|
|
98
98
|
attemptedModels?: string[];
|
|
99
99
|
modelAttempts?: NonNullable<AsyncStatus["steps"]>[number]["modelAttempts"];
|
|
100
|
+
contextOverflow?: boolean;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
interface ResultRepairData {
|
|
@@ -163,6 +164,7 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
|
|
|
163
164
|
thinking,
|
|
164
165
|
attemptedModels: child?.attemptedModels ?? step.attemptedModels,
|
|
165
166
|
modelAttempts: child?.modelAttempts ?? step.modelAttempts,
|
|
167
|
+
contextOverflow: child?.contextOverflow ?? step.contextOverflow,
|
|
166
168
|
};
|
|
167
169
|
});
|
|
168
170
|
const terminalStatus: AsyncStatus = {
|
|
@@ -258,6 +260,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
258
260
|
model: step.model,
|
|
259
261
|
attemptedModels: step.attemptedModels,
|
|
260
262
|
modelAttempts: step.modelAttempts,
|
|
263
|
+
contextOverflow: step.contextOverflow,
|
|
261
264
|
sessionFile: step.sessionFile,
|
|
262
265
|
})),
|
|
263
266
|
exitCode: 1,
|