pi-subagents 0.24.3 → 0.25.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 (40) hide show
  1. package/CHANGELOG.md +26 -5
  2. package/README.md +19 -11
  3. package/package.json +4 -8
  4. package/prompts/review-loop.md +1 -1
  5. package/skills/pi-subagents/SKILL.md +46 -10
  6. package/src/agents/agent-management.ts +5 -0
  7. package/src/agents/agent-serializer.ts +2 -0
  8. package/src/agents/agents.ts +30 -6
  9. package/src/agents/skills.ts +25 -23
  10. package/src/extension/config.ts +16 -0
  11. package/src/extension/fanout-child.ts +170 -0
  12. package/src/extension/index.ts +13 -25
  13. package/src/intercom/intercom-bridge.ts +2 -1
  14. package/src/intercom/result-intercom.ts +108 -0
  15. package/src/runs/background/async-execution.ts +107 -7
  16. package/src/runs/background/async-job-tracker.ts +57 -14
  17. package/src/runs/background/async-resume.ts +28 -15
  18. package/src/runs/background/async-status.ts +60 -30
  19. package/src/runs/background/result-watcher.ts +111 -54
  20. package/src/runs/background/run-id-resolver.ts +83 -0
  21. package/src/runs/background/run-status.ts +79 -3
  22. package/src/runs/background/stale-run-reconciler.ts +46 -1
  23. package/src/runs/background/subagent-runner.ts +66 -18
  24. package/src/runs/foreground/chain-execution.ts +6 -0
  25. package/src/runs/foreground/execution.ts +21 -5
  26. package/src/runs/foreground/subagent-executor.ts +314 -18
  27. package/src/runs/shared/completion-guard.ts +23 -1
  28. package/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
  29. package/src/runs/shared/nested-events.ts +819 -0
  30. package/src/runs/shared/nested-path.ts +52 -0
  31. package/src/runs/shared/nested-render.ts +115 -0
  32. package/src/runs/shared/parallel-utils.ts +1 -0
  33. package/src/runs/shared/pi-args.ts +67 -5
  34. package/src/runs/shared/run-history.ts +12 -7
  35. package/src/runs/shared/single-output.ts +12 -2
  36. package/src/runs/shared/subagent-prompt-runtime.ts +25 -5
  37. package/src/shared/artifacts.ts +2 -2
  38. package/src/shared/types.ts +95 -0
  39. package/src/shared/utils.ts +11 -1
  40. package/src/tui/render.ts +254 -153
