@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.1
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 +90 -4
- package/dist/cli.js +869 -825
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +66 -34
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +1 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -2
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +3 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/session/agent-session.d.ts +35 -30
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +23 -0
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/index.d.ts +11 -4
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +55 -51
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +1 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/tools/render-utils.d.ts +22 -0
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/capability/mcp.ts +1 -0
- package/src/cli/gallery-cli.ts +6 -5
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli.ts +20 -6
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/config/keybindings.ts +6 -1
- package/src/config/mcp-schema.json +4 -0
- package/src/config/settings-schema.ts +68 -41
- package/src/config/settings.ts +7 -0
- package/src/edit/renderer.ts +96 -46
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/prelude.py +5 -6
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +44 -14
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +8 -60
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/oauth-discovery.ts +27 -2
- package/src/mcp/oauth-flow.ts +47 -1
- package/src/mcp/transports/stdio.ts +3 -0
- package/src/mcp/types.ts +2 -0
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +15 -0
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/mcp-add-wizard.ts +13 -0
- package/src/modes/components/settings-selector.ts +2 -0
- package/src/modes/components/status-line/component.ts +22 -12
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/transcript-container.ts +99 -18
- package/src/modes/components/tree-selector.ts +6 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/event-controller.ts +93 -4
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +18 -2
- package/src/modes/controllers/mcp-command-controller.ts +34 -2
- package/src/modes/controllers/selector-controller.ts +25 -17
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +17 -15
- package/src/modes/theme/theme.ts +24 -5
- package/src/modes/types.ts +3 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +43 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +29 -9
- package/src/session/agent-session.ts +268 -241
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +60 -0
- package/src/task/executor.ts +855 -466
- package/src/task/index.ts +723 -794
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +142 -66
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +73 -66
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +15 -5
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/index.ts +4 -12
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/render-utils.ts +56 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/tools/write.ts +65 -47
- package/src/web/search/providers/anthropic.ts +29 -4
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
package/src/task/index.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* - .omp/agents/*.md (project-level)
|
|
8
8
|
*
|
|
9
9
|
* Supports:
|
|
10
|
-
* - Single agent
|
|
11
|
-
* -
|
|
10
|
+
* - Single agent spawn per call (parallelism = parallel task calls)
|
|
11
|
+
* - Batch spawning + shared context per call when `task.batch` is enabled
|
|
12
|
+
* - Background execution through AsyncJobManager when `async.enabled` is enabled
|
|
12
13
|
* - Progress tracking via JSON events
|
|
13
14
|
* - Session artifacts for debugging
|
|
14
15
|
*/
|
|
@@ -27,29 +28,33 @@ import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.m
|
|
|
27
28
|
import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
|
|
28
29
|
import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
|
|
29
30
|
import { truncateForPrompt } from "../tools/approval";
|
|
31
|
+
import { isIrcEnabled } from "../tools/irc";
|
|
30
32
|
import { formatBytes, formatDuration } from "../tools/render-utils";
|
|
31
33
|
import {
|
|
32
34
|
type AgentDefinition,
|
|
33
35
|
type AgentProgress,
|
|
34
36
|
getTaskSchema,
|
|
35
37
|
type SingleResult,
|
|
38
|
+
type TaskItem,
|
|
36
39
|
type TaskParams,
|
|
37
40
|
type TaskToolDetails,
|
|
38
41
|
type TaskToolSchemaInstance,
|
|
39
42
|
} from "./types";
|
|
40
43
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
41
44
|
import "../tools/review";
|
|
45
|
+
import type { AsyncJobManager } from "../async";
|
|
42
46
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
43
47
|
import { loadOverallPlanReference } from "../plan-mode/plan-handoff";
|
|
48
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
44
49
|
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
45
50
|
import * as git from "../utils/git";
|
|
46
51
|
import { type DiscoveryResult, discoverAgents, getAgent } from "./discovery";
|
|
47
52
|
import { runSubprocess } from "./executor";
|
|
53
|
+
import { generateTaskName } from "./name-generator";
|
|
48
54
|
import { AgentOutputManager } from "./output-manager";
|
|
49
55
|
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
50
56
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
51
57
|
import { repairTaskParams } from "./repair-args";
|
|
52
|
-
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
53
58
|
import {
|
|
54
59
|
applyNestedPatches,
|
|
55
60
|
captureBaseline,
|
|
@@ -65,12 +70,12 @@ import {
|
|
|
65
70
|
type WorktreeBaseline,
|
|
66
71
|
} from "./worktree";
|
|
67
72
|
|
|
68
|
-
function renderSubagentUserPrompt(assignment: string
|
|
73
|
+
function renderSubagentUserPrompt(assignment: string): string {
|
|
69
74
|
return prompt.render(subagentUserPromptTemplate, {
|
|
70
75
|
assignment: assignment.trim(),
|
|
71
|
-
independentMode: simpleMode === "independent",
|
|
72
76
|
});
|
|
73
77
|
}
|
|
78
|
+
|
|
74
79
|
function createUsageTotals(): Usage {
|
|
75
80
|
return {
|
|
76
81
|
input: 0,
|
|
@@ -165,6 +170,17 @@ export function isReadOnlyAgent(agent: AgentDefinition): boolean {
|
|
|
165
170
|
return !!agent.tools?.length && agent.tools.every(tool => READ_ONLY_TOOL_NAMES.has(tool));
|
|
166
171
|
}
|
|
167
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Preview text for a child result. Falls back to "(no output)" — annotated
|
|
175
|
+
* with the request count when the child actually did work, so the parent can
|
|
176
|
+
* tell a no-op child from one that burned requests before being cancelled.
|
|
177
|
+
*/
|
|
178
|
+
export function formatResultOutputFallback(result: Pick<SingleResult, "output" | "stderr" | "requests">): string {
|
|
179
|
+
const base = result.output.trim() || result.stderr.trim();
|
|
180
|
+
if (base) return base;
|
|
181
|
+
return result.requests > 0 ? `(no output) after ${result.requests} req` : "(no output)";
|
|
182
|
+
}
|
|
183
|
+
|
|
168
184
|
/**
|
|
169
185
|
* Render the tool description from a cached agent list and current settings.
|
|
170
186
|
*/
|
|
@@ -172,9 +188,9 @@ function renderDescription(
|
|
|
172
188
|
agents: AgentDefinition[],
|
|
173
189
|
maxConcurrency: number,
|
|
174
190
|
isolationEnabled: boolean,
|
|
175
|
-
asyncEnabled: boolean,
|
|
176
191
|
disabledAgents: string[],
|
|
177
|
-
|
|
192
|
+
batchEnabled: boolean,
|
|
193
|
+
asyncEnabled: boolean,
|
|
178
194
|
ircEnabled: boolean,
|
|
179
195
|
parentSpawns: string,
|
|
180
196
|
): string {
|
|
@@ -196,19 +212,14 @@ function renderDescription(
|
|
|
196
212
|
description: agent.description,
|
|
197
213
|
readOnly: isReadOnlyAgent(agent),
|
|
198
214
|
}));
|
|
199
|
-
const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
|
|
200
215
|
return prompt.render(taskDescriptionTemplate, {
|
|
201
216
|
agents: renderedAgents,
|
|
202
217
|
spawningDisabled,
|
|
203
218
|
MAX_CONCURRENCY: maxConcurrency,
|
|
204
219
|
isolationEnabled,
|
|
220
|
+
batchEnabled,
|
|
205
221
|
asyncEnabled,
|
|
206
|
-
contextEnabled,
|
|
207
|
-
customSchemaEnabled,
|
|
208
222
|
ircEnabled,
|
|
209
|
-
defaultMode: simpleMode === "default",
|
|
210
|
-
schemaFreeMode: simpleMode === "schema-free",
|
|
211
|
-
independentMode: simpleMode === "independent",
|
|
212
223
|
});
|
|
213
224
|
}
|
|
214
225
|
|
|
@@ -219,88 +230,120 @@ function createTaskModeError(text: string): AgentToolResult<TaskToolDetails> {
|
|
|
219
230
|
};
|
|
220
231
|
}
|
|
221
232
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (disallowedFields.length === 0) {
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (simpleMode === "schema-free") {
|
|
236
|
-
return "task.simple is set to schema-free, so the task tool does not accept `schema`. Remove it and rely on the selected agent definition or inherited session schema.";
|
|
233
|
+
/**
|
|
234
|
+
* Reject fields the current configuration does not accept. `schema` is never
|
|
235
|
+
* accepted (structured output comes from the agent definition's `output`
|
|
236
|
+
* frontmatter, the inherited session schema, or an eval-workflow
|
|
237
|
+
* `agent(..., schema)` call); `tasks`/`context` require `task.batch`.
|
|
238
|
+
*/
|
|
239
|
+
function validateShapeParams(batchEnabled: boolean, params: TaskParams): string | undefined {
|
|
240
|
+
if ((params as Record<string, unknown>).schema !== undefined) {
|
|
241
|
+
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)`.";
|
|
237
242
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
if (!batchEnabled) {
|
|
244
|
+
const disallowed = (["tasks", "context"] as const).filter(field => params[field] !== undefined);
|
|
245
|
+
if (disallowed.length > 0) {
|
|
246
|
+
return `task.batch is disabled, so the task tool does not accept ${disallowed.map(f => `\`${f}\``).join(" or ")}. Spawn one agent per call with \`assignment\`, or enable the task.batch setting.`;
|
|
247
|
+
}
|
|
241
248
|
}
|
|
242
|
-
|
|
243
|
-
return "task.simple is set to independent, so the task tool does not accept `context` or `schema`. Put all required background and output expectations inside each task assignment or the selected agent definition.";
|
|
249
|
+
return undefined;
|
|
244
250
|
}
|
|
245
251
|
|
|
246
|
-
/** Sentinel for async jobs whose subagent finished with a failing result; batch counters are already updated. */
|
|
247
|
-
class TaskJobError extends Error {}
|
|
248
|
-
|
|
249
252
|
/**
|
|
250
|
-
* Validate
|
|
251
|
-
*
|
|
253
|
+
* Validate the spawn parameter contract against the wire shapes. `agent` is
|
|
254
|
+
* always required. With `task.batch` the model-facing shape is
|
|
255
|
+
* `{ agent, context, tasks[] }` — `tasks` non-empty with per-item assignments
|
|
256
|
+
* and unique ids, `context` non-empty, no top-level `assignment` alongside.
|
|
257
|
+
* The flat `{ agent, ...item }` form stays accepted at runtime under either
|
|
258
|
+
* setting (internal callers, stale transcripts). Returns a problem
|
|
259
|
+
* description, or undefined when valid.
|
|
252
260
|
*/
|
|
253
|
-
function
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
function validateSpawnParams(params: TaskParams, batchEnabled: boolean): string | undefined {
|
|
262
|
+
const agent = typeof params.agent === "string" ? params.agent.trim() : "";
|
|
263
|
+
if (!agent) {
|
|
264
|
+
return "Missing `agent`. Provide an agent type to spawn.";
|
|
265
|
+
}
|
|
266
|
+
const hasAssignment = typeof params.assignment === "string" && params.assignment.trim() !== "";
|
|
267
|
+
const tasks = params.tasks;
|
|
268
|
+
if (batchEnabled && tasks !== undefined) {
|
|
269
|
+
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
270
|
+
return "Missing `tasks`. Provide at least one task item ({ id?, description?, assignment }).";
|
|
262
271
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (indexes) {
|
|
266
|
-
indexes.push(i);
|
|
267
|
-
} else {
|
|
268
|
-
idIndexes.set(normalizedId, [i]);
|
|
272
|
+
if (hasAssignment) {
|
|
273
|
+
return "Top-level `assignment` is not part of the batch shape. Put the work in `tasks[]` items.";
|
|
269
274
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
275
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
276
|
+
const item = tasks[i];
|
|
277
|
+
if (!item || typeof item.assignment !== "string" || item.assignment.trim() === "") {
|
|
278
|
+
return `Task ${i + 1}${item?.id ? ` (\`${item.id}\`)` : ""} is missing \`assignment\`. Every task needs complete, self-contained instructions.`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const seen = new Map<string, string>();
|
|
282
|
+
for (const item of tasks) {
|
|
283
|
+
const id = item.id?.trim();
|
|
284
|
+
if (!id) continue;
|
|
285
|
+
const key = id.toLowerCase();
|
|
286
|
+
const existing = seen.get(key);
|
|
287
|
+
if (existing !== undefined) {
|
|
288
|
+
return `Duplicate task id ${existing === id ? `\`${id}\`` : `\`${existing}\` / \`${id}\``}. Provided ids must be unique within a call (case-insensitive).`;
|
|
289
|
+
}
|
|
290
|
+
seen.set(key, id);
|
|
291
|
+
}
|
|
292
|
+
if (typeof params.context !== "string" || params.context.trim() === "") {
|
|
293
|
+
return "Missing `context`. Provide the shared background for this batch — goal, constraints, and any contract the tasks share.";
|
|
279
294
|
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (missingTaskIndexes.length === 0 && duplicateIds.length === 0) {
|
|
283
295
|
return undefined;
|
|
284
296
|
}
|
|
297
|
+
if (!hasAssignment) {
|
|
298
|
+
return batchEnabled
|
|
299
|
+
? "Missing `tasks`. Provide a `tasks` array (one subagent per item) with a shared `context`."
|
|
300
|
+
: "Missing `assignment`. Provide complete, self-contained instructions for the agent.";
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
285
304
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Normalize a validated call into its spawn list: the `tasks[]` batch when
|
|
307
|
+
* provided, otherwise the single top-level spawn.
|
|
308
|
+
*/
|
|
309
|
+
function resolveSpawnItems(params: TaskParams): TaskItem[] {
|
|
310
|
+
if (Array.isArray(params.tasks) && params.tasks.length > 0) {
|
|
311
|
+
return params.tasks;
|
|
289
312
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
313
|
+
return [{ id: params.id, description: params.description, assignment: params.assignment }];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Per-spawn params handed to the executor path: top-level call fields with the
|
|
318
|
+
* item's identity substituted in. `tasks` never leaks into a spawn; the shared
|
|
319
|
+
* `context` rides along unchanged. Keys are only materialized when present —
|
|
320
|
+
* `#runSpawn` distinguishes an absent `isolated` from an explicit one. The
|
|
321
|
+
* item's `isolated` (batch form) wins over the top-level flag (flat form).
|
|
322
|
+
*/
|
|
323
|
+
function spawnParamsFor(params: TaskParams, item: TaskItem): TaskParams {
|
|
324
|
+
const spawn: TaskParams = { agent: params.agent };
|
|
325
|
+
if (item.id !== undefined) spawn.id = item.id;
|
|
326
|
+
if (item.description !== undefined) spawn.description = item.description;
|
|
327
|
+
if (item.assignment !== undefined) spawn.assignment = item.assignment;
|
|
328
|
+
if (params.context !== undefined) spawn.context = params.context;
|
|
329
|
+
if (item.isolated !== undefined) {
|
|
330
|
+
spawn.isolated = item.isolated;
|
|
331
|
+
} else if ("isolated" in params) {
|
|
332
|
+
spawn.isolated = params.isolated;
|
|
293
333
|
}
|
|
294
|
-
return
|
|
334
|
+
return spawn;
|
|
295
335
|
}
|
|
296
336
|
|
|
337
|
+
/** Sentinel for async jobs whose subagent finished with a failing result; progress is already updated. */
|
|
338
|
+
class TaskJobError extends Error {}
|
|
339
|
+
|
|
297
340
|
/**
|
|
298
341
|
* Process-level memo for create-time agent discovery, keyed by resolved cwd.
|
|
299
342
|
*
|
|
300
343
|
* `TaskTool.create` runs for every (sub)agent session in this process and the
|
|
301
344
|
* walk-up + plugin-registry scan in `discoverAgents` is identical for a given
|
|
302
345
|
* cwd, so repeat creations reuse the first scan. Execution-time discovery
|
|
303
|
-
* (`#
|
|
346
|
+
* (`#runSpawn`) intentionally stays fresh. The memo also tracks the live
|
|
304
347
|
* `discoverAgents` binding: test spies swap that binding, which invalidates
|
|
305
348
|
* the memo automatically.
|
|
306
349
|
*/
|
|
@@ -332,8 +375,9 @@ function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
|
|
|
332
375
|
/**
|
|
333
376
|
* Task tool - Delegate tasks to specialized agents.
|
|
334
377
|
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
378
|
+
* Each call spawns one subagent — or, with `task.batch`, one per `tasks[]`
|
|
379
|
+
* item. When `async.enabled` is on, spawns run as AsyncJobManager jobs; when
|
|
380
|
+
* disabled, the tool blocks until every spawn finishes.
|
|
337
381
|
*/
|
|
338
382
|
export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetails, Theme> {
|
|
339
383
|
readonly name = "task";
|
|
@@ -344,11 +388,24 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
344
388
|
if (typeof params.agent === "string") {
|
|
345
389
|
lines.push(`Agent: ${truncateForPrompt(params.agent)}`);
|
|
346
390
|
}
|
|
391
|
+
if (typeof params.id === "string" && params.id.trim()) {
|
|
392
|
+
lines.push(`Task: ${truncateForPrompt(params.id)}`);
|
|
393
|
+
}
|
|
394
|
+
if (typeof params.assignment === "string") {
|
|
395
|
+
lines.push(`Assignment:\n${truncateForPrompt(params.assignment)}`);
|
|
396
|
+
}
|
|
397
|
+
if (typeof params.context === "string" && params.context.trim()) {
|
|
398
|
+
lines.push(`Context:\n${truncateForPrompt(params.context)}`);
|
|
399
|
+
}
|
|
347
400
|
const tasks = Array.isArray(params.tasks) ? params.tasks : [];
|
|
348
401
|
const firstTask = tasks[0];
|
|
349
402
|
if (firstTask) {
|
|
350
|
-
|
|
351
|
-
|
|
403
|
+
if (typeof firstTask.id === "string" && firstTask.id.trim()) {
|
|
404
|
+
lines.push(`Task: ${truncateForPrompt(firstTask.id)}`);
|
|
405
|
+
}
|
|
406
|
+
if (typeof firstTask.assignment === "string") {
|
|
407
|
+
lines.push(`Assignment:\n${truncateForPrompt(firstTask.assignment)}`);
|
|
408
|
+
}
|
|
352
409
|
if (tasks.length > 1) {
|
|
353
410
|
lines.push(`+${tasks.length - 1} more task${tasks.length === 2 ? "" : "s"}`);
|
|
354
411
|
}
|
|
@@ -356,7 +413,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
356
413
|
return lines;
|
|
357
414
|
};
|
|
358
415
|
readonly label = "Task";
|
|
359
|
-
readonly summary = "Spawn
|
|
416
|
+
readonly summary = "Spawn subagents to complete delegated tasks";
|
|
360
417
|
readonly strict = true;
|
|
361
418
|
readonly loadMode = "discoverable";
|
|
362
419
|
readonly renderResult = renderResult;
|
|
@@ -366,10 +423,16 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
366
423
|
readonly mergeCallAndResult = true;
|
|
367
424
|
readonly #discoveredAgents: AgentDefinition[];
|
|
368
425
|
readonly #blockedAgent: string | undefined;
|
|
426
|
+
/**
|
|
427
|
+
* One semaphore per TaskTool instance (i.e. per session): bounds concurrent
|
|
428
|
+
* subagents across parallel `task` calls within the session. Sized from
|
|
429
|
+
* `task.maxConcurrency` at first use; later setting changes do not resize it.
|
|
430
|
+
*/
|
|
431
|
+
#spawnSemaphore: Semaphore | undefined;
|
|
369
432
|
|
|
370
433
|
get parameters(): TaskToolSchemaInstance {
|
|
371
434
|
const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
|
|
372
|
-
return getTaskSchema({ isolationEnabled,
|
|
435
|
+
return getTaskSchema({ isolationEnabled, batchEnabled: this.#isBatchEnabled() });
|
|
373
436
|
}
|
|
374
437
|
|
|
375
438
|
renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme) {
|
|
@@ -385,10 +448,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
385
448
|
this.#discoveredAgents,
|
|
386
449
|
maxConcurrency,
|
|
387
450
|
isolationMode !== "none",
|
|
388
|
-
this.session.settings.get("async.enabled"),
|
|
389
451
|
disabledAgents,
|
|
390
|
-
this.#
|
|
391
|
-
this.session.settings.get("
|
|
452
|
+
this.#isBatchEnabled(),
|
|
453
|
+
this.session.settings.get("async.enabled"),
|
|
454
|
+
isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
|
|
392
455
|
this.session.getSessionSpawns() ?? "*",
|
|
393
456
|
);
|
|
394
457
|
}
|
|
@@ -400,8 +463,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
400
463
|
this.#discoveredAgents = discoveredAgents;
|
|
401
464
|
}
|
|
402
465
|
|
|
403
|
-
#
|
|
404
|
-
return this.session.settings.get("task.
|
|
466
|
+
#isBatchEnabled(): boolean {
|
|
467
|
+
return this.session.settings.get("task.batch");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
#getSpawnSemaphore(): Semaphore {
|
|
471
|
+
this.#spawnSemaphore ??= new Semaphore(this.session.settings.get("task.maxConcurrency"));
|
|
472
|
+
return this.#spawnSemaphore;
|
|
405
473
|
}
|
|
406
474
|
|
|
407
475
|
/**
|
|
@@ -419,327 +487,423 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
419
487
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
420
488
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
421
489
|
const params = repairTaskParams(rawParams as TaskParams);
|
|
422
|
-
const
|
|
423
|
-
const validationError =
|
|
490
|
+
const batchEnabled = this.#isBatchEnabled();
|
|
491
|
+
const validationError = validateShapeParams(batchEnabled, params) ?? validateSpawnParams(params, batchEnabled);
|
|
424
492
|
if (validationError) {
|
|
425
493
|
return createTaskModeError(validationError);
|
|
426
494
|
}
|
|
427
495
|
|
|
428
|
-
const
|
|
496
|
+
const spawnItems = resolveSpawnItems(params);
|
|
429
497
|
const selectedAgent = this.#discoveredAgents.find(agent => agent.name === params.agent);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return this.#executeSync(toolCallId, params, signal, onUpdate);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const taskItems = params.tasks ?? [];
|
|
445
|
-
if (taskItems.length === 0) {
|
|
446
|
-
return this.#executeSync(toolCallId, params, signal, onUpdate);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const taskIdProblem = validateTaskIds(taskItems);
|
|
450
|
-
if (taskIdProblem) {
|
|
451
|
-
return createTaskModeError(taskIdProblem);
|
|
498
|
+
const asyncEnabled = this.session.settings.get("async.enabled");
|
|
499
|
+
const manager = asyncEnabled ? this.session.asyncJobManager : undefined;
|
|
500
|
+
if (!asyncEnabled || !manager || selectedAgent?.blocking === true) {
|
|
501
|
+
// Sync fallback: async execution disabled, orphaned host that never
|
|
502
|
+
// wired a job manager, or an agent definition that declares
|
|
503
|
+
// `blocking: true`. The session-scoped semaphore still bounds fan-out
|
|
504
|
+
// across parallel task calls.
|
|
505
|
+
if (asyncEnabled && !manager) {
|
|
506
|
+
logger.warn("task: no AsyncJobManager registered; falling back to sync execution");
|
|
507
|
+
}
|
|
508
|
+
return this.#executeSyncFanout(toolCallId, params, spawnItems, signal, onUpdate);
|
|
452
509
|
}
|
|
453
510
|
|
|
511
|
+
// Resolve agent ids up front so the immediate result can name them.
|
|
454
512
|
const outputManager =
|
|
455
513
|
this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
const assignment =
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
514
|
+
const agentLabel = params.agent ?? "task";
|
|
515
|
+
const agentSource = selectedAgent?.source ?? "bundled";
|
|
516
|
+
const spawns: Array<{ agentId: string; item: TaskItem; progress: AgentProgress }> = [];
|
|
517
|
+
for (let index = 0; index < spawnItems.length; index++) {
|
|
518
|
+
const item = spawnItems[index];
|
|
519
|
+
const agentId = await outputManager.allocate(item.id?.trim() || generateTaskName());
|
|
520
|
+
const assignment = (item.assignment ?? "").trim();
|
|
521
|
+
spawns.push({
|
|
522
|
+
agentId,
|
|
523
|
+
item,
|
|
524
|
+
progress: {
|
|
525
|
+
index,
|
|
526
|
+
id: agentId,
|
|
527
|
+
agent: agentLabel,
|
|
528
|
+
agentSource,
|
|
529
|
+
status: "pending",
|
|
530
|
+
task: renderSubagentUserPrompt(assignment),
|
|
531
|
+
assignment,
|
|
532
|
+
description: item.description,
|
|
533
|
+
recentTools: [],
|
|
534
|
+
recentOutput: [],
|
|
535
|
+
toolCount: 0,
|
|
536
|
+
requests: 0,
|
|
537
|
+
tokens: 0,
|
|
538
|
+
cost: 0,
|
|
539
|
+
durationMs: 0,
|
|
540
|
+
},
|
|
478
541
|
});
|
|
479
542
|
}
|
|
480
543
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
// immutable once attached — structuredClone here cost O(batch × payload)
|
|
490
|
-
// per progress event.
|
|
491
|
-
return Array.from(progressByTaskId.values())
|
|
492
|
-
.sort((a, b) => a.index - b.index)
|
|
493
|
-
.map(progress => ({ ...progress }));
|
|
494
|
-
};
|
|
495
|
-
|
|
544
|
+
// Aggregate async state for the one tool call: every spawn's job reports
|
|
545
|
+
// into the shared progress snapshot; the call stays "running" until all
|
|
546
|
+
// jobs settle, then turns "failed" if any spawn failed. The single-spawn
|
|
547
|
+
// case passes the job's own suggestion through (pre-batch behavior).
|
|
548
|
+
const single = spawns.length === 1;
|
|
549
|
+
let settledCount = 0;
|
|
550
|
+
let failedCount = 0;
|
|
551
|
+
let primaryJobId = spawns[0].agentId;
|
|
496
552
|
const buildAsyncDetails = (state: "running" | "completed" | "failed", jobId: string): TaskToolDetails => ({
|
|
497
553
|
projectAgentsDir: null,
|
|
498
554
|
results: [],
|
|
499
555
|
totalDurationMs: 0,
|
|
500
|
-
progress:
|
|
501
|
-
async: {
|
|
556
|
+
progress: spawns.map(spawn => ({ ...spawn.progress })),
|
|
557
|
+
async: {
|
|
558
|
+
state: single ? state : settledCount < spawns.length ? "running" : failedCount > 0 ? "failed" : "completed",
|
|
559
|
+
jobId: single ? jobId : primaryJobId,
|
|
560
|
+
type: "task",
|
|
561
|
+
},
|
|
502
562
|
});
|
|
503
563
|
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
details: buildAsyncDetails(state, primaryJobId),
|
|
509
|
-
});
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
513
|
-
const semaphore = new Semaphore(maxConcurrency);
|
|
514
|
-
|
|
515
|
-
for (let i = 0; i < taskItems.length; i++) {
|
|
516
|
-
const taskItem = taskItems[i];
|
|
517
|
-
if (signal?.aborted) {
|
|
518
|
-
failedSchedules.push(`${taskItem.id}: cancelled before scheduling`);
|
|
519
|
-
completedJobs += 1;
|
|
520
|
-
const progress = progressByTaskId.get(taskItem.id);
|
|
521
|
-
if (progress) {
|
|
522
|
-
progress.status = "aborted";
|
|
523
|
-
}
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const uniqueId = uniqueIds[i];
|
|
528
|
-
const singleParams: TaskParams = { ...params, tasks: [taskItem] };
|
|
529
|
-
const label = uniqueId;
|
|
564
|
+
const ircEnabled = isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0);
|
|
565
|
+
const started: Array<{ agentId: string; jobId: string; description?: string }> = [];
|
|
566
|
+
const failedSchedules: string[] = [];
|
|
567
|
+
for (const spawn of spawns) {
|
|
530
568
|
try {
|
|
531
|
-
const jobId =
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
completedJobs += 1;
|
|
544
|
-
failedJobs += 1;
|
|
545
|
-
throw new Error("Aborted before execution");
|
|
546
|
-
}
|
|
547
|
-
markRunning();
|
|
548
|
-
if (progress) {
|
|
549
|
-
progress.status = "running";
|
|
550
|
-
}
|
|
551
|
-
await reportProgress(
|
|
552
|
-
`Running background task ${taskItem.id}...`,
|
|
553
|
-
buildAsyncDetails("running", startedJobs[0]?.jobId ?? label) as unknown as Record<string, unknown>,
|
|
554
|
-
);
|
|
555
|
-
try {
|
|
556
|
-
const result = await this.#executeSync(toolCallId, singleParams, runSignal, undefined, [uniqueId]);
|
|
557
|
-
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
558
|
-
const singleResult = result.details?.results[0];
|
|
559
|
-
// A missing per-task result means #executeSync failed at the
|
|
560
|
-
// tool level (results: []) — treat it as a failure, not success.
|
|
561
|
-
const resultFailed =
|
|
562
|
-
!singleResult || (singleResult.aborted ?? false) || singleResult.exitCode !== 0;
|
|
563
|
-
if (progress) {
|
|
564
|
-
progress.status = singleResult?.aborted ? "aborted" : resultFailed ? "failed" : "completed";
|
|
565
|
-
progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
|
|
566
|
-
progress.tokens = singleResult?.tokens ?? 0;
|
|
567
|
-
progress.contextTokens = singleResult?.contextTokens;
|
|
568
|
-
progress.contextWindow = singleResult?.contextWindow;
|
|
569
|
-
progress.cost = singleResult?.usage?.cost.total ?? 0;
|
|
570
|
-
progress.extractedToolData = singleResult?.extractedToolData;
|
|
571
|
-
progress.retryFailure = singleResult?.retryFailure;
|
|
572
|
-
progress.retryState = undefined;
|
|
573
|
-
}
|
|
574
|
-
completedJobs += 1;
|
|
575
|
-
if (resultFailed) {
|
|
576
|
-
failedJobs += 1;
|
|
577
|
-
}
|
|
578
|
-
const remaining = taskItems.length - completedJobs;
|
|
579
|
-
const isDone = remaining === 0;
|
|
580
|
-
await reportProgress(
|
|
581
|
-
isDone
|
|
582
|
-
? `Background task batch complete: ${completedJobs}/${taskItems.length} finished.`
|
|
583
|
-
: `Background task batch progress: ${completedJobs}/${taskItems.length} finished (${remaining} running).`,
|
|
584
|
-
buildAsyncDetails(
|
|
585
|
-
isDone ? (failedJobs > 0 || failedSchedules.length > 0 ? "failed" : "completed") : "running",
|
|
586
|
-
startedJobs[0]?.jobId ?? label,
|
|
587
|
-
) as unknown as Record<string, unknown>,
|
|
588
|
-
);
|
|
589
|
-
if (isDone) {
|
|
590
|
-
emitAsyncUpdate(
|
|
591
|
-
failedJobs > 0 || failedSchedules.length > 0 ? "failed" : "completed",
|
|
592
|
-
`Background task batch complete: ${completedJobs}/${taskItems.length} finished.`,
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
if (resultFailed) {
|
|
596
|
-
// Mark the job itself failed; counters above are already updated.
|
|
597
|
-
throw new TaskJobError(finalText);
|
|
598
|
-
}
|
|
599
|
-
return finalText;
|
|
600
|
-
} catch (error) {
|
|
601
|
-
if (error instanceof TaskJobError) {
|
|
602
|
-
throw error;
|
|
603
|
-
}
|
|
604
|
-
if (progress) {
|
|
605
|
-
progress.status = "failed";
|
|
606
|
-
progress.durationMs = Math.max(0, Date.now() - startedAt);
|
|
607
|
-
}
|
|
608
|
-
completedJobs += 1;
|
|
609
|
-
failedJobs += 1;
|
|
610
|
-
const remaining = taskItems.length - completedJobs;
|
|
611
|
-
const isDone = remaining === 0;
|
|
612
|
-
await reportProgress(
|
|
613
|
-
isDone
|
|
614
|
-
? `Background task batch complete with failures: ${failedJobs} failed.`
|
|
615
|
-
: `Background task batch progress: ${completedJobs}/${taskItems.length} finished (${remaining} running).`,
|
|
616
|
-
buildAsyncDetails(
|
|
617
|
-
isDone ? "failed" : "running",
|
|
618
|
-
startedJobs[0]?.jobId ?? label,
|
|
619
|
-
) as unknown as Record<string, unknown>,
|
|
620
|
-
);
|
|
621
|
-
if (isDone) {
|
|
622
|
-
emitAsyncUpdate(
|
|
623
|
-
"failed",
|
|
624
|
-
`Background task batch complete with failures: ${failedJobs} failed.`,
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
throw error;
|
|
628
|
-
} finally {
|
|
629
|
-
semaphore.release();
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
{
|
|
633
|
-
id: label,
|
|
634
|
-
queued: true,
|
|
635
|
-
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
636
|
-
onProgress: (text, details) => {
|
|
637
|
-
const progressDetails =
|
|
638
|
-
(details as TaskToolDetails | undefined) ??
|
|
639
|
-
buildAsyncDetails("running", startedJobs[0]?.jobId ?? label);
|
|
640
|
-
onUpdate?.({ content: [{ type: "text", text }], details: progressDetails });
|
|
641
|
-
},
|
|
569
|
+
const jobId = this.#registerSpawnJob({
|
|
570
|
+
manager,
|
|
571
|
+
toolCallId,
|
|
572
|
+
spawnParams: spawnParamsFor(params, spawn.item),
|
|
573
|
+
agentId: spawn.agentId,
|
|
574
|
+
progress: spawn.progress,
|
|
575
|
+
ircEnabled,
|
|
576
|
+
buildDetails: buildAsyncDetails,
|
|
577
|
+
onUpdate,
|
|
578
|
+
onSettled: failed => {
|
|
579
|
+
settledCount += 1;
|
|
580
|
+
if (failed) failedCount += 1;
|
|
642
581
|
},
|
|
643
|
-
);
|
|
644
|
-
|
|
582
|
+
});
|
|
583
|
+
if (started.length === 0) primaryJobId = jobId;
|
|
584
|
+
started.push({ agentId: spawn.agentId, jobId, description: spawn.item.description });
|
|
645
585
|
} catch (error) {
|
|
646
586
|
const message = error instanceof Error ? error.message : String(error);
|
|
647
|
-
failedSchedules.push(`${
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
progress.status = "failed";
|
|
652
|
-
}
|
|
587
|
+
failedSchedules.push(`${spawn.agentId}: ${message}`);
|
|
588
|
+
spawn.progress.status = "failed";
|
|
589
|
+
settledCount += 1;
|
|
590
|
+
failedCount += 1;
|
|
653
591
|
}
|
|
654
592
|
}
|
|
655
593
|
|
|
656
|
-
if (
|
|
657
|
-
const failureText = `Failed to start background task jobs: ${failedSchedules.join("; ")}`;
|
|
594
|
+
if (started.length === 0) {
|
|
658
595
|
return {
|
|
659
|
-
content: [
|
|
596
|
+
content: [
|
|
597
|
+
{
|
|
598
|
+
type: "text",
|
|
599
|
+
text: `Failed to start background task job${single ? "" : "s"}: ${failedSchedules.join("; ")}`,
|
|
600
|
+
},
|
|
601
|
+
],
|
|
660
602
|
details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
|
|
661
603
|
};
|
|
662
604
|
}
|
|
663
605
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
606
|
+
if (single) {
|
|
607
|
+
const { agentId, jobId, description } = started[0];
|
|
608
|
+
const coordinationHint = ircEnabled
|
|
609
|
+
? `DM \`${agentId}\` via \`irc\` to coordinate while it runs; use \`job\` only to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task.`
|
|
610
|
+
: `Use \`job\` to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task.`;
|
|
611
|
+
const descriptionSuffix = description ? ` — ${description}` : "";
|
|
612
|
+
onUpdate?.({
|
|
613
|
+
content: [{ type: "text", text: `Spawned agent \`${agentId}\`...` }],
|
|
614
|
+
details: buildAsyncDetails("running", jobId),
|
|
615
|
+
});
|
|
616
|
+
return {
|
|
617
|
+
content: [
|
|
618
|
+
{
|
|
619
|
+
type: "text",
|
|
620
|
+
text: `Spawned agent \`${agentId}\` (job \`${jobId}\`)${descriptionSuffix}. The result will be delivered when it yields. ${coordinationHint}`,
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
details: buildAsyncDetails("running", jobId),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
668
626
|
|
|
627
|
+
const coordinationHint = ircEnabled
|
|
628
|
+
? `DM these ids via \`irc\` to coordinate while they run; use \`job\` only to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task.`
|
|
629
|
+
: `Use \`job\` to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task by id.`;
|
|
669
630
|
const scheduleFailureSummary =
|
|
670
631
|
failedSchedules.length > 0
|
|
671
|
-
? ` Failed to schedule ${failedSchedules.length}
|
|
632
|
+
? ` Failed to schedule ${failedSchedules.length} spawn${failedSchedules.length === 1 ? "" : "s"}: ${failedSchedules.join("; ")}.`
|
|
672
633
|
: "";
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
taskIdByItemId.set(taskItems[i].id, uniqueIds[i]);
|
|
678
|
-
}
|
|
679
|
-
const startedListing = startedJobs
|
|
680
|
-
.map(({ taskId, jobId }) => {
|
|
681
|
-
const id = taskIdByItemId.get(taskId) ?? taskId;
|
|
682
|
-
const desc = progressByTaskId.get(taskId)?.description;
|
|
683
|
-
const prefix = `- \`${id}\` (job \`${jobId}\`)`;
|
|
684
|
-
return desc ? `${prefix} — ${desc}` : prefix;
|
|
634
|
+
const startedListing = started
|
|
635
|
+
.map(({ agentId, jobId, description }) => {
|
|
636
|
+
const prefix = `- \`${agentId}\` (job \`${jobId}\`)`;
|
|
637
|
+
return description ? `${prefix} — ${description}` : prefix;
|
|
685
638
|
})
|
|
686
639
|
.join("\n");
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
:
|
|
690
|
-
|
|
640
|
+
onUpdate?.({
|
|
641
|
+
content: [{ type: "text", text: `Spawned ${started.length} agents...` }],
|
|
642
|
+
details: buildAsyncDetails("running", primaryJobId),
|
|
643
|
+
});
|
|
691
644
|
return {
|
|
692
645
|
content: [
|
|
693
646
|
{
|
|
694
647
|
type: "text",
|
|
695
|
-
text: `
|
|
648
|
+
text: `Spawned ${started.length} background agents using ${agentLabel}.${scheduleFailureSummary} Each result will be delivered when that agent yields.\n${startedListing}\n${coordinationHint}`,
|
|
696
649
|
},
|
|
697
650
|
],
|
|
651
|
+
details: buildAsyncDetails("running", primaryJobId),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Register one background job that runs a single spawn to completion and
|
|
657
|
+
* delivers its yield text. The job body mirrors the sync path; `buildDetails`
|
|
658
|
+
* supplies the (possibly batch-shared) progress snapshot and `onSettled`
|
|
659
|
+
* feeds the caller's aggregate counters.
|
|
660
|
+
*/
|
|
661
|
+
#registerSpawnJob(options: {
|
|
662
|
+
manager: AsyncJobManager;
|
|
663
|
+
toolCallId: string;
|
|
664
|
+
spawnParams: TaskParams;
|
|
665
|
+
agentId: string;
|
|
666
|
+
progress: AgentProgress;
|
|
667
|
+
ircEnabled: boolean;
|
|
668
|
+
buildDetails: (state: "running" | "completed" | "failed", jobId: string) => TaskToolDetails;
|
|
669
|
+
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>;
|
|
670
|
+
onSettled?: (failed: boolean) => void;
|
|
671
|
+
}): string {
|
|
672
|
+
const { manager, toolCallId, spawnParams, agentId, progress, ircEnabled, buildDetails, onUpdate, onSettled } =
|
|
673
|
+
options;
|
|
674
|
+
const buildFollowUpHint = (aborted: boolean): string => {
|
|
675
|
+
if (aborted) {
|
|
676
|
+
return `\n\n${agentId} was aborted — transcript at history://${agentId}`;
|
|
677
|
+
}
|
|
678
|
+
const followUp = ircEnabled ? "message it via `irc` to follow up; " : "";
|
|
679
|
+
return `\n\n${agentId} is now idle — ${followUp}transcript at history://${agentId}`;
|
|
680
|
+
};
|
|
681
|
+
return manager.register(
|
|
682
|
+
"task",
|
|
683
|
+
agentId,
|
|
684
|
+
async ({ jobId: ownJobId, signal: runSignal, reportProgress, markRunning }) => {
|
|
685
|
+
const startedAt = Date.now();
|
|
686
|
+
const semaphore = this.#getSpawnSemaphore();
|
|
687
|
+
await semaphore.acquire();
|
|
688
|
+
if (runSignal.aborted) {
|
|
689
|
+
semaphore.release();
|
|
690
|
+
progress.status = "aborted";
|
|
691
|
+
onSettled?.(true);
|
|
692
|
+
throw new Error("Aborted before execution");
|
|
693
|
+
}
|
|
694
|
+
markRunning();
|
|
695
|
+
progress.status = "running";
|
|
696
|
+
await reportProgress(
|
|
697
|
+
`Running background task ${agentId}...`,
|
|
698
|
+
buildDetails("running", ownJobId) as unknown as Record<string, unknown>,
|
|
699
|
+
);
|
|
700
|
+
try {
|
|
701
|
+
const result = await this.#executeSync(toolCallId, spawnParams, runSignal, undefined, agentId);
|
|
702
|
+
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
703
|
+
const singleResult = result.details?.results[0];
|
|
704
|
+
// A missing result means the sync path failed at the tool level
|
|
705
|
+
// (results: []) — treat it as a failure, not success.
|
|
706
|
+
const resultFailed = !singleResult || (singleResult.aborted ?? false) || singleResult.exitCode !== 0;
|
|
707
|
+
progress.status = singleResult?.aborted ? "aborted" : resultFailed ? "failed" : "completed";
|
|
708
|
+
progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
|
|
709
|
+
progress.tokens = singleResult?.tokens ?? 0;
|
|
710
|
+
progress.requests = singleResult?.requests ?? 0;
|
|
711
|
+
progress.contextTokens = singleResult?.contextTokens;
|
|
712
|
+
progress.contextWindow = singleResult?.contextWindow;
|
|
713
|
+
progress.cost = singleResult?.usage?.cost.total ?? 0;
|
|
714
|
+
progress.extractedToolData = singleResult?.extractedToolData;
|
|
715
|
+
progress.retryFailure = singleResult?.retryFailure;
|
|
716
|
+
progress.retryState = undefined;
|
|
717
|
+
onSettled?.(resultFailed);
|
|
718
|
+
const statusText = resultFailed
|
|
719
|
+
? `Background task ${agentId} failed.`
|
|
720
|
+
: `Background task ${agentId} complete.`;
|
|
721
|
+
await reportProgress(
|
|
722
|
+
statusText,
|
|
723
|
+
buildDetails(resultFailed ? "failed" : "completed", ownJobId) as unknown as Record<string, unknown>,
|
|
724
|
+
);
|
|
725
|
+
onUpdate?.({
|
|
726
|
+
content: [{ type: "text", text: statusText }],
|
|
727
|
+
details: buildDetails(resultFailed ? "failed" : "completed", ownJobId),
|
|
728
|
+
});
|
|
729
|
+
const deliveryText = `${finalText}${buildFollowUpHint(singleResult?.aborted === true)}`;
|
|
730
|
+
if (resultFailed) {
|
|
731
|
+
// Mark the job itself failed; the failed agent stays interrogable.
|
|
732
|
+
throw new TaskJobError(deliveryText);
|
|
733
|
+
}
|
|
734
|
+
return deliveryText;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (error instanceof TaskJobError) {
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
progress.status = "failed";
|
|
740
|
+
progress.durationMs = Math.max(0, Date.now() - startedAt);
|
|
741
|
+
onSettled?.(true);
|
|
742
|
+
const statusText = `Background task ${agentId} failed.`;
|
|
743
|
+
await reportProgress(statusText, buildDetails("failed", ownJobId) as unknown as Record<string, unknown>);
|
|
744
|
+
onUpdate?.({
|
|
745
|
+
content: [{ type: "text", text: statusText }],
|
|
746
|
+
details: buildDetails("failed", ownJobId),
|
|
747
|
+
});
|
|
748
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
749
|
+
const hint = AgentRegistry.global().get(agentId) ? buildFollowUpHint(false) : "";
|
|
750
|
+
throw new TaskJobError(`${message}${hint}`);
|
|
751
|
+
} finally {
|
|
752
|
+
semaphore.release();
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
id: agentId,
|
|
757
|
+
queued: true,
|
|
758
|
+
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
759
|
+
onProgress: (text, details) => {
|
|
760
|
+
const progressDetails = (details as TaskToolDetails | undefined) ?? buildDetails("running", agentId);
|
|
761
|
+
onUpdate?.({ content: [{ type: "text", text }], details: progressDetails });
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Sync fallback fan-out (no job manager, or a `blocking: true` agent): run
|
|
769
|
+
* every spawn to completion inline and merge the per-spawn payloads into a
|
|
770
|
+
* single tool result. The session-scoped semaphore still bounds concurrency
|
|
771
|
+
* across parallel task calls.
|
|
772
|
+
*/
|
|
773
|
+
async #executeSyncFanout(
|
|
774
|
+
toolCallId: string,
|
|
775
|
+
params: TaskParams,
|
|
776
|
+
spawnItems: TaskItem[],
|
|
777
|
+
signal?: AbortSignal,
|
|
778
|
+
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
779
|
+
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
780
|
+
const semaphore = this.#getSpawnSemaphore();
|
|
781
|
+
if (spawnItems.length === 1) {
|
|
782
|
+
await semaphore.acquire();
|
|
783
|
+
try {
|
|
784
|
+
return await this.#executeSync(toolCallId, spawnParamsFor(params, spawnItems[0]), signal, onUpdate);
|
|
785
|
+
} finally {
|
|
786
|
+
semaphore.release();
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const startTime = Date.now();
|
|
791
|
+
const latestProgress = new Map<number, AgentProgress>();
|
|
792
|
+
const emitCombined = () => {
|
|
793
|
+
onUpdate?.({
|
|
794
|
+
content: [{ type: "text", text: `Running ${spawnItems.length} agents...` }],
|
|
795
|
+
details: {
|
|
796
|
+
projectAgentsDir: null,
|
|
797
|
+
results: [],
|
|
798
|
+
totalDurationMs: Date.now() - startTime,
|
|
799
|
+
progress: Array.from(latestProgress.entries())
|
|
800
|
+
.sort((a, b) => a[0] - b[0])
|
|
801
|
+
.map(([, progress]) => progress),
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const { results: payloads } = await mapWithConcurrencyLimit(
|
|
807
|
+
spawnItems,
|
|
808
|
+
spawnItems.length,
|
|
809
|
+
async (item, index, workerSignal) => {
|
|
810
|
+
await semaphore.acquire();
|
|
811
|
+
try {
|
|
812
|
+
const itemOnUpdate: AgentToolUpdateCallback<TaskToolDetails> | undefined = onUpdate
|
|
813
|
+
? update => {
|
|
814
|
+
const progress = update.details?.progress?.[0];
|
|
815
|
+
if (progress) {
|
|
816
|
+
latestProgress.set(index, { ...progress, index });
|
|
817
|
+
emitCombined();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
: undefined;
|
|
821
|
+
return await this.#executeSync(toolCallId, spawnParamsFor(params, item), workerSignal, itemOnUpdate);
|
|
822
|
+
} finally {
|
|
823
|
+
semaphore.release();
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
signal,
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
const results: SingleResult[] = [];
|
|
830
|
+
const contentParts: string[] = [];
|
|
831
|
+
const outputPaths: string[] = [];
|
|
832
|
+
const usageTotals = createUsageTotals();
|
|
833
|
+
let hasUsage = false;
|
|
834
|
+
let projectAgentsDir: string | null = null;
|
|
835
|
+
for (let index = 0; index < spawnItems.length; index++) {
|
|
836
|
+
const payload = payloads[index];
|
|
837
|
+
if (!payload) {
|
|
838
|
+
contentParts.push(`Task ${spawnItems[index].id?.trim() || `#${index + 1}`}: cancelled before start.`);
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
projectAgentsDir ??= payload.details?.projectAgentsDir ?? null;
|
|
842
|
+
const text = payload.content.find(part => part.type === "text")?.text;
|
|
843
|
+
if (text) contentParts.push(text);
|
|
844
|
+
for (const result of payload.details?.results ?? []) {
|
|
845
|
+
results.push({ ...result, index });
|
|
846
|
+
if (result.usage) {
|
|
847
|
+
addUsageTotals(usageTotals, result.usage);
|
|
848
|
+
hasUsage = true;
|
|
849
|
+
}
|
|
850
|
+
if (result.outputPath) outputPaths.push(result.outputPath);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
content: [{ type: "text", text: contentParts.join("\n\n") }],
|
|
698
856
|
details: {
|
|
699
|
-
projectAgentsDir
|
|
700
|
-
results
|
|
701
|
-
totalDurationMs:
|
|
702
|
-
|
|
703
|
-
|
|
857
|
+
projectAgentsDir,
|
|
858
|
+
results,
|
|
859
|
+
totalDurationMs: Date.now() - startTime,
|
|
860
|
+
usage: hasUsage ? usageTotals : undefined,
|
|
861
|
+
outputPaths: outputPaths.length > 0 ? outputPaths : undefined,
|
|
704
862
|
},
|
|
705
863
|
};
|
|
706
864
|
}
|
|
707
865
|
|
|
866
|
+
/**
|
|
867
|
+
* Synchronous execution of one spawn. Used as the body of every
|
|
868
|
+
* async job and directly by the sync fallback (no job manager / blocking
|
|
869
|
+
* agent) and by in-process callers that need the result inline (e.g. the
|
|
870
|
+
* commit flow's analyze_files tool).
|
|
871
|
+
*/
|
|
708
872
|
async #executeSync(
|
|
709
873
|
toolCallId: string,
|
|
710
874
|
params: TaskParams,
|
|
711
875
|
signal?: AbortSignal,
|
|
712
876
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
713
|
-
|
|
877
|
+
preAllocatedId?: string,
|
|
878
|
+
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
879
|
+
return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Spawn a fresh subagent and run it to completion. */
|
|
883
|
+
async #runSpawn(
|
|
884
|
+
toolCallId: string,
|
|
885
|
+
params: TaskParams,
|
|
886
|
+
signal?: AbortSignal,
|
|
887
|
+
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
888
|
+
preAllocatedId?: string,
|
|
714
889
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
715
890
|
const startTime = Date.now();
|
|
716
891
|
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
|
|
717
|
-
const
|
|
718
|
-
const
|
|
719
|
-
const
|
|
720
|
-
const sharedContext = contextEnabled ? context?.trim() : undefined;
|
|
892
|
+
const agentName = params.agent ?? "";
|
|
893
|
+
const sharedContext = this.#isBatchEnabled() ? params.context?.trim() || undefined : undefined;
|
|
894
|
+
const assignment = (params.assignment ?? "").trim();
|
|
721
895
|
const isolationMode = this.session.settings.get("task.isolation.mode");
|
|
722
896
|
const isolationRequested = "isolated" in params ? params.isolated === true : false;
|
|
723
897
|
const isIsolated = isolationMode !== "none" && isolationRequested;
|
|
724
898
|
const mergeMode = this.session.settings.get("task.isolation.merge");
|
|
725
899
|
const commitStyle = this.session.settings.get("task.isolation.commits");
|
|
726
|
-
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
727
900
|
const taskDepth = this.session.taskDepth ?? 0;
|
|
728
901
|
const subagentLspEnabled = (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp");
|
|
729
902
|
|
|
730
903
|
if (isolationMode === "none" && "isolated" in params) {
|
|
731
904
|
return {
|
|
732
|
-
content: [
|
|
733
|
-
|
|
734
|
-
type: "text",
|
|
735
|
-
text: "Task isolation is disabled.",
|
|
736
|
-
},
|
|
737
|
-
],
|
|
738
|
-
details: {
|
|
739
|
-
projectAgentsDir,
|
|
740
|
-
results: [],
|
|
741
|
-
totalDurationMs: 0,
|
|
742
|
-
},
|
|
905
|
+
content: [{ type: "text", text: "Task isolation is disabled." }],
|
|
906
|
+
details: { projectAgentsDir, results: [], totalDurationMs: 0 },
|
|
743
907
|
};
|
|
744
908
|
}
|
|
745
909
|
|
|
@@ -748,17 +912,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
748
912
|
if (!agent) {
|
|
749
913
|
const available = agents.map(a => a.name).join(", ") || "none";
|
|
750
914
|
return {
|
|
751
|
-
content: [
|
|
752
|
-
|
|
753
|
-
type: "text",
|
|
754
|
-
text: `Unknown agent "${agentName}". Available: ${available}`,
|
|
755
|
-
},
|
|
756
|
-
],
|
|
757
|
-
details: {
|
|
758
|
-
projectAgentsDir,
|
|
759
|
-
results: [],
|
|
760
|
-
totalDurationMs: 0,
|
|
761
|
-
},
|
|
915
|
+
content: [{ type: "text", text: `Unknown agent "${agentName}". Available: ${available}` }],
|
|
916
|
+
details: { projectAgentsDir, results: [], totalDurationMs: 0 },
|
|
762
917
|
};
|
|
763
918
|
}
|
|
764
919
|
|
|
@@ -773,11 +928,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
773
928
|
text: `Agent "${agentName}" is disabled in settings. Enable it via /agents, or use a different agent type.${enabled.length > 0 ? ` Available: ${enabled.join(", ")}` : ""}`,
|
|
774
929
|
},
|
|
775
930
|
],
|
|
776
|
-
details: {
|
|
777
|
-
projectAgentsDir,
|
|
778
|
-
results: [],
|
|
779
|
-
totalDurationMs: 0,
|
|
780
|
-
},
|
|
931
|
+
details: { projectAgentsDir, results: [], totalDurationMs: 0 },
|
|
781
932
|
};
|
|
782
933
|
}
|
|
783
934
|
|
|
@@ -811,43 +962,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
811
962
|
});
|
|
812
963
|
const thinkingLevelOverride = effectiveAgent.thinkingLevel;
|
|
813
964
|
|
|
814
|
-
// Output schema priority:
|
|
815
|
-
// task
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
: (effectiveAgent.output ?? this.session.outputSchema);
|
|
819
|
-
|
|
820
|
-
// Handle empty or missing tasks
|
|
821
|
-
if (!params.tasks || params.tasks.length === 0) {
|
|
822
|
-
return {
|
|
823
|
-
content: [
|
|
824
|
-
{
|
|
825
|
-
type: "text",
|
|
826
|
-
text: contextEnabled
|
|
827
|
-
? "No tasks provided. Use: { agent, context?, tasks: [{ id, description, assignment }, ...] }"
|
|
828
|
-
: "No tasks provided. Use: { agent, tasks: [{ id, description, assignment }, ...] }",
|
|
829
|
-
},
|
|
830
|
-
],
|
|
831
|
-
details: {
|
|
832
|
-
projectAgentsDir,
|
|
833
|
-
results: [],
|
|
834
|
-
totalDurationMs: 0,
|
|
835
|
-
},
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const tasks = params.tasks;
|
|
840
|
-
const taskIdProblem = validateTaskIds(tasks);
|
|
841
|
-
if (taskIdProblem) {
|
|
842
|
-
return {
|
|
843
|
-
content: [{ type: "text", text: taskIdProblem }],
|
|
844
|
-
details: {
|
|
845
|
-
projectAgentsDir,
|
|
846
|
-
results: [],
|
|
847
|
-
totalDurationMs: 0,
|
|
848
|
-
},
|
|
849
|
-
};
|
|
850
|
-
}
|
|
965
|
+
// Output schema priority: agent frontmatter > inherited parent session.
|
|
966
|
+
// The task call itself never carries a schema; workflows needing ad-hoc
|
|
967
|
+
// structured output go through eval agent(prompt, schema).
|
|
968
|
+
const effectiveOutputSchema = effectiveAgent.output ?? this.session.outputSchema;
|
|
851
969
|
|
|
852
970
|
let repoRoot: string | null = null;
|
|
853
971
|
let baseline: WorktreeBaseline | null = null;
|
|
@@ -858,17 +976,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
858
976
|
} catch (err) {
|
|
859
977
|
const message = err instanceof Error ? err.message : String(err);
|
|
860
978
|
return {
|
|
861
|
-
content: [
|
|
862
|
-
|
|
863
|
-
type: "text",
|
|
864
|
-
text: `Isolated task execution requires a git repository. ${message}`,
|
|
865
|
-
},
|
|
866
|
-
],
|
|
867
|
-
details: {
|
|
868
|
-
projectAgentsDir,
|
|
869
|
-
results: [],
|
|
870
|
-
totalDurationMs: Date.now() - startTime,
|
|
871
|
-
},
|
|
979
|
+
content: [{ type: "text", text: `Isolated task execution requires a git repository. ${message}` }],
|
|
980
|
+
details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
|
|
872
981
|
};
|
|
873
982
|
}
|
|
874
983
|
}
|
|
@@ -901,23 +1010,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
901
1010
|
localProtocolOptions,
|
|
902
1011
|
);
|
|
903
1012
|
|
|
904
|
-
// Initialize progress tracking
|
|
905
|
-
const progressMap = new Map<number, AgentProgress>();
|
|
906
|
-
|
|
907
|
-
// Update callback
|
|
908
|
-
const emitProgress = () => {
|
|
909
|
-
const progress = Array.from(progressMap.values()).sort((a, b) => a.index - b.index);
|
|
910
|
-
onUpdate?.({
|
|
911
|
-
content: [{ type: "text", text: `Running ${params.tasks.length} agents...` }],
|
|
912
|
-
details: {
|
|
913
|
-
projectAgentsDir,
|
|
914
|
-
results: [],
|
|
915
|
-
totalDurationMs: Date.now() - startTime,
|
|
916
|
-
progress,
|
|
917
|
-
},
|
|
918
|
-
});
|
|
919
|
-
};
|
|
920
|
-
|
|
921
1013
|
try {
|
|
922
1014
|
// Check self-recursion prevention
|
|
923
1015
|
if (this.#blockedAgent && agentName === this.#blockedAgent) {
|
|
@@ -928,11 +1020,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
928
1020
|
text: `Cannot spawn ${this.#blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
|
|
929
1021
|
},
|
|
930
1022
|
],
|
|
931
|
-
details: {
|
|
932
|
-
projectAgentsDir,
|
|
933
|
-
results: [],
|
|
934
|
-
totalDurationMs: Date.now() - startTime,
|
|
935
|
-
},
|
|
1023
|
+
details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
|
|
936
1024
|
};
|
|
937
1025
|
}
|
|
938
1026
|
|
|
@@ -949,36 +1037,21 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
949
1037
|
const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
|
|
950
1038
|
return {
|
|
951
1039
|
content: [{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${allowed}` }],
|
|
952
|
-
details: {
|
|
953
|
-
projectAgentsDir,
|
|
954
|
-
results: [],
|
|
955
|
-
totalDurationMs: Date.now() - startTime,
|
|
956
|
-
},
|
|
1040
|
+
details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
|
|
957
1041
|
};
|
|
958
1042
|
}
|
|
959
1043
|
|
|
960
|
-
// Write parent conversation context for subagents. When IRC is available,
|
|
961
|
-
// subagents should ask live peers instead of reading a stale markdown dump.
|
|
962
1044
|
await fs.mkdir(effectiveArtifactsDir, { recursive: true });
|
|
963
|
-
const shouldWriteConversationContext = this.session.settings.get("irc.enabled") !== true;
|
|
964
|
-
const compactContext = shouldWriteConversationContext ? this.session.getCompactContext?.() : undefined;
|
|
965
|
-
let contextFilePath: string | undefined;
|
|
966
|
-
if (compactContext) {
|
|
967
|
-
contextFilePath = path.join(effectiveArtifactsDir, "context.md");
|
|
968
|
-
await Bun.write(contextFilePath, compactContext);
|
|
969
|
-
}
|
|
970
1045
|
|
|
971
|
-
//
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
uniqueIds = preAllocatedIds;
|
|
1046
|
+
// Allocate a unique ID across the session to prevent artifact collisions
|
|
1047
|
+
let agentId: string;
|
|
1048
|
+
if (preAllocatedId) {
|
|
1049
|
+
agentId = preAllocatedId;
|
|
976
1050
|
} else {
|
|
977
1051
|
const outputManager =
|
|
978
1052
|
this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
|
|
979
|
-
|
|
1053
|
+
agentId = await outputManager.allocate(params.id?.trim() || generateTaskName());
|
|
980
1054
|
}
|
|
981
|
-
const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
|
|
982
1055
|
|
|
983
1056
|
const availableSkills = [...(this.session.skills ?? [])];
|
|
984
1057
|
// Resolve autoload skills from agent definition against available skills
|
|
@@ -995,85 +1068,102 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
995
1068
|
const parentEvalSessionId = this.session.getEvalSessionId?.() ?? undefined;
|
|
996
1069
|
const mcpManager = this.session.mcpManager ?? MCPManager.instance();
|
|
997
1070
|
|
|
998
|
-
//
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1071
|
+
// Progress tracking for the single agent
|
|
1072
|
+
let latestProgress: AgentProgress = {
|
|
1073
|
+
index: 0,
|
|
1074
|
+
id: agentId,
|
|
1075
|
+
agent: agentName,
|
|
1076
|
+
agentSource: agent.source,
|
|
1077
|
+
status: "pending",
|
|
1078
|
+
task: renderSubagentUserPrompt(assignment),
|
|
1079
|
+
assignment,
|
|
1080
|
+
recentTools: [],
|
|
1081
|
+
recentOutput: [],
|
|
1082
|
+
toolCount: 0,
|
|
1083
|
+
requests: 0,
|
|
1084
|
+
tokens: 0,
|
|
1085
|
+
cost: 0,
|
|
1086
|
+
durationMs: 0,
|
|
1087
|
+
modelOverride,
|
|
1088
|
+
description: params.description,
|
|
1089
|
+
};
|
|
1090
|
+
const emitProgress = () => {
|
|
1091
|
+
onUpdate?.({
|
|
1092
|
+
content: [{ type: "text", text: `Running agent ${agentId}...` }],
|
|
1093
|
+
details: {
|
|
1094
|
+
projectAgentsDir,
|
|
1095
|
+
results: [],
|
|
1096
|
+
totalDurationMs: Date.now() - startTime,
|
|
1097
|
+
progress: [latestProgress],
|
|
1098
|
+
},
|
|
1018
1099
|
});
|
|
1019
|
-
}
|
|
1100
|
+
};
|
|
1020
1101
|
emitProgress();
|
|
1021
1102
|
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1103
|
+
const buildCommitMessageFn = () =>
|
|
1104
|
+
commitStyle === "ai" && this.session.modelRegistry
|
|
1105
|
+
? async (diff: string) => {
|
|
1106
|
+
return generateCommitMessage(
|
|
1107
|
+
diff,
|
|
1108
|
+
this.session.modelRegistry!,
|
|
1109
|
+
this.session.settings,
|
|
1110
|
+
this.session.getSessionId?.() ?? undefined,
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
: undefined;
|
|
1114
|
+
|
|
1115
|
+
const sharedRunOptions = {
|
|
1116
|
+
cwd: this.session.cwd,
|
|
1117
|
+
agent: effectiveAgent,
|
|
1118
|
+
task: renderSubagentUserPrompt(assignment),
|
|
1119
|
+
assignment,
|
|
1120
|
+
context: sharedContext,
|
|
1121
|
+
planReference,
|
|
1122
|
+
description: params.description,
|
|
1123
|
+
index: 0,
|
|
1124
|
+
parentToolCallId: toolCallId,
|
|
1125
|
+
id: agentId,
|
|
1126
|
+
taskDepth,
|
|
1127
|
+
modelOverride,
|
|
1128
|
+
parentActiveModelPattern,
|
|
1129
|
+
thinkingLevel: thinkingLevelOverride,
|
|
1130
|
+
outputSchema: effectiveOutputSchema,
|
|
1131
|
+
sessionFile,
|
|
1132
|
+
persistArtifacts: !!artifactsDir,
|
|
1133
|
+
artifactsDir: effectiveArtifactsDir,
|
|
1134
|
+
enableLsp: subagentLspEnabled,
|
|
1135
|
+
signal,
|
|
1136
|
+
eventBus: this.session.eventBus,
|
|
1137
|
+
onProgress: (progress: AgentProgress) => {
|
|
1138
|
+
// Shallow snapshot; recentTools is mutated in place by the
|
|
1139
|
+
// executor, the rest is reassigned or immutable. A deep clone
|
|
1140
|
+
// here cost O(extractedToolData) per progress event.
|
|
1141
|
+
latestProgress = { ...progress, recentTools: progress.recentTools.slice() };
|
|
1142
|
+
emitProgress();
|
|
1143
|
+
},
|
|
1144
|
+
authStorage: this.session.authStorage,
|
|
1145
|
+
modelRegistry: this.session.modelRegistry,
|
|
1146
|
+
settings: this.session.settings,
|
|
1147
|
+
mcpManager,
|
|
1148
|
+
contextFiles,
|
|
1149
|
+
skills: availableSkills,
|
|
1150
|
+
autoloadSkills: resolvedAutoloadSkills,
|
|
1151
|
+
workspaceTree: this.session.workspaceTree,
|
|
1152
|
+
promptTemplates,
|
|
1153
|
+
rules: this.session.rules,
|
|
1154
|
+
preloadedExtensionPaths: this.session.extensionPaths,
|
|
1155
|
+
preloadedCustomToolPaths: this.session.customToolPaths,
|
|
1156
|
+
localProtocolOptions,
|
|
1157
|
+
parentArtifactManager,
|
|
1158
|
+
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
1159
|
+
parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
|
|
1160
|
+
parentTelemetry: this.session.getTelemetry?.(),
|
|
1161
|
+
parentEvalSessionId,
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
const runTask = async (): Promise<SingleResult> => {
|
|
1027
1165
|
if (!isIsolated) {
|
|
1028
|
-
return runSubprocess(
|
|
1029
|
-
cwd: this.session.cwd,
|
|
1030
|
-
agent: effectiveAgent,
|
|
1031
|
-
task: renderSubagentUserPrompt(task.assignment, simpleMode),
|
|
1032
|
-
assignment: task.assignment.trim(),
|
|
1033
|
-
context: sharedContext,
|
|
1034
|
-
planReference,
|
|
1035
|
-
description: task.description,
|
|
1036
|
-
index,
|
|
1037
|
-
parentToolCallId: toolCallId,
|
|
1038
|
-
id: task.id,
|
|
1039
|
-
taskDepth,
|
|
1040
|
-
modelOverride,
|
|
1041
|
-
parentActiveModelPattern,
|
|
1042
|
-
thinkingLevel: thinkingLevelOverride,
|
|
1043
|
-
outputSchema: effectiveOutputSchema,
|
|
1044
|
-
sessionFile,
|
|
1045
|
-
persistArtifacts: !!artifactsDir,
|
|
1046
|
-
artifactsDir: effectiveArtifactsDir,
|
|
1047
|
-
contextFile: contextFilePath,
|
|
1048
|
-
enableLsp: subagentLspEnabled,
|
|
1049
|
-
signal: workerSignal ?? signal,
|
|
1050
|
-
eventBus: this.session.eventBus,
|
|
1051
|
-
onProgress: progress => {
|
|
1052
|
-
// Shallow snapshot; recentTools is mutated in place by the
|
|
1053
|
-
// executor, the rest is reassigned or immutable. A deep clone
|
|
1054
|
-
// here cost O(extractedToolData) per progress event.
|
|
1055
|
-
progressMap.set(index, { ...progress, recentTools: progress.recentTools.slice() });
|
|
1056
|
-
emitProgress();
|
|
1057
|
-
},
|
|
1058
|
-
authStorage: this.session.authStorage,
|
|
1059
|
-
modelRegistry: this.session.modelRegistry,
|
|
1060
|
-
settings: this.session.settings,
|
|
1061
|
-
mcpManager,
|
|
1062
|
-
contextFiles,
|
|
1063
|
-
skills: availableSkills,
|
|
1064
|
-
autoloadSkills: resolvedAutoloadSkills,
|
|
1065
|
-
workspaceTree: this.session.workspaceTree,
|
|
1066
|
-
promptTemplates,
|
|
1067
|
-
rules: this.session.rules,
|
|
1068
|
-
preloadedExtensionPaths: this.session.extensionPaths,
|
|
1069
|
-
preloadedCustomToolPaths: this.session.customToolPaths,
|
|
1070
|
-
localProtocolOptions,
|
|
1071
|
-
parentArtifactManager,
|
|
1072
|
-
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
1073
|
-
parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
|
|
1074
|
-
parentTelemetry: this.session.getTelemetry?.(),
|
|
1075
|
-
parentEvalSessionId,
|
|
1076
|
-
});
|
|
1166
|
+
return runSubprocess(sharedRunOptions);
|
|
1077
1167
|
}
|
|
1078
1168
|
|
|
1079
1169
|
const taskStart = Date.now();
|
|
@@ -1084,73 +1174,25 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1084
1174
|
}
|
|
1085
1175
|
const taskBaseline = structuredClone(baseline);
|
|
1086
1176
|
|
|
1087
|
-
isolationHandle = await ensureIsolation(repoRoot,
|
|
1177
|
+
isolationHandle = await ensureIsolation(repoRoot, agentId, preferredIsolationBackend);
|
|
1088
1178
|
const isolationDir = isolationHandle.mergedDir;
|
|
1089
1179
|
|
|
1180
|
+
// Isolated runs re-discover extensions/custom tools inside the
|
|
1181
|
+
// worktree instead of reusing the parent's source paths.
|
|
1090
1182
|
const result = await runSubprocess({
|
|
1091
|
-
|
|
1183
|
+
...sharedRunOptions,
|
|
1092
1184
|
worktree: isolationDir,
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
assignment: task.assignment.trim(),
|
|
1096
|
-
context: sharedContext,
|
|
1097
|
-
planReference,
|
|
1098
|
-
description: task.description,
|
|
1099
|
-
index,
|
|
1100
|
-
parentToolCallId: toolCallId,
|
|
1101
|
-
id: task.id,
|
|
1102
|
-
taskDepth,
|
|
1103
|
-
modelOverride,
|
|
1104
|
-
parentActiveModelPattern,
|
|
1105
|
-
thinkingLevel: thinkingLevelOverride,
|
|
1106
|
-
outputSchema: effectiveOutputSchema,
|
|
1107
|
-
sessionFile,
|
|
1108
|
-
persistArtifacts: !!artifactsDir,
|
|
1109
|
-
artifactsDir: effectiveArtifactsDir,
|
|
1110
|
-
contextFile: contextFilePath,
|
|
1111
|
-
enableLsp: subagentLspEnabled,
|
|
1112
|
-
signal: workerSignal ?? signal,
|
|
1113
|
-
eventBus: this.session.eventBus,
|
|
1114
|
-
onProgress: progress => {
|
|
1115
|
-
progressMap.set(index, { ...progress, recentTools: progress.recentTools.slice() });
|
|
1116
|
-
emitProgress();
|
|
1117
|
-
},
|
|
1118
|
-
authStorage: this.session.authStorage,
|
|
1119
|
-
modelRegistry: this.session.modelRegistry,
|
|
1120
|
-
settings: this.session.settings,
|
|
1121
|
-
mcpManager,
|
|
1122
|
-
contextFiles,
|
|
1123
|
-
skills: availableSkills,
|
|
1124
|
-
autoloadSkills: resolvedAutoloadSkills,
|
|
1125
|
-
workspaceTree: this.session.workspaceTree,
|
|
1126
|
-
promptTemplates,
|
|
1127
|
-
rules: this.session.rules,
|
|
1128
|
-
localProtocolOptions,
|
|
1129
|
-
parentArtifactManager,
|
|
1130
|
-
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
1131
|
-
parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
|
|
1132
|
-
parentTelemetry: this.session.getTelemetry?.(),
|
|
1133
|
-
parentEvalSessionId,
|
|
1185
|
+
preloadedExtensionPaths: undefined,
|
|
1186
|
+
preloadedCustomToolPaths: undefined,
|
|
1134
1187
|
});
|
|
1135
1188
|
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
1136
1189
|
try {
|
|
1137
|
-
const commitMsg =
|
|
1138
|
-
commitStyle === "ai" && this.session.modelRegistry
|
|
1139
|
-
? async (diff: string) => {
|
|
1140
|
-
return generateCommitMessage(
|
|
1141
|
-
diff,
|
|
1142
|
-
this.session.modelRegistry!,
|
|
1143
|
-
this.session.settings,
|
|
1144
|
-
this.session.getSessionId?.() ?? undefined,
|
|
1145
|
-
);
|
|
1146
|
-
}
|
|
1147
|
-
: undefined;
|
|
1148
1190
|
const commitResult = await commitToBranch(
|
|
1149
1191
|
isolationDir,
|
|
1150
1192
|
taskBaseline,
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1193
|
+
agentId,
|
|
1194
|
+
params.description,
|
|
1195
|
+
buildCommitMessageFn(),
|
|
1154
1196
|
);
|
|
1155
1197
|
return {
|
|
1156
1198
|
...result,
|
|
@@ -1159,7 +1201,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1159
1201
|
};
|
|
1160
1202
|
} catch (mergeErr) {
|
|
1161
1203
|
// Agent succeeded but branch commit failed — clean up stale branch
|
|
1162
|
-
const branchName = `omp/task/${
|
|
1204
|
+
const branchName = `omp/task/${agentId}`;
|
|
1163
1205
|
await git.branch.tryDelete(repoRoot, branchName);
|
|
1164
1206
|
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
1165
1207
|
return { ...result, error: `Merge failed: ${msg}` };
|
|
@@ -1168,7 +1210,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1168
1210
|
if (result.exitCode === 0) {
|
|
1169
1211
|
try {
|
|
1170
1212
|
const delta = await captureDeltaPatch(isolationDir, taskBaseline);
|
|
1171
|
-
const patchPath = path.join(effectiveArtifactsDir, `${
|
|
1213
|
+
const patchPath = path.join(effectiveArtifactsDir, `${agentId}.patch`);
|
|
1172
1214
|
await Bun.write(patchPath, delta.rootPatch);
|
|
1173
1215
|
return {
|
|
1174
1216
|
...result,
|
|
@@ -1183,21 +1225,21 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1183
1225
|
return result;
|
|
1184
1226
|
} catch (err) {
|
|
1185
1227
|
const message = err instanceof Error ? err.message : String(err);
|
|
1186
|
-
const assignment = task.assignment.trim();
|
|
1187
1228
|
return {
|
|
1188
|
-
index,
|
|
1189
|
-
id:
|
|
1229
|
+
index: 0,
|
|
1230
|
+
id: agentId,
|
|
1190
1231
|
agent: agent.name,
|
|
1191
1232
|
agentSource: agent.source,
|
|
1192
|
-
task: renderSubagentUserPrompt(assignment
|
|
1233
|
+
task: renderSubagentUserPrompt(assignment),
|
|
1193
1234
|
assignment,
|
|
1194
|
-
description:
|
|
1235
|
+
description: params.description,
|
|
1195
1236
|
exitCode: 1,
|
|
1196
1237
|
output: "",
|
|
1197
1238
|
stderr: message,
|
|
1198
1239
|
truncated: false,
|
|
1199
1240
|
durationMs: Date.now() - taskStart,
|
|
1200
1241
|
tokens: 0,
|
|
1242
|
+
requests: 0,
|
|
1201
1243
|
modelOverride,
|
|
1202
1244
|
error: message,
|
|
1203
1245
|
};
|
|
@@ -1208,147 +1250,68 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1208
1250
|
}
|
|
1209
1251
|
};
|
|
1210
1252
|
|
|
1211
|
-
|
|
1212
|
-
const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
|
|
1213
|
-
tasksWithUniqueIds,
|
|
1214
|
-
maxConcurrency,
|
|
1215
|
-
runTask,
|
|
1216
|
-
signal,
|
|
1217
|
-
);
|
|
1218
|
-
|
|
1219
|
-
// Fill in skipped tasks (undefined entries from abort) with placeholder results
|
|
1220
|
-
const results: SingleResult[] = partialResults.map((result, index) => {
|
|
1221
|
-
if (result !== undefined) {
|
|
1222
|
-
return result;
|
|
1223
|
-
}
|
|
1224
|
-
const task = tasksWithUniqueIds[index];
|
|
1225
|
-
const assignment = task.assignment.trim();
|
|
1226
|
-
return {
|
|
1227
|
-
index,
|
|
1228
|
-
id: task.id,
|
|
1229
|
-
agent: agentName,
|
|
1230
|
-
agentSource: agent.source,
|
|
1231
|
-
task: renderSubagentUserPrompt(assignment, simpleMode),
|
|
1232
|
-
assignment,
|
|
1233
|
-
description: task.description,
|
|
1234
|
-
exitCode: 1,
|
|
1235
|
-
output: "",
|
|
1236
|
-
stderr: "Skipped (cancelled before start)",
|
|
1237
|
-
truncated: false,
|
|
1238
|
-
durationMs: 0,
|
|
1239
|
-
tokens: 0,
|
|
1240
|
-
modelOverride,
|
|
1241
|
-
error: "Cancelled before start",
|
|
1242
|
-
aborted: true,
|
|
1243
|
-
abortReason: "Cancelled before start",
|
|
1244
|
-
};
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
// Aggregate usage from executor results (already accumulated incrementally)
|
|
1248
|
-
const aggregatedUsage = createUsageTotals();
|
|
1249
|
-
let hasAggregatedUsage = false;
|
|
1250
|
-
for (const result of results) {
|
|
1251
|
-
if (result.usage) {
|
|
1252
|
-
addUsageTotals(aggregatedUsage, result.usage);
|
|
1253
|
-
hasAggregatedUsage = true;
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// Collect output paths (artifacts already written by executor in real-time)
|
|
1258
|
-
const outputPaths: string[] = [];
|
|
1259
|
-
const patchPaths: string[] = [];
|
|
1260
|
-
for (const result of results) {
|
|
1261
|
-
if (result.outputPath) {
|
|
1262
|
-
outputPaths.push(result.outputPath);
|
|
1263
|
-
}
|
|
1264
|
-
if (result.patchPath) {
|
|
1265
|
-
patchPaths.push(result.patchPath);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1253
|
+
const result = await runTask();
|
|
1268
1254
|
|
|
1269
1255
|
let mergeSummary = "";
|
|
1270
1256
|
let changesApplied: boolean | null = null;
|
|
1271
1257
|
let hadAnyChanges = false;
|
|
1272
|
-
let
|
|
1258
|
+
let mergedBranchForNestedPatches = false;
|
|
1273
1259
|
if (isIsolated && repoRoot) {
|
|
1274
1260
|
try {
|
|
1275
1261
|
if (mergeMode === "branch") {
|
|
1276
|
-
|
|
1277
|
-
const branchEntries = results
|
|
1278
|
-
.filter(r => r.branchName && r.exitCode === 0 && !r.aborted)
|
|
1279
|
-
.map(r => ({ branchName: r.branchName!, taskId: r.id, description: r.description }));
|
|
1280
|
-
|
|
1281
|
-
if (branchEntries.length === 0) {
|
|
1262
|
+
if (!result.branchName || result.exitCode !== 0 || result.aborted) {
|
|
1282
1263
|
changesApplied = true;
|
|
1283
|
-
hadAnyChanges = false;
|
|
1284
1264
|
mergeSummary = "\n\nNo changes to apply.";
|
|
1285
1265
|
} else {
|
|
1286
|
-
const mergeResult = await mergeTaskBranches(repoRoot,
|
|
1287
|
-
|
|
1266
|
+
const mergeResult = await mergeTaskBranches(repoRoot, [
|
|
1267
|
+
{ branchName: result.branchName, taskId: result.id, description: result.description },
|
|
1268
|
+
]);
|
|
1269
|
+
mergedBranchForNestedPatches = mergeResult.merged.includes(result.branchName);
|
|
1288
1270
|
changesApplied = mergeResult.failed.length === 0;
|
|
1289
1271
|
hadAnyChanges = changesApplied && mergeResult.merged.length > 0;
|
|
1290
1272
|
|
|
1291
1273
|
if (changesApplied) {
|
|
1292
1274
|
mergeSummary = hadAnyChanges
|
|
1293
|
-
? `\n\nMerged
|
|
1275
|
+
? `\n\nMerged branch: ${result.branchName}`
|
|
1294
1276
|
: "\n\nNo changes to apply.";
|
|
1295
1277
|
} else {
|
|
1296
|
-
const mergedPart =
|
|
1297
|
-
mergeResult.merged.length > 0 ? `Merged: ${mergeResult.merged.join(", ")}.\n` : "";
|
|
1298
|
-
const failedPart = `Failed: ${mergeResult.failed.join(", ")}.`;
|
|
1299
1278
|
const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
|
|
1300
|
-
mergeSummary = `\n\n<system-notification>Branch merge failed
|
|
1279
|
+
mergeSummary = `\n\n<system-notification>Branch merge failed: ${result.branchName}.${conflictPart}\nThe unmerged branch remains for manual resolution.</system-notification>`;
|
|
1301
1280
|
}
|
|
1302
1281
|
if (mergeResult.stashConflict) {
|
|
1303
1282
|
mergeSummary += `\n\n<system-notification>${mergeResult.stashConflict}</system-notification>`;
|
|
1304
1283
|
}
|
|
1305
|
-
}
|
|
1306
1284
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1285
|
+
// Clean up the merged branch (keep failed ones for manual resolution)
|
|
1286
|
+
if (changesApplied) {
|
|
1287
|
+
await cleanupTaskBranches(repoRoot, [result.branchName]);
|
|
1288
|
+
}
|
|
1311
1289
|
}
|
|
1312
1290
|
} else {
|
|
1313
|
-
// Patch mode: apply
|
|
1314
|
-
// aborted
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1291
|
+
// Patch mode: apply the patch from a successful run. A failed or
|
|
1292
|
+
// aborted run has nothing to apply and must not block the result.
|
|
1293
|
+
const succeeded = result.exitCode === 0 && !result.error && !result.aborted;
|
|
1294
|
+
if (!succeeded) {
|
|
1295
|
+
changesApplied = true;
|
|
1296
|
+
hadAnyChanges = false;
|
|
1297
|
+
} else if (!result.patchPath) {
|
|
1319
1298
|
changesApplied = false;
|
|
1320
1299
|
hadAnyChanges = false;
|
|
1321
1300
|
} else {
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
patchPath,
|
|
1325
|
-
size: (await fs.stat(patchPath)).size,
|
|
1326
|
-
})),
|
|
1327
|
-
);
|
|
1328
|
-
const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
|
|
1329
|
-
if (nonEmptyPatches.length === 0) {
|
|
1301
|
+
const patchText = await Bun.file(result.patchPath).text();
|
|
1302
|
+
if (!patchText.trim()) {
|
|
1330
1303
|
changesApplied = true;
|
|
1331
1304
|
hadAnyChanges = false;
|
|
1332
1305
|
} else {
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
)
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
} else {
|
|
1343
|
-
changesApplied = await git.patch.canApplyText(repoRoot, combinedPatch);
|
|
1344
|
-
if (changesApplied) {
|
|
1345
|
-
try {
|
|
1346
|
-
await git.patch.applyText(repoRoot, combinedPatch);
|
|
1347
|
-
hadAnyChanges = true;
|
|
1348
|
-
} catch {
|
|
1349
|
-
changesApplied = false;
|
|
1350
|
-
hadAnyChanges = false;
|
|
1351
|
-
}
|
|
1306
|
+
const normalized = patchText.endsWith("\n") ? patchText : `${patchText}\n`;
|
|
1307
|
+
changesApplied = await git.patch.canApplyText(repoRoot, normalized);
|
|
1308
|
+
if (changesApplied) {
|
|
1309
|
+
try {
|
|
1310
|
+
await git.patch.applyText(repoRoot, normalized);
|
|
1311
|
+
hadAnyChanges = true;
|
|
1312
|
+
} catch {
|
|
1313
|
+
changesApplied = false;
|
|
1314
|
+
hadAnyChanges = false;
|
|
1352
1315
|
}
|
|
1353
1316
|
}
|
|
1354
1317
|
}
|
|
@@ -1359,10 +1322,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1359
1322
|
} else {
|
|
1360
1323
|
const notification =
|
|
1361
1324
|
"<system-notification>Patches were not applied and must be handled manually.</system-notification>";
|
|
1362
|
-
const patchList =
|
|
1363
|
-
patchPaths.length > 0
|
|
1364
|
-
? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
|
|
1365
|
-
: "";
|
|
1325
|
+
const patchList = result.patchPath ? `\n\nPatch artifact:\n- ${result.patchPath}` : "";
|
|
1366
1326
|
mergeSummary = `\n\n${notification}${patchList}`;
|
|
1367
1327
|
}
|
|
1368
1328
|
}
|
|
@@ -1376,34 +1336,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1376
1336
|
|
|
1377
1337
|
// Apply nested repo patches (separate from parent git)
|
|
1378
1338
|
if (isIsolated && repoRoot && (mergeMode === "branch" || changesApplied !== false)) {
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
}
|
|
1387
|
-
if (!r.branchName || !mergedBranchesForNestedPatches) {
|
|
1388
|
-
return false;
|
|
1389
|
-
}
|
|
1390
|
-
return mergedBranchesForNestedPatches.has(r.branchName);
|
|
1391
|
-
})
|
|
1392
|
-
.flatMap(r => r.nestedPatches!);
|
|
1393
|
-
if (allNestedPatches.length > 0) {
|
|
1339
|
+
const nestedPatches = result.nestedPatches ?? [];
|
|
1340
|
+
const eligible =
|
|
1341
|
+
nestedPatches.length > 0 &&
|
|
1342
|
+
result.exitCode === 0 &&
|
|
1343
|
+
!result.aborted &&
|
|
1344
|
+
(mergeMode !== "branch" || mergedBranchForNestedPatches);
|
|
1345
|
+
if (eligible) {
|
|
1394
1346
|
try {
|
|
1395
|
-
|
|
1396
|
-
commitStyle === "ai" && this.session.modelRegistry
|
|
1397
|
-
? async (diff: string) => {
|
|
1398
|
-
return generateCommitMessage(
|
|
1399
|
-
diff,
|
|
1400
|
-
this.session.modelRegistry!,
|
|
1401
|
-
this.session.settings,
|
|
1402
|
-
this.session.getSessionId?.() ?? undefined,
|
|
1403
|
-
);
|
|
1404
|
-
}
|
|
1405
|
-
: undefined;
|
|
1406
|
-
await applyNestedPatches(repoRoot, allNestedPatches, commitMsg);
|
|
1347
|
+
await applyNestedPatches(repoRoot, nestedPatches, buildCommitMessageFn());
|
|
1407
1348
|
} catch {
|
|
1408
1349
|
// Nested patch failures are non-fatal to the parent merge
|
|
1409
1350
|
mergeSummary +=
|
|
@@ -1412,58 +1353,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1412
1353
|
}
|
|
1413
1354
|
}
|
|
1414
1355
|
|
|
1415
|
-
// Build final output - match plugin format
|
|
1416
|
-
const cancelledCount = results.filter(r => r.aborted).length;
|
|
1417
|
-
const successCount = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted).length;
|
|
1418
|
-
const totalDuration = Date.now() - startTime;
|
|
1419
|
-
|
|
1420
|
-
const summaries = results.map(r => {
|
|
1421
|
-
const status = r.aborted
|
|
1422
|
-
? "cancelled"
|
|
1423
|
-
: r.exitCode === 0 && r.error
|
|
1424
|
-
? "merge failed"
|
|
1425
|
-
: r.exitCode === 0
|
|
1426
|
-
? "completed"
|
|
1427
|
-
: `failed (exit ${r.exitCode})`;
|
|
1428
|
-
const output = r.output.trim() || r.stderr.trim() || "(no output)";
|
|
1429
|
-
const outputCharCount = r.outputMeta?.charCount ?? output.length;
|
|
1430
|
-
const fullOutputThreshold = 5000;
|
|
1431
|
-
let preview = output;
|
|
1432
|
-
let truncated = false;
|
|
1433
|
-
if (outputCharCount > fullOutputThreshold) {
|
|
1434
|
-
const slice = output.slice(0, fullOutputThreshold);
|
|
1435
|
-
const lastNewline = slice.lastIndexOf("\n");
|
|
1436
|
-
preview = lastNewline >= 0 ? slice.slice(0, lastNewline) : slice;
|
|
1437
|
-
truncated = true;
|
|
1438
|
-
}
|
|
1439
|
-
return {
|
|
1440
|
-
agent: r.agent,
|
|
1441
|
-
status,
|
|
1442
|
-
id: r.id,
|
|
1443
|
-
preview,
|
|
1444
|
-
truncated,
|
|
1445
|
-
meta: r.outputMeta
|
|
1446
|
-
? {
|
|
1447
|
-
lineCount: r.outputMeta.lineCount,
|
|
1448
|
-
charSize: formatBytes(r.outputMeta.charCount),
|
|
1449
|
-
}
|
|
1450
|
-
: undefined,
|
|
1451
|
-
};
|
|
1452
|
-
});
|
|
1453
|
-
|
|
1454
|
-
const outputIds = results.filter(r => !r.aborted || r.output.trim()).map(r => `agent://${r.id}`);
|
|
1455
|
-
const summary = prompt.render(taskSummaryTemplate, {
|
|
1456
|
-
successCount,
|
|
1457
|
-
totalCount: results.length,
|
|
1458
|
-
cancelledCount,
|
|
1459
|
-
hasCancelledNote: aborted && cancelledCount > 0,
|
|
1460
|
-
duration: formatDuration(totalDuration),
|
|
1461
|
-
summaries,
|
|
1462
|
-
outputIds,
|
|
1463
|
-
agentName,
|
|
1464
|
-
mergeSummary,
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
1356
|
// Cleanup temp directory if used
|
|
1468
1357
|
const shouldCleanupTempArtifacts =
|
|
1469
1358
|
tempArtifactsDir && (!isIsolated || changesApplied === true || changesApplied === null);
|
|
@@ -1471,25 +1360,65 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1471
1360
|
await fs.rm(tempArtifactsDir, { recursive: true, force: true });
|
|
1472
1361
|
}
|
|
1473
1362
|
|
|
1474
|
-
return
|
|
1475
|
-
content: [{ type: "text", text: summary }],
|
|
1476
|
-
details: {
|
|
1477
|
-
projectAgentsDir,
|
|
1478
|
-
results: results,
|
|
1479
|
-
totalDurationMs: totalDuration,
|
|
1480
|
-
usage: hasAggregatedUsage ? aggregatedUsage : undefined,
|
|
1481
|
-
outputPaths,
|
|
1482
|
-
},
|
|
1483
|
-
};
|
|
1363
|
+
return this.#buildResultPayload(result, projectAgentsDir, Date.now() - startTime, mergeSummary);
|
|
1484
1364
|
} catch (err) {
|
|
1485
1365
|
return {
|
|
1486
1366
|
content: [{ type: "text", text: `Task execution failed: ${err}` }],
|
|
1487
|
-
details: {
|
|
1488
|
-
projectAgentsDir,
|
|
1489
|
-
results: [],
|
|
1490
|
-
totalDurationMs: Date.now() - startTime,
|
|
1491
|
-
},
|
|
1367
|
+
details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
|
|
1492
1368
|
};
|
|
1493
1369
|
}
|
|
1494
1370
|
}
|
|
1371
|
+
|
|
1372
|
+
/** Build the tool result (summary text + details) for a settled run. */
|
|
1373
|
+
#buildResultPayload(
|
|
1374
|
+
result: SingleResult,
|
|
1375
|
+
projectAgentsDir: string | null,
|
|
1376
|
+
totalDurationMs: number,
|
|
1377
|
+
mergeSummary: string,
|
|
1378
|
+
): AgentToolResult<TaskToolDetails> {
|
|
1379
|
+
const status = result.aborted
|
|
1380
|
+
? "cancelled"
|
|
1381
|
+
: result.exitCode === 0 && result.error
|
|
1382
|
+
? "merge failed"
|
|
1383
|
+
: result.exitCode === 0
|
|
1384
|
+
? "completed"
|
|
1385
|
+
: `failed (exit ${result.exitCode})`;
|
|
1386
|
+
const output = formatResultOutputFallback(result);
|
|
1387
|
+
const outputCharCount = result.outputMeta?.charCount ?? output.length;
|
|
1388
|
+
const fullOutputThreshold = 5000;
|
|
1389
|
+
let preview = output;
|
|
1390
|
+
let truncated = false;
|
|
1391
|
+
if (outputCharCount > fullOutputThreshold) {
|
|
1392
|
+
const slice = output.slice(0, fullOutputThreshold);
|
|
1393
|
+
const lastNewline = slice.lastIndexOf("\n");
|
|
1394
|
+
preview = lastNewline >= 0 ? slice.slice(0, lastNewline) : slice;
|
|
1395
|
+
truncated = true;
|
|
1396
|
+
}
|
|
1397
|
+
const summary = prompt.render(taskSummaryTemplate, {
|
|
1398
|
+
agentName: result.agent,
|
|
1399
|
+
id: result.id,
|
|
1400
|
+
status,
|
|
1401
|
+
duration: formatDuration(totalDurationMs),
|
|
1402
|
+
preview,
|
|
1403
|
+
truncated,
|
|
1404
|
+
meta: result.outputMeta
|
|
1405
|
+
? {
|
|
1406
|
+
lineCount: result.outputMeta.lineCount,
|
|
1407
|
+
charSize: formatBytes(result.outputMeta.charCount),
|
|
1408
|
+
}
|
|
1409
|
+
: undefined,
|
|
1410
|
+
mergeSummary,
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
return {
|
|
1414
|
+
content: [{ type: "text", text: summary }],
|
|
1415
|
+
details: {
|
|
1416
|
+
projectAgentsDir,
|
|
1417
|
+
results: [result],
|
|
1418
|
+
totalDurationMs,
|
|
1419
|
+
usage: result.usage,
|
|
1420
|
+
outputPaths: result.outputPath ? [result.outputPath] : undefined,
|
|
1421
|
+
},
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1495
1424
|
}
|