@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.
- package/CHANGELOG.md +80 -20
- package/dist/cli.js +4087 -4108
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +29 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +14 -3
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-manager.d.ts +5 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/package.json +12 -12
- package/src/cli/file-processor.ts +1 -2
- package/src/config/settings-schema.ts +4 -3
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
- package/src/lsp/client.ts +57 -0
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +152 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/transcript-container.ts +13 -7
- package/src/modes/interactive-mode.ts +30 -8
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +129 -87
- package/src/session/agent-session.ts +75 -21
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-loader.ts +31 -3
- package/src/session/session-manager.ts +9 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/stt/recorder.ts +68 -55
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +99 -21
- package/src/task/index.ts +258 -429
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +61 -0
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/fetch.ts +28 -105
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/index.ts +47 -14
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +14 -26
- package/src/tools/todo.ts +126 -13
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- 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
|
|
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
|
|
49
|
-
import {
|
|
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 {
|
|
47
|
+
import { mapWithConcurrencyLimitAllSettled, Semaphore } from "./parallel";
|
|
64
48
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
65
49
|
import { repairTaskParams } from "./repair-args";
|
|
66
|
-
import {
|
|
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
|
|
222
|
-
*
|
|
223
|
-
*
|
|
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
|
|
228
|
-
return "The task tool
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
//
|
|
687
|
-
|
|
688
|
-
|
|
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 (
|
|
698
|
-
const item = spawnItems[index]
|
|
699
|
-
const agentType = resolvedAgents[index]
|
|
700
|
-
const
|
|
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 =
|
|
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
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
|
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
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
1155
|
+
spawns: SyncSpawnRef[],
|
|
1054
1156
|
defaultAgent: string,
|
|
1055
1157
|
signal?: AbortSignal,
|
|
1056
1158
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
1057
1159
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
1058
|
-
if (
|
|
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,
|
|
1169
|
+
spawnParamsFor(params, spawn.item, defaultAgent),
|
|
1067
1170
|
signal,
|
|
1068
1171
|
onUpdate,
|
|
1069
|
-
|
|
1070
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1212
|
-
|
|
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
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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: ${
|
|
1551
|
-
details: {
|
|
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
|
}
|