@@ -14,6 +14,7 @@ import {
14
14
  type AsyncParallelGroupStatus,
15
15
  type AsyncStatus,
16
16
  type ModelAttempt,
17
+ type NestedRouteInfo,
17
18
  type ResolvedControlConfig,
18
19
  type SubagentRunMode,
19
20
  type Usage,
@@ -40,6 +41,7 @@ import {
40
41
  MAX_PARALLEL_CONCURRENCY,
41
42
  } from "../shared/parallel-utils.ts";
42
43
  import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
44
+ import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
43
45
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
44
46
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
45
47
  import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
@@ -92,6 +94,8 @@ interface SubagentRunConfig {
92
94
  controlIntercomTarget?: string;
93
95
  childIntercomTargets?: Array<string | undefined>;
94
96
  resultMode?: SubagentRunMode;
97
+ nestedRoute?: NestedRouteInfo;
98
+ nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
95
99
  }
96
100
 
97
101
  interface StepResult {
@@ -222,7 +226,12 @@ function runPiStreaming(
222
226
  ...(piPackageRoot ? { piPackageRoot } : {}),
223
227
  ...(piArgv1 ? { argv1: piArgv1 } : {}),
224
228
  });
225
- const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
229
+ const child = spawn(spawnSpec.command, spawnSpec.args, {
230
+ cwd,
231
+ stdio: ["ignore", "pipe", "pipe"],
232
+ env: spawnEnv,
233
+ windowsHide: true,
234
+ });
226
235
  let stderr = "";
227
236
  let stdoutBuf = "";
228
237
  let stderrBuf = "";
@@ -230,6 +239,7 @@ function runPiStreaming(
230
239
  const usage = emptyUsage();
231
240
  let model: string | undefined;
232
241
  let error: string | undefined;
242
+ let assistantError: string | undefined;
233
243
  let interrupted = false;
234
244
  let observedMutationAttempt = false;
235
245
  const rawStdoutLines: string[] = [];
@@ -290,7 +300,7 @@ function runPiStreaming(
290
300
 
291
301
  if (event.type !== "message_end" || event.message.role !== "assistant") return;
292
302
  if (event.message.model) model = event.message.model;
293
- if (event.message.errorMessage) error = event.message.errorMessage;
303
+ if (event.message.errorMessage) assistantError = event.message.errorMessage;
294
304
  const eventUsage = event.message.usage;
295
305
  if (eventUsage) {
296
306
  usage.turns++;
@@ -304,6 +314,7 @@ function runPiStreaming(
304
314
  const hasToolCall = Array.isArray(event.message.content)
305
315
  && event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
306
316
  if (stopReason === "stop" && !hasToolCall) {
317
+ if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
307
318
  cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
308
319
  startFinalDrain();
309
320
  }
@@ -371,7 +382,7 @@ function runPiStreaming(
371
382
  const termSent = trySignalChild(child, "SIGTERM");
372
383
  if (!termSent) return;
373
384
  forcedTerminationSignal = true;
374
- if (!cleanTerminalAssistantStopReceived && !error) {
385
+ if (!cleanTerminalAssistantStopReceived && !error && !assistantError) {
375
386
  error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its final message. Forcing termination.`;
376
387
  }
377
388
  finalHardKillTimer = setTimeout(() => {
@@ -395,14 +406,15 @@ function runPiStreaming(
395
406
  if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
396
407
  outputStream.end();
397
408
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
398
- const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !error;
409
+ const finalError = error ?? assistantError;
410
+ const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
399
411
  resolve({
400
412
  stderr,
401
413
  exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
402
414
  messages,
403
415
  usage,
404
416
  model,
405
- error: interrupted || forcedDrainAfterFinalSuccess ? undefined : error,
417
+ error: interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
406
418
  finalOutput,
407
419
  interrupted,
408
420
  observedMutationAttempt,
@@ -417,7 +429,7 @@ function runPiStreaming(
417
429
  outputStream.end();
418
430
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
419
431
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
420
- resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
432
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
421
433
  });
422
434
  });
423
435
  }
@@ -546,6 +558,7 @@ interface SingleStepContext {
546
558
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
547
559
  childIntercomTarget?: string;
548
560
  orchestratorIntercomTarget?: string;
561
+ nestedRoute?: NestedRouteInfo;
549
562
  onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
550
563
  onChildEvent?: (event: ChildEvent) => void;
551
564
  }
@@ -614,12 +627,17 @@ async function runSingleStep(
614
627
  systemPrompt: step.systemPrompt,
615
628
  systemPromptMode: step.systemPromptMode,
616
629
  mcpDirectTools: step.mcpDirectTools,
630
+ cwd: step.cwd ?? ctx.cwd,
617
631
  promptFileStem: step.agent,
618
632
  intercomSessionName: ctx.childIntercomTarget,
619
633
  orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
620
634
  runId: ctx.id,
621
635
  childAgentName: step.agent,
622
636
  childIndex: ctx.flatIndex,
637
+ parentEventSink: ctx.nestedRoute?.eventSink,
638
+ parentControlInbox: ctx.nestedRoute?.controlInbox,
639
+ parentRootRunId: ctx.nestedRoute?.rootRunId,
640
+ parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
623
641
  });
624
642
  const run = await runPiStreaming(
625
643
  args,
@@ -636,11 +654,13 @@ async function runSingleStep(
636
654
  cleanupTempDir(tempDir);
637
655
 
638
656
  const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
639
- const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError
657
+ const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
640
658
  ? evaluateCompletionMutationGuard({
641
659
  agent: step.agent,
642
660
  task,
643
661
  messages: run.messages,
662
+ tools: step.tools,
663
+ mcpDirectTools: step.mcpDirectTools,
644
664
  })
645
665
  : undefined;
646
666
  const completionGuardTriggered = completionGuard?.triggered === true && !run.observedMutationAttempt;
@@ -927,6 +947,32 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
927
947
 
928
948
  fs.mkdirSync(asyncDir, { recursive: true });
929
949
  writeAtomicJson(statusPath, statusPayload);
950
+ const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
951
+ if (!config.nestedRoute || !config.nestedSelf) return;
952
+ try {
953
+ writeNestedEvent(config.nestedRoute, {
954
+ type,
955
+ ts: Date.now(),
956
+ parentRunId: config.nestedSelf.parentRunId,
957
+ parentStepIndex: config.nestedSelf.parentStepIndex,
958
+ child: nestedSummaryFromAsyncStatus(statusPayload, asyncDir, {
959
+ id,
960
+ parentRunId: config.nestedSelf.parentRunId,
961
+ parentStepIndex: config.nestedSelf.parentStepIndex,
962
+ depth: config.nestedSelf.depth,
963
+ path: config.nestedSelf.path,
964
+ mode: statusPayload.mode,
965
+ ts: Date.now(),
966
+ }),
967
+ });
968
+ } catch (error) {
969
+ console.error("Failed to emit nested async status event:", error);
970
+ }
971
+ };
972
+ const writeStatusPayload = (): void => {
973
+ writeAtomicJson(statusPath, statusPayload);
974
+ emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
975
+ };
930
976
 
931
977
  const stepOutputActivityAt = (index: number): number => {
932
978
  const step = statusPayload.steps[index];
@@ -1017,7 +1063,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1017
1063
  step.model = model;
1018
1064
  step.thinking = thinking;
1019
1065
  statusPayload.lastUpdate = now;
1020
- writeAtomicJson(statusPath, statusPayload);
1066
+ writeStatusPayload();
1021
1067
  };
1022
1068
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
1023
1069
  const step = statusPayload.steps[flatIndex];
@@ -1105,7 +1151,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1105
1151
  statusPayload.lastActivityAt = now;
1106
1152
  statusPayload.lastUpdate = now;
1107
1153
  maybeEmitActiveLongRunning(flatIndex, now);
1108
- writeAtomicJson(statusPath, statusPayload);
1154
+ writeStatusPayload();
1109
1155
  };
1110
1156
  const updateRunnerActivityState = (now: number): boolean => {
1111
1157
  if (!controlConfig.enabled) return false;
@@ -1160,7 +1206,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1160
1206
  changed = true;
1161
1207
  }
1162
1208
  statusPayload.lastUpdate = now;
1163
- if (changed) writeAtomicJson(statusPath, statusPayload);
1209
+ if (changed) writeStatusPayload();
1164
1210
  return changed;
1165
1211
  };
1166
1212
  if (controlConfig.enabled) {
@@ -1189,7 +1235,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1189
1235
  step.lastActivityAt = now;
1190
1236
  }
1191
1237
  }
1192
- writeAtomicJson(statusPath, statusPayload);
1238
+ writeStatusPayload();
1193
1239
  appendJsonl(eventsPath, JSON.stringify({
1194
1240
  type: "subagent.run.paused",
1195
1241
  ts: now,
@@ -1300,7 +1346,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1300
1346
  statusPayload.steps[fi].exitCode = -1;
1301
1347
  statusPayload.steps[fi].activityState = undefined;
1302
1348
  statusPayload.lastUpdate = skippedAt;
1303
- writeAtomicJson(statusPath, statusPayload);
1349
+ writeStatusPayload();
1304
1350
  appendJsonl(eventsPath, JSON.stringify({
1305
1351
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: -1, durationMs: 0,
1306
1352
  }));
@@ -1320,7 +1366,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1320
1366
  statusPayload.outputFile = path.join(asyncDir, `output-${fi}.log`);
1321
1367
  statusPayload.lastActivityAt = taskStartTime;
1322
1368
  statusPayload.lastUpdate = taskStartTime;
1323
- writeAtomicJson(statusPath, statusPayload);
1369
+ writeStatusPayload();
1324
1370
 
1325
1371
  appendJsonl(eventsPath, JSON.stringify({
1326
1372
  type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent,
@@ -1341,6 +1387,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1341
1387
  piArgv1: config.piArgv1,
1342
1388
  childIntercomTarget: config.childIntercomTargets?.[fi],
1343
1389
  orchestratorIntercomTarget: config.controlIntercomTarget,
1390
+ nestedRoute: config.nestedRoute,
1344
1391
  registerInterrupt: (interrupt) => {
1345
1392
  activeChildInterrupt = interrupt;
1346
1393
  },
@@ -1364,7 +1411,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1364
1411
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1365
1412
  statusPayload.steps[fi].error = singleResult.error;
1366
1413
  statusPayload.lastUpdate = taskEndTime;
1367
- writeAtomicJson(statusPath, statusPayload);
1414
+ writeStatusPayload();
1368
1415
 
1369
1416
  appendJsonl(eventsPath, JSON.stringify({
1370
1417
  type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
@@ -1408,7 +1455,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1408
1455
  }
1409
1456
  statusPayload.totalTokens = { ...previousCumulativeTokens };
1410
1457
  statusPayload.lastUpdate = Date.now();
1411
- writeAtomicJson(statusPath, statusPayload);
1458
+ writeStatusPayload();
1412
1459
 
1413
1460
  for (const pr of parallelResults) {
1414
1461
  results.push({
@@ -1466,7 +1513,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1466
1513
  statusPayload.lastActivityAt = stepStartTime;
1467
1514
  statusPayload.lastUpdate = stepStartTime;
1468
1515
  statusPayload.outputFile = path.join(asyncDir, `output-${flatIndex}.log`);
1469
- writeAtomicJson(statusPath, statusPayload);
1516
+ writeStatusPayload();
1470
1517
 
1471
1518
  appendJsonl(eventsPath, JSON.stringify({
1472
1519
  type: "subagent.step.started",
@@ -1486,6 +1533,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1486
1533
  piArgv1: config.piArgv1,
1487
1534
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
1488
1535
  orchestratorIntercomTarget: config.controlIntercomTarget,
1536
+ nestedRoute: config.nestedRoute,
1489
1537
  registerInterrupt: (interrupt) => {
1490
1538
  activeChildInterrupt = interrupt;
1491
1539
  },
@@ -1546,7 +1594,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1546
1594
  statusPayload.totalTokens = { ...previousCumulativeTokens };
1547
1595
  }
1548
1596
  statusPayload.lastUpdate = stepEndTime;
1549
- writeAtomicJson(statusPath, statusPayload);
1597
+ writeStatusPayload();
1550
1598
 
1551
1599
  appendJsonl(eventsPath, JSON.stringify({
1552
1600
  type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
@@ -1648,7 +1696,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1648
1696
  statusPayload.error = `Step failed: ${failedStep.agent}`;
1649
1697
  }
1650
1698
  }
1651
- writeAtomicJson(statusPath, statusPayload);
1699
+ writeStatusPayload();
1652
1700
  appendJsonl(
1653
1701
  eventsPath,
1654
1702
  JSON.stringify({
@@ -51,6 +51,7 @@ import {
51
51
  type ControlEvent,
52
52
  type Details,
53
53
  type IntercomEventBus,
54
+ type NestedRouteInfo,
54
55
  type ResolvedControlConfig,
55
56
  type SingleResult,
56
57
  MAX_CONCURRENCY,
@@ -112,6 +113,7 @@ interface ParallelChainRunInput {
112
113
  totalSteps: number;
113
114
  worktreeSetup?: WorktreeSetup;
114
115
  maxSubagentDepth: number;
116
+ nestedRoute?: NestedRouteInfo;
115
117
  }
116
118
 
117
119
  function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
@@ -244,6 +246,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
244
246
  onControlEvent: input.onControlEvent,
245
247
  intercomSessionName: input.childIntercomTarget?.(task.agent, input.globalTaskIndex + taskIndex),
246
248
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
249
+ nestedRoute: input.nestedRoute,
247
250
  modelOverride: effectiveModel,
248
251
  availableModels: input.availableModels,
249
252
  preferredModelProvider: input.ctx.model?.provider,
@@ -331,6 +334,7 @@ interface ChainExecutionParams {
331
334
  chainSkills?: string[];
332
335
  chainDir?: string;
333
336
  maxSubagentDepth: number;
337
+ nestedRoute?: NestedRouteInfo;
334
338
  worktreeSetupHook?: string;
335
339
  worktreeSetupHookTimeoutMs?: number;
336
340
  }
@@ -591,6 +595,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
591
595
  childIntercomTarget,
592
596
  orchestratorIntercomTarget,
593
597
  foregroundControl,
598
+ nestedRoute: params.nestedRoute,
594
599
  worktreeSetup,
595
600
  maxSubagentDepth: params.maxSubagentDepth,
596
601
  });
@@ -793,6 +798,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
793
798
  onControlEvent,
794
799
  intercomSessionName: childIntercomTarget?.(seqStep.agent, globalTaskIndex),
795
800
  orchestratorIntercomTarget,
801
+ nestedRoute: params.nestedRoute,
796
802
  modelOverride: effectiveModel,
797
803
  availableModels,
798
804
  preferredModelProvider: ctx.model?.provider,
@@ -151,12 +151,17 @@ async function runSingleAttempt(
151
151
  extensions: agent.extensions,
152
152
  systemPrompt: shared.systemPrompt,
153
153
  mcpDirectTools: agent.mcpDirectTools,
154
+ cwd: options.cwd ?? runtimeCwd,
154
155
  promptFileStem: agent.name,
155
156
  intercomSessionName: options.intercomSessionName,
156
157
  orchestratorIntercomTarget: options.orchestratorIntercomTarget,
157
158
  runId: options.runId,
158
159
  childAgentName: agent.name,
159
160
  childIndex: options.index ?? 0,
161
+ parentEventSink: options.nestedRoute?.eventSink,
162
+ parentControlInbox: options.nestedRoute?.controlInbox,
163
+ parentRootRunId: options.nestedRoute?.rootRunId,
164
+ parentCapabilityToken: options.nestedRoute?.capabilityToken,
160
165
  });
161
166
 
162
167
  const result: SingleResult = {
@@ -207,6 +212,7 @@ async function runSingleAttempt(
207
212
  cwd: options.cwd ?? runtimeCwd,
208
213
  env: spawnEnv,
209
214
  stdio: ["ignore", "pipe", "pipe"],
215
+ windowsHide: true,
210
216
  });
211
217
  const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
212
218
  let buf = "";
@@ -214,6 +220,7 @@ async function runSingleAttempt(
214
220
  let settled = false;
215
221
  let detached = false;
216
222
  let intercomStarted = false;
223
+ let assistantError: string | undefined;
217
224
  let removeAbortListener: (() => void) | undefined;
218
225
  let removeInterruptListener: (() => void) | undefined;
219
226
  let activityTimer: NodeJS.Timeout | undefined;
@@ -259,7 +266,7 @@ async function runSingleAttempt(
259
266
  const termSent = trySignalChild(proc, "SIGTERM");
260
267
  if (!termSent) return;
261
268
  forcedTerminationSignal = true;
262
- if (!cleanTerminalAssistantStopReceived) {
269
+ if (!cleanTerminalAssistantStopReceived && !assistantError) {
263
270
  result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its final message. Forcing termination.`;
264
271
  }
265
272
  finalHardKillTimer = setTimeout(() => {
@@ -465,13 +472,15 @@ async function runSingleAttempt(
465
472
  progress.tokens = result.usage.input + result.usage.output;
466
473
  }
467
474
  if (!result.model && evt.message.model) result.model = evt.message.model;
468
- if (evt.message.errorMessage) result.error = evt.message.errorMessage;
469
- appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
475
+ if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
476
+ const assistantText = extractTextFromContent(evt.message.content);
477
+ appendRecentOutput(progress, assistantText.split("\n").slice(-10));
470
478
  // Final assistant message: start the exit drain window.
471
479
  const stopReason = (evt.message as { stopReason?: string }).stopReason;
472
480
  const hasToolCall = Array.isArray(evt.message.content)
473
481
  && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
474
482
  if (stopReason === "stop" && !hasToolCall) {
483
+ if (!evt.message.errorMessage && assistantText.trim()) assistantError = undefined;
475
484
  cleanTerminalAssistantStopReceived ||= !evt.message.errorMessage;
476
485
  startFinalDrain();
477
486
  }
@@ -551,6 +560,7 @@ async function runSingleAttempt(
551
560
  }
552
561
  processClosed = true;
553
562
  if (buf.trim()) processLine(buf);
563
+ if (!result.error && assistantError) result.error = assistantError;
554
564
  const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !result.error;
555
565
  if (code !== 0 && stderrBuf.trim() && !result.error && !forcedDrainAfterFinalSuccess) {
556
566
  result.error = stderrBuf.trim();
@@ -662,8 +672,14 @@ async function runSingleAttempt(
662
672
  };
663
673
 
664
674
  let fullOutput = getFinalOutput(result.messages);
665
- const completionGuard = result.exitCode === 0 && !result.error
666
- ? evaluateCompletionMutationGuard({ agent: agent.name, task, messages: result.messages })
675
+ const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
676
+ ? evaluateCompletionMutationGuard({
677
+ agent: agent.name,
678
+ task,
679
+ messages: result.messages,
680
+ tools: agent.tools,
681
+ mcpDirectTools: agent.mcpDirectTools,
682
+ })
667
683
  : undefined;
668
684
  if (completionGuard?.triggered && !observedMutationAttempt) {
669
685
  result.exitCode = 1;