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