@oh-my-pi/pi-coding-agent 17.0.2 → 17.0.4

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 (80) hide show
  1. package/CHANGELOG.md +80 -20
  2. package/dist/cli.js +4087 -4108
  3. package/dist/types/config/settings-schema.d.ts +7 -3
  4. package/dist/types/eval/agent-bridge.d.ts +7 -19
  5. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
  6. package/dist/types/lsp/client.d.ts +2 -0
  7. package/dist/types/lsp/types.d.ts +3 -0
  8. package/dist/types/mcp/transports/stdio.d.ts +29 -0
  9. package/dist/types/mnemopi/state.d.ts +32 -9
  10. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  11. package/dist/types/registry/persisted-agents.d.ts +3 -0
  12. package/dist/types/sdk.d.ts +14 -3
  13. package/dist/types/session/session-entries.d.ts +6 -1
  14. package/dist/types/session/session-manager.d.ts +5 -0
  15. package/dist/types/task/executor.d.ts +26 -1
  16. package/dist/types/task/index.d.ts +1 -1
  17. package/dist/types/task/parallel.d.ts +14 -0
  18. package/dist/types/task/structured-subagent.d.ts +111 -0
  19. package/dist/types/task/types.d.ts +51 -0
  20. package/dist/types/tools/fetch.d.ts +4 -5
  21. package/dist/types/tools/hub/messaging.d.ts +5 -1
  22. package/dist/types/tools/index.d.ts +16 -1
  23. package/dist/types/tools/todo.d.ts +31 -0
  24. package/dist/types/tui/tree-list.d.ts +7 -0
  25. package/dist/types/utils/markit.d.ts +11 -0
  26. package/package.json +12 -12
  27. package/src/cli/file-processor.ts +1 -2
  28. package/src/config/settings-schema.ts +4 -3
  29. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  30. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  31. package/src/eval/agent-bridge.ts +104 -477
  32. package/src/eval/jl/prelude.jl +7 -6
  33. package/src/eval/js/shared/prelude.txt +5 -4
  34. package/src/eval/py/prelude.py +11 -39
  35. package/src/eval/rb/prelude.rb +5 -6
  36. package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
  37. package/src/lsp/client.ts +57 -0
  38. package/src/lsp/index.ts +62 -6
  39. package/src/lsp/types.ts +3 -0
  40. package/src/mcp/oauth-flow.ts +20 -0
  41. package/src/mcp/transports/stdio.test.ts +269 -1
  42. package/src/mcp/transports/stdio.ts +152 -1
  43. package/src/mnemopi/backend.ts +1 -1
  44. package/src/mnemopi/embed-worker.ts +8 -7
  45. package/src/mnemopi/state.ts +45 -18
  46. package/src/modes/components/agent-hub.ts +1 -72
  47. package/src/modes/components/bash-execution.ts +7 -3
  48. package/src/modes/components/eval-execution.ts +3 -1
  49. package/src/modes/components/transcript-container.ts +13 -7
  50. package/src/modes/interactive-mode.ts +30 -8
  51. package/src/prompts/system/system-prompt.md +1 -1
  52. package/src/prompts/tools/eval.md +5 -2
  53. package/src/prompts/tools/read.md +1 -1
  54. package/src/prompts/tools/task.md +12 -8
  55. package/src/prompts/tools/write.md +1 -1
  56. package/src/registry/persisted-agents.ts +74 -0
  57. package/src/sdk.ts +129 -87
  58. package/src/session/agent-session.ts +75 -21
  59. package/src/session/session-entries.ts +6 -1
  60. package/src/session/session-loader.ts +31 -3
  61. package/src/session/session-manager.ts +9 -0
  62. package/src/session/streaming-output.ts +41 -1
  63. package/src/stt/recorder.ts +68 -55
  64. package/src/system-prompt.ts +7 -2
  65. package/src/task/executor.ts +99 -21
  66. package/src/task/index.ts +258 -429
  67. package/src/task/parallel.ts +43 -0
  68. package/src/task/persisted-revive.ts +19 -7
  69. package/src/task/structured-subagent.ts +642 -0
  70. package/src/task/types.ts +61 -0
  71. package/src/tools/__tests__/eval-description.test.ts +1 -1
  72. package/src/tools/fetch.ts +28 -105
  73. package/src/tools/hub/messaging.ts +16 -3
  74. package/src/tools/index.ts +47 -14
  75. package/src/tools/path-utils.ts +1 -0
  76. package/src/tools/read.ts +14 -26
  77. package/src/tools/todo.ts +126 -13
  78. package/src/tui/tree-list.ts +39 -0
  79. package/src/utils/markit.ts +12 -0
  80. package/src/utils/zip.ts +16 -1
package/src/task/index.ts CHANGED
@@ -13,17 +13,12 @@
13
13
  * - Progress tracking via JSON events
14
14
  * - Session artifacts for debugging
15
15
  */
16
- import * as fs from "node:fs/promises";
17
- import * as os from "node:os";
18
16
  import path from "node:path";
19
17
  import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
20
18
  import type { Usage } from "@oh-my-pi/pi-ai";
21
- import { $env, logger, prompt, Snowflake } from "@oh-my-pi/pi-utils";
19
+ import { $env, logger, prompt } from "@oh-my-pi/pi-utils";
22
20
  import type { ToolSession } from "..";
23
- import { resolveAgentModelPatterns } from "../config/model-resolver";
24
- import { MCPManager } from "../mcp/manager";
25
21
  import type { Theme } from "../modes/theme/theme";
26
- import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
27
22
  import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
28
23
  import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
29
24
  import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
