pi-subagents 0.55.0 → 0.57.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 (97) hide show
  1. package/CHANGELOG.md +81 -3
  2. package/agents/claude-code-writer.md +15 -0
  3. package/agents/claude-code.md +15 -0
  4. package/agents/codex-exec-writer.md +15 -0
  5. package/agents/codex-exec.md +15 -0
  6. package/agents/cursor-agent-writer.md +14 -0
  7. package/agents/cursor-agent.md +14 -0
  8. package/docs/agents.md +111 -15
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +2 -2
  11. package/docs/models.md +6 -0
  12. package/docs/observability.md +8 -7
  13. package/docs/tool-reference.md +11 -1
  14. package/docs/workflows.md +20 -0
  15. package/package.json +1 -1
  16. package/skills/pi-subagents/references/execution-controls.md +1 -1
  17. package/src/agents/agent-management.ts +13 -5
  18. package/src/agents/agent-refinements.ts +4 -4
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +242 -49
  21. package/src/agents/builtin-names.ts +6 -0
  22. package/src/agents/runtime-agent-registry.ts +8 -2
  23. package/src/api/preflight.ts +22 -3
  24. package/src/extension/config.ts +70 -0
  25. package/src/extension/doctor.ts +3 -3
  26. package/src/extension/index.ts +18 -4
  27. package/src/extension/public-execution.ts +29 -13
  28. package/src/extension/rpc.ts +20 -5
  29. package/src/extension/schemas.ts +9 -3
  30. package/src/extension/tool-description.ts +11 -9
  31. package/src/inspectors/herdr/actions.ts +2 -1
  32. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  33. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  34. package/src/integrations/herdr-status.ts +46 -3
  35. package/src/runs/background/active-async-capacity.ts +77 -10
  36. package/src/runs/background/async-execution.ts +148 -27
  37. package/src/runs/background/async-job-tracker.ts +5 -0
  38. package/src/runs/background/async-resume.ts +3 -0
  39. package/src/runs/background/async-retention.ts +20 -3
  40. package/src/runs/background/async-status.ts +5 -0
  41. package/src/runs/background/chain-root-attachment.ts +15 -1
  42. package/src/runs/background/fleet-view.ts +16 -10
  43. package/src/runs/background/inspect-rpc.ts +8 -8
  44. package/src/runs/background/result-files.ts +27 -14
  45. package/src/runs/background/result-watcher.ts +9 -5
  46. package/src/runs/background/run-status.ts +33 -6
  47. package/src/runs/background/scheduled-runs.ts +7 -1
  48. package/src/runs/background/subagent-runner.ts +254 -49
  49. package/src/runs/background/wait-completions.ts +4 -0
  50. package/src/runs/foreground/execution.ts +107 -16
  51. package/src/runs/foreground/foreground-control.ts +6 -0
  52. package/src/runs/foreground/foreground-history.ts +25 -3
  53. package/src/runs/foreground/subagent-executor.ts +324 -84
  54. package/src/runs/foreground/workflow-detach-reconcile.ts +31 -7
  55. package/src/runs/shared/acceptance.ts +10 -5
  56. package/src/runs/shared/agent-contract.ts +1 -1
  57. package/src/runs/shared/child-protocol.ts +21 -7
  58. package/src/runs/shared/claude-code-adapter.ts +129 -0
  59. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  60. package/src/runs/shared/completion-guard.ts +3 -2
  61. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  62. package/src/runs/shared/dynamic-fanout.ts +1 -1
  63. package/src/runs/shared/extension-bindings.ts +78 -0
  64. package/src/runs/shared/external-cli-contract.ts +167 -0
  65. package/src/runs/shared/external-cli-preflight.ts +122 -0
  66. package/src/runs/shared/external-cli-runner.ts +349 -54
  67. package/src/runs/shared/fast-mode-extension.ts +10 -0
  68. package/src/runs/shared/model-exclusions.ts +59 -8
  69. package/src/runs/shared/model-fallback.ts +25 -8
  70. package/src/runs/shared/mutation-evidence.ts +150 -0
  71. package/src/runs/shared/nested-events.ts +3 -1
  72. package/src/runs/shared/nested-render.ts +2 -2
  73. package/src/runs/shared/parallel-utils.ts +3 -0
  74. package/src/runs/shared/pi-args.ts +47 -0
  75. package/src/runs/shared/process-signal.ts +13 -0
  76. package/src/runs/shared/run-history.ts +21 -1
  77. package/src/runs/shared/structured-output.ts +18 -4
  78. package/src/runs/shared/subagent-prompt-runtime.ts +9 -5
  79. package/src/shared/fork-context.ts +21 -0
  80. package/src/shared/formatters.ts +7 -1
  81. package/src/shared/launch-contract.ts +6 -0
  82. package/src/shared/pruned-fork.ts +450 -0
  83. package/src/shared/session-file-trust.ts +19 -0
  84. package/src/shared/session-tokens.ts +14 -3
  85. package/src/shared/settings.ts +16 -3
  86. package/src/shared/shortcuts.ts +17 -0
  87. package/src/shared/types.ts +166 -9
  88. package/src/shared/workflow-child-permit.ts +116 -0
  89. package/src/slash/delegation-adapters.ts +0 -1
  90. package/src/slash/slash-commands.ts +8 -6
  91. package/src/tui/fleet-status.ts +27 -10
  92. package/src/tui/fleet-transcript.ts +11 -5
  93. package/src/tui/fleet.ts +28 -13
  94. package/src/tui/render.ts +47 -21
  95. package/src/workflows/scripted-workflow.ts +298 -30
  96. package/src/workflows/workflow-child-summary.ts +117 -0
  97. package/src/workflows/workflow-receipt.ts +119 -4
