pi-subagents 0.42.1 → 0.44.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 +50 -0
- package/README.md +3 -5
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -2
- package/skills/pi-subagents/references/constraints-and-recipes.md +35 -32
- package/skills/pi-subagents/references/execution-controls.md +30 -44
- package/skills/pi-subagents/references/management-authoring-rpc.md +20 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +16 -49
- package/src/agents/agent-refinements.ts +624 -0
- package/src/agents/agents.ts +0 -2
- package/src/agents/proactive-skills.ts +1 -1
- package/src/api/delegation.ts +1 -2
- package/src/extension/config.ts +28 -7
- package/src/extension/control-notices.ts +2 -2
- package/src/extension/fanout-child.ts +46 -22
- package/src/extension/index.ts +32 -13
- package/src/extension/public-execution.ts +71 -0
- package/src/extension/rpc.ts +7 -8
- package/src/extension/schemas.ts +14 -14
- package/src/extension/tool-description.ts +14 -14
- package/src/missions/actions.ts +47 -11
- package/src/missions/goal-driver.ts +162 -0
- package/src/missions/lifecycle.ts +44 -12
- package/src/missions/store.ts +68 -3
- package/src/missions/types.ts +25 -3
- package/src/missions/workflow-state.ts +250 -0
- package/src/profiles/profiles.ts +1 -3
- package/src/runs/background/async-execution.ts +5 -1
- package/src/runs/background/async-job-tracker.ts +2 -17
- package/src/runs/background/async-status.ts +2 -0
- package/src/runs/background/control-channel.ts +50 -6
- package/src/runs/background/retained-children.ts +68 -0
- package/src/runs/background/run-id-resolver.ts +71 -0
- package/src/runs/background/run-status.ts +3 -2
- package/src/runs/background/scheduled-runs.ts +17 -13
- package/src/runs/background/steering.ts +7 -5
- package/src/runs/background/subagent-runner.ts +50 -23
- package/src/runs/foreground/async-steering-action.ts +36 -10
- package/src/runs/foreground/chain-clarify.ts +7 -3
- package/src/runs/foreground/execution.ts +29 -19
- package/src/runs/foreground/subagent-executor.ts +282 -77
- package/src/runs/shared/acceptance.ts +154 -9
- package/src/runs/shared/subagent-prompt-runtime.ts +97 -25
- package/src/shared/settings.ts +10 -6
- package/src/shared/types.ts +22 -2
- package/src/slash/delegation-adapters.ts +0 -13
- package/src/slash/prompt-template-bridge.ts +14 -12
- package/src/slash/prompt-workflows.ts +10 -4
- package/src/slash/slash-bridge.ts +8 -6
- package/src/slash/slash-commands.ts +24 -9
- package/src/slash/slash-live-state.ts +4 -2
- package/src/tui/fleet-status.ts +8 -4
- package/src/tui/fleet.ts +15 -5
- package/src/tui/render.ts +10 -29
- package/src/workflows/scripted-workflow.ts +91 -6
- package/agents/context-builder.md +0 -46
- package/agents/planner.md +0 -56
- package/prompts/parallel-context-build.md +0 -55
- package/prompts/parallel-handoff-plan.md +0 -61
|
@@ -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" }));
|
|
@@ -9,6 +9,7 @@ import type { AsyncStatus, Details, ExtensionConfig } from "../../shared/types.t
|
|
|
9
9
|
import type { SubagentParamsLike } from "../foreground/subagent-executor.ts";
|
|
10
10
|
import { validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
11
11
|
import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
12
|
+
import { previewSimpleWorkflowRun } from "../../workflows/scripted-workflow.ts";
|
|
12
13
|
|
|
13
14
|
export const SCHEDULED_RUN_ACTIONS = [
|
|
14
15
|
"schedule.create",
|
|
@@ -34,7 +35,7 @@ export type ScheduleRunState = "running" | "skipped" | "missed" | "completed" |
|
|
|
34
35
|
export type ScheduleTrigger =
|
|
35
36
|
| { kind: "once"; at: string; nextRunAt?: string }
|
|
36
37
|
| { kind: "interval"; every: string; everyMs: number; anchorAt: string; nextRunAt: string };
|
|
37
|
-
export type ScheduleTarget = { workflowScript: string }
|
|
38
|
+
export type ScheduleTarget = { workflowScript: string };
|
|
38
39
|
|
|
39
40
|
export interface ScheduleRecord {
|
|
40
41
|
schemaVersion: 1;
|
|
@@ -188,6 +189,14 @@ function readJson(file: string, label: string): unknown {
|
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
function parseScheduleTarget(value: unknown, file: string): ScheduleTarget {
|
|
193
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Schedule record '${file}' has invalid trigger or target.`);
|
|
194
|
+
const target = value as { workflowScript?: unknown; agent?: unknown; task?: unknown };
|
|
195
|
+
if (typeof target.workflowScript === "string" && target.workflowScript.trim()) return { workflowScript: target.workflowScript.trim() };
|
|
196
|
+
if (target.agent !== undefined || target.task !== undefined) throw new Error(`Schedule record '${file}' uses a removed legacy agent target; recreate it with target.workflowScript.`);
|
|
197
|
+
throw new Error(`Schedule record '${file}' requires a workflowScript target.`);
|
|
198
|
+
}
|
|
199
|
+
|
|
191
200
|
function parseSchedule(value: unknown, file: string): ScheduleRecord {
|
|
192
201
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Schedule record '${file}' must be a JSON object.`);
|
|
193
202
|
const record = value as Partial<ScheduleRecord>;
|
|
@@ -200,10 +209,7 @@ function parseSchedule(value: unknown, file: string): ScheduleRecord {
|
|
|
200
209
|
} else if (record.trigger.kind === "interval") {
|
|
201
210
|
if (typeof record.trigger.every !== "string" || typeof record.trigger.everyMs !== "number" || typeof record.trigger.anchorAt !== "string" || typeof record.trigger.nextRunAt !== "string") throw new Error(`Schedule record '${file}' has an invalid interval trigger.`);
|
|
202
211
|
} else throw new Error(`Schedule record '${file}' has an unsupported trigger.`);
|
|
203
|
-
|
|
204
|
-
const agent = "agent" in record.target && typeof record.target.agent === "string";
|
|
205
|
-
if (workflow === agent) throw new Error(`Schedule record '${file}' has an invalid target.`);
|
|
206
|
-
return record as ScheduleRecord;
|
|
212
|
+
return { ...record, target: parseScheduleTarget(record.target, file) } as ScheduleRecord;
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
class ScheduleStore {
|
|
@@ -304,27 +310,25 @@ function textResult(text: string, schedules?: ScheduleRecord[], runs?: ScheduleR
|
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
function targetLabel(target: ScheduleTarget): string {
|
|
307
|
-
|
|
313
|
+
const preview = previewSimpleWorkflowRun(target.workflowScript);
|
|
314
|
+
return preview?.agent ? `workflowScript -> agent ${preview.agent}` : "workflowScript (dynamic)";
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
function sanitizeTarget(params: SubagentParamsLike): { target?: ScheduleTarget; error?: string } {
|
|
311
|
-
if (params.tasks || params.chain) return { error: "Recurring schedules
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (hasWorkflow === hasAgent) return { error: "schedule.create requires exactly one target: workflowScript or agent with optional task." };
|
|
318
|
+
if (params.tasks || params.chain) return { error: "Recurring schedules require workflowScript; legacy tasks and chain inputs are unsupported." };
|
|
319
|
+
if (params.agent !== undefined || params.task !== undefined) return { error: "schedule.create requires workflowScript. Use workflowScript: \"return runs.run('main', { agent, task })\"." };
|
|
320
|
+
if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) return { error: "schedule.create requires a non-empty workflowScript." };
|
|
315
321
|
if (params.context === "fork") return { error: "Scheduled runs require fresh context." };
|
|
316
322
|
if (params.async === false) return { error: "Scheduled runs are always async." };
|
|
317
|
-
if (params.clarify === true) return { error: "Scheduled runs cannot open clarify UI." };
|
|
318
323
|
const acceptanceErrors = validateExecutionAcceptance(params as Parameters<typeof validateExecutionAcceptance>[0]);
|
|
319
324
|
if (acceptanceErrors.length) return { error: acceptanceErrors.join(" ") };
|
|
320
|
-
return
|
|
325
|
+
return { target: { workflowScript: params.workflowScript.trim() } };
|
|
321
326
|
}
|
|
322
327
|
|
|
323
328
|
function executionParams(schedule: ScheduleRecord): SubagentParamsLike {
|
|
324
329
|
return {
|
|
325
330
|
...schedule.target,
|
|
326
331
|
async: true,
|
|
327
|
-
clarify: false,
|
|
328
332
|
context: "fresh",
|
|
329
333
|
cwd: schedule.cwd,
|
|
330
334
|
mission: false,
|
|
@@ -47,7 +47,7 @@ export function recordSteeringRequest(
|
|
|
47
47
|
|
|
48
48
|
function incrementStateCount(status: SteeringStatus, state: SteeringTargetState): void {
|
|
49
49
|
if (state === "scheduled") status.scheduled++;
|
|
50
|
-
else if (state === "routed") status.pending++;
|
|
50
|
+
else if (state === "routed" || state === "queued") status.pending++;
|
|
51
51
|
else if (state === "delivered" || state === "late") status.delivered++;
|
|
52
52
|
else if (state === "failed") status.failed++;
|
|
53
53
|
else if (state === "recovered") status.recovered++;
|
|
@@ -81,7 +81,7 @@ export function updateSteeringTarget(
|
|
|
81
81
|
if (fields.replacementRunId) target.replacementRunId = fields.replacementRunId;
|
|
82
82
|
return target;
|
|
83
83
|
}
|
|
84
|
-
if (target.state === "routed" && state !== "routed") status.pending = Math.max(0, status.pending - 1);
|
|
84
|
+
if ((target.state === "routed" || target.state === "queued") && state !== "routed" && state !== "queued") status.pending = Math.max(0, status.pending - 1);
|
|
85
85
|
target.state = state;
|
|
86
86
|
if (state === "routed") target.routedAt = now;
|
|
87
87
|
if (state === "delivered") {
|
|
@@ -123,16 +123,18 @@ export function actionResultFromSteeringStatus(status: SteeringStatus, sourceRun
|
|
|
123
123
|
else if (states.length > 0 && states.every((candidate) => candidate === "failed" || candidate === "late")) state = "failed";
|
|
124
124
|
else if (states.some((candidate) => candidate === "failed" || candidate === "late") && states.some((candidate) => candidate !== "failed" && candidate !== "late")) state = "partial";
|
|
125
125
|
const effectiveReplacementRunId = replacementRunId ?? request.targets.find((target) => target.replacementRunId)?.replacementRunId;
|
|
126
|
-
|
|
126
|
+
const deliveryStatus = states.length > 0 && states.every((candidate) => candidate === "delivered" || candidate === "recovered") ? "delivered" as const : "queued" as const;
|
|
127
|
+
return { requestId, state, deliveryStatus, sourceRunId, ...(effectiveReplacementRunId ? { replacementRunId: effectiveReplacementRunId } : {}), targets };
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
export function steeringActionIsTerminal(result: SteerActionResult | undefined): boolean {
|
|
130
|
-
return result?.
|
|
131
|
+
return (result?.targets.length ?? 0) > 0 && result?.targets.every((target) => target.state === "queued" || target.state === "delivered") === true
|
|
132
|
+
|| result?.state === "delivered" || result?.state === "scheduled" || result?.state === "partial" || result?.state === "recovered" || result?.state === "failed";
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
export function terminalSteeringNoticeState(status: SteeringStatus, requestId: string): "failed" | "partial" | undefined {
|
|
134
136
|
const request = status.recent.find((candidate) => candidate.id === requestId);
|
|
135
|
-
if (!request || request.targets.some((target) => target.state === "routed" || target.state === "scheduled")) return undefined;
|
|
137
|
+
if (!request || request.targets.some((target) => target.state === "routed" || target.state === "queued" || target.state === "scheduled")) return undefined;
|
|
136
138
|
const hasSuccess = request.targets.some((target) => target.state === "delivered" || target.state === "recovered");
|
|
137
139
|
const hasFailure = request.targets.some((target) => target.state === "failed" || target.state === "late");
|
|
138
140
|
if (hasSuccess && hasFailure) return "partial";
|
|
@@ -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,
|
|
@@ -1635,6 +1656,8 @@ async function runSingleStep(
|
|
|
1635
1656
|
signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
|
|
1636
1657
|
abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
|
|
1637
1658
|
reportOptional: isAgentContractV1(step.agentContract),
|
|
1659
|
+
artifactsDir: ctx.artifactsDir,
|
|
1660
|
+
runId: ctx.id,
|
|
1638
1661
|
}))
|
|
1639
1662
|
: undefined;
|
|
1640
1663
|
const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
|
|
@@ -2611,7 +2634,10 @@ async function runSubagent(
|
|
|
2611
2634
|
const now = Date.now();
|
|
2612
2635
|
if (ack.state === "delivered") {
|
|
2613
2636
|
updateSteeringLifecycleTarget(ack.requestId, ack.index, late ? "late" : "delivered", now, omitUndefinedProperties({ reason: late ? "acknowledged after recovery commit" : undefined }));
|
|
2614
|
-
emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { late, message: ack.message });
|
|
2637
|
+
emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { late, deliveryStatus: "delivered", message: ack.message });
|
|
2638
|
+
} else if (ack.state === "queued") {
|
|
2639
|
+
updateSteeringLifecycleTarget(ack.requestId, ack.index, "queued", now);
|
|
2640
|
+
emitSteeringEvent("subagent.steer.queued", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { deliveryStatus: "queued", message: ack.message });
|
|
2615
2641
|
} else {
|
|
2616
2642
|
markSteeringAttention(ack.index);
|
|
2617
2643
|
updateSteeringLifecycleTarget(ack.requestId, ack.index, "failed", now, { reason: ack.message });
|
|
@@ -4370,11 +4396,12 @@ async function runSubagent(
|
|
|
4370
4396
|
for (const request of steeringLifecycle.recent) {
|
|
4371
4397
|
let changed = false;
|
|
4372
4398
|
for (const target of request.targets) {
|
|
4373
|
-
if (target.state !== "scheduled" && target.state !== "routed") continue;
|
|
4399
|
+
if (target.state !== "scheduled" && target.state !== "routed" && target.state !== "queued") continue;
|
|
4374
4400
|
changed = true;
|
|
4375
|
-
|
|
4401
|
+
const reason = target.state === "queued" ? "run ended before queued follow-up delivery" : "child terminated before steering delivery";
|
|
4402
|
+
updateSteeringLifecycleTarget(request.id, target.index, "failed", Date.now(), { reason });
|
|
4376
4403
|
markSteeringAttention(target.index);
|
|
4377
|
-
emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message:
|
|
4404
|
+
emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: reason }, target.index, { reason });
|
|
4378
4405
|
}
|
|
4379
4406
|
if (changed) emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: child terminated before delivery.`);
|
|
4380
4407
|
}
|
|
@@ -5,15 +5,16 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
5
5
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
6
6
|
import type { AsyncStatus, Details, SubagentState, ToolBudgetConfig, TurnBudgetConfig } from "../../shared/types.ts";
|
|
7
7
|
import { readStatus } from "../../shared/utils.ts";
|
|
8
|
-
import { consumeSteerAcks, deliverInterruptRequest, requestAsyncSteer } from "../background/control-channel.ts";
|
|
8
|
+
import { consumeSteerAcks, deliverInterruptRequest, queueRevivalBrief, requestAsyncSteer, type SteerDeliveryMode, type SteerRequest } from "../background/control-channel.ts";
|
|
9
9
|
import { resolveAsyncResumeTarget } from "../background/async-resume.ts";
|
|
10
10
|
import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
|
|
11
|
-
import { actionResultFromSteeringStatus, claimSteeringRecovery, remainingSteeringRecoveryLimits, updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
|
|
11
|
+
import { actionResultFromSteeringStatus, claimSteeringRecovery, createSteeringStatus, recordSteeringRequest, remainingSteeringRecoveryLimits, updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
|
|
12
12
|
|
|
13
13
|
export async function steerAsyncRun(input: {
|
|
14
14
|
state: SubagentState;
|
|
15
15
|
runId: string;
|
|
16
16
|
message: string;
|
|
17
|
+
mode?: SteerDeliveryMode;
|
|
17
18
|
index?: number;
|
|
18
19
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
19
20
|
location: { asyncDir: string | null };
|
|
@@ -41,14 +42,38 @@ export async function steerAsyncRun(input: {
|
|
|
41
42
|
details: { mode: "management", results: [] },
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
|
-
if (!status
|
|
45
|
+
if (!status) {
|
|
45
46
|
return {
|
|
46
|
-
content: [{ type: "text", text: `Async run '${input.runId}'
|
|
47
|
+
content: [{ type: "text", text: `Async run '${input.runId}' was not found.` }],
|
|
47
48
|
isError: true,
|
|
48
49
|
details: { mode: "management", results: [] },
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
const steps = status.steps ?? [];
|
|
53
|
+
if (status.state !== "running" && status.state !== "queued") {
|
|
54
|
+
const retained = input.mode === "follow_up" && status.state === "complete" && Boolean(status.parentWorkflowRunId) && steps.length === 1 && (steps[0]?.status === "complete" || steps[0]?.status === "completed") && Boolean(steps[0]?.sessionFile ?? status.sessionFile);
|
|
55
|
+
if (!retained) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: `Async run '${input.runId}' is not running or queued and cannot be steered.` }],
|
|
58
|
+
isError: true,
|
|
59
|
+
details: { mode: "management", results: [] },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (input.index !== undefined && input.index !== 0) {
|
|
63
|
+
return { content: [{ type: "text", text: `Retained async run '${status.runId}' has one child. Index ${input.index} is out of range.` }], isError: true, details: { mode: "management", results: [] } };
|
|
64
|
+
}
|
|
65
|
+
const request: SteerRequest = { type: "steer", id: randomUUID(), ts: Date.now(), message: input.message.trim(), mode: "follow_up", targetIndex: 0, source: "steer-action" };
|
|
66
|
+
try {
|
|
67
|
+
queueRevivalBrief(asyncDir, request);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { content: [{ type: "text", text: `Failed to queue retained follow-up for async run ${status.runId}: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "management", results: [] } };
|
|
70
|
+
}
|
|
71
|
+
status.steering ??= createSteeringStatus();
|
|
72
|
+
recordSteeringRequest(status.steering, { id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets: [{ index: 0, state: "scheduled" }] });
|
|
73
|
+
writeAtomicJson(path.join(asyncDir, "status.json"), status);
|
|
74
|
+
const queued = { requestId: request.id, state: "scheduled" as const, deliveryStatus: "queued" as const, sourceRunId: status.runId, targets: [{ index: 0, state: "scheduled" as const }] };
|
|
75
|
+
return { content: [{ type: "text", text: `Follow-up queued for the next resume of retained async run ${status.runId} (request ${request.id}).` }], details: { mode: "management", results: [], steering: queued } };
|
|
76
|
+
}
|
|
52
77
|
if (input.index !== undefined) {
|
|
53
78
|
if (input.index < 0 || input.index >= steps.length) {
|
|
54
79
|
return {
|
|
@@ -92,6 +117,7 @@ export async function steerAsyncRun(input: {
|
|
|
92
117
|
try {
|
|
93
118
|
requestPath = requestAsyncSteer(asyncDir, {
|
|
94
119
|
message: input.message,
|
|
120
|
+
mode: input.mode,
|
|
95
121
|
...(effectiveTargetIndex !== undefined ? { targetIndex: effectiveTargetIndex } : { targetIndexes }),
|
|
96
122
|
source: "steer-action",
|
|
97
123
|
id: requestId,
|
|
@@ -108,11 +134,11 @@ export async function steerAsyncRun(input: {
|
|
|
108
134
|
if (tracked) tracked.updatedAt = Date.now();
|
|
109
135
|
const targets = targetIndexes.map((index) => ({ index, state: status.steps?.[index]?.status === "pending" ? "scheduled" as const : "pending" as const }));
|
|
110
136
|
if (targets.every((target) => target.state === "scheduled")) {
|
|
111
|
-
const scheduled = { requestId, state: "scheduled" as const, sourceRunId: status.runId, targets };
|
|
137
|
+
const scheduled = { requestId, state: "scheduled" as const, deliveryStatus: "queued" as const, sourceRunId: status.runId, targets };
|
|
112
138
|
return { content: [{ type: "text", text: `Steering scheduled for async run ${status.runId} (request ${requestId}).` }], details: { mode: "management", results: [], steering: scheduled } };
|
|
113
139
|
}
|
|
114
140
|
const waited = await waitForSteeringAction({ asyncDir, sourceRunId: status.runId, requestId, timeoutMs: input.ackTimeoutMs ?? 3_000, signal: input.signal });
|
|
115
|
-
const result = waited ?? { requestId, state: "pending" as const, sourceRunId: status.runId, targets };
|
|
141
|
+
const result = waited ?? { requestId, state: "pending" as const, deliveryStatus: "queued" as const, sourceRunId: status.runId, targets };
|
|
116
142
|
if (input.signal?.aborted) {
|
|
117
143
|
return { content: [{ type: "text", text: `Steering pending for async run ${status.runId} (request ${requestId}); caller aborted before recovery.` }], details: { mode: "management", results: [], steering: result } };
|
|
118
144
|
}
|
|
@@ -122,7 +148,7 @@ export async function steerAsyncRun(input: {
|
|
|
122
148
|
return { content: [{ type: "text", text: `Steering delivered for async run ${status.runId} (request ${requestId}).` }], details: { mode: "management", results: [], steering: finalResult } };
|
|
123
149
|
}
|
|
124
150
|
const running = (finalStatus?.steps ?? status.steps ?? []).filter((step) => step.status === "running");
|
|
125
|
-
const recoveryAllowed = status.mode === "single" && status.isNested !== true && running.length === 1 && Boolean(finalStatus?.steering) && (input.index === undefined || input.index === 0);
|
|
151
|
+
const recoveryAllowed = (input.mode ?? "steer") === "steer" && status.mode === "single" && status.isNested !== true && running.length === 1 && Boolean(finalStatus?.steering) && (input.index === undefined || input.index === 0);
|
|
126
152
|
if (recoveryAllowed && finalResult?.state !== "scheduled" && input.recover) {
|
|
127
153
|
const appendSteeringNotice = (state: "failed" | "recovered", message: string): void => {
|
|
128
154
|
try {
|
|
@@ -203,7 +229,7 @@ export async function steerAsyncRun(input: {
|
|
|
203
229
|
}
|
|
204
230
|
const recovered = sourceStatus?.steering ? actionResultFromSteeringStatus(sourceStatus.steering, status.runId, requestId, revived.details.asyncId) : undefined;
|
|
205
231
|
appendSteeringNotice("recovered", `Steering recovered for run ${status.runId}; replacement ${revived.details.asyncId} launched.`);
|
|
206
|
-
return { content: [{ type: "text", text: `Steering recovered for async run ${status.runId}; replacement ${revived.details.asyncId} launched after the source paused.` }], details: { mode: "management", results: [], steering: recovered ?? { requestId, state: "recovered", sourceRunId: status.runId, replacementRunId: revived.details.asyncId, targets: [{ index: input.index ?? 0, state: "recovered" }] } } };
|
|
232
|
+
return { content: [{ type: "text", text: `Steering recovered for async run ${status.runId}; replacement ${revived.details.asyncId} launched after the source paused.` }], details: { mode: "management", results: [], steering: recovered ?? { requestId, state: "recovered", sourceRunId: status.runId, replacementRunId: revived.details.asyncId, deliveryStatus: "delivered", targets: [{ index: input.index ?? 0, state: "recovered" }] } } };
|
|
207
233
|
} catch (error) {
|
|
208
234
|
const reason = error instanceof Error ? error.message : String(error);
|
|
209
235
|
const failedStatus = readStatus(asyncDir);
|
|
@@ -217,10 +243,10 @@ export async function steerAsyncRun(input: {
|
|
|
217
243
|
}
|
|
218
244
|
const failed = failedStatus?.steering ? actionResultFromSteeringStatus(failedStatus.steering, status.runId, requestId) : undefined;
|
|
219
245
|
appendSteeringNotice("failed", `Steering failed for run ${status.runId}: ${reason}`);
|
|
220
|
-
return { content: [{ type: "text", text: `Steering failed for async run ${status.runId} (request ${requestId}): ${reason}` }], isError: true, details: { mode: "management", results: [], steering: failed ?? { requestId, state: "failed", sourceRunId: status.runId, targets: [{ index: input.index ?? 0, state: "failed", reason }] } } };
|
|
246
|
+
return { content: [{ type: "text", text: `Steering failed for async run ${status.runId} (request ${requestId}): ${reason}` }], isError: true, details: { mode: "management", results: [], steering: failed ?? { requestId, state: "failed", sourceRunId: status.runId, deliveryStatus: "queued", targets: [{ index: input.index ?? 0, state: "failed", reason }] } } };
|
|
221
247
|
}
|
|
222
248
|
}
|
|
223
|
-
const stateText = result.state === "
|
|
249
|
+
const stateText = result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : result.deliveryStatus === "queued" ? "queued" : result.state === "delivered" ? "delivered" : result.state === "scheduled" ? "scheduled" : result.state === "recovered" ? "recovered" : "pending";
|
|
224
250
|
const isError = result.state === "failed" || result.state === "partial";
|
|
225
251
|
return {
|
|
226
252
|
content: [{ type: "text", text: `Steering ${stateText} for async run ${status.runId} (request ${requestId}).` }],
|
|
@@ -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
|
|
|
@@ -7,6 +7,7 @@ import { existsSync, unlinkSync } from "node:fs";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
10
|
+
import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
|
|
10
11
|
import {
|
|
11
12
|
ensureArtifactsDir,
|
|
12
13
|
formatOutputArtifactContent,
|
|
@@ -458,6 +459,7 @@ async function runSingleAttempt(
|
|
|
458
459
|
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
|
|
459
460
|
let observedMutationAttempt = false;
|
|
460
461
|
let structuredOutputToolInvoked = false;
|
|
462
|
+
let structuredOutputMessageStartIndex: number | undefined;
|
|
461
463
|
|
|
462
464
|
const exitCode = await new Promise<number>((resolve) => {
|
|
463
465
|
const spawnSpec = getPiSpawnCommand(args);
|
|
@@ -873,7 +875,10 @@ async function runSingleAttempt(
|
|
|
873
875
|
const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
|
|
874
876
|
? evt.args as Record<string, unknown>
|
|
875
877
|
: {};
|
|
876
|
-
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
|
+
}
|
|
877
882
|
if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
|
|
878
883
|
intercomStarted = true;
|
|
879
884
|
}
|
|
@@ -1182,24 +1187,7 @@ async function runSingleAttempt(
|
|
|
1182
1187
|
if (result.error && result.exitCode === 0) {
|
|
1183
1188
|
result.exitCode = 1;
|
|
1184
1189
|
}
|
|
1185
|
-
|
|
1186
|
-
const messages = result.messages ?? [];
|
|
1187
|
-
const finalText = getFinalOutput(messages);
|
|
1188
|
-
const missingStructuredOutput = options.structuredOutput
|
|
1189
|
-
? !existsSync(options.structuredOutput.outputPath)
|
|
1190
|
-
: false;
|
|
1191
|
-
const errInfo = detectSubagentError(messages);
|
|
1192
|
-
const missingOutput = !finalText?.trim() && (!options.structuredOutput || missingStructuredOutput);
|
|
1193
|
-
if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
|
|
1194
|
-
result.exitCode = 1;
|
|
1195
|
-
result.error = "Subagent produced no output (possible model cold-start or empty response).";
|
|
1196
|
-
} else if (errInfo.hasError) {
|
|
1197
|
-
result.exitCode = errInfo.exitCode ?? 1;
|
|
1198
|
-
result.error = errInfo.details
|
|
1199
|
-
? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
|
|
1200
|
-
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1190
|
+
let validatedStructuredOutput = false;
|
|
1203
1191
|
if (options.structuredOutput && result.exitCode === 0 && !result.error) {
|
|
1204
1192
|
result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
|
|
1205
1193
|
result.structuredOutputPath = options.structuredOutput.outputPath;
|
|
@@ -1219,9 +1207,28 @@ async function runSingleAttempt(
|
|
|
1219
1207
|
result.structuredOutputFailed = true;
|
|
1220
1208
|
} else {
|
|
1221
1209
|
result.structuredOutput = structured.value;
|
|
1210
|
+
validatedStructuredOutput = true;
|
|
1222
1211
|
}
|
|
1223
1212
|
}
|
|
1224
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
|
+
}
|
|
1225
1232
|
|
|
1226
1233
|
progress.status = result.exitCode === 0 ? "completed" : "failed";
|
|
1227
1234
|
progress.durationMs = Date.now() - startTime;
|
|
@@ -1438,6 +1445,7 @@ async function runSyncCompletion(
|
|
|
1438
1445
|
if (memoryInjection) {
|
|
1439
1446
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
|
|
1440
1447
|
}
|
|
1448
|
+
systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: skillCwd, agentName });
|
|
1441
1449
|
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, options.outputPath, agent);
|
|
1442
1450
|
|
|
1443
1451
|
const candidates = buildModelCandidates(
|
|
@@ -1699,6 +1707,8 @@ async function runSyncCompletion(
|
|
|
1699
1707
|
: undefined,
|
|
1700
1708
|
cwd: options.cwd ?? runtimeCwd,
|
|
1701
1709
|
reportOptional: isAgentContractV1(options.agentContract),
|
|
1710
|
+
artifactsDir: options.artifactsDir,
|
|
1711
|
+
runId: options.runId,
|
|
1702
1712
|
});
|
|
1703
1713
|
}
|
|
1704
1714
|
} catch (error) {
|