pi-subagents 0.43.0 → 0.45.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 +37 -0
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +13 -1
- package/src/extension/config.ts +28 -7
- package/src/extension/index.ts +3 -1
- package/src/extension/tool-description.ts +2 -2
- package/src/inspectors/herdr/actions.ts +1 -1
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/missions/lifecycle.ts +1 -1
- package/src/missions/workflow-state.ts +188 -15
- package/src/runs/background/async-execution.ts +12 -3
- package/src/runs/background/async-status.ts +6 -1
- package/src/runs/background/result-watcher.ts +5 -0
- package/src/runs/background/run-id-resolver.ts +71 -0
- package/src/runs/background/run-status.ts +3 -2
- package/src/runs/background/subagent-runner.ts +73 -53
- package/src/runs/background/subagent-wait.ts +12 -2
- package/src/runs/background/wait-completions.ts +112 -0
- package/src/runs/foreground/chain-clarify.ts +7 -3
- package/src/runs/foreground/execution.ts +25 -19
- package/src/runs/foreground/subagent-executor.ts +159 -31
- package/src/runs/shared/subagent-prompt-runtime.ts +6 -3
- package/src/shared/settings.ts +26 -9
- package/src/shared/types.ts +40 -0
- package/src/workflows/scripted-workflow.ts +6 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { DIRS, type SubagentState } from "../../shared/types.ts";
|
|
4
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
4
5
|
import { findAsyncRunPrefixMatches, type AsyncRunLocation } from "./async-resume.ts";
|
|
5
6
|
import { assertSafeNestedId, findNestedRunMatchesById, type NestedRoute, type NestedRunMatch, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
6
7
|
|
|
@@ -27,6 +28,70 @@ function exactAsyncLocation(id: string, asyncDirRoot: string, resultsDir: string
|
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
type AsyncRunMatch = { id: string; location: AsyncRunLocation };
|
|
32
|
+
|
|
33
|
+
type WorkflowResultIdentity = {
|
|
34
|
+
id?: string;
|
|
35
|
+
runId?: string;
|
|
36
|
+
toolCallId?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function readWorkflowResultIdentity(resultPath: string): WorkflowResultIdentity | undefined {
|
|
40
|
+
let parsed: unknown;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as unknown;
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
47
|
+
const record = parsed as Record<string, unknown>;
|
|
48
|
+
return {
|
|
49
|
+
...(typeof record.id === "string" ? { id: record.id } : {}),
|
|
50
|
+
...(typeof record.runId === "string" ? { runId: record.runId } : {}),
|
|
51
|
+
...(typeof record.toolCallId === "string" ? { toolCallId: record.toolCallId } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function directoryEntries(root: string): string[] {
|
|
56
|
+
try {
|
|
57
|
+
return fs.readdirSync(root);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resultPathFor(resultsDir: string, runId: string): string | null {
|
|
65
|
+
const resultPath = path.join(resultsDir, `${runId}.json`);
|
|
66
|
+
return fs.existsSync(resultPath) ? resultPath : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toolCallIdMatches(value: string | undefined, query: string, options: { prefix?: boolean }): boolean {
|
|
70
|
+
if (value === undefined) return false;
|
|
71
|
+
return options.prefix === true ? value.startsWith(query) : value === query;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toolCallIdAsyncLocations(toolCallId: string, asyncDirRoot: string, resultsDir: string, options: { prefix?: boolean } = {}): AsyncRunMatch[] {
|
|
75
|
+
const byId = new Map<string, AsyncRunLocation>();
|
|
76
|
+
for (const entry of directoryEntries(asyncDirRoot)) {
|
|
77
|
+
const asyncDir = path.join(asyncDirRoot, entry);
|
|
78
|
+
const status = readStatus(asyncDir);
|
|
79
|
+
if (!status || !toolCallIdMatches(status.toolCallId, toolCallId, options)) continue;
|
|
80
|
+
const runId = status.runId || entry;
|
|
81
|
+
byId.set(runId, { asyncDir, resultPath: resultPathFor(resultsDir, runId), resolvedId: runId });
|
|
82
|
+
}
|
|
83
|
+
for (const entry of directoryEntries(resultsDir)) {
|
|
84
|
+
if (!entry.endsWith(".json")) continue;
|
|
85
|
+
const resultPath = path.join(resultsDir, entry);
|
|
86
|
+
const identity = readWorkflowResultIdentity(resultPath);
|
|
87
|
+
if (!identity || !toolCallIdMatches(identity.toolCallId, toolCallId, options)) continue;
|
|
88
|
+
const runId = identity.runId ?? identity.id ?? entry.slice(0, -".json".length);
|
|
89
|
+
const asyncDir = path.join(asyncDirRoot, runId);
|
|
90
|
+
byId.set(runId, { asyncDir: fs.existsSync(asyncDir) ? asyncDir : null, resultPath, resolvedId: runId });
|
|
91
|
+
}
|
|
92
|
+
return [...byId.entries()].map(([id, location]) => ({ id, location }));
|
|
93
|
+
}
|
|
94
|
+
|
|
30
95
|
function foregroundIds(state: SubagentState | undefined): string[] {
|
|
31
96
|
if (!state) return [];
|
|
32
97
|
const remembered = state.currentSessionId
|
|
@@ -73,6 +138,9 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
73
138
|
if (hasExactForegroundId(deps.state, id)) return { kind: "foreground", id };
|
|
74
139
|
const exactAsync = exactAsyncLocation(id, asyncDirRoot, resultsDir);
|
|
75
140
|
if (exactAsync) return { kind: "async", id, location: exactAsync };
|
|
141
|
+
const exactToolCallIdMatches = toolCallIdAsyncLocations(id, asyncDirRoot, resultsDir);
|
|
142
|
+
if (exactToolCallIdMatches.length > 1) throw new Error(`Subagent tool-call id '${id}' is ambiguous across async runs. Use the returned asyncId instead.`);
|
|
143
|
+
if (exactToolCallIdMatches[0]) return { kind: "async", id: exactToolCallIdMatches[0].id, location: exactToolCallIdMatches[0].location };
|
|
76
144
|
const exactNested = findNestedRunMatchesById(id, nestedScope ? { scope: nestedScope } : {});
|
|
77
145
|
if (exactNested.length > 1) throw new Error(`Nested run id '${id}' is ambiguous across authorized registries. Provide the full id after stale registries are cleaned up.`);
|
|
78
146
|
if (exactNested[0]) return { kind: "nested", id, match: exactNested[0] };
|
|
@@ -84,6 +152,9 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
84
152
|
for (const match of asyncPrefixMatches(id, asyncDirRoot, resultsDir)) {
|
|
85
153
|
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
86
154
|
}
|
|
155
|
+
for (const match of toolCallIdAsyncLocations(id, asyncDirRoot, resultsDir, { prefix: true })) {
|
|
156
|
+
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
157
|
+
}
|
|
87
158
|
for (const match of findNestedRunMatchesById(id, nestedScope ? { prefix: true, scope: nestedScope } : { prefix: true })) {
|
|
88
159
|
matches.push({ kind: "nested", id: match.run.id, match });
|
|
89
160
|
}
|
|
@@ -382,6 +382,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
382
382
|
const workflowEmitPreview = status.workflow?.emits.length ? formatWorkflowJsonPreview(status.workflow.emits.at(-1), 240) : undefined;
|
|
383
383
|
const lines = [
|
|
384
384
|
`Run: ${status.runId}`,
|
|
385
|
+
status.toolCallId ? `Tool call: ${status.toolCallId}` : undefined,
|
|
385
386
|
missionId ? `Mission: ${missionId}` : undefined,
|
|
386
387
|
`State: ${status.state}`,
|
|
387
388
|
processTerminal ? `Process terminal: ${processTerminal.state}${processTerminal.reason ? ` (${processTerminal.reason})` : ""}` : undefined,
|
|
@@ -454,7 +455,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
454
455
|
if (resultPath) {
|
|
455
456
|
try {
|
|
456
457
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
457
|
-
const data = JSON.parse(raw) as { id?: string; runId?: 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 }> };
|
|
458
|
+
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 }> };
|
|
458
459
|
if (params.view === "transcript") {
|
|
459
460
|
try {
|
|
460
461
|
return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
|
|
@@ -479,7 +480,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
479
480
|
? "stopped"
|
|
480
481
|
: data.success ? "complete" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
481
482
|
const runId = data.runId ?? data.id ?? resolvedId;
|
|
482
|
-
const lines = [`Run: ${runId}`, `State: ${status}`, `Result: ${resultPath}`];
|
|
483
|
+
const lines = [`Run: ${runId}`, data.toolCallId ? `Tool call: ${data.toolCallId}` : undefined, `State: ${status}`, `Result: ${resultPath}`].filter((line): line is string => Boolean(line));
|
|
483
484
|
if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
|
|
484
485
|
const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
|
|
485
486
|
lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
|
|
70
70
|
import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
|
|
71
71
|
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
72
|
-
import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
|
|
72
|
+
import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
|
|
73
73
|
import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
|
|
74
74
|
import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
75
75
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
@@ -503,6 +503,8 @@ interface RunPiStreamingResult {
|
|
|
503
503
|
toolBudget?: ToolBudgetState;
|
|
504
504
|
toolBudgetBlocked?: boolean;
|
|
505
505
|
observedMutationAttempt?: boolean;
|
|
506
|
+
structuredOutputToolInvoked?: boolean;
|
|
507
|
+
structuredOutputMessageStartIndex?: number;
|
|
506
508
|
watchdog?: ChildWatchdogStateSnapshot;
|
|
507
509
|
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
|
|
508
510
|
processInstanceId: string;
|
|
@@ -568,6 +570,8 @@ function runPiStreaming(
|
|
|
568
570
|
let turnBudgetMessage: string | undefined;
|
|
569
571
|
let turnBudget: TurnBudgetState | undefined;
|
|
570
572
|
let observedMutationAttempt = false;
|
|
573
|
+
let structuredOutputToolInvoked = false;
|
|
574
|
+
let structuredOutputMessageStartIndex: number | undefined;
|
|
571
575
|
let toolCount = 0;
|
|
572
576
|
const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
|
|
573
577
|
let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
|
|
@@ -658,6 +662,10 @@ function runPiStreaming(
|
|
|
658
662
|
|
|
659
663
|
if (event.type === "tool_execution_start" && event.toolName) {
|
|
660
664
|
toolCount += 1;
|
|
665
|
+
if (event.toolName === "structured_output") {
|
|
666
|
+
structuredOutputToolInvoked = true;
|
|
667
|
+
structuredOutputMessageStartIndex = messages.length;
|
|
668
|
+
}
|
|
661
669
|
observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args);
|
|
662
670
|
const toolArgs = extractToolArgsPreview(event.args ?? {});
|
|
663
671
|
writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
|
|
@@ -923,6 +931,8 @@ function runPiStreaming(
|
|
|
923
931
|
turnBudgetExceeded,
|
|
924
932
|
wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
|
|
925
933
|
observedMutationAttempt,
|
|
934
|
+
structuredOutputToolInvoked,
|
|
935
|
+
structuredOutputMessageStartIndex,
|
|
926
936
|
watchdog: childWatchdogState,
|
|
927
937
|
processInstanceId,
|
|
928
938
|
processCloseObservedAt,
|
|
@@ -949,7 +959,7 @@ function runPiStreaming(
|
|
|
949
959
|
const stderr = stderrTail.text();
|
|
950
960
|
const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
|
|
951
961
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
952
|
-
resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId }));
|
|
962
|
+
resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, structuredOutputToolInvoked, structuredOutputMessageStartIndex, watchdog: childWatchdogState, processInstanceId }));
|
|
953
963
|
});
|
|
954
964
|
});
|
|
955
965
|
}
|
|
@@ -1444,31 +1454,42 @@ async function runSingleStep(
|
|
|
1444
1454
|
const runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
|
|
1445
1455
|
cleanupTempDir(tempDir);
|
|
1446
1456
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1457
|
+
let structuredOutput: unknown;
|
|
1458
|
+
let structuredError: string | undefined;
|
|
1459
|
+
let validatedStructuredOutput = false;
|
|
1460
|
+
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError) {
|
|
1461
|
+
if (!run.structuredOutputToolInvoked) {
|
|
1462
|
+
structuredError = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
|
|
1463
|
+
} else {
|
|
1464
|
+
const structured = await readStructuredOutput({
|
|
1465
|
+
schema: effectiveStructuredOutput.schema,
|
|
1466
|
+
schemaPath: effectiveStructuredOutput.schemaPath,
|
|
1467
|
+
outputPath: effectiveStructuredOutput.outputPath,
|
|
1468
|
+
});
|
|
1469
|
+
if (structured.error) structuredError = structured.error;
|
|
1470
|
+
else {
|
|
1471
|
+
structuredOutput = structured.value;
|
|
1472
|
+
validatedStructuredOutput = true;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const errorMessages = validatedStructuredOutput
|
|
1477
|
+
? run.messages.slice(run.structuredOutputMessageStartIndex ?? run.messages.length)
|
|
1478
|
+
: run.messages;
|
|
1479
|
+
const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError
|
|
1480
|
+
? detectSubagentError(errorMessages)
|
|
1481
|
+
: null;
|
|
1451
1482
|
const emptyOutputError = run.exitCode === 0
|
|
1452
1483
|
&& !run.error
|
|
1453
1484
|
&& !toolAvailabilityError
|
|
1485
|
+
&& !structuredError
|
|
1454
1486
|
&& !run.finalOutput.trim()
|
|
1455
|
-
&&
|
|
1487
|
+
&& !validatedStructuredOutput
|
|
1456
1488
|
&& (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
|
|
1457
1489
|
? "Subagent produced no output (possible model cold-start or empty response)."
|
|
1458
1490
|
: undefined;
|
|
1459
|
-
let structuredOutput: unknown;
|
|
1460
|
-
let structuredError: string | undefined;
|
|
1461
|
-
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError) {
|
|
1462
|
-
const structured = await readStructuredOutput({
|
|
1463
|
-
schema: effectiveStructuredOutput.schema,
|
|
1464
|
-
schemaPath: effectiveStructuredOutput.schemaPath,
|
|
1465
|
-
outputPath: effectiveStructuredOutput.outputPath,
|
|
1466
|
-
});
|
|
1467
|
-
if (structured.error) structuredError = structured.error;
|
|
1468
|
-
else structuredOutput = structured.value;
|
|
1469
|
-
}
|
|
1470
1491
|
const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
|
|
1471
|
-
const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
|
|
1492
|
+
const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
|
|
1472
1493
|
? evaluateCompletionMutationGuard(omitUndefinedProperties({
|
|
1473
1494
|
agent: step.agent,
|
|
1474
1495
|
task: taskForCompletionGuard,
|
|
@@ -4418,40 +4439,6 @@ async function runSubagent(
|
|
|
4418
4439
|
statusPayload.error = `Step failed: ${failedStep.agent}`;
|
|
4419
4440
|
}
|
|
4420
4441
|
}
|
|
4421
|
-
writeStatusPayload();
|
|
4422
|
-
appendJsonl(
|
|
4423
|
-
eventsPath,
|
|
4424
|
-
JSON.stringify({
|
|
4425
|
-
type: "subagent.run.completed",
|
|
4426
|
-
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
4427
|
-
ts: runEndedAt,
|
|
4428
|
-
runId: id,
|
|
4429
|
-
status: statusPayload.state,
|
|
4430
|
-
durationMs: runEndedAt - overallStartTime,
|
|
4431
|
-
totalTokens: statusPayload.totalTokens,
|
|
4432
|
-
totalCost: finalTotalCost,
|
|
4433
|
-
usageBudget: statusPayload.usageBudget,
|
|
4434
|
-
}),
|
|
4435
|
-
);
|
|
4436
|
-
writeRunLog(logPath, omitUndefinedProperties({
|
|
4437
|
-
id,
|
|
4438
|
-
mode: statusPayload.mode,
|
|
4439
|
-
cwd,
|
|
4440
|
-
startedAt: overallStartTime,
|
|
4441
|
-
endedAt: runEndedAt,
|
|
4442
|
-
steps: statusPayload.steps.map((step) => omitUndefinedProperties({
|
|
4443
|
-
agent: step.agent,
|
|
4444
|
-
status: step.status,
|
|
4445
|
-
durationMs: step.durationMs,
|
|
4446
|
-
})),
|
|
4447
|
-
summary,
|
|
4448
|
-
truncated,
|
|
4449
|
-
artifactsDir,
|
|
4450
|
-
sessionFile: effectiveSessionFile,
|
|
4451
|
-
shareUrl,
|
|
4452
|
-
shareError,
|
|
4453
|
-
}));
|
|
4454
|
-
|
|
4455
4442
|
try {
|
|
4456
4443
|
writeAtomicJson(resultPath, {
|
|
4457
4444
|
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -4551,6 +4538,38 @@ async function runSubagent(
|
|
|
4551
4538
|
} catch (err) {
|
|
4552
4539
|
console.error(`Failed to write result file ${resultPath}:`, err);
|
|
4553
4540
|
}
|
|
4541
|
+
appendJsonl(
|
|
4542
|
+
eventsPath,
|
|
4543
|
+
JSON.stringify({
|
|
4544
|
+
type: "subagent.run.completed",
|
|
4545
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
4546
|
+
ts: runEndedAt,
|
|
4547
|
+
runId: id,
|
|
4548
|
+
status: statusPayload.state,
|
|
4549
|
+
durationMs: runEndedAt - overallStartTime,
|
|
4550
|
+
totalTokens: statusPayload.totalTokens,
|
|
4551
|
+
totalCost: finalTotalCost,
|
|
4552
|
+
usageBudget: statusPayload.usageBudget,
|
|
4553
|
+
}),
|
|
4554
|
+
);
|
|
4555
|
+
writeRunLog(logPath, omitUndefinedProperties({
|
|
4556
|
+
id,
|
|
4557
|
+
mode: statusPayload.mode,
|
|
4558
|
+
cwd,
|
|
4559
|
+
startedAt: overallStartTime,
|
|
4560
|
+
endedAt: runEndedAt,
|
|
4561
|
+
steps: statusPayload.steps.map((step) => omitUndefinedProperties({
|
|
4562
|
+
agent: step.agent,
|
|
4563
|
+
status: step.status,
|
|
4564
|
+
durationMs: step.durationMs,
|
|
4565
|
+
})),
|
|
4566
|
+
summary,
|
|
4567
|
+
truncated,
|
|
4568
|
+
artifactsDir,
|
|
4569
|
+
sessionFile: effectiveSessionFile,
|
|
4570
|
+
shareUrl,
|
|
4571
|
+
shareError,
|
|
4572
|
+
}));
|
|
4554
4573
|
if (config.runnerProcessInstanceId) {
|
|
4555
4574
|
const writers: Record<string, Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }>> = {};
|
|
4556
4575
|
const expectedWriters: Record<string, number> = {};
|
|
@@ -4573,6 +4592,7 @@ async function runSubagent(
|
|
|
4573
4592
|
console.error(`Failed to write process-terminal candidate for '${id}':`, error);
|
|
4574
4593
|
}
|
|
4575
4594
|
}
|
|
4595
|
+
writeStatusPayload();
|
|
4576
4596
|
}
|
|
4577
4597
|
|
|
4578
4598
|
async function waitForStartupControl(
|
|
@@ -58,8 +58,10 @@ import {
|
|
|
58
58
|
type Details,
|
|
59
59
|
type ForegroundResumeRun,
|
|
60
60
|
type SubagentState,
|
|
61
|
+
type WaitCompletion,
|
|
61
62
|
} from "../../shared/types.ts";
|
|
62
63
|
import { formatDuration, shortenPath } from "../../shared/formatters.ts";
|
|
64
|
+
import { collectWaitCompletions } from "./wait-completions.ts";
|
|
63
65
|
export { WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, type ResolvedWaitToolConfig } from "./wait-config.ts";
|
|
64
66
|
|
|
65
67
|
/** States that mean a run is still in flight (not yet resolved). */
|
|
@@ -303,11 +305,15 @@ function summarizeTerminalRuns(runs: AsyncRunSummary[], providerFinishedCount =
|
|
|
303
305
|
return parts.join(", ");
|
|
304
306
|
}
|
|
305
307
|
|
|
306
|
-
function result(text: string, isError = false): AgentToolResult<Details> {
|
|
308
|
+
function result(text: string, isError = false, completions?: WaitCompletion[]): AgentToolResult<Details> {
|
|
307
309
|
return {
|
|
308
310
|
content: [{ type: "text", text }],
|
|
309
311
|
...(isError ? { isError: true } : {}),
|
|
310
|
-
details: {
|
|
312
|
+
details: {
|
|
313
|
+
mode: "management",
|
|
314
|
+
results: [],
|
|
315
|
+
...(completions && completions.length > 0 ? { completions } : {}),
|
|
316
|
+
},
|
|
311
317
|
};
|
|
312
318
|
}
|
|
313
319
|
|
|
@@ -597,6 +603,7 @@ export async function waitForSubagents(
|
|
|
597
603
|
let terminalSummary: string;
|
|
598
604
|
let finishedAsyncCount: number;
|
|
599
605
|
let failedAsyncCount: number;
|
|
606
|
+
let completions: WaitCompletion[] | undefined;
|
|
600
607
|
const activeProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
601
608
|
const providerFinishedCount = [...initialProviderIds].filter((id) => !activeProviderIds.has(id)).length;
|
|
602
609
|
try {
|
|
@@ -605,6 +612,7 @@ export async function waitForSubagents(
|
|
|
605
612
|
finishedAsyncCount = terminal.length;
|
|
606
613
|
failedAsyncCount = terminal.filter((run) => run.state === "failed").length;
|
|
607
614
|
terminalSummary = summarizeTerminalRuns(terminal, providerFinishedCount);
|
|
615
|
+
completions = collectWaitCompletions(terminal, deps.state, deps.resultsDir ?? DIRS.results);
|
|
608
616
|
} catch (error) {
|
|
609
617
|
return result(error instanceof Error ? error.message : String(error), true);
|
|
610
618
|
}
|
|
@@ -631,6 +639,7 @@ export async function waitForSubagents(
|
|
|
631
639
|
return result(
|
|
632
640
|
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
633
641
|
deps.failOnFailedRuns === true && failedAsyncCount > 0,
|
|
642
|
+
completions,
|
|
634
643
|
);
|
|
635
644
|
}
|
|
636
645
|
|
|
@@ -647,5 +656,6 @@ export async function waitForSubagents(
|
|
|
647
656
|
return result(
|
|
648
657
|
`Waited ${elapsed}; ${progress}.${outcome}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
649
658
|
deps.failOnFailedRuns === true && failedAsyncCount > 0,
|
|
659
|
+
completions,
|
|
650
660
|
);
|
|
651
661
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ArtifactPaths, SubagentState, WaitCompletion, WaitCompletionChild } from "../../shared/types.ts";
|
|
4
|
+
import type { AsyncRunSummary } from "./async-status.ts";
|
|
5
|
+
|
|
6
|
+
function asNonEmptyString(value: unknown): string | undefined {
|
|
7
|
+
return typeof value === "string" && value ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function errorCode(error: unknown): string | undefined {
|
|
11
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
12
|
+
? (error as NodeJS.ErrnoException).code
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function errorMessage(error: unknown): string {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Project a terminal result payload into the slim shape that is safe to surface in
|
|
22
|
+
* tool_result details: run identity, per-child outcome, and the artifact trail.
|
|
23
|
+
* Output text is deliberately excluded — it already travels in the tool result
|
|
24
|
+
* content, and duplicating it in details would double the payload for every wait.
|
|
25
|
+
*/
|
|
26
|
+
export function toWaitCompletion(data: Record<string, unknown>, runId: string): WaitCompletion {
|
|
27
|
+
const results = Array.isArray(data.results)
|
|
28
|
+
? data.results.flatMap((entry): WaitCompletionChild[] => {
|
|
29
|
+
if (entry === null || typeof entry !== "object") return [];
|
|
30
|
+
const child = entry as Record<string, unknown>;
|
|
31
|
+
const outputState = child.outputState === "present" || child.outputState === "absent" || child.outputState === "unknown"
|
|
32
|
+
? child.outputState
|
|
33
|
+
: undefined;
|
|
34
|
+
const artifactPaths = child.artifactPaths !== null && typeof child.artifactPaths === "object"
|
|
35
|
+
? (child.artifactPaths as Partial<ArtifactPaths>)
|
|
36
|
+
: undefined;
|
|
37
|
+
const agent = asNonEmptyString(child.agent);
|
|
38
|
+
const childRunId = asNonEmptyString(child.runId);
|
|
39
|
+
const error = asNonEmptyString(child.error);
|
|
40
|
+
const model = asNonEmptyString(child.model);
|
|
41
|
+
return [{
|
|
42
|
+
...(agent ? { agent } : {}),
|
|
43
|
+
...(childRunId ? { runId: childRunId } : {}),
|
|
44
|
+
...(typeof child.success === "boolean" ? { success: child.success } : {}),
|
|
45
|
+
...(outputState ? { outputState } : {}),
|
|
46
|
+
...(error ? { error } : {}),
|
|
47
|
+
...(model ? { model } : {}),
|
|
48
|
+
...(artifactPaths ? { artifactPaths } : {}),
|
|
49
|
+
}];
|
|
50
|
+
})
|
|
51
|
+
: undefined;
|
|
52
|
+
const agent = asNonEmptyString(data.agent);
|
|
53
|
+
const mode = asNonEmptyString(data.mode);
|
|
54
|
+
const state = asNonEmptyString(data.state);
|
|
55
|
+
return {
|
|
56
|
+
runId,
|
|
57
|
+
...(agent ? { agent } : {}),
|
|
58
|
+
...(mode ? { mode } : {}),
|
|
59
|
+
...(state ? { state } : {}),
|
|
60
|
+
...(typeof data.success === "boolean" ? { success: data.success } : {}),
|
|
61
|
+
...(results && results.length > 0 ? { results } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Record a consumed terminal payload for later surfacing by subagent_wait, pruning
|
|
67
|
+
* stale entries with the same TTL that dedupes completion notifications. The result
|
|
68
|
+
* file is deleted after delivery, so this record is the only in-process source once
|
|
69
|
+
* the watcher has consumed it.
|
|
70
|
+
*/
|
|
71
|
+
export function recordWaitCompletion(state: SubagentState, runId: string, data: Record<string, unknown>, now: number, ttlMs: number): void {
|
|
72
|
+
const store = state.completedResults ??= new Map();
|
|
73
|
+
for (const [key, entry] of store) {
|
|
74
|
+
if (now - entry.seenAt > ttlMs) store.delete(key);
|
|
75
|
+
}
|
|
76
|
+
store.set(runId, { seenAt: now, completion: toWaitCompletion(data, runId) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Terminal payloads for the runs a wait covered: the watcher's in-memory record
|
|
81
|
+
* first, then the not-yet-consumed result file. Result files are written atomically,
|
|
82
|
+
* so a direct read never observes a torn write; the read is deliberately read-only —
|
|
83
|
+
* the watcher owns notification and cleanup.
|
|
84
|
+
*/
|
|
85
|
+
export function collectWaitCompletions(terminal: AsyncRunSummary[], state: SubagentState, resultsDir: string): WaitCompletion[] | undefined {
|
|
86
|
+
if (terminal.length === 0) return undefined;
|
|
87
|
+
const completions: WaitCompletion[] = [];
|
|
88
|
+
for (const run of terminal) {
|
|
89
|
+
const recorded = state.completedResults?.get(run.id);
|
|
90
|
+
if (recorded) {
|
|
91
|
+
completions.push(recorded.completion);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const resultPath = path.join(resultsDir, `${run.id}.json`);
|
|
95
|
+
try {
|
|
96
|
+
const raw = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as Record<string, unknown>;
|
|
97
|
+
completions.push(toWaitCompletion(raw, run.id));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (errorCode(error) !== "ENOENT") {
|
|
100
|
+
throw new Error(`Failed to read subagent result '${resultPath}': ${errorMessage(error)}`, {
|
|
101
|
+
cause: error instanceof Error ? error : undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// The watcher may have consumed the file between the store check and the
|
|
105
|
+
// read; its record is authoritative when present, otherwise the payload
|
|
106
|
+
// is gone and the text summary remains the only surface for this run.
|
|
107
|
+
const late = state.completedResults?.get(run.id);
|
|
108
|
+
if (late) completions.push(late.completion);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return completions.length > 0 ? completions : undefined;
|
|
112
|
+
}
|
|
@@ -532,7 +532,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
532
532
|
buffer = template.split("\n")[0] ?? "";
|
|
533
533
|
} else if (mode === "output") {
|
|
534
534
|
const behavior = this.getEffectiveBehavior(this.selectedStep);
|
|
535
|
-
buffer = behavior.output ===
|
|
535
|
+
buffer = typeof behavior.output === "string" ? behavior.output : "";
|
|
536
536
|
} else if (mode === "reads") {
|
|
537
537
|
const behavior = this.getEffectiveBehavior(this.selectedStep);
|
|
538
538
|
buffer = behavior.reads === false ? "" : (behavior.reads?.join(", ") || "");
|
|
@@ -1165,7 +1165,9 @@ export class ChainClarifyComponent implements Component {
|
|
|
1165
1165
|
|
|
1166
1166
|
const writesValue = behavior.output === false
|
|
1167
1167
|
? th.fg("dim", "(disabled)")
|
|
1168
|
-
: (behavior.output
|
|
1168
|
+
: (typeof behavior.output === "string" && behavior.output
|
|
1169
|
+
? behavior.output
|
|
1170
|
+
: th.fg("dim", "(none)"));
|
|
1169
1171
|
const writesLabel = th.fg("dim", "writes: ");
|
|
1170
1172
|
lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
|
|
1171
1173
|
|
|
@@ -1296,7 +1298,9 @@ export class ChainClarifyComponent implements Component {
|
|
|
1296
1298
|
|
|
1297
1299
|
const writesValue = behavior.output === false
|
|
1298
1300
|
? th.fg("dim", "(disabled)")
|
|
1299
|
-
: (behavior.output
|
|
1301
|
+
: (typeof behavior.output === "string" && behavior.output
|
|
1302
|
+
? behavior.output
|
|
1303
|
+
: th.fg("dim", "(none)"));
|
|
1300
1304
|
const writesLabel = th.fg("dim", "writes: ");
|
|
1301
1305
|
lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
|
|
1302
1306
|
|
|
@@ -459,6 +459,7 @@ async function runSingleAttempt(
|
|
|
459
459
|
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
|
|
460
460
|
let observedMutationAttempt = false;
|
|
461
461
|
let structuredOutputToolInvoked = false;
|
|
462
|
+
let structuredOutputMessageStartIndex: number | undefined;
|
|
462
463
|
|
|
463
464
|
const exitCode = await new Promise<number>((resolve) => {
|
|
464
465
|
const spawnSpec = getPiSpawnCommand(args);
|
|
@@ -874,7 +875,10 @@ async function runSingleAttempt(
|
|
|
874
875
|
const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
|
|
875
876
|
? evt.args as Record<string, unknown>
|
|
876
877
|
: {};
|
|
877
|
-
if (options.structuredOutput && evt.toolName === "structured_output")
|
|
878
|
+
if (options.structuredOutput && evt.toolName === "structured_output") {
|
|
879
|
+
structuredOutputToolInvoked = true;
|
|
880
|
+
structuredOutputMessageStartIndex = result.messages?.length ?? 0;
|
|
881
|
+
}
|
|
878
882
|
if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
|
|
879
883
|
intercomStarted = true;
|
|
880
884
|
}
|
|
@@ -1183,24 +1187,7 @@ async function runSingleAttempt(
|
|
|
1183
1187
|
if (result.error && result.exitCode === 0) {
|
|
1184
1188
|
result.exitCode = 1;
|
|
1185
1189
|
}
|
|
1186
|
-
|
|
1187
|
-
const messages = result.messages ?? [];
|
|
1188
|
-
const finalText = getFinalOutput(messages);
|
|
1189
|
-
const missingStructuredOutput = options.structuredOutput
|
|
1190
|
-
? !existsSync(options.structuredOutput.outputPath)
|
|
1191
|
-
: false;
|
|
1192
|
-
const errInfo = detectSubagentError(messages);
|
|
1193
|
-
const missingOutput = !finalText?.trim() && (!options.structuredOutput || missingStructuredOutput);
|
|
1194
|
-
if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
|
|
1195
|
-
result.exitCode = 1;
|
|
1196
|
-
result.error = "Subagent produced no output (possible model cold-start or empty response).";
|
|
1197
|
-
} else if (errInfo.hasError) {
|
|
1198
|
-
result.exitCode = errInfo.exitCode ?? 1;
|
|
1199
|
-
result.error = errInfo.details
|
|
1200
|
-
? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
|
|
1201
|
-
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1190
|
+
let validatedStructuredOutput = false;
|
|
1204
1191
|
if (options.structuredOutput && result.exitCode === 0 && !result.error) {
|
|
1205
1192
|
result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
|
|
1206
1193
|
result.structuredOutputPath = options.structuredOutput.outputPath;
|
|
@@ -1220,9 +1207,28 @@ async function runSingleAttempt(
|
|
|
1220
1207
|
result.structuredOutputFailed = true;
|
|
1221
1208
|
} else {
|
|
1222
1209
|
result.structuredOutput = structured.value;
|
|
1210
|
+
validatedStructuredOutput = true;
|
|
1223
1211
|
}
|
|
1224
1212
|
}
|
|
1225
1213
|
}
|
|
1214
|
+
if (result.exitCode === 0 && !result.error) {
|
|
1215
|
+
const messages = result.messages ?? [];
|
|
1216
|
+
const finalText = getFinalOutput(messages);
|
|
1217
|
+
const errorMessages = validatedStructuredOutput
|
|
1218
|
+
? messages.slice(structuredOutputMessageStartIndex ?? messages.length)
|
|
1219
|
+
: messages;
|
|
1220
|
+
const errInfo = detectSubagentError(errorMessages);
|
|
1221
|
+
const missingOutput = !finalText?.trim() && !validatedStructuredOutput;
|
|
1222
|
+
if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
|
|
1223
|
+
result.exitCode = 1;
|
|
1224
|
+
result.error = "Subagent produced no output (possible model cold-start or empty response).";
|
|
1225
|
+
} else if (errInfo.hasError) {
|
|
1226
|
+
result.exitCode = errInfo.exitCode ?? 1;
|
|
1227
|
+
result.error = errInfo.details
|
|
1228
|
+
? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
|
|
1229
|
+
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1226
1232
|
|
|
1227
1233
|
progress.status = result.exitCode === 0 ? "completed" : "failed";
|
|
1228
1234
|
progress.durationMs = Date.now() - startTime;
|