@@ -45,25 +40,14 @@ import {
45
40
  // Import review tools for side effects (registers subagent tool handlers)
46
41
  import "../tools/review";
47
42
  import type { AsyncJobManager } from "../async";
48
- import type { LocalProtocolOptions } from "../internal-urls";
49
- import { loadOverallPlanReference } from "../plan-mode/plan-handoff";
50
- import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
51
- import { type DiscoveryResult, discoverAgents, getAgent } from "./discovery";
52
- import { runSubprocess } from "./executor";
53
- import {
54
- applyEligibleNestedPatches,
55
- type IsolationContext,
56
- makeIsolationCommitMessage,
57
- mergeIsolatedChanges,
58
- prepareIsolationContext,
59
- runIsolatedSubprocess,
60
- } from "./isolation-runner";
43
+ import { AgentRegistry } from "../registry/agent-registry";
44
+ import { type DiscoveryResult, discoverAgents } from "./discovery";
61
45
  import { generateTaskName } from "./name-generator";
62
46
  import { AgentOutputManager } from "./output-manager";
63
- import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
47
+ import { mapWithConcurrencyLimitAllSettled, Semaphore } from "./parallel";
64
48
  import { renderResult, renderCall as renderTaskCall } from "./render";
65
49
  import { repairTaskParams } from "./repair-args";
66
- import { parseIsolationMode } from "./worktree";
50
+ import { resolveEffectiveSubagentPolicy, runStructuredSubagent, StructuredSubagentError } from "./structured-subagent";
67
51
 
68
52
  function renderSubagentUserPrompt(assignment: string): string {
69
53
  return prompt.render(subagentUserPromptTemplate, {
@@ -154,8 +138,6 @@ export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
154
138
  "rewind",
155
139
  ]);
156
140
 
157
- const PLAN_MODE_AGENT_TOOL_ALLOWLIST: ReadonlySet<string> = new Set(["ast_grep"]);
158
-
159
141
  export function isReadOnlyAgent(agent: AgentDefinition): boolean {
160
142
  return !!agent.tools?.length && agent.tools.every(tool => READ_ONLY_TOOL_NAMES.has(tool));
161
143
  }
@@ -202,6 +184,7 @@ function renderDescription(
202
184
  agents: renderedAgents,
203
185
  spawningDisabled,
204
186
  defaultAgent: spawnPolicy.defaultAgent,
187
+ allowedAgentsText: spawnPolicy.allowedPromptText,
205
188
  isolationEnabled,
206
189
  batchEnabled,
207
190
  asyncEnabled,
@@ -218,14 +201,13 @@ function createTaskModeError(text: string): AgentToolResult<TaskToolDetails> {
218
201
  }
219
202
 
220
203
  /**
221
- * Reject fields the current configuration does not accept. `schema` is never
222
- * accepted (structured output comes from the agent definition's `output`
223
- * frontmatter, the inherited session schema, or an eval-workflow
224
- * `agent(..., schema)` call); `tasks`/`context` require `task.batch`.
204
+ * Reject legacy fields and shape/configuration combinations the current tool
205
+ * cannot accept. `outputSchema` is a first-class per-spawn field; stale
206
+ * `schema` remains an eval-only alias and is rejected.
225
207
  */
226
208
  function validateShapeParams(batchEnabled: boolean, params: TaskParams): string | undefined {
227
- if ((params as Record<string, unknown>).schema !== undefined) {
228
- return "The task tool does not accept `schema`. Rely on the selected agent definition's `output` schema or the inherited session schema; workflows needing ad-hoc structured output use eval `agent(prompt, schema)`.";
209
+ if (Object.hasOwn(params, "schema")) {
210
+ return "The task tool uses `outputSchema`; rename the stale `schema` field.";
229
211
  }
230
212
  if (!batchEnabled) {
231
213
  const disallowed = (["tasks", "context"] as const).filter(field => params[field] !== undefined);
@@ -297,6 +279,8 @@ function resolveSpawnItems(params: TaskParams): TaskItem[] {
297
279
  return params.tasks;
298
280
  }
299
281
  const item: TaskItem = { name: params.name, agent: params.agent, task: params.task };
282
+ if ("outputSchema" in params) item.outputSchema = params.outputSchema;
283
+ if ("schemaMode" in params) item.schemaMode = params.schemaMode;
300
284
  if ("isolated" in params) item.isolated = params.isolated;
301
285
  return [item];
302
286
  }
@@ -315,6 +299,8 @@ function spawnParamsFor(params: TaskParams, item: TaskItem, defaultAgent: string
315
299
  if (item.name !== undefined) spawn.name = item.name;
316
300
  if (item.task !== undefined) spawn.task = item.task;
317
301
  if (params.context !== undefined) spawn.context = params.context;
302
+ if ("outputSchema" in item) spawn.outputSchema = item.outputSchema;
303
+ if ("schemaMode" in item) spawn.schemaMode = item.schemaMode;
318
304
  if (item.isolated !== undefined) {
319
305
  spawn.isolated = item.isolated;
320
306
  } else if ("isolated" in params) {
@@ -531,7 +517,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
531
517
  };
532
518
  readonly label = "Task";
533
519
  readonly summary = "Spawn subagents to complete delegated tasks";
534
- readonly strict = true;
520
+ readonly strict = false;
535
521
  readonly loadMode = "essential";
536
522
  readonly renderResult = renderResult;
537
523
  // Suppress the streaming call preview once a (partial or final) result exists
@@ -550,7 +536,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
550
536
  #spawnSemaphore: Semaphore | undefined;
551
537
 
552
538
  get parameters(): TaskToolSchemaInstance {
553
- const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
539
+ const planMode = this.session.getPlanModeState?.()?.enabled === true;
540
+ const isolationEnabled = !planMode && this.session.settings.get("task.isolation.mode") !== "none";
554
541
  const defaultAgent = resolveSpawnPolicy(this.session.getSessionSpawns()).defaultAgent;
555
542
  return getTaskSchema({ isolationEnabled, batchEnabled: this.#isBatchEnabled(), defaultAgent });
556
543
  }
@@ -562,10 +549,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
562
549
  /** Dynamic description that reflects current disabled-agent settings */
563
550
  get description(): string {
564
551
  const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
552
+ const planMode = this.session.getPlanModeState?.()?.enabled === true;
565
553
  const isolationMode = this.session.settings.get("task.isolation.mode");
566
554
  return renderDescription(
567
555
  this.#discoveredAgents,
568
- isolationMode !== "none",
556
+ !planMode && isolationMode !== "none",
569
557
  disabledAgents,
570
558
  this.#isBatchEnabled(),
571
559
  this.session.settings.get("async.enabled"),
@@ -599,6 +587,29 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
599
587
  this.#getSpawnSemaphore().release();
600
588
  }
601
589
 
590
+ /**
591
+ * Resolve the shared policy before detached work exists. The resulting
592
+ * policy intentionally stays local: executor dispatch resolves again from
593
+ * normalized task params rather than smuggling internal policy over the
594
+ * task wire contract.
595
+ */
596
+ #resolveSpawnPreflight(params: TaskParams) {
597
+ return resolveEffectiveSubagentPolicy({
598
+ session: this.session,
599
+ invocationKind: "task",
600
+ assignment: (params.task ?? "").trim(),
601
+ context: this.#isBatchEnabled() ? params.context?.trim() || undefined : undefined,
602
+ agent: params.agent,
603
+ ...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
604
+ ...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
605
+ ...("isolated" in params ? { isolation: { requested: params.isolated } } : {}),
606
+ blockedAgent: this.#blockedAgent,
607
+ enableLsp: (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp"),
608
+ enableIrc: isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
609
+ maxRuntimeMs: this.session.settings.get("task.maxRuntimeMs"),
610
+ });
611
+ }
612
+
602
613
  /**
603
614
  * Create a TaskTool instance with async agent discovery.
604
615
  */
@@ -625,34 +636,103 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
625
636
  }
626
637
 
627
638
  const spawnItems = resolveSpawnItems(params);
628
- const resolvedAgents = spawnItems.map(item => item.agent?.trim() || defaultAgent);
639
+ const normalizedSpawnParams = spawnItems.map(item => spawnParamsFor(params, item, defaultAgent));
640
+ const resolvedAgents = normalizedSpawnParams.map(spawn => spawn.agent ?? defaultAgent);
629
641
  // Execution mode is per item: an item whose agent type declares
630
642
  // `blocking: true` runs inline on this turn (the parent waits on its
631
643
  // result); every other item becomes a background job when async
632
644
  // execution is available.
633
- const itemBlocking = resolvedAgents.map(
645
+ const provisionalBlocking = resolvedAgents.map(
634
646
  name => this.#discoveredAgents.find(agent => agent.name === name)?.blocking === true,
635
647
  );
636
648
  const asyncEnabled = this.session.settings.get("async.enabled");
637
649
  const manager = asyncEnabled ? this.session.asyncJobManager : undefined;
638
- const asyncItems = manager ? spawnItems.filter((_, index) => !itemBlocking[index]) : [];
650
+ const provisionalAsyncItems = manager ? spawnItems.filter((_, index) => !provisionalBlocking[index]) : [];
639
651
  const depthCapacity = canSpawnAtDepth(
640
652
  this.session.settings.get("task.maxRecursionDepth") ?? 2,
641
653
  this.session.taskDepth ?? 0,
642
654
  );
643
655
  const ircEnabled = isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0);
656
+
657
+ if (!manager || provisionalAsyncItems.length === 0) {
658
+ // Sync fallback: async execution disabled, orphaned host that never
659
+ // wired a job manager, or every item's agent type declares
660
+ // `blocking: true`. `runStructuredSubagent` performs its own shared
661
+ // preflight before reserving an id in these inline paths.
662
+ if (asyncEnabled && !this.session.asyncJobManager) {
663
+ logger.warn("task: no AsyncJobManager registered; falling back to sync execution");
664
+ }
665
+ const advisory = this.session.suppressSpawnAdvisory
666
+ ? undefined
667
+ : composeSpawnAdvisory({
668
+ agents: resolvedAgents,
669
+ items: provisionalAsyncItems,
670
+ depthCapacity,
671
+ ircEnabled,
672
+ willRunAsync: false,
673
+ });
674
+ const result = await this.#executeSyncFanout(
675
+ toolCallId,
676
+ params,
677
+ spawnItems.map((item, index) => ({ item, index })),
678
+ defaultAgent,
679
+ signal,
680
+ onUpdate,
681
+ );
682
+ if (!advisory) return result;
683
+ let appended = false;
684
+ const content = result.content.map(part => {
685
+ if (!appended && part.type === "text" && typeof part.text === "string") {
686
+ appended = true;
687
+ return { ...part, text: `${part.text}\n\n${advisory}` };
688
+ }
689
+ return part;
690
+ });
691
+ if (!appended) content.push({ type: "text", text: advisory });
692
+ return { ...result, content };
693
+ }
694
+
695
+ // Async jobs are otherwise registered before their body can reach
696
+ // `runStructuredSubagent`. Resolve the shared policy first so policy
697
+ // failures remain synchronous and cannot leave a queued invalid job.
698
+ const preflights = await Promise.all(
699
+ normalizedSpawnParams.map(async spawn => {
700
+ try {
701
+ return { policy: await this.#resolveSpawnPreflight(spawn) };
702
+ } catch (error) {
703
+ return { error: error instanceof StructuredSubagentError ? error.message : String(error) };
704
+ }
705
+ }),
706
+ );
707
+ const preflightFailures = preflights
708
+ .map((preflight, index) => ("error" in preflight ? { index, error: preflight.error } : undefined))
709
+ .filter((failure): failure is { index: number; error: string } => failure !== undefined);
710
+ const renderPreflightFailures = () =>
711
+ preflightFailures
712
+ .map(({ index, error }) => {
713
+ const item = spawnItems[index]!;
714
+ return `Task ${item.name?.trim() || `#${index + 1}`} failed preflight: ${error}`;
715
+ })
716
+ .join("\n");
717
+ if (preflightFailures.length === spawnItems.length) {
718
+ return createTaskModeError(renderPreflightFailures());
719
+ }
720
+
721
+ const validIndices = preflights.flatMap((preflight, index) => (preflight.policy ? [index] : []));
722
+ const validSpawns = validIndices.map(index => ({ item: spawnItems[index]!, index }));
723
+ const itemBlocking = preflights.map(preflight => preflight.policy?.effectiveAgent.blocking === true);
724
+ const asyncItems = validIndices.filter(index => !itemBlocking[index]).map(index => spawnItems[index]!);
644
725
  // Coordination only makes sense for spawns that keep running after this
645
726
  // call returns (the async subset). Blocking items have already completed
646
727
  // by then, so a "coordinate while they run" hint would misfire.
647
- const willRunAsync = asyncItems.length > 0;
648
728
  const advisory = this.session.suppressSpawnAdvisory
649
729
  ? undefined
650
730
  : composeSpawnAdvisory({
651
- agents: resolvedAgents,
731
+ agents: validIndices.map(index => resolvedAgents[index]!),
652
732
  items: asyncItems,
653
733
  depthCapacity,
654
734
  ircEnabled,
655
- willRunAsync,
735
+ willRunAsync: asyncItems.length > 0,
656
736
  });
657
737
  // Returns a fresh result (copied content array, copied text part) rather
658
738
  // than mutating the caller's — task results are short-lived here, but an
@@ -670,22 +750,35 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
670
750
  if (!appended) content.push({ type: "text", text: advisory });
671
751
  return { ...result, content };
672
752
  };
673
- if (!manager || asyncItems.length === 0) {
674
- // Sync fallback: async execution disabled, orphaned host that never
675
- // wired a job manager, or every item's agent type declares
676
- // `blocking: true`. The session-scoped semaphore still bounds fan-out
677
- // across parallel task calls.
678
- if (asyncEnabled && !this.session.asyncJobManager) {
679
- logger.warn("task: no AsyncJobManager registered; falling back to sync execution");
680
- }
681
- return withAdvisory(
682
- await this.#executeSyncFanout(toolCallId, params, spawnItems, defaultAgent, signal, onUpdate),
753
+ const withPreflightFailures = (result: AgentToolResult<TaskToolDetails>): AgentToolResult<TaskToolDetails> => {
754
+ if (preflightFailures.length === 0) return result;
755
+ const failures = renderPreflightFailures();
756
+ let prepended = false;
757
+ const content = result.content.map(part => {
758
+ if (!prepended && part.type === "text" && typeof part.text === "string") {
759
+ prepended = true;
760
+ return { ...part, text: `${failures}\n\n${part.text}` };
761
+ }
762
+ return part;
763
+ });
764
+ if (!prepended) content.unshift({ type: "text", text: failures });
765
+ return { ...result, content };
766
+ };
767
+ if (asyncItems.length === 0) {
768
+ return withPreflightFailures(
769
+ withAdvisory(
770
+ await this.#executeSyncFanout(toolCallId, params, validSpawns, defaultAgent, signal, onUpdate),
771
+ ),
683
772
  );
684
773
  }
685
774
 
686
- // Resolve agent ids up front so the immediate result can name them.
687
- const outputManager =
688
- this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
775
+ // Async IDs are claimed before job registration, so retain the fallback
776
+ // manager on the session rather than recreating it for every call.
777
+ let outputManager = this.session.agentOutputManager;
778
+ if (!outputManager) {
779
+ outputManager = new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
780
+ this.session.agentOutputManager = outputManager;
781
+ }
689
782
  const callStartedAt = Date.now();
690
783
  const spawns: Array<{
691
784
  agentId: string;
@@ -694,10 +787,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
694
787
  blocking: boolean;
695
788
  progress: AgentProgress;
696
789
  }> = [];
697
- for (let index = 0; index < spawnItems.length; index++) {
698
- const item = spawnItems[index];
699
- const agentType = resolvedAgents[index];
700
- const agentSource = this.#discoveredAgents.find(agent => agent.name === agentType)?.source ?? "bundled";
790
+ for (const index of validIndices) {
791
+ const item = spawnItems[index]!;
792
+ const agentType = resolvedAgents[index]!;
793
+ const preflight = preflights[index]!;
794
+ const policy = preflight.policy;
795
+ if (!policy) continue;
796
+ const agentSource = policy.agent.source;
701
797
  const agentId = await outputManager.allocate(item.name?.trim() || generateTaskName());
702
798
  const assignment = (item.task ?? "").trim();
703
799
  spawns.push({
@@ -733,7 +829,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
733
829
  // failed. Blocking spawns run inline below and land in `results` before
734
830
  // the call returns, so post-return job updates never drop them.
735
831
  let settledCount = 0;
736
- let failedCount = 0;
832
+ let failedCount = preflightFailures.length;
737
833
  let primaryJobId = asyncSpawns[0].agentId;
738
834
  const syncResults: SingleResult[] = [];
739
835
  let syncUsage: Usage | undefined;
@@ -783,7 +879,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
783
879
  }
784
880
 
785
881
  if (started.length === 0 && syncSpawns.length === 0) {
786
- return {
882
+ return withPreflightFailures({
787
883
  content: [
788
884
  {
789
885
  type: "text",
@@ -791,7 +887,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
791
887
  },
792
888
  ],
793
889
  details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
794
- };
890
+ });
795
891
  }
796
892
 
797
893
  const scheduleFailureSummary =
@@ -814,30 +910,34 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
814
910
  content: [{ type: "text", text: `Spawned agent \`${agentId}\`...` }],
815
911
  details: buildAsyncDetails(),
816
912
  });
817
- return withAdvisory({
818
- content: [
819
- {
820
- type: "text",
821
- text: `Spawned agent \`${agentId}\` (job \`${jobId}\`). The result will be delivered when it yields. ${coordinationHint}`,
822
- },
823
- ],
824
- details: buildAsyncDetails(),
825
- });
913
+ return withPreflightFailures(
914
+ withAdvisory({
915
+ content: [
916
+ {
917
+ type: "text",
918
+ text: `Spawned agent \`${agentId}\` (job \`${jobId}\`). The result will be delivered when it yields. ${coordinationHint}`,
919
+ },
920
+ ],
921
+ details: buildAsyncDetails(),
922
+ }),
923
+ );
826
924
  }
827
925
  const startedListing = started.map(({ agentId, jobId }) => `- \`${agentId}\` (job \`${jobId}\`)`).join("\n");
828
926
  onUpdate?.({
829
927
  content: [{ type: "text", text: `Spawned ${started.length} agents...` }],
830
928
  details: buildAsyncDetails(),
831
929
  });
832
- return withAdvisory({
833
- content: [
834
- {
835
- type: "text",
836
- text: `Spawned ${started.length} background agents using ${agentLabel}.${scheduleFailureSummary} Each result will be delivered when that agent yields.\n${startedListing}\n${coordinationHint}`,
837
- },
838
- ],
839
- details: buildAsyncDetails(),
840
- });
930
+ return withPreflightFailures(
931
+ withAdvisory({
932
+ content: [
933
+ {
934
+ type: "text",
935
+ text: `Spawned ${started.length} background agents using ${agentLabel}.${scheduleFailureSummary} Each result will be delivered when that agent yields.\n${startedListing}\n${coordinationHint}`,
936
+ },
937
+ ],
938
+ details: buildAsyncDetails(),
939
+ }),
940
+ );
841
941
  }
842
942
 
843
943
  // Mixed call: the async jobs above already run detached; the blocking
@@ -861,7 +961,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
861
961
  spawns: syncSpawns.map(spawn => ({ item: spawn.item, index: spawn.index, preAllocatedId: spawn.agentId })),
862
962
  onItemProgress: onUpdate
863
963
  ? (index, progress) => {
864
- const spawn = spawns[index];
964
+ const spawn = spawns.find(candidate => candidate.index === index);
865
965
  if (spawn) spawn.progress = { ...progress, index };
866
966
  onUpdate({
867
967
  content: [{ type: "text", text: `Running ${syncLabel} inline...` }],
@@ -902,10 +1002,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
902
1002
  const text = [merged.contentParts.join("\n\n"), spawnedSummary]
903
1003
  .filter(section => section.trim().length > 0)
904
1004
  .join("\n\n");
905
- return withAdvisory({
906
- content: [{ type: "text", text: text.length > 0 ? text : "No results." }],
907
- details: buildAsyncDetails(),
908
- });
1005
+ return withPreflightFailures(
1006
+ withAdvisory({
1007
+ content: [{ type: "text", text: text.length > 0 ? text : "No results." }],
1008
+ details: buildAsyncDetails(),
1009
+ }),
1010
+ );
909
1011
  }
910
1012
 
911
1013
  /**
@@ -1050,12 +1152,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1050
1152
  async #executeSyncFanout(
1051
1153
  toolCallId: string,
1052
1154
  params: TaskParams,
1053
- spawnItems: TaskItem[],
1155
+ spawns: SyncSpawnRef[],
1054
1156
  defaultAgent: string,
1055
1157
  signal?: AbortSignal,
1056
1158
  onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
1057
1159
  ): Promise<AgentToolResult<TaskToolDetails>> {
1058
- if (spawnItems.length === 1) {
1160
+ if (spawns.length === 1) {
1161
+ const spawn = spawns[0]!;
1059
1162
  const semaphore = this.#getSpawnSemaphore();
1060
1163
  const invokedAt = Date.now();
1061
1164
  await semaphore.acquire(signal);
@@ -1063,11 +1166,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1063
1166
  try {
1064
1167
  return await this.#executeSync(
1065
1168
  toolCallId,
1066
- spawnParamsFor(params, spawnItems[0], defaultAgent),
1169
+ spawnParamsFor(params, spawn.item, defaultAgent),
1067
1170
  signal,
1068
1171
  onUpdate,
1069
- undefined,
1070
- 0,
1172
+ spawn.preAllocatedId,
1173
+ spawn.index,
1071
1174
  false,
1072
1175
  { invokedAt, acquiredAt },
1073
1176
  );
@@ -1080,7 +1183,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1080
1183
  const latestProgress = new Map<number, AgentProgress>();
1081
1184
  const emitCombined = () => {
1082
1185
  onUpdate?.({
1083
- content: [{ type: "text", text: `Running ${spawnItems.length} agents...` }],
1186
+ content: [{ type: "text", text: `Running ${spawns.length} agents...` }],
1084
1187
  details: {
1085
1188
  projectAgentsDir: null,
1086
1189
  results: [],
@@ -1097,7 +1200,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1097
1200
  params,
1098
1201
  defaultAgent,
1099
1202
  signal,
1100
- spawns: spawnItems.map((item, index) => ({ item, index })),
1203
+ spawns,
1101
1204
  onItemProgress: onUpdate
1102
1205
  ? (index, progress) => {
1103
1206
  latestProgress.set(index, { ...progress, index });
@@ -1106,10 +1209,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1106
1209
  : undefined,
1107
1210
  });
1108
1211
 
1109
- const merged = mergeSyncPayloads(
1110
- spawnItems.map((item, index) => ({ item, index })),
1111
- payloads,
1112
- );
1212
+ const merged = mergeSyncPayloads(spawns, payloads);
1113
1213
  return {
1114
1214
  content: [{ type: "text", text: merged.contentParts.join("\n\n") }],
1115
1215
  details: {
@@ -1140,12 +1240,19 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1140
1240
  }): Promise<(AgentToolResult<TaskToolDetails> | undefined)[]> {
1141
1241
  const { toolCallId, params, defaultAgent, spawns, signal, onItemProgress } = args;
1142
1242
  const semaphore = this.#getSpawnSemaphore();
1143
- const { results } = await mapWithConcurrencyLimit(
1243
+ const { results } = await mapWithConcurrencyLimitAllSettled(
1144
1244
  spawns,
1145
1245
  spawns.length,
1146
1246
  async (spawn, _position, workerSignal) => {
1147
1247
  const invokedAt = Date.now();
1148
- await semaphore.acquire(workerSignal);
1248
+ let semaphoreHeld = false;
1249
+ try {
1250
+ await semaphore.acquire(workerSignal);
1251
+ semaphoreHeld = true;
1252
+ } catch (error) {
1253
+ if (workerSignal.aborted) return undefined;
1254
+ throw error;
1255
+ }
1149
1256
  const acquiredAt = Date.now();
1150
1257
  try {
1151
1258
  const itemOnUpdate: AgentToolUpdateCallback<TaskToolDetails> | undefined = onItemProgress
@@ -1165,12 +1272,26 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1165
1272
  { invokedAt, acquiredAt },
1166
1273
  );
1167
1274
  } finally {
1168
- this.#releaseSpawnSemaphore();
1275
+ if (semaphoreHeld) this.#releaseSpawnSemaphore();
1169
1276
  }
1170
1277
  },
1171
1278
  signal,
1172
1279
  );
1173
- return results;
1280
+ return results.map((settled, position) => {
1281
+ if (!settled) return undefined;
1282
+ if (settled.status === "fulfilled") return settled.value;
1283
+ const message = settled.reason instanceof Error ? settled.reason.message : String(settled.reason);
1284
+ const item = spawns[position].item;
1285
+ return {
1286
+ content: [
1287
+ {
1288
+ type: "text",
1289
+ text: `Task ${item.name?.trim() || `#${spawns[position].index + 1}`} failed: ${message}`,
1290
+ },
1291
+ ],
1292
+ details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
1293
+ };
1294
+ });
1174
1295
  }
1175
1296
 
1176
1297
  /**
@@ -1204,351 +1325,59 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1204
1325
  launchTiming?: { invokedAt: number; acquiredAt: number },
1205
1326
  ): Promise<AgentToolResult<TaskToolDetails>> {
1206
1327
  const startTime = Date.now();
1207
- const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
1208
- const agentName = params.agent ?? "";
1209
- const sharedContext = this.#isBatchEnabled() ? params.context?.trim() || undefined : undefined;
1210
1328
  const assignment = (params.task ?? "").trim();
1211
- const isolationMode = this.session.settings.get("task.isolation.mode");
1212
- const isolationRequested = "isolated" in params ? params.isolated === true : false;
1213
- const isIsolated = isolationMode !== "none" && isolationRequested;
1214
- const mergeMode = this.session.settings.get("task.isolation.merge");
1215
- const taskDepth = this.session.taskDepth ?? 0;
1216
- const subagentLspEnabled = (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp");
1217
-
1218
- if (isolationMode === "none" && "isolated" in params) {
1219
- return {
1220
- content: [{ type: "text", text: "Task isolation is disabled." }],
1221
- details: { projectAgentsDir, results: [], totalDurationMs: 0 },
1222
- };
1223
- }
1224
-
1225
- // Validate agent exists
1226
- const agent = getAgent(agents, agentName);
1227
- if (!agent) {
1228
- const available = agents.map(a => a.name).join(", ") || "none";
1229
- return {
1230
- content: [{ type: "text", text: `Unknown agent "${agentName}". Available: ${available}` }],
1231
- details: { projectAgentsDir, results: [], totalDurationMs: 0 },
1232
- };
1233
- }
1234
-
1235
- // Check if agent is disabled in settings
1236
- const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
1237
- if (disabledAgents.length > 0 && disabledAgents.includes(agentName)) {
1238
- const enabled = agents.filter(a => !disabledAgents.includes(a.name)).map(a => a.name);
1239
- return {
1240
- content: [
1241
- {
1242
- type: "text",
1243
- text: `Agent "${agentName}" is disabled in settings. Enable it via /agents, or use a different agent type.${enabled.length > 0 ? ` Available: ${enabled.join(", ")}` : ""}`,
1244
- },
1245
- ],
1246
- details: { projectAgentsDir, results: [], totalDurationMs: 0 },
1247
- };
1248
- }
1249
-
1250
- const planModeState = this.session.getPlanModeState?.();
1251
- const planModeBaseTools = ["read", "grep", "glob", "lsp", "web_search"];
1252
- const planModeTools = [
1253
- ...planModeBaseTools,
1254
- ...(agent.tools ?? []).filter(
1255
- tool => PLAN_MODE_AGENT_TOOL_ALLOWLIST.has(tool) && !planModeBaseTools.includes(tool),
1256
- ),
1257
- ];
1258
- const effectiveAgent: typeof agent = planModeState?.enabled
1259
- ? {
1260
- ...agent,
1261
- systemPrompt: `${planModeSubagentPrompt}\n\n${agent.systemPrompt}`,
1262
- tools: planModeTools,
1263
- spawns: undefined,
1264
- // Read-only exploration: never arm prewalk (its plan/implement
1265
- // nudges assume edit tools the plan-mode toolset doesn't have).
1266
- prewalk: undefined,
1267
- }
1268
- : agent;
1269
-
1270
- // Apply per-agent model override from settings (highest priority)
1271
- const agentModelOverrides = this.session.settings.get("task.agentModelOverrides");
1272
- const settingsModelOverride = agentModelOverrides[agentName];
1273
- const parentActiveModelPattern = this.session.getActiveModelString?.();
1274
- const modelOverride = resolveAgentModelPatterns({
1275
- settingsOverride: settingsModelOverride,
1276
- agentModel: effectiveAgent.model,
1277
- settings: this.session.settings,
1278
- activeModelPattern: parentActiveModelPattern,
1279
- fallbackModelPattern: this.session.getModelString?.(),
1280
- });
1281
- const thinkingLevelOverride = effectiveAgent.thinkingLevel;
1282
-
1283
- // Output schema priority: agent frontmatter > inherited parent session.
1284
- // The task call itself never carries a schema; workflows needing ad-hoc
1285
- // structured output go through eval agent(prompt, schema).
1286
- const effectiveOutputSchema = effectiveAgent.output ?? this.session.outputSchema;
1287
-
1288
- let isolationContext: IsolationContext | null = null;
1289
- if (isIsolated) {
1290
- try {
1291
- isolationContext = await prepareIsolationContext(this.session.cwd);
1292
- } catch (err) {
1293
- const message = err instanceof Error ? err.message : String(err);
1294
- return {
1295
- content: [{ type: "text", text: `Isolated task execution requires a git repository. ${message}` }],
1296
- details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
1297
- };
1298
- }
1299
- }
1300
- const repoRoot = isolationContext?.repoRoot ?? null;
1301
-
1302
- const preferredIsolationBackend = parseIsolationMode(isolationMode);
1303
-
1304
- // Derive artifacts directory
1305
- const sessionFile = this.session.getSessionFile();
1306
- const artifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
1307
- const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `omp-task-${Snowflake.next()}`);
1308
- const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
1309
-
1310
- const localProtocolOptions: LocalProtocolOptions = this.session.localProtocolOptions ?? {
1311
- getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
1312
- getSessionId: this.session.getSessionId ?? (() => null),
1313
- };
1314
-
1315
- // Subagents adopt the parent's ArtifactManager so artifact IDs are unique
1316
- // across the whole tree and outputs land flat in the parent's dir.
1317
- const parentArtifactManager = this.session.getArtifactManager?.() ?? undefined;
1318
-
1319
- // When the session is executing an approved plan, hand the overall plan to
1320
- // every subagent so they share the main agent's plan context. Skipped in
1321
- // plan mode (read-only exploration uses planModeSubagentPrompt instead) and
1322
- // when no plan file exists at the session's reference path.
1323
- const planReference = planModeState?.enabled
1324
- ? undefined
1325
- : await loadOverallPlanReference(
1326
- this.session.getPlanReferencePath?.() ?? "local://PLAN.md",
1327
- localProtocolOptions,
1328
- );
1329
-
1329
+ const context = this.#isBatchEnabled() ? params.context?.trim() || undefined : undefined;
1330
+ let latestProgress: AgentProgress | undefined;
1330
1331
  try {
1331
- // Check self-recursion prevention
1332
- if (this.#blockedAgent && agentName === this.#blockedAgent) {
1333
- return {
1334
- content: [
1335
- {
1336
- type: "text",
1337
- text: `Cannot spawn ${this.#blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
1338
- },
1339
- ],
1340
- details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
1341
- };
1342
- }
1343
-
1344
- // Check spawn restrictions from parent
1345
- const spawnPolicy = resolveSpawnPolicy(this.session.getSessionSpawns());
1346
- const spawnAllowed =
1347
- spawnPolicy.enabled &&
1348
- (spawnPolicy.allowedAgents === null || spawnPolicy.allowedAgents.includes(agentName));
1349
- if (!spawnAllowed) {
1350
- return {
1351
- content: [
1352
- { type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${spawnPolicy.allowedErrorText}` },
1353
- ],
1354
- details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
1355
- };
1356
- }
1357
-
1358
- await fs.mkdir(effectiveArtifactsDir, { recursive: true });
1359
-
1360
- // Allocate a unique ID across the session to prevent artifact collisions
1361
- let agentId: string;
1362
- if (preAllocatedId) {
1363
- agentId = preAllocatedId;
1364
- } else {
1365
- const outputManager =
1366
- this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
1367
- agentId = await outputManager.allocate(params.name?.trim() || generateTaskName());
1368
- }
1369
-
1370
- const availableSkills = [...(this.session.skills ?? [])];
1371
- // Resolve autoload skills from agent definition against available skills
1372
- const resolvedAutoloadSkills =
1373
- agent.autoloadSkills?.length && availableSkills.length > 0
1374
- ? agent.autoloadSkills
1375
- .map(name => availableSkills.find(s => s.name === name))
1376
- .filter((s): s is NonNullable<typeof s> => s !== undefined)
1377
- : [];
1378
- const contextFiles = this.session.contextFiles?.filter(
1379
- file => path.basename(file.path).toLowerCase() !== "agents.md",
1380
- );
1381
- const promptTemplates = this.session.promptTemplates;
1382
- const parentEvalSessionId = this.session.getEvalSessionId?.() ?? undefined;
1383
- const mcpManager = this.session.mcpManager ?? MCPManager.instance();
1384
-
1385
- // Progress tracking for the single agent
1386
- let latestProgress: AgentProgress = {
1387
- index: spawnIndex,
1388
- id: agentId,
1389
- agent: agentName,
1390
- agentSource: agent.source,
1391
- status: "pending",
1392
- task: renderSubagentUserPrompt(assignment),
1332
+ const execution = await runStructuredSubagent({
1333
+ session: this.session,
1334
+ invocationKind: "task",
1393
1335
  assignment,
1394
- recentTools: [],
1395
- recentOutput: [],
1396
- toolCount: 0,
1397
- requests: 0,
1398
- tokens: 0,
1399
- cost: 0,
1400
- durationMs: 0,
1401
- modelOverride,
1402
- };
1403
- const emitProgress = () => {
1404
- onUpdate?.({
1405
- content: [{ type: "text", text: `Running agent ${agentId}...` }],
1406
- details: {
1407
- projectAgentsDir,
1408
- results: [],
1409
- totalDurationMs: Date.now() - startTime,
1410
- progress: [latestProgress],
1411
- },
1412
- });
1413
- };
1414
- emitProgress();
1415
-
1416
- const buildCommitMessageFn = makeIsolationCommitMessage(this.session);
1417
-
1418
- const sharedRunOptions = {
1419
- cwd: this.session.cwd,
1420
- agent: effectiveAgent,
1421
- task: renderSubagentUserPrompt(assignment),
1422
- assignment,
1423
- context: sharedContext,
1424
- planReference,
1336
+ context,
1337
+ agent: params.agent,
1338
+ ...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
1339
+ ...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
1340
+ identity: { id: preAllocatedId, label: params.name },
1425
1341
  index: spawnIndex,
1426
1342
  parentToolCallId: toolCallId,
1427
1343
  detached,
1428
- id: agentId,
1429
- taskDepth,
1430
1344
  invokedAt: launchTiming?.invokedAt,
1431
1345
  acquiredAt: launchTiming?.acquiredAt,
1432
- modelOverride,
1433
- parentActiveModelPattern,
1434
- thinkingLevel: thinkingLevelOverride,
1435
- outputSchema: effectiveOutputSchema,
1436
- sessionFile,
1437
- persistArtifacts: !!artifactsDir,
1438
- artifactsDir: effectiveArtifactsDir,
1439
- enableLsp: subagentLspEnabled,
1346
+ ...("isolated" in params ? { isolation: { requested: params.isolated } } : {}),
1347
+ blockedAgent: this.#blockedAgent,
1348
+ enableLsp: (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp"),
1349
+ enableIrc: isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
1350
+ maxRuntimeMs: this.session.settings.get("task.maxRuntimeMs"),
1440
1351
  signal,
1441
- eventBus: this.session.eventBus,
1442
- onProgress: (progress: AgentProgress) => {
1443
- // Shallow snapshot; recentTools is mutated in place by the
1444
- // executor, the rest is reassigned or immutable. A deep clone
1445
- // here cost O(extractedToolData) per progress event.
1352
+ onProgress: progress => {
1446
1353
  latestProgress = { ...progress, recentTools: progress.recentTools.slice() };
1447
- emitProgress();
1354
+ onUpdate?.({
1355
+ content: [{ type: "text", text: `Running agent ${progress.id}...` }],
1356
+ details: {
1357
+ projectAgentsDir: null,
1358
+ results: [],
1359
+ totalDurationMs: Date.now() - startTime,
1360
+ progress: [latestProgress],
1361
+ },
1362
+ });
1448
1363
  },
1449
- authStorage: this.session.authStorage,
1450
- modelRegistry: this.session.modelRegistry,
1451
- settings: this.session.settings,
1452
- mcpManager,
1453
- contextFiles,
1454
- skills: availableSkills,
1455
- autoloadSkills: resolvedAutoloadSkills,
1456
- workspaceTree: this.session.workspaceTree,
1457
- promptTemplates,
1458
- rules: this.session.rules,
1459
- preloadedExtensionPaths: this.session.extensionPaths,
1460
- preloadedCustomToolPaths: this.session.customToolPaths,
1461
- localProtocolOptions,
1462
- parentArtifactManager,
1463
- parentHindsightSessionState: this.session.getHindsightSessionState?.(),
1464
- parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
1465
- parentTelemetry: this.session.getTelemetry?.(),
1466
- parentEvalSessionId,
1467
- parentAgentId: this.session.getAgentId?.() ?? MAIN_AGENT_ID,
1468
- // Live source of truth for `tier.subagent: inherit`. When the session
1469
- // exposes a tier accessor, pass the per-family map or null (null =
1470
- // explicit none, e.g. /fast off); otherwise leave undefined so inherit
1471
- // falls back to the subagent's configured tier.* settings.
1472
- parentServiceTier: this.session.getServiceTierByFamily
1473
- ? (this.session.getServiceTierByFamily() ?? null)
1474
- : undefined,
1475
- };
1476
-
1477
- const runTask = async (): Promise<SingleResult> => {
1478
- if (!isIsolated) {
1479
- return runSubprocess(sharedRunOptions);
1480
- }
1481
- if (!isolationContext) {
1482
- throw new Error("Isolated task execution not initialized.");
1483
- }
1484
- const taskStart = Date.now();
1485
- return runIsolatedSubprocess({
1486
- baseOptions: sharedRunOptions,
1487
- context: isolationContext,
1488
- preferredBackend: preferredIsolationBackend,
1489
- agentId,
1490
- mergeMode,
1491
- artifactsDir: effectiveArtifactsDir,
1492
- buildCommitMessage: buildCommitMessageFn,
1493
- buildFailureResult: err => {
1494
- const message = err instanceof Error ? err.message : String(err);
1495
- return {
1496
- index: spawnIndex,
1497
- id: agentId,
1498
- agent: agent.name,
1499
- agentSource: agent.source,
1500
- task: renderSubagentUserPrompt(assignment),
1501
- assignment,
1502
- exitCode: 1,
1503
- output: "",
1504
- stderr: message,
1505
- truncated: false,
1506
- durationMs: Date.now() - taskStart,
1507
- tokens: 0,
1508
- requests: 0,
1509
- modelOverride,
1510
- error: message,
1511
- };
1512
- },
1513
- });
1514
- };
1515
-
1516
- const result = await runTask();
1517
-
1518
- let mergeSummary = "";
1519
- let changesApplied: boolean | null = null;
1520
- let mergedBranchForNestedPatches = false;
1521
- if (isIsolated && repoRoot) {
1522
- const outcome = await mergeIsolatedChanges({ result, repoRoot, mergeMode });
1523
- mergeSummary = outcome.summary;
1524
- changesApplied = outcome.changesApplied;
1525
- mergedBranchForNestedPatches = outcome.mergedBranchForNestedPatches;
1526
- }
1527
-
1528
- // Apply nested repo patches (separate from parent git).
1529
- if (isIsolated && repoRoot) {
1530
- mergeSummary += await applyEligibleNestedPatches({
1531
- result,
1532
- repoRoot,
1533
- mergeMode,
1534
- changesApplied,
1535
- mergedBranchForNestedPatches,
1536
- commitMessage: buildCommitMessageFn(),
1537
- });
1538
- }
1539
-
1540
- // Cleanup temp directory if used
1541
- const shouldCleanupTempArtifacts =
1542
- tempArtifactsDir && (!isIsolated || changesApplied === true || changesApplied === null);
1543
- if (shouldCleanupTempArtifacts) {
1544
- await fs.rm(tempArtifactsDir, { recursive: true, force: true });
1545
- }
1546
-
1547
- return this.#buildResultPayload(result, projectAgentsDir, Date.now() - startTime, mergeSummary);
1548
- } catch (err) {
1364
+ });
1365
+ return this.#buildResultPayload(
1366
+ execution.result,
1367
+ execution.policy.discovery.projectAgentsDir,
1368
+ Date.now() - startTime,
1369
+ execution.mergeSummary,
1370
+ );
1371
+ } catch (error) {
1372
+ const message = error instanceof StructuredSubagentError ? error.message : String(error);
1549
1373
  return {
1550
- content: [{ type: "text", text: `Task execution failed: ${err}` }],
1551
- details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
1374
+ content: [{ type: "text", text: `Task execution failed: ${message}` }],
1375
+ details: {
1376
+ projectAgentsDir: null,
1377
+ results: [],
1378
+ totalDurationMs: Date.now() - startTime,
1379
+ ...(latestProgress ? { progress: [latestProgress] } : {}),
1380
+ },
1552
1381
  };
1553
1382
  }
1554
1383
  }