pi-subagents 0.35.0 → 0.36.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 (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +132 -24
  3. package/agents/advisor.md +73 -0
  4. package/package.json +8 -12
  5. package/skills/pi-subagents/SKILL.md +22 -9
  6. package/src/agents/agents.ts +22 -5
  7. package/src/api/delegation.ts +125 -0
  8. package/src/extension/config.ts +7 -1
  9. package/src/extension/index.ts +50 -38
  10. package/src/extension/rpc.ts +27 -2
  11. package/src/extension/schemas.ts +22 -2
  12. package/src/extension/tool-description.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +1 -1
  14. package/src/intercom/native-supervisor-channel.ts +45 -6
  15. package/src/intercom/result-intercom.ts +7 -0
  16. package/src/runs/background/async-execution.ts +34 -4
  17. package/src/runs/background/async-job-tracker.ts +4 -0
  18. package/src/runs/background/async-resume.ts +27 -5
  19. package/src/runs/background/async-status.ts +76 -3
  20. package/src/runs/background/chain-append.ts +2 -0
  21. package/src/runs/background/completion-batcher.ts +6 -4
  22. package/src/runs/background/completion-dedupe.ts +2 -11
  23. package/src/runs/background/fleet-view.ts +9 -4
  24. package/src/runs/background/notify.ts +132 -120
  25. package/src/runs/background/result-watcher.ts +138 -78
  26. package/src/runs/background/run-status.ts +3 -1
  27. package/src/runs/background/subagent-runner.ts +225 -43
  28. package/src/runs/background/subagent-wait.ts +130 -4
  29. package/src/runs/background/wait-tool.ts +2 -2
  30. package/src/runs/foreground/chain-execution.ts +176 -111
  31. package/src/runs/foreground/execution.ts +90 -36
  32. package/src/runs/foreground/foreground-control.ts +90 -0
  33. package/src/runs/foreground/subagent-executor.ts +394 -163
  34. package/src/runs/shared/acceptance.ts +55 -13
  35. package/src/runs/shared/agent-contract.ts +38 -0
  36. package/src/runs/shared/child-protocol.ts +1 -1
  37. package/src/runs/shared/completion-guard.ts +36 -5
  38. package/src/runs/shared/context-mode.ts +44 -0
  39. package/src/runs/shared/dynamic-fanout.ts +4 -4
  40. package/src/runs/shared/long-running-guard.ts +4 -0
  41. package/src/runs/shared/nested-events.ts +27 -2
  42. package/src/runs/shared/parallel-handoff.ts +154 -0
  43. package/src/runs/shared/parallel-utils.ts +6 -0
  44. package/src/runs/shared/pi-args.ts +23 -14
  45. package/src/runs/shared/run-history.ts +90 -5
  46. package/src/runs/shared/structured-output.ts +112 -7
  47. package/src/runs/shared/subagent-control.ts +4 -0
  48. package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
  49. package/src/runs/shared/task-intent.ts +10 -5
  50. package/src/runs/shared/tool-availability.ts +3 -1
  51. package/src/runs/shared/tool-budget.ts +11 -5
  52. package/src/runs/shared/turn-budget.ts +2 -1
  53. package/src/runs/shared/worktree.ts +63 -14
  54. package/src/shared/accessible-dir.ts +25 -0
  55. package/src/shared/artifacts.ts +37 -7
  56. package/src/shared/atomic-json.ts +14 -42
  57. package/src/shared/child-transcript.ts +52 -0
  58. package/src/shared/file-system-retry.ts +47 -0
  59. package/src/shared/settings.ts +9 -1
  60. package/src/shared/types.ts +211 -22
  61. package/src/slash/delegation-adapters.ts +152 -5
  62. package/src/slash/delegation-json.ts +108 -0
  63. package/src/slash/delegation-request.ts +182 -36
  64. package/src/slash/prompt-template-bridge.ts +222 -37
  65. package/src/slash/selector.ts +147 -0
  66. package/src/slash/slash-commands.ts +14 -5
  67. package/src/slash/slash-live-state.ts +2 -2
  68. package/src/slash/subagents-admin.ts +42 -42
  69. package/src/tui/fleet-status.ts +362 -0
  70. package/src/tui/fleet-transcript.ts +472 -0
  71. package/src/tui/fleet.ts +318 -59
  72. package/src/tui/render.ts +25 -15
  73. package/src/watchdog/change-signature.ts +105 -12
  74. package/src/watchdog/review.ts +7 -2
  75. package/src/watchdog/runtime.ts +5 -3
  76. package/src/slash/subagents-editor.ts +0 -86
@@ -9,6 +9,7 @@ import type { Message } from "@earendil-works/pi-ai";
9
9
  import type { AgentConfig } from "../../agents/agents.ts";
10
10
  import {
11
11
  ensureArtifactsDir,
12
+ formatOutputArtifactContent,
12
13
  getArtifactPaths,
13
14
  writeArtifact,
14
15
  writeMetadata,
@@ -51,7 +52,8 @@ import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
51
52
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
52
53
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
53
54
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
54
- import { readStructuredOutput } from "../shared/structured-output.ts";
55
+ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
56
+ import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
55
57
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
56
58
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
57
59
  import {
@@ -71,6 +73,7 @@ import {
71
73
  summarizeRecentMutatingFailures,
72
74
  } from "../shared/long-running-guard.ts";
73
75
  import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
76
+ import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
74
77
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
75
78
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
76
79
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
@@ -90,6 +93,12 @@ function emptyUsage(): Usage {
90
93
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
91
94
  }
92
95
 
96
+ function withRunContext<T extends SingleResult>(result: T, context: RunSyncOptions["context"]): T {
97
+ if (!context) return result;
98
+ result.context = context;
99
+ return result;
100
+ }
101
+
93
102
  function sumUsage(target: Usage, source: Usage): void {
94
103
  target.input += source.input;
95
104
  target.output += source.output;
@@ -196,6 +205,7 @@ async function runSingleAttempt(
196
205
  ): Promise<SingleResult> {
197
206
  const effectiveThinking = options.thinkingOverride ?? agent.thinking;
198
207
  const modelArg = applyThinkingSuffix(model, effectiveThinking, options.thinkingOverride !== undefined);
208
+ const resolvedThinking = resolveEffectiveThinking(modelArg, effectiveThinking);
199
209
  const watchdogConfig = resolveWatchdogConfig(options.cwd ?? runtimeCwd);
200
210
  const childWatchdog = watchdogConfig.ok
201
211
  ? resolveChildWatchdogConfig({
@@ -236,24 +246,27 @@ async function runSingleAttempt(
236
246
  parentSessionId: options.parentSessionId,
237
247
  structuredOutput: options.structuredOutput,
238
248
  toolBudget: options.toolBudget,
249
+ allowZeroToolBudget: options.allowZeroToolBudget,
239
250
  childWatchdog,
240
251
  waitToolEnabled: options.waitToolEnabled,
241
252
  });
242
253
 
243
- const result: SingleResult = {
254
+ const result: SingleResult = withRunContext({
244
255
  agent: agent.name,
245
256
  task: shared.originalTask ?? task,
257
+ ...(options.agentContract ? { agentContract: options.agentContract } : {}),
246
258
  exitCode: 0,
247
259
  messages: [],
248
260
  usage: emptyUsage(),
249
261
  model: modelArg,
262
+ ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
250
263
  artifactPaths: shared.artifactPaths,
251
264
  transcriptPath: shared.transcriptWriter ? shared.artifactPaths?.transcriptPath : undefined,
252
265
  skills: shared.resolvedSkillNames,
253
266
  skillsWarning: shared.skillsWarning,
254
267
  ...(options.turnBudget ? { turnBudget: initialTurnBudgetState(options.turnBudget) } : {}),
255
268
  ...(options.toolBudget ? { toolBudget: initialToolBudgetState(options.toolBudget) } : {}),
256
- };
269
+ }, options.context);
257
270
  const startTime = Date.now();
258
271
  if (options.structuredOutput) {
259
272
  try {
@@ -307,6 +320,7 @@ async function runSingleAttempt(
307
320
  }
308
321
  const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
309
322
  let observedMutationAttempt = false;
323
+ let structuredOutputToolInvoked = false;
310
324
 
311
325
  const exitCode = await new Promise<number>((resolve) => {
312
326
  const spawnSpec = getPiSpawnCommand(args);
@@ -592,7 +606,7 @@ async function runSingleAttempt(
592
606
  result.wrapUpRequested = true;
593
607
  appendRecentOutput(progress, [turnBudgetSoftNote(budget, turnCount)]);
594
608
  }
595
- const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting);
609
+ const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting, options.enforceHardTurnLimit);
596
610
  if (decision === "defer") {
597
611
  result.turnBudget = turnBudgetDeferredState(
598
612
  budget,
@@ -646,7 +660,7 @@ async function runSingleAttempt(
646
660
  const fireUpdate = () => {
647
661
  if (!options.onUpdate || processClosed) return;
648
662
  progress.durationMs = Date.now() - startTime;
649
- const output = (result.timedOut || result.turnBudgetExceeded) && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages);
663
+ const output = (result.timedOut || result.turnBudgetExceeded) && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
650
664
  emitUpdateSnapshot(output || "(running...)");
651
665
  };
652
666
 
@@ -696,6 +710,7 @@ async function runSingleAttempt(
696
710
  const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
697
711
  ? evt.args as Record<string, unknown>
698
712
  : {};
713
+ if (options.structuredOutput && evt.toolName === "structured_output") structuredOutputToolInvoked = true;
699
714
  if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
700
715
  intercomStarted = true;
701
716
  }
@@ -729,15 +744,20 @@ async function runSingleAttempt(
729
744
  }
730
745
 
731
746
  if (evt.type === "message_end" && evt.message) {
732
- result.messages.push(evt.message);
747
+ result.messages!.push(evt.message);
733
748
  if (evt.message.role === "assistant") {
734
749
  result.usage.turns++;
735
750
  progress.turnCount = result.usage.turns;
736
751
  const stopReason = (evt.message as { stopReason?: string }).stopReason;
737
- const hasToolCall = Array.isArray(evt.message.content)
738
- && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
752
+ const toolCalls = Array.isArray(evt.message.content)
753
+ ? evt.message.content.filter((part) => (part as { type?: string }).type === "toolCall")
754
+ : [];
755
+ const hasToolCall = toolCalls.length > 0;
739
756
  const terminalAssistantStop = stopReason === "stop" && !hasToolCall;
740
- updateTurnBudget(result.usage.turns, terminalAssistantStop, hasToolCall || Boolean(progress.currentTool));
757
+ const terminalStructuredOutputCall = Boolean(options.structuredOutput)
758
+ && toolCalls.length === 1
759
+ && (toolCalls[0] as { name?: string }).name === "structured_output";
760
+ updateTurnBudget(result.usage.turns, terminalAssistantStop || terminalStructuredOutputCall, hasToolCall || Boolean(progress.currentTool));
741
761
  const u = evt.message.usage;
742
762
  if (u) {
743
763
  result.usage.input += u.input || 0;
@@ -763,7 +783,7 @@ async function runSingleAttempt(
763
783
  }
764
784
 
765
785
  if (evt.type === "tool_result_end" && evt.message) {
766
- result.messages.push(evt.message);
786
+ result.messages!.push(evt.message);
767
787
  const resultText = extractTextFromContent(evt.message.content);
768
788
  if (options.toolBudget && pendingToolResult && resultText.includes("Tool budget hard limit reached")) {
769
789
  result.toolBudgetBlocked = true;
@@ -1021,7 +1041,7 @@ async function runSingleAttempt(
1021
1041
  result.exitCode = 1;
1022
1042
  }
1023
1043
  if (result.exitCode === 0 && !result.error) {
1024
- const errInfo = detectSubagentError(result.messages);
1044
+ const errInfo = detectSubagentError(result.messages ?? []);
1025
1045
  if (errInfo.hasError) {
1026
1046
  result.exitCode = errInfo.exitCode ?? 1;
1027
1047
  result.error = errInfo.details
@@ -1030,7 +1050,7 @@ async function runSingleAttempt(
1030
1050
  }
1031
1051
  }
1032
1052
  if (result.exitCode === 0 && !result.error) {
1033
- const finalText = getFinalOutput(result.messages);
1053
+ const finalText = getFinalOutput(result.messages ?? []);
1034
1054
  const missingStructuredOutput = options.structuredOutput
1035
1055
  ? !existsSync(options.structuredOutput.outputPath)
1036
1056
  : false;
@@ -1040,18 +1060,25 @@ async function runSingleAttempt(
1040
1060
  }
1041
1061
  }
1042
1062
  if (options.structuredOutput && result.exitCode === 0 && !result.error) {
1043
- const structured = readStructuredOutput({
1044
- schema: options.structuredOutput.schema,
1045
- schemaPath: options.structuredOutput.schemaPath,
1046
- outputPath: options.structuredOutput.outputPath,
1047
- });
1048
1063
  result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
1049
1064
  result.structuredOutputPath = options.structuredOutput.outputPath;
1050
- if (structured.error) {
1065
+ if (!structuredOutputToolInvoked) {
1051
1066
  result.exitCode = 1;
1052
- result.error = structured.error;
1067
+ result.error = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
1068
+ result.structuredOutputFailed = true;
1053
1069
  } else {
1054
- result.structuredOutput = structured.value;
1070
+ const structured = await readStructuredOutput({
1071
+ schema: options.structuredOutput.schema,
1072
+ schemaPath: options.structuredOutput.schemaPath,
1073
+ outputPath: options.structuredOutput.outputPath,
1074
+ });
1075
+ if (structured.error) {
1076
+ result.exitCode = 1;
1077
+ result.error = structured.error;
1078
+ result.structuredOutputFailed = true;
1079
+ } else {
1080
+ result.structuredOutput = structured.value;
1081
+ }
1055
1082
  }
1056
1083
  }
1057
1084
 
@@ -1070,7 +1097,7 @@ async function runSingleAttempt(
1070
1097
  durationMs: progress.durationMs,
1071
1098
  };
1072
1099
 
1073
- const acceptanceOutput = getFinalOutput(result.messages);
1100
+ const acceptanceOutput = getFinalOutput(result.messages ?? []);
1074
1101
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
1075
1102
  if (result.timedOut) {
1076
1103
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
@@ -1086,16 +1113,29 @@ async function runSingleAttempt(
1086
1113
  const note = turnBudgetSoftNote(result.turnBudget, result.turnBudget.wrapUpRequestedAtTurn ?? result.turnBudget.turnCount);
1087
1114
  fullOutput = fullOutput.trim() ? `${note}\n\n${fullOutput}` : note;
1088
1115
  }
1089
- const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
1116
+ const completionGuardEnabled = isAgentContractV1(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
1117
+ const completionGuard = result.exitCode === 0 && !result.error && completionGuardEnabled
1090
1118
  ? evaluateCompletionMutationGuard({
1091
1119
  agent: agent.name,
1092
1120
  task: shared.originalTask ?? task,
1093
- messages: result.messages,
1121
+ messages: result.messages ?? [],
1094
1122
  tools: agent.tools,
1095
1123
  mcpDirectTools: agent.mcpDirectTools,
1096
1124
  })
1097
1125
  : undefined;
1098
- if (completionGuard?.triggered && !observedMutationAttempt) {
1126
+ const completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
1127
+ if (completionGuard) {
1128
+ result.effects = {
1129
+ ...(result.effects ?? {}),
1130
+ fileMutation: {
1131
+ status: completionGuard.expectedMutation ? completionGuardTriggered ? "missing" : "observed" : "not-applicable",
1132
+ expected: completionGuard.expectedMutation,
1133
+ attempted: completionGuard.attemptedMutation || observedMutationAttempt,
1134
+ ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1135
+ },
1136
+ };
1137
+ }
1138
+ if (completionGuardTriggered && !isAgentContractV1(options.agentContract)) {
1099
1139
  result.exitCode = 1;
1100
1140
  result.error = "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
1101
1141
  progress.status = "failed";
@@ -1156,18 +1196,18 @@ export async function runSync(
1156
1196
  ): Promise<SingleResult> {
1157
1197
  const agent = agents.find((a) => a.name === agentName);
1158
1198
  if (!agent) {
1159
- return {
1199
+ return withRunContext({
1160
1200
  agent: agentName,
1161
1201
  task,
1162
1202
  exitCode: 1,
1163
1203
  messages: [],
1164
1204
  usage: emptyUsage(),
1165
1205
  error: `Unknown agent: ${agentName}`,
1166
- };
1206
+ }, options.context);
1167
1207
  }
1168
1208
  const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
1169
1209
  if (outputModeValidationError) {
1170
- return {
1210
+ return withRunContext({
1171
1211
  agent: agentName,
1172
1212
  task,
1173
1213
  exitCode: 1,
@@ -1175,7 +1215,7 @@ export async function runSync(
1175
1215
  usage: emptyUsage(),
1176
1216
  outputMode: options.outputMode,
1177
1217
  error: outputModeValidationError,
1178
- };
1218
+ }, options.context);
1179
1219
  }
1180
1220
 
1181
1221
  const shareEnabled = options.share === true;
@@ -1188,8 +1228,9 @@ export async function runSync(
1188
1228
  async: options.acceptanceContext?.async,
1189
1229
  dynamic: options.acceptanceContext?.dynamic,
1190
1230
  dynamicGroup: options.acceptanceContext?.dynamicGroup,
1231
+ agentContract: options.agentContract,
1191
1232
  });
1192
- const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance);
1233
+ const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContractV1(options.agentContract) });
1193
1234
  const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
1194
1235
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
1195
1236
  const skillNames = options.skills ?? agent.skills ?? [];
@@ -1202,14 +1243,14 @@ export async function runSync(
1202
1243
  agent.filePath ? path.dirname(agent.filePath) : skillCwd,
1203
1244
  );
1204
1245
  if (skillNames.some((skill) => skill.trim() === "pi-subagents") && missingSkills.includes("pi-subagents")) {
1205
- return {
1246
+ return withRunContext({
1206
1247
  agent: agentName,
1207
1248
  task,
1208
1249
  exitCode: 1,
1209
1250
  messages: [],
1210
1251
  usage: emptyUsage(),
1211
1252
  error: "Skills not found: pi-subagents",
1212
- };
1253
+ }, options.context);
1213
1254
  }
1214
1255
  let systemPrompt = agent.systemPrompt?.trim() || "";
1215
1256
  if (resolvedSkills.length > 0) {
@@ -1275,7 +1316,11 @@ export async function runSync(
1275
1316
  durationMs: target.progressSummary?.durationMs,
1276
1317
  toolCount: target.progressSummary?.toolCount,
1277
1318
  error: target.error,
1319
+ agentContract: target.agentContract,
1320
+ execution: target.execution,
1278
1321
  acceptance: target.acceptance,
1322
+ review: target.review,
1323
+ effects: target.effects,
1279
1324
  ...(transcriptWriter ? { transcriptPath: artifactPathsResult.transcriptPath } : {}),
1280
1325
  transcriptError: target.transcriptError,
1281
1326
  skills: target.skills,
@@ -1299,10 +1344,11 @@ export async function runSync(
1299
1344
  ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1300
1345
  : undefined,
1301
1346
  cwd: options.cwd ?? runtimeCwd,
1347
+ reportOptional: isAgentContractV1(options.agentContract),
1302
1348
  });
1303
1349
  const acceptanceFailure = acceptanceFailureMessage(recoveredResult.acceptance);
1304
1350
  stripAcceptanceReportsFromMessages(recoveredResult.messages);
1305
- if (acceptanceFailure && recoveredResult.acceptance.explicit && recoveredResult.exitCode === 0) {
1351
+ if (acceptanceFailure && recoveredResult.acceptance.explicit && recoveredResult.exitCode === 0 && !isAgentContractV1(options.agentContract)) {
1306
1352
  recoveredResult.exitCode = 1;
1307
1353
  recoveredResult.error = recoveredResult.error ? `${recoveredResult.error}\n${acceptanceFailure}` : acceptanceFailure;
1308
1354
  if (recoveredResult.progress) {
@@ -1310,6 +1356,7 @@ export async function runSync(
1310
1356
  recoveredResult.progress.error = recoveredResult.error;
1311
1357
  }
1312
1358
  }
1359
+ if (isAgentContractV1(options.agentContract)) attachContractProjections(recoveredResult);
1313
1360
  persistResultMetadata(recoveredResult);
1314
1361
  options.onDetachedExit?.(recoveredResult);
1315
1362
  })().catch((error) => {
@@ -1366,14 +1413,14 @@ export async function runSync(
1366
1413
  attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
1367
1414
  }
1368
1415
 
1369
- const result = lastResult ?? {
1416
+ const result = withRunContext(lastResult ?? {
1370
1417
  agent: agentName,
1371
1418
  task,
1372
1419
  exitCode: 1,
1373
1420
  messages: [],
1374
1421
  usage: emptyUsage(),
1375
1422
  error: "Subagent did not produce a result.",
1376
- } satisfies SingleResult;
1423
+ } satisfies SingleResult, options.context);
1377
1424
 
1378
1425
  result.usage = aggregateUsage;
1379
1426
  result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
@@ -1396,7 +1443,12 @@ export async function runSync(
1396
1443
  if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
1397
1444
  result.artifactPaths = artifactPathsResult;
1398
1445
  if (options.artifactConfig?.includeOutput !== false) {
1399
- writeArtifact(artifactPathsResult.outputPath, artifactOutputByResult.get(result) ?? result.finalOutput ?? "");
1446
+ writeArtifact(artifactPathsResult.outputPath, formatOutputArtifactContent({
1447
+ output: artifactOutputByResult.get(result) ?? result.finalOutput ?? "",
1448
+ error: result.error,
1449
+ transcriptPath: result.transcriptPath,
1450
+ metadataPath: options.artifactConfig?.includeMetadata === false ? undefined : artifactPathsResult.metadataPath,
1451
+ }));
1400
1452
  }
1401
1453
  if (options.maxOutput) {
1402
1454
  const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
@@ -1435,11 +1487,12 @@ export async function runSync(
1435
1487
  ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1436
1488
  : undefined,
1437
1489
  cwd: options.cwd ?? runtimeCwd,
1490
+ reportOptional: isAgentContractV1(options.agentContract),
1438
1491
  });
1439
1492
  }
1440
1493
  const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
1441
1494
  stripAcceptanceReportsFromMessages(result.messages);
1442
- if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut) {
1495
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
1443
1496
  result.exitCode = 1;
1444
1497
  result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
1445
1498
  if (result.progress) {
@@ -1447,6 +1500,7 @@ export async function runSync(
1447
1500
  result.progress.error = result.error;
1448
1501
  }
1449
1502
  }
1503
+ if (isAgentContractV1(options.agentContract)) attachContractProjections(result);
1450
1504
  persistResultMetadata(result);
1451
1505
 
1452
1506
  return result;
@@ -0,0 +1,90 @@
1
+ import type { AgentProgress, ForegroundChildControl, ForegroundRunControl } from "../../shared/types.ts";
2
+
3
+ interface BeginForegroundChildInput {
4
+ index: number;
5
+ agent: string;
6
+ description?: string;
7
+ interrupt: () => boolean;
8
+ }
9
+
10
+ function copyProgress(target: ForegroundChildControl, progress: AgentProgress | undefined): void {
11
+ if (!progress) return;
12
+ target.currentActivityState = progress.activityState;
13
+ target.lastActivityAt = progress.lastActivityAt;
14
+ target.currentTool = progress.currentTool;
15
+ target.currentToolStartedAt = progress.currentToolStartedAt;
16
+ target.currentPath = progress.currentPath;
17
+ target.turnCount = progress.turnCount;
18
+ target.tokens = progress.tokens;
19
+ target.toolCount = progress.toolCount;
20
+ }
21
+
22
+ function syncCurrentChild(control: ForegroundRunControl, child: ForegroundChildControl): void {
23
+ control.currentAgent = child.agent;
24
+ control.currentIndex = child.index;
25
+ control.description = child.description;
26
+ control.currentActivityState = child.currentActivityState;
27
+ control.lastActivityAt = child.lastActivityAt;
28
+ control.currentTool = child.currentTool;
29
+ control.currentToolStartedAt = child.currentToolStartedAt;
30
+ control.currentPath = child.currentPath;
31
+ control.turnCount = child.turnCount;
32
+ control.tokens = child.tokens;
33
+ control.toolCount = child.toolCount;
34
+ control.interrupt = child.interrupt;
35
+ control.updatedAt = child.updatedAt;
36
+ }
37
+
38
+ function clearCurrentChild(control: ForegroundRunControl): void {
39
+ control.currentAgent = undefined;
40
+ control.currentIndex = undefined;
41
+ control.currentActivityState = undefined;
42
+ control.lastActivityAt = undefined;
43
+ control.currentTool = undefined;
44
+ control.currentToolStartedAt = undefined;
45
+ control.currentPath = undefined;
46
+ control.turnCount = undefined;
47
+ control.tokens = undefined;
48
+ control.toolCount = undefined;
49
+ control.interrupt = undefined;
50
+ }
51
+
52
+ export function beginForegroundChild(control: ForegroundRunControl, input: BeginForegroundChildInput): void {
53
+ const now = Date.now();
54
+ const child: ForegroundChildControl = {
55
+ index: input.index,
56
+ agent: input.agent,
57
+ ...(input.description ? { description: input.description } : {}),
58
+ startedAt: now,
59
+ updatedAt: now,
60
+ };
61
+ child.interrupt = () => {
62
+ if (!input.interrupt()) return false;
63
+ child.currentActivityState = undefined;
64
+ child.updatedAt = Date.now();
65
+ syncCurrentChild(control, child);
66
+ return true;
67
+ };
68
+ control.activeChildren ??= new Map();
69
+ control.activeChildren.set(input.index, child);
70
+ syncCurrentChild(control, child);
71
+ }
72
+
73
+ export function updateForegroundChild(control: ForegroundRunControl, index: number, progress: AgentProgress | undefined): void {
74
+ const child = control.activeChildren?.get(index);
75
+ if (!child) return;
76
+ copyProgress(child, progress);
77
+ child.updatedAt = Date.now();
78
+ syncCurrentChild(control, child);
79
+ }
80
+
81
+ export function finishForegroundChild(control: ForegroundRunControl, index: number): void {
82
+ control.activeChildren?.delete(index);
83
+ if (control.currentIndex === index) {
84
+ const next = [...(control.activeChildren?.values() ?? [])]
85
+ .sort((left, right) => right.updatedAt - left.updatedAt || left.index - right.index)[0];
86
+ if (next) syncCurrentChild(control, next);
87
+ else clearCurrentChild(control);
88
+ }
89
+ control.updatedAt = Date.now();
90
+ }