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.
Files changed (59) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -5
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +2 -2
  5. package/skills/pi-subagents/references/constraints-and-recipes.md +35 -32
  6. package/skills/pi-subagents/references/execution-controls.md +30 -44
  7. package/skills/pi-subagents/references/management-authoring-rpc.md +20 -3
  8. package/skills/pi-subagents/references/prompting-and-roles.md +16 -49
  9. package/src/agents/agent-refinements.ts +624 -0
  10. package/src/agents/agents.ts +0 -2
  11. package/src/agents/proactive-skills.ts +1 -1
  12. package/src/api/delegation.ts +1 -2
  13. package/src/extension/config.ts +28 -7
  14. package/src/extension/control-notices.ts +2 -2
  15. package/src/extension/fanout-child.ts +46 -22
  16. package/src/extension/index.ts +32 -13
  17. package/src/extension/public-execution.ts +71 -0
  18. package/src/extension/rpc.ts +7 -8
  19. package/src/extension/schemas.ts +14 -14
  20. package/src/extension/tool-description.ts +14 -14
  21. package/src/missions/actions.ts +47 -11
  22. package/src/missions/goal-driver.ts +162 -0
  23. package/src/missions/lifecycle.ts +44 -12
  24. package/src/missions/store.ts +68 -3
  25. package/src/missions/types.ts +25 -3
  26. package/src/missions/workflow-state.ts +250 -0
  27. package/src/profiles/profiles.ts +1 -3
  28. package/src/runs/background/async-execution.ts +5 -1
  29. package/src/runs/background/async-job-tracker.ts +2 -17
  30. package/src/runs/background/async-status.ts +2 -0
  31. package/src/runs/background/control-channel.ts +50 -6
  32. package/src/runs/background/retained-children.ts +68 -0
  33. package/src/runs/background/run-id-resolver.ts +71 -0
  34. package/src/runs/background/run-status.ts +3 -2
  35. package/src/runs/background/scheduled-runs.ts +17 -13
  36. package/src/runs/background/steering.ts +7 -5
  37. package/src/runs/background/subagent-runner.ts +50 -23
  38. package/src/runs/foreground/async-steering-action.ts +36 -10
  39. package/src/runs/foreground/chain-clarify.ts +7 -3
  40. package/src/runs/foreground/execution.ts +29 -19
  41. package/src/runs/foreground/subagent-executor.ts +282 -77
  42. package/src/runs/shared/acceptance.ts +154 -9
  43. package/src/runs/shared/subagent-prompt-runtime.ts +97 -25
  44. package/src/shared/settings.ts +10 -6
  45. package/src/shared/types.ts +22 -2
  46. package/src/slash/delegation-adapters.ts +0 -13
  47. package/src/slash/prompt-template-bridge.ts +14 -12
  48. package/src/slash/prompt-workflows.ts +10 -4
  49. package/src/slash/slash-bridge.ts +8 -6
  50. package/src/slash/slash-commands.ts +24 -9
  51. package/src/slash/slash-live-state.ts +4 -2
  52. package/src/tui/fleet-status.ts +8 -4
  53. package/src/tui/fleet.ts +15 -5
  54. package/src/tui/render.ts +10 -29
  55. package/src/workflows/scripted-workflow.ts +91 -6
  56. package/agents/context-builder.md +0 -46
  57. package/agents/planner.md +0 -56
  58. package/prompts/parallel-context-build.md +0 -55
  59. 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 } | { agent: string; task?: 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
- const workflow = "workflowScript" in record.target && typeof record.target.workflowScript === "string";
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
- return "workflowScript" in target ? "workflowScript" : `agent ${target.agent}`;
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 support workflowScript or one agent/task target, not legacy tasks or chain inputs." };
312
- const hasWorkflow = typeof params.workflowScript === "string" && params.workflowScript.trim().length > 0;
313
- const hasAgent = typeof params.agent === "string" && params.agent.trim().length > 0;
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 hasWorkflow ? { target: { workflowScript: params.workflowScript!.trim() } } : { target: { agent: params.agent!.trim(), ...(params.task === undefined ? {} : { task: params.task }) } };
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
- return { requestId, state, sourceRunId, ...(effectiveReplacementRunId ? { replacementRunId: effectiveReplacementRunId } : {}), targets };
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?.state === "delivered" || result?.state === "scheduled" || result?.state === "partial" || result?.state === "recovered" || result?.state === "failed";
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
- const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError ? detectSubagentError(run.messages) : null;
1448
- const missingStructuredOutput = effectiveStructuredOutput
1449
- ? !fs.existsSync(effectiveStructuredOutput.outputPath)
1450
- : false;
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
- && (!effectiveStructuredOutput || missingStructuredOutput)
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
- updateSteeringLifecycleTarget(request.id, target.index, "failed", Date.now(), { reason: "child terminated before steering delivery" });
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: "child terminated before steering delivery" }, target.index, { reason: "child terminated before steering delivery" });
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 || (status.state !== "running" && status.state !== "queued")) {
45
+ if (!status) {
45
46
  return {
46
- content: [{ type: "text", text: `Async run '${input.runId}' is not running or queued and cannot be steered.` }],
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 === "delivered" ? "delivered" : result.state === "scheduled" ? "scheduled" : result.state === "recovered" ? "recovered" : result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : "pending";
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 === false ? "" : (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 || th.fg("dim", "(none)"));
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 || th.fg("dim", "(none)"));
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") structuredOutputToolInvoked = true;
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
- if (result.exitCode === 0 && !result.error) {
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) {