@@ -3,6 +3,7 @@ import type { ArtifactPaths, SubagentState, WaitCompletion, WaitCompletionChild
3
3
  import type { AsyncRunSummary } from "./async-status.ts";
4
4
  import { readCompletionReplay, writeCompletionReplay } from "./completion-replay.ts";
5
5
  import { fallbackResultPayloadPathForSessionRun, resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
6
+ import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
6
7
 
7
8
  function asNonEmptyString(value: unknown): string | undefined {
8
9
  return typeof value === "string" && value ? value : undefined;
@@ -60,6 +61,8 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
60
61
  const agent = asNonEmptyString(data.agent);
61
62
  const mode = asNonEmptyString(data.mode);
62
63
  const state = asNonEmptyString(data.state);
64
+ const workflowChildren = parseWorkflowChildSummary(data.workflowChildren);
65
+ if (workflowChildren && workflowChildren.workflowRunId !== runId) throw new Error("workflowChildren.workflowRunId does not match its completion run id.");
63
66
  return {
64
67
  runId,
65
68
  ...(agent ? { agent } : {}),
@@ -67,6 +70,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
67
70
  ...(state ? { state } : {}),
68
71
  ...(typeof data.success === "boolean" ? { success: data.success } : {}),
69
72
  ...(results && results.length > 0 ? { results } : {}),
73
+ ...(workflowChildren ? { workflowChildren } : {}),
70
74
  };
71
75
  }
72
76
 
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
6
6
  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
- import type { AgentConfig } from "../../agents/agents.ts";
9
+ import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig } from "../../agents/agents.ts";
10
10
  import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
11
11
  import { alignForkedSessionCwd } from "../../shared/fork-context.ts";
12
12
  import {
@@ -67,16 +67,17 @@ import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledge
67
67
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
68
68
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
69
69
  import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
70
- import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
71
- import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
70
+ import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
71
+ import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
72
72
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
73
+ import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
73
74
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
74
75
  import {
75
76
  buildModelCandidates,
76
77
  formatSubagentModelVerificationError,
77
78
  formatModelAttemptNote,
78
79
  isContextOverflow,
79
- isRetryableModelFailure,
80
+ isRetryableModelFailureAttempt,
80
81
  recordRetryableModelFailure,
81
82
  } from "../shared/model-fallback.ts";
82
83
  import {
@@ -105,7 +106,8 @@ import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudget
105
106
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
106
107
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
107
108
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
108
- import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
109
+ import { consumeWorkflowChildPermit } from "../../shared/workflow-child-permit.ts";
110
+ import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
109
111
  import {
110
112
  acceptChildWatchdogEvent,
111
113
  childWatchdogIsActive,
@@ -368,6 +370,8 @@ async function runSingleAttempt(
368
370
  steerCapabilityPath: options.steerCapabilityPath,
369
371
  steerAckDir: options.steerAckDir,
370
372
  structuredOutput: options.structuredOutput,
373
+ fast: options.fast ?? agent.fast,
374
+ modelCandidates: shared.modelCandidates,
371
375
  toolBudget: options.toolBudget,
372
376
  allowZeroToolBudget: options.allowZeroToolBudget,
373
377
  permissionRules,
@@ -376,6 +380,7 @@ async function runSingleAttempt(
376
380
  waitToolEnabled: options.waitToolEnabled,
377
381
  capabilityCeiling: options.capabilityCeiling,
378
382
  thinkingCeiling: options.thinkingCeiling,
383
+ extensionBindings: options.extensionBindings,
379
384
  });
380
385
  if (!shared.launchWarnings.emitted && warnings.length > 0) {
381
386
  for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
@@ -391,6 +396,9 @@ async function runSingleAttempt(
391
396
  cwd: options.cwd ?? runtimeCwd,
392
397
  requireReadTool: Boolean(shared.resolvedSkillNames?.length),
393
398
  structuredOutput: Boolean(options.structuredOutput),
399
+ fast: options.fast ?? agent.fast,
400
+ model: modelArg,
401
+ modelCandidates: shared.modelCandidates,
394
402
  capabilityCeiling: options.capabilityCeiling,
395
403
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
396
404
  agentName: agent.name,
@@ -432,6 +440,7 @@ async function runSingleAttempt(
432
440
  task: shared.originalTask ?? task,
433
441
  ...(modelArg ? { model: modelArg } : {}),
434
442
  modelCandidates: shared.modelCandidates,
443
+ ...((options.fast ?? agent.fast) !== undefined ? { fast: options.fast ?? agent.fast } : {}),
435
444
  ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
436
445
  ...(options.thinkingCeiling ? { thinkingCeiling: options.thinkingCeiling } : {}),
437
446
  systemPrompt: effectiveSystemPrompt,
@@ -445,6 +454,7 @@ async function runSingleAttempt(
445
454
  ...(options.outputPath ? { outputPath: options.outputPath } : {}),
446
455
  outputMode: options.outputMode ?? "inline",
447
456
  ...(options.structuredOutput ? { structuredOutputSchema: options.structuredOutput.schema } : {}),
457
+ ...(options.extensionBindings ? { extensionBindings: options.extensionBindings } : {}),
448
458
  });
449
459
  const result: SingleResult = withRunContext({
450
460
  index: options.index ?? 0,
@@ -524,13 +534,34 @@ async function runSingleAttempt(
524
534
  return result;
525
535
  }
526
536
  const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
537
+ const mutationSnapshot = snapshotTrackedMutations(options.cwd ?? runtimeCwd);
527
538
  let observedMutationAttempt = false;
528
539
  let structuredOutputToolInvoked = false;
529
540
  let structuredOutputMessageStartIndex: number | undefined;
530
541
  let toolAvailabilityError: string | undefined;
542
+ let abortedBySignal = options.signal?.aborted === true;
531
543
 
544
+ const spawnSpec = getPiSpawnCommand(args);
545
+ if (options.workflowChildPermitLaunch) {
546
+ const permitError = consumeWorkflowChildPermit(options.workflowChildPermitLaunch.permit, {
547
+ workflowRunId: options.workflowChildPermitLaunch.workflowRunId,
548
+ childKey: options.workflowChildPermitLaunch.childKey,
549
+ agent: agent.name,
550
+ launchContractDigest,
551
+ context: options.context ?? "fresh",
552
+ runner: "pi",
553
+ });
554
+ if (permitError) {
555
+ cleanupTempDir(tempDir);
556
+ result.exitCode = 1;
557
+ result.error = permitError;
558
+ result.finalOutput = permitError;
559
+ progress.status = "failed";
560
+ progress.error = permitError;
561
+ return result;
562
+ }
563
+ }
532
564
  const exitCode = await new Promise<number>((resolve) => {
533
- const spawnSpec = getPiSpawnCommand(args);
534
565
  const proc = spawn(spawnSpec.command, spawnSpec.args, {
535
566
  cwd: options.cwd ?? runtimeCwd,
536
567
  env: spawnEnv,
@@ -697,6 +728,7 @@ async function runSingleAttempt(
697
728
  }
698
729
  if (action === "start-drain") startFinalDrain();
699
730
  };
731
+ const childLifecycleState: ChildLifecycleState = { compactionRetryActive: false };
700
732
 
701
733
  const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
702
734
  if (!options.allowIntercomDetach || processClosed) return;
@@ -984,8 +1016,9 @@ async function runSingleAttempt(
984
1016
  }
985
1017
  shared.transcriptWriter?.writeChildEvent(evt);
986
1018
  shared.orcaProgressTab?.event(evt);
987
- if (evt.type === "agent_settled") agentSettledReceived = true;
988
- applyChildLifecycle(projectChildLifecycle(evt));
1019
+ const lifecycleAction = projectChildLifecycle(evt, false, childLifecycleState);
1020
+ if (evt.type === "agent_settled" && lifecycleAction === "start-drain") agentSettledReceived = true;
1021
+ applyChildLifecycle(lifecycleAction);
989
1022
 
990
1023
  if (isChildWatchdogStatusEvent(evt)) {
991
1024
  if (!childWatchdog) return;
@@ -1068,6 +1101,7 @@ async function runSingleAttempt(
1068
1101
  updateTurnBudget(result.usage.turns, terminalAssistantStop || terminalStructuredOutputCall, hasToolCall || Boolean(progress.currentTool));
1069
1102
  const u = evt.message.usage;
1070
1103
  if (u) {
1104
+ const window = (u.input || 0) + (u.cacheRead || 0);
1071
1105
  result.usage.input += u.input || 0;
1072
1106
  result.usage.output += u.output || 0;
1073
1107
  result.usage.cacheRead += u.cacheRead || 0;
@@ -1076,6 +1110,8 @@ async function runSingleAttempt(
1076
1110
  progress.tokens = result.usage.input + result.usage.output;
1077
1111
  progress.inputTokens = result.usage.input;
1078
1112
  progress.outputTokens = result.usage.output;
1113
+ progress.window = window;
1114
+ progress.windowPeak = Math.max(progress.windowPeak ?? 0, window);
1079
1115
  }
1080
1116
  if (evt.message.model) {
1081
1117
  progress.model = evt.message.model;
@@ -1092,7 +1128,11 @@ async function runSingleAttempt(
1092
1128
  if (terminalAssistantStop) {
1093
1129
  if (!evt.message.errorMessage && assistantText.trim()) assistantError = undefined;
1094
1130
  cleanTerminalAssistantStopReceived ||= !evt.message.errorMessage;
1095
- applyChildLifecycle(projectChildLifecycle(evt, true));
1131
+ clearAllToolTimeouts();
1132
+ activeToolCalls.clear();
1133
+ activeToolKeysByName.clear();
1134
+ refreshCurrentTool();
1135
+ applyChildLifecycle(projectChildLifecycle(evt, true, childLifecycleState));
1096
1136
  }
1097
1137
  }
1098
1138
  updateActivityState(now);
@@ -1359,6 +1399,7 @@ async function runSingleAttempt(
1359
1399
  if (options.signal) {
1360
1400
  const kill = () => {
1361
1401
  if (processClosed || lifecycleFinished) return;
1402
+ abortedBySignal = true;
1362
1403
  proc.kill("SIGTERM");
1363
1404
  setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
1364
1405
  };
@@ -1421,6 +1462,22 @@ async function runSingleAttempt(
1421
1462
  };
1422
1463
  return result;
1423
1464
  }
1465
+ if (progress.currentTool
1466
+ && isOrdinaryToolForMidToolExit(progress.currentTool)
1467
+ && !abortedBySignal
1468
+ && !result.timedOut
1469
+ && !result.stopped
1470
+ && !result.turnBudgetExceeded
1471
+ && !result.protocolError
1472
+ && !toolAvailabilityError) {
1473
+ const processExitCode = result.exitCode;
1474
+ result.exitCode = 1;
1475
+ result.error = formatMidToolExitError({
1476
+ toolName: progress.currentTool,
1477
+ exitCode: processExitCode,
1478
+ processSignal: result.processSignal,
1479
+ });
1480
+ }
1424
1481
  result.error = formatSubagentExtensionConflictError(result.error, {
1425
1482
  agent: agent.name,
1426
1483
  ambientExtensionsEnabled: !launchResolvedExtensions.disableAmbientExtensions,
@@ -1448,6 +1505,9 @@ async function runSingleAttempt(
1448
1505
  result.structuredOutputFailed = true;
1449
1506
  } else {
1450
1507
  result.structuredOutput = structured.value;
1508
+ const acceptanceReport = readStructuredOutputAcceptanceReport(options.structuredOutput);
1509
+ (result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReport = acceptanceReport.value;
1510
+ (result as SingleResult & { structuredAcceptanceReport?: unknown; structuredAcceptanceReportError?: string }).structuredAcceptanceReportError = acceptanceReport.error;
1451
1511
  validatedStructuredOutput = true;
1452
1512
  }
1453
1513
  }
@@ -1485,6 +1545,7 @@ async function runSingleAttempt(
1485
1545
  tokens: progress.tokens,
1486
1546
  durationMs: progress.durationMs,
1487
1547
  };
1548
+ const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, options.cwd ?? runtimeCwd);
1488
1549
 
1489
1550
  const acceptanceOutput = getFinalOutput(result.messages ?? []);
1490
1551
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
@@ -1492,9 +1553,19 @@ async function runSingleAttempt(
1492
1553
  result.outputState = fullOutput.trim() || result.structuredOutput !== undefined ? "present" : "absent";
1493
1554
  if (result.timedOut) {
1494
1555
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
1556
+ result.timeoutRecovery = buildTimeoutRecoverySummary({
1557
+ termination: "timed-out",
1558
+ evidence: mutationEvidence,
1559
+ currentTool: progress.currentTool,
1560
+ currentToolArgs: progress.currentToolArgs,
1561
+ currentPath: progress.currentPath,
1562
+ sessionFile: options.sessionFile,
1563
+ transcriptPath: shared.transcriptWriter ? shared.artifactPaths?.transcriptPath : undefined,
1564
+ artifactPaths: shared.artifactPaths,
1565
+ });
1495
1566
  fullOutput = fullOutput.trim()
1496
- ? `${timeoutMessage}\n\nPartial output before timeout:\n${fullOutput}`
1497
- : timeoutMessage;
1567
+ ? `${timeoutMessage}\n\n${result.timeoutRecovery.message}\n\nPartial output before timeout:\n${fullOutput}`
1568
+ : `${timeoutMessage}\n\n${result.timeoutRecovery.message}`;
1498
1569
  } else if (result.turnBudgetExceeded && result.turnBudget) {
1499
1570
  fullOutput = formatTurnBudgetOutput(turnBudgetExceededMessage(result.turnBudget, result.turnBudget.turnCount), fullOutput);
1500
1571
  } else if (result.turnBudget?.outcome === "termination-deferred") {
@@ -1513,9 +1584,11 @@ async function runSingleAttempt(
1513
1584
  tools: contractTools,
1514
1585
  mcpDirectTools: toolPlan.effectiveMcpTools,
1515
1586
  toolAvailabilityError,
1587
+ mutationEvidence,
1516
1588
  })
1517
1589
  : undefined;
1518
- let completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
1590
+ const mutationAttemptObserved = observedMutationAttempt || mutationEvidence.attemptedMutation;
1591
+ let completionGuardTriggered = completionGuard?.triggered === true && !mutationAttemptObserved;
1519
1592
  const completionGuardBlocked = completionGuard?.blocked === true;
1520
1593
  // The classifier is deliberately narrow, so a read-only review task can
1521
1594
  // still be misread as implementation. Arbitrate BEFORE any failure side
@@ -1547,7 +1620,8 @@ async function runSingleAttempt(
1547
1620
  : "observed"
1548
1621
  : "not-applicable",
1549
1622
  expected: completionGuard.expectedMutation,
1550
- attempted: completionGuardBlocked ? false : completionGuard.attemptedMutation || observedMutationAttempt,
1623
+ attempted: completionGuardBlocked ? false : completionGuard.attemptedMutation || mutationAttemptObserved,
1624
+ evidence: mutationEvidence,
1551
1625
  ...(completionGuardBlocked && completionGuard.message ? { message: completionGuard.message } : {}),
1552
1626
  ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1553
1627
  ...(arbiterRescued ? { resolvedBy: "llm-intent-arbiter" } : {}),
@@ -1620,6 +1694,8 @@ async function runSyncCompletionInner(
1620
1694
  };
1621
1695
  const agent = agents.find((a) => a.name === agentName);
1622
1696
  if (!agent) {
1697
+ const diagnosticContext = options.unknownAgentDiagnosticContext
1698
+ ?? unknownAgentDiagnosticContext(discoverAgents(path.resolve(options.cwd ?? runtimeCwd), "both"));
1623
1699
  return redactResultPrompt(withRunContext({
1624
1700
  index: options.index ?? 0,
1625
1701
  agent: agentName,
@@ -1627,7 +1703,7 @@ async function runSyncCompletionInner(
1627
1703
  exitCode: 1,
1628
1704
  messages: [],
1629
1705
  usage: emptyUsage(),
1630
- error: `Unknown agent: ${agentName}`,
1706
+ error: formatUnknownAgentError(agentName, diagnosticContext),
1631
1707
  }, options.context));
1632
1708
  }
1633
1709
  options = {
@@ -1708,7 +1784,7 @@ async function runSyncCompletionInner(
1708
1784
  dynamicGroup: options.acceptanceContext?.dynamicGroup,
1709
1785
  agentContract: options.agentContract,
1710
1786
  });
1711
- const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContractV1(options.agentContract) });
1787
+ const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContractV1(options.agentContract), structuredOutput: Boolean(options.structuredOutput?.acceptanceReportPath) });
1712
1788
  const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
1713
1789
  options.onEffectivePrompt?.(taskWithAcceptance);
1714
1790
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
@@ -1754,6 +1830,18 @@ async function runSyncCompletionInner(
1754
1830
  agent.modelProvider ?? options.preferredModelProvider,
1755
1831
  { scope: options.modelScope, primaryModelFromParent: options.modelOverrideFromParent },
1756
1832
  );
1833
+ if (options.workflowChildPermitLaunch && candidates.length > 1) {
1834
+ const error = "Workflow child permit does not support model fallback.";
1835
+ return redactResultPrompt(withRunContext({
1836
+ index: options.index ?? 0,
1837
+ agent: agent.name,
1838
+ task,
1839
+ exitCode: 1,
1840
+ messages: [],
1841
+ usage: emptyUsage(),
1842
+ error,
1843
+ }, options.context));
1844
+ }
1757
1845
  try {
1758
1846
  for (const candidate of candidates) {
1759
1847
  const model = applyThinkingSuffix(candidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined);
@@ -1887,6 +1975,7 @@ async function runSyncCompletionInner(
1887
1975
  usage: { ...result.usage },
1888
1976
  };
1889
1977
  modelAttempts.push(attempt);
1978
+ if (options.workflowChildPermitLaunch && !attemptSucceeded) break modelAttemptsLoop;
1890
1979
  // Preserve the legacy intercom handoff contract: once this logical run has
1891
1980
  // been handed to a supervisor, terminating that attempt must not launch a
1892
1981
  // startup retry or model fallback. Explicit user detach retains fallback.
@@ -1959,7 +2048,7 @@ async function runSyncCompletionInner(
1959
2048
  attempt.error = startupError;
1960
2049
  break modelAttemptsLoop;
1961
2050
  }
1962
- const retryableModelFailure = isRetryableModelFailure(result.error);
2051
+ const retryableModelFailure = isRetryableModelFailureAttempt({ error: result.error, messages: result.messages, toolCount: result.progressSummary?.toolCount });
1963
2052
  if (retryableModelFailure) recordRetryableModelFailure(result.model ?? candidate, result.error);
1964
2053
  if (isContextOverflow(result.error)) {
1965
2054
  result.contextOverflow = true;
@@ -2045,6 +2134,8 @@ async function runSyncCompletionInner(
2045
2134
  result.acceptance = await evaluateAcceptance({
2046
2135
  acceptance: effectiveAcceptance,
2047
2136
  output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
2137
+ report: (result as SingleResult & { structuredAcceptanceReport?: import("../../shared/types.ts").AcceptanceReport; structuredAcceptanceReportError?: string }).structuredAcceptanceReport,
2138
+ reportError: (result as SingleResult & { structuredAcceptanceReport?: import("../../shared/types.ts").AcceptanceReport; structuredAcceptanceReportError?: string }).structuredAcceptanceReportError,
2048
2139
  fileOutput: childWrittenOutput !== undefined && options.outputPath
2049
2140
  ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
2050
2141
  : undefined,
@@ -27,6 +27,8 @@ function copyProgress(target: ForegroundChildControl, progress: AgentProgress |
27
27
  target.tokens = progress.tokens;
28
28
  target.inputTokens = progress.inputTokens;
29
29
  target.outputTokens = progress.outputTokens;
30
+ target.window = progress.window;
31
+ target.windowPeak = progress.windowPeak;
30
32
  target.model = progress.model;
31
33
  target.thinking = progress.thinking;
32
34
  target.toolCount = progress.toolCount;
@@ -45,6 +47,8 @@ function syncCurrentChild(control: ForegroundRunControl, child: ForegroundChildC
45
47
  control.tokens = child.tokens;
46
48
  control.inputTokens = child.inputTokens;
47
49
  control.outputTokens = child.outputTokens;
50
+ control.window = child.window;
51
+ control.windowPeak = child.windowPeak;
48
52
  control.model = child.model;
49
53
  control.thinking = child.thinking;
50
54
  control.toolCount = child.toolCount;
@@ -65,6 +69,8 @@ function clearCurrentChild(control: ForegroundRunControl): void {
65
69
  control.tokens = undefined;
66
70
  control.inputTokens = undefined;
67
71
  control.outputTokens = undefined;
72
+ control.window = undefined;
73
+ control.windowPeak = undefined;
68
74
  control.model = undefined;
69
75
  control.thinking = undefined;
70
76
  control.toolCount = undefined;
@@ -2,12 +2,14 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { ForegroundResumeChild, ForegroundResumeRun, SubagentState } from "../../shared/types.ts";
4
4
  import { DIRS } from "../../shared/types.ts";
5
- import { writeAtomicJson } from "../../shared/atomic-json.ts";
5
+ import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
6
6
  import { utf8Tail } from "../../shared/utf8.ts";
7
+ import { validateAcceptanceInput } from "../shared/acceptance.ts";
7
8
 
8
9
  export const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
9
10
  const HISTORY_VERSION = 1;
10
11
  const MAX_INLINE_OUTPUT_BYTES = 64 * 1024;
12
+ const MAX_RESUME_CONTRACT_BYTES = 64 * 1024;
11
13
 
12
14
  interface ForegroundHistoryIndex {
13
15
  version: 1;
@@ -51,7 +53,9 @@ function compactChild(child: ForegroundResumeChild): ForegroundResumeChild {
51
53
  ...(child.transcriptPath ? { transcriptPath: child.transcriptPath } : {}),
52
54
  ...(child.transcriptError ? { transcriptError: child.transcriptError } : {}),
53
55
  ...(child.acceptance ? { acceptance: child.acceptance } : {}),
56
+ ...(child.resumeContract ? { resumeContract: child.resumeContract } : {}),
54
57
  ...(child.launchContractDigest ? { launchContractDigest: child.launchContractDigest } : {}),
58
+ ...(child.extensionBindings ? { extensionBindings: child.extensionBindings } : {}),
55
59
  ...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
56
60
  ...(child.updatedAt !== undefined ? { updatedAt: child.updatedAt } : {}),
57
61
  };
@@ -61,9 +65,26 @@ function isRestorableForegroundStatus(status: unknown): status is ForegroundResu
61
65
  return status === "completed" || status === "failed" || status === "paused" || status === "stopped";
62
66
  }
63
67
 
68
+ function isRestorableResumeContract(value: unknown): boolean {
69
+ if (value === undefined) return true;
70
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
71
+ try {
72
+ if (Buffer.byteLength(JSON.stringify(value), "utf8") > MAX_RESUME_CONTRACT_BYTES) return false;
73
+ } catch {
74
+ return false;
75
+ }
76
+ const contract = value as NonNullable<ForegroundResumeChild["resumeContract"]>;
77
+ if (Object.keys(contract).some((key) => !["outputSchema", "agentContract", "acceptance", "output", "outputMode"].includes(key))) return false;
78
+ if (contract.outputSchema !== undefined && (!contract.outputSchema || typeof contract.outputSchema !== "object" || Array.isArray(contract.outputSchema))) return false;
79
+ if (contract.agentContract !== undefined && (!contract.agentContract || typeof contract.agentContract !== "object" || Array.isArray(contract.agentContract) || contract.agentContract.version !== 1)) return false;
80
+ if (validateAcceptanceInput(contract.acceptance).length > 0) return false;
81
+ if (contract.output !== undefined && typeof contract.output !== "string" && typeof contract.output !== "boolean") return false;
82
+ return contract.outputMode === undefined || contract.outputMode === "inline" || contract.outputMode === "file-only";
83
+ }
84
+
64
85
  function compactRun(run: ForegroundResumeRun): ForegroundResumeRun | undefined {
65
86
  if (!run.sessionId) return undefined;
66
- if (run.children.length === 0 || !run.children.every((child) => isRestorableForegroundStatus(child.status))) return undefined;
87
+ if (run.children.length === 0 || !run.children.every((child) => isRestorableForegroundStatus(child.status) && isRestorableResumeContract(child.resumeContract))) return undefined;
67
88
  return {
68
89
  runId: run.runId,
69
90
  mode: run.mode,
@@ -101,6 +122,7 @@ function isRestorableRun(value: unknown): value is ForegroundResumeRun {
101
122
  && run.children.every((child) => Boolean(child && typeof child === "object" && !Array.isArray(child)
102
123
  && typeof (child as Partial<ForegroundResumeChild>).agent === "string"
103
124
  && typeof (child as Partial<ForegroundResumeChild>).index === "number"
125
+ && isRestorableResumeContract((child as Partial<ForegroundResumeChild>).resumeContract)
104
126
  && isRestorableForegroundStatus((child as Partial<ForegroundResumeChild>).status)));
105
127
  }
106
128
 
@@ -118,7 +140,7 @@ export function persistForegroundRunHistory(state: SubagentState, options: { res
118
140
  if (compact) merged.set(compact.runId, compact);
119
141
  }
120
142
  const runs = sortAndBound([...merged.values()], limit);
121
- writeAtomicJson(historyPath(resultsDir), { version: HISTORY_VERSION, runs });
143
+ writePrivateAtomicJson(historyPath(resultsDir), { version: HISTORY_VERSION, runs });
122
144
  }
123
145
 
124
146
  export function restoreForegroundRunHistory(state: SubagentState, options: { resultsDir?: string; sessionId?: string | null; limit?: number } = {}): number {