@oh-my-pi/pi-coding-agent 15.10.11 → 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 +103 -2
- package/dist/cli.js +5790 -5731
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +85 -34
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- 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/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- 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/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- 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/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +5 -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/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +49 -32
- 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 +46 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +12 -2
- package/dist/types/task/index.d.ts +13 -6
- 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 +63 -51
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- 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 +7 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- 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/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- 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-commands.ts +29 -0
- package/src/cli.ts +28 -15
- package/src/commands/launch.ts +4 -0
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/keybindings.ts +6 -1
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +99 -55
- package/src/config/settings.ts +68 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/prelude.py +5 -6
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- 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/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -2
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- 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 +26 -66
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- 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/command-controller.ts +1 -1
- 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 +19 -2
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +47 -22
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/theme/theme.ts +18 -5
- package/src/modes/types.ts +5 -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 +37 -10
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +422 -291
- 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 +226 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +851 -461
- package/src/task/index.ts +721 -796
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +148 -63
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +82 -66
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +61 -10
- 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/image-gen.ts +11 -4
- package/src/tools/index.ts +17 -13
- package/src/tools/inspect-image.ts +1 -0
- 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/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- 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,
|
|
@@ -119,6 +124,7 @@ export type {
|
|
|
119
124
|
AgentDefinition,
|
|
120
125
|
AgentProgress,
|
|
121
126
|
SingleResult,
|
|
127
|
+
SubagentEventPayload,
|
|
122
128
|
SubagentLifecyclePayload,
|
|
123
129
|
SubagentProgressPayload,
|
|
124
130
|
TaskParams,
|
|
@@ -164,6 +170,17 @@ export function isReadOnlyAgent(agent: AgentDefinition): boolean {
|
|
|
164
170
|
return !!agent.tools?.length && agent.tools.every(tool => READ_ONLY_TOOL_NAMES.has(tool));
|
|
165
171
|
}
|
|
166
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
|
+
|
|
167
184
|
/**
|
|
168
185
|
* Render the tool description from a cached agent list and current settings.
|
|
169
186
|
*/
|
|
@@ -171,9 +188,8 @@ function renderDescription(
|
|
|
171
188
|
agents: AgentDefinition[],
|
|
172
189
|
maxConcurrency: number,
|
|
173
190
|
isolationEnabled: boolean,
|
|
174
|
-
asyncEnabled: boolean,
|
|
175
191
|
disabledAgents: string[],
|
|
176
|
-
|
|
192
|
+
batchEnabled: boolean,
|
|
177
193
|
ircEnabled: boolean,
|
|
178
194
|
parentSpawns: string,
|
|
179
195
|
): string {
|
|
@@ -195,19 +211,13 @@ function renderDescription(
|
|
|
195
211
|
description: agent.description,
|
|
196
212
|
readOnly: isReadOnlyAgent(agent),
|
|
197
213
|
}));
|
|
198
|
-
const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
|
|
199
214
|
return prompt.render(taskDescriptionTemplate, {
|
|
200
215
|
agents: renderedAgents,
|
|
201
216
|
spawningDisabled,
|
|
202
217
|
MAX_CONCURRENCY: maxConcurrency,
|
|
203
218
|
isolationEnabled,
|
|
204
|
-
|
|
205
|
-
contextEnabled,
|
|
206
|
-
customSchemaEnabled,
|
|
219
|
+
batchEnabled,
|
|
207
220
|
ircEnabled,
|
|
208
|
-
defaultMode: simpleMode === "default",
|
|
209
|
-
schemaFreeMode: simpleMode === "schema-free",
|
|
210
|
-
independentMode: simpleMode === "independent",
|
|
211
221
|
});
|
|
212
222
|
}
|
|
213
223
|
|
|
@@ -218,88 +228,120 @@ function createTaskModeError(text: string): AgentToolResult<TaskToolDetails> {
|
|
|
218
228
|
};
|
|
219
229
|
}
|
|
220
230
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (disallowedFields.length === 0) {
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (simpleMode === "schema-free") {
|
|
235
|
-
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)`.";
|
|
236
240
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
}
|
|
240
246
|
}
|
|
241
|
-
|
|
242
|
-
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;
|
|
243
248
|
}
|
|
244
249
|
|
|
245
|
-
/** Sentinel for async jobs whose subagent finished with a failing result; batch counters are already updated. */
|
|
246
|
-
class TaskJobError extends Error {}
|
|
247
|
-
|
|
248
250
|
/**
|
|
249
|
-
* Validate
|
|
250
|
-
*
|
|
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.
|
|
251
258
|
*/
|
|
252
|
-
function
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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 }).";
|
|
261
269
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (indexes) {
|
|
265
|
-
indexes.push(i);
|
|
266
|
-
} else {
|
|
267
|
-
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.";
|
|
268
272
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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.";
|
|
278
292
|
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (missingTaskIndexes.length === 0 && duplicateIds.length === 0) {
|
|
282
293
|
return undefined;
|
|
283
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
|
+
}
|
|
284
302
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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;
|
|
288
310
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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;
|
|
292
331
|
}
|
|
293
|
-
return
|
|
332
|
+
return spawn;
|
|
294
333
|
}
|
|
295
334
|
|
|
335
|
+
/** Sentinel for async jobs whose subagent finished with a failing result; progress is already updated. */
|
|
336
|
+
class TaskJobError extends Error {}
|
|
337
|
+
|
|
296
338
|
/**
|
|
297
339
|
* Process-level memo for create-time agent discovery, keyed by resolved cwd.
|
|
298
340
|
*
|
|
299
341
|
* `TaskTool.create` runs for every (sub)agent session in this process and the
|
|
300
342
|
* walk-up + plugin-registry scan in `discoverAgents` is identical for a given
|
|
301
343
|
* cwd, so repeat creations reuse the first scan. Execution-time discovery
|
|
302
|
-
* (`#
|
|
344
|
+
* (`#runSpawn`) intentionally stays fresh. The memo also tracks the live
|
|
303
345
|
* `discoverAgents` binding: test spies swap that binding, which invalidates
|
|
304
346
|
* the memo automatically.
|
|
305
347
|
*/
|
|
@@ -331,8 +373,9 @@ function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
|
|
|
331
373
|
/**
|
|
332
374
|
* Task tool - Delegate tasks to specialized agents.
|
|
333
375
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
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.
|
|
336
379
|
*/
|
|
337
380
|
export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetails, Theme> {
|
|
338
381
|
readonly name = "task";
|
|
@@ -343,11 +386,24 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
343
386
|
if (typeof params.agent === "string") {
|
|
344
387
|
lines.push(`Agent: ${truncateForPrompt(params.agent)}`);
|
|
345
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
|
+
}
|
|
346
398
|
const tasks = Array.isArray(params.tasks) ? params.tasks : [];
|
|
347
399
|
const firstTask = tasks[0];
|
|
348
400
|
if (firstTask) {
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
}
|
|
351
407
|
if (tasks.length > 1) {
|
|
352
408
|
lines.push(`+${tasks.length - 1} more task${tasks.length === 2 ? "" : "s"}`);
|
|
353
409
|
}
|
|
@@ -355,7 +411,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
355
411
|
return lines;
|
|
356
412
|
};
|
|
357
413
|
readonly label = "Task";
|
|
358
|
-
readonly summary = "Spawn a subagent to complete a
|
|
414
|
+
readonly summary = "Spawn a subagent to complete a task in the background";
|
|
359
415
|
readonly strict = true;
|
|
360
416
|
readonly loadMode = "discoverable";
|
|
361
417
|
readonly renderResult = renderResult;
|
|
@@ -365,10 +421,16 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
365
421
|
readonly mergeCallAndResult = true;
|
|
366
422
|
readonly #discoveredAgents: AgentDefinition[];
|
|
367
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;
|
|
368
430
|
|
|
369
431
|
get parameters(): TaskToolSchemaInstance {
|
|
370
432
|
const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
|
|
371
|
-
return getTaskSchema({ isolationEnabled,
|
|
433
|
+
return getTaskSchema({ isolationEnabled, batchEnabled: this.#isBatchEnabled() });
|
|
372
434
|
}
|
|
373
435
|
|
|
374
436
|
renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme) {
|
|
@@ -384,10 +446,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
384
446
|
this.#discoveredAgents,
|
|
385
447
|
maxConcurrency,
|
|
386
448
|
isolationMode !== "none",
|
|
387
|
-
this.session.settings.get("async.enabled"),
|
|
388
449
|
disabledAgents,
|
|
389
|
-
this.#
|
|
390
|
-
this.session.settings.
|
|
450
|
+
this.#isBatchEnabled(),
|
|
451
|
+
isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
|
|
391
452
|
this.session.getSessionSpawns() ?? "*",
|
|
392
453
|
);
|
|
393
454
|
}
|
|
@@ -399,8 +460,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
399
460
|
this.#discoveredAgents = discoveredAgents;
|
|
400
461
|
}
|
|
401
462
|
|
|
402
|
-
#
|
|
403
|
-
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;
|
|
404
470
|
}
|
|
405
471
|
|
|
406
472
|
/**
|
|
@@ -412,335 +478,427 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
412
478
|
}
|
|
413
479
|
|
|
414
480
|
async execute(
|
|
415
|
-
|
|
481
|
+
toolCallId: string,
|
|
416
482
|
rawParams: unknown,
|
|
417
483
|
signal?: AbortSignal,
|
|
418
484
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
419
485
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
420
486
|
const params = repairTaskParams(rawParams as TaskParams);
|
|
421
|
-
const
|
|
422
|
-
const validationError =
|
|
487
|
+
const batchEnabled = this.#isBatchEnabled();
|
|
488
|
+
const validationError = validateShapeParams(batchEnabled, params) ?? validateSpawnParams(params, batchEnabled);
|
|
423
489
|
if (validationError) {
|
|
424
490
|
return createTaskModeError(validationError);
|
|
425
491
|
}
|
|
426
492
|
|
|
427
|
-
const
|
|
493
|
+
const spawnItems = resolveSpawnItems(params);
|
|
428
494
|
const selectedAgent = this.#discoveredAgents.find(agent => agent.name === params.agent);
|
|
429
|
-
if (!asyncEnabled || selectedAgent?.blocking === true) {
|
|
430
|
-
return this.#executeSync(_toolCallId, params, signal, onUpdate);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
495
|
const manager = this.session.asyncJobManager;
|
|
434
|
-
if (!manager) {
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const taskItems = params.tasks ?? [];
|
|
444
|
-
if (taskItems.length === 0) {
|
|
445
|
-
return this.#executeSync(_toolCallId, params, signal, onUpdate);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const taskIdProblem = validateTaskIds(taskItems);
|
|
449
|
-
if (taskIdProblem) {
|
|
450
|
-
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);
|
|
451
504
|
}
|
|
452
505
|
|
|
506
|
+
// Resolve agent ids up front so the immediate result can name them.
|
|
453
507
|
const outputManager =
|
|
454
508
|
this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
const assignment =
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
},
|
|
477
536
|
});
|
|
478
537
|
}
|
|
479
538
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// immutable once attached — structuredClone here cost O(batch × payload)
|
|
489
|
-
// per progress event.
|
|
490
|
-
return Array.from(progressByTaskId.values())
|
|
491
|
-
.sort((a, b) => a.index - b.index)
|
|
492
|
-
.map(progress => ({ ...progress }));
|
|
493
|
-
};
|
|
494
|
-
|
|
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;
|
|
495
547
|
const buildAsyncDetails = (state: "running" | "completed" | "failed", jobId: string): TaskToolDetails => ({
|
|
496
548
|
projectAgentsDir: null,
|
|
497
549
|
results: [],
|
|
498
550
|
totalDurationMs: 0,
|
|
499
|
-
progress:
|
|
500
|
-
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
|
+
},
|
|
501
557
|
});
|
|
502
558
|
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
details: buildAsyncDetails(state, primaryJobId),
|
|
508
|
-
});
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
512
|
-
const semaphore = new Semaphore(maxConcurrency);
|
|
513
|
-
|
|
514
|
-
for (let i = 0; i < taskItems.length; i++) {
|
|
515
|
-
const taskItem = taskItems[i];
|
|
516
|
-
if (signal?.aborted) {
|
|
517
|
-
failedSchedules.push(`${taskItem.id}: cancelled before scheduling`);
|
|
518
|
-
completedJobs += 1;
|
|
519
|
-
const progress = progressByTaskId.get(taskItem.id);
|
|
520
|
-
if (progress) {
|
|
521
|
-
progress.status = "aborted";
|
|
522
|
-
}
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const uniqueId = uniqueIds[i];
|
|
527
|
-
const singleParams: TaskParams = { ...params, tasks: [taskItem] };
|
|
528
|
-
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) {
|
|
529
563
|
try {
|
|
530
|
-
const jobId =
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
completedJobs += 1;
|
|
543
|
-
failedJobs += 1;
|
|
544
|
-
throw new Error("Aborted before execution");
|
|
545
|
-
}
|
|
546
|
-
markRunning();
|
|
547
|
-
if (progress) {
|
|
548
|
-
progress.status = "running";
|
|
549
|
-
}
|
|
550
|
-
await reportProgress(
|
|
551
|
-
`Running background task ${taskItem.id}...`,
|
|
552
|
-
buildAsyncDetails("running", startedJobs[0]?.jobId ?? label) as unknown as Record<string, unknown>,
|
|
553
|
-
);
|
|
554
|
-
try {
|
|
555
|
-
const result = await this.#executeSync(_toolCallId, singleParams, runSignal, undefined, [
|
|
556
|
-
uniqueId,
|
|
557
|
-
]);
|
|
558
|
-
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
559
|
-
const singleResult = result.details?.results[0];
|
|
560
|
-
// A missing per-task result means #executeSync failed at the
|
|
561
|
-
// tool level (results: []) — treat it as a failure, not success.
|
|
562
|
-
const resultFailed =
|
|
563
|
-
!singleResult || (singleResult.aborted ?? false) || singleResult.exitCode !== 0;
|
|
564
|
-
if (progress) {
|
|
565
|
-
progress.status = singleResult?.aborted ? "aborted" : resultFailed ? "failed" : "completed";
|
|
566
|
-
progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
|
|
567
|
-
progress.tokens = singleResult?.tokens ?? 0;
|
|
568
|
-
progress.contextTokens = singleResult?.contextTokens;
|
|
569
|
-
progress.contextWindow = singleResult?.contextWindow;
|
|
570
|
-
progress.cost = singleResult?.usage?.cost.total ?? 0;
|
|
571
|
-
progress.extractedToolData = singleResult?.extractedToolData;
|
|
572
|
-
progress.retryFailure = singleResult?.retryFailure;
|
|
573
|
-
progress.retryState = undefined;
|
|
574
|
-
}
|
|
575
|
-
completedJobs += 1;
|
|
576
|
-
if (resultFailed) {
|
|
577
|
-
failedJobs += 1;
|
|
578
|
-
}
|
|
579
|
-
const remaining = taskItems.length - completedJobs;
|
|
580
|
-
const isDone = remaining === 0;
|
|
581
|
-
await reportProgress(
|
|
582
|
-
isDone
|
|
583
|
-
? `Background task batch complete: ${completedJobs}/${taskItems.length} finished.`
|
|
584
|
-
: `Background task batch progress: ${completedJobs}/${taskItems.length} finished (${remaining} running).`,
|
|
585
|
-
buildAsyncDetails(
|
|
586
|
-
isDone ? (failedJobs > 0 || failedSchedules.length > 0 ? "failed" : "completed") : "running",
|
|
587
|
-
startedJobs[0]?.jobId ?? label,
|
|
588
|
-
) as unknown as Record<string, unknown>,
|
|
589
|
-
);
|
|
590
|
-
if (isDone) {
|
|
591
|
-
emitAsyncUpdate(
|
|
592
|
-
failedJobs > 0 || failedSchedules.length > 0 ? "failed" : "completed",
|
|
593
|
-
`Background task batch complete: ${completedJobs}/${taskItems.length} finished.`,
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
if (resultFailed) {
|
|
597
|
-
// Mark the job itself failed; counters above are already updated.
|
|
598
|
-
throw new TaskJobError(finalText);
|
|
599
|
-
}
|
|
600
|
-
return finalText;
|
|
601
|
-
} catch (error) {
|
|
602
|
-
if (error instanceof TaskJobError) {
|
|
603
|
-
throw error;
|
|
604
|
-
}
|
|
605
|
-
if (progress) {
|
|
606
|
-
progress.status = "failed";
|
|
607
|
-
progress.durationMs = Math.max(0, Date.now() - startedAt);
|
|
608
|
-
}
|
|
609
|
-
completedJobs += 1;
|
|
610
|
-
failedJobs += 1;
|
|
611
|
-
const remaining = taskItems.length - completedJobs;
|
|
612
|
-
const isDone = remaining === 0;
|
|
613
|
-
await reportProgress(
|
|
614
|
-
isDone
|
|
615
|
-
? `Background task batch complete with failures: ${failedJobs} failed.`
|
|
616
|
-
: `Background task batch progress: ${completedJobs}/${taskItems.length} finished (${remaining} running).`,
|
|
617
|
-
buildAsyncDetails(
|
|
618
|
-
isDone ? "failed" : "running",
|
|
619
|
-
startedJobs[0]?.jobId ?? label,
|
|
620
|
-
) as unknown as Record<string, unknown>,
|
|
621
|
-
);
|
|
622
|
-
if (isDone) {
|
|
623
|
-
emitAsyncUpdate(
|
|
624
|
-
"failed",
|
|
625
|
-
`Background task batch complete with failures: ${failedJobs} failed.`,
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
throw error;
|
|
629
|
-
} finally {
|
|
630
|
-
semaphore.release();
|
|
631
|
-
}
|
|
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;
|
|
632
576
|
},
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
637
|
-
onProgress: (text, details) => {
|
|
638
|
-
const progressDetails =
|
|
639
|
-
(details as TaskToolDetails | undefined) ??
|
|
640
|
-
buildAsyncDetails("running", startedJobs[0]?.jobId ?? label);
|
|
641
|
-
onUpdate?.({ content: [{ type: "text", text }], details: progressDetails });
|
|
642
|
-
},
|
|
643
|
-
},
|
|
644
|
-
);
|
|
645
|
-
startedJobs.push({ jobId, taskId: taskItem.id });
|
|
577
|
+
});
|
|
578
|
+
if (started.length === 0) primaryJobId = jobId;
|
|
579
|
+
started.push({ agentId: spawn.agentId, jobId, description: spawn.item.description });
|
|
646
580
|
} catch (error) {
|
|
647
581
|
const message = error instanceof Error ? error.message : String(error);
|
|
648
|
-
failedSchedules.push(`${
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
progress.status = "failed";
|
|
653
|
-
}
|
|
582
|
+
failedSchedules.push(`${spawn.agentId}: ${message}`);
|
|
583
|
+
spawn.progress.status = "failed";
|
|
584
|
+
settledCount += 1;
|
|
585
|
+
failedCount += 1;
|
|
654
586
|
}
|
|
655
587
|
}
|
|
656
588
|
|
|
657
|
-
if (
|
|
658
|
-
const failureText = `Failed to start background task jobs: ${failedSchedules.join("; ")}`;
|
|
589
|
+
if (started.length === 0) {
|
|
659
590
|
return {
|
|
660
|
-
content: [
|
|
591
|
+
content: [
|
|
592
|
+
{
|
|
593
|
+
type: "text",
|
|
594
|
+
text: `Failed to start background task job${single ? "" : "s"}: ${failedSchedules.join("; ")}`,
|
|
595
|
+
},
|
|
596
|
+
],
|
|
661
597
|
details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
|
|
662
598
|
};
|
|
663
599
|
}
|
|
664
600
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
+
}
|
|
669
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.`;
|
|
670
625
|
const scheduleFailureSummary =
|
|
671
626
|
failedSchedules.length > 0
|
|
672
|
-
? ` Failed to schedule ${failedSchedules.length}
|
|
627
|
+
? ` Failed to schedule ${failedSchedules.length} spawn${failedSchedules.length === 1 ? "" : "s"}: ${failedSchedules.join("; ")}.`
|
|
673
628
|
: "";
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
taskIdByItemId.set(taskItems[i].id, uniqueIds[i]);
|
|
679
|
-
}
|
|
680
|
-
const startedListing = startedJobs
|
|
681
|
-
.map(({ taskId, jobId }) => {
|
|
682
|
-
const id = taskIdByItemId.get(taskId) ?? taskId;
|
|
683
|
-
const desc = progressByTaskId.get(taskId)?.description;
|
|
684
|
-
const prefix = `- \`${id}\` (job \`${jobId}\`)`;
|
|
685
|
-
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;
|
|
686
633
|
})
|
|
687
634
|
.join("\n");
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
:
|
|
691
|
-
|
|
635
|
+
onUpdate?.({
|
|
636
|
+
content: [{ type: "text", text: `Spawned ${started.length} agents...` }],
|
|
637
|
+
details: buildAsyncDetails("running", primaryJobId),
|
|
638
|
+
});
|
|
692
639
|
return {
|
|
693
640
|
content: [
|
|
694
641
|
{
|
|
695
642
|
type: "text",
|
|
696
|
-
text: `
|
|
643
|
+
text: `Spawned ${started.length} background agents using ${agentLabel}.${scheduleFailureSummary} Each result will be delivered when that agent yields.\n${startedListing}\n${coordinationHint}`,
|
|
697
644
|
},
|
|
698
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") }],
|
|
699
851
|
details: {
|
|
700
|
-
projectAgentsDir
|
|
701
|
-
results
|
|
702
|
-
totalDurationMs:
|
|
703
|
-
|
|
704
|
-
|
|
852
|
+
projectAgentsDir,
|
|
853
|
+
results,
|
|
854
|
+
totalDurationMs: Date.now() - startTime,
|
|
855
|
+
usage: hasUsage ? usageTotals : undefined,
|
|
856
|
+
outputPaths: outputPaths.length > 0 ? outputPaths : undefined,
|
|
705
857
|
},
|
|
706
858
|
};
|
|
707
859
|
}
|
|
708
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
|
+
*/
|
|
709
867
|
async #executeSync(
|
|
710
|
-
|
|
868
|
+
toolCallId: string,
|
|
711
869
|
params: TaskParams,
|
|
712
870
|
signal?: AbortSignal,
|
|
713
871
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
714
|
-
|
|
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,
|
|
715
884
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
716
885
|
const startTime = Date.now();
|
|
717
886
|
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
|
|
718
|
-
const
|
|
719
|
-
const
|
|
720
|
-
const
|
|
721
|
-
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();
|
|
722
890
|
const isolationMode = this.session.settings.get("task.isolation.mode");
|
|
723
891
|
const isolationRequested = "isolated" in params ? params.isolated === true : false;
|
|
724
892
|
const isIsolated = isolationMode !== "none" && isolationRequested;
|
|
725
893
|
const mergeMode = this.session.settings.get("task.isolation.merge");
|
|
726
894
|
const commitStyle = this.session.settings.get("task.isolation.commits");
|
|
727
|
-
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
728
895
|
const taskDepth = this.session.taskDepth ?? 0;
|
|
729
896
|
const subagentLspEnabled = (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp");
|
|
730
897
|
|
|
731
898
|
if (isolationMode === "none" && "isolated" in params) {
|
|
732
899
|
return {
|
|
733
|
-
content: [
|
|
734
|
-
|
|
735
|
-
type: "text",
|
|
736
|
-
text: "Task isolation is disabled.",
|
|
737
|
-
},
|
|
738
|
-
],
|
|
739
|
-
details: {
|
|
740
|
-
projectAgentsDir,
|
|
741
|
-
results: [],
|
|
742
|
-
totalDurationMs: 0,
|
|
743
|
-
},
|
|
900
|
+
content: [{ type: "text", text: "Task isolation is disabled." }],
|
|
901
|
+
details: { projectAgentsDir, results: [], totalDurationMs: 0 },
|
|
744
902
|
};
|
|
745
903
|
}
|
|
746
904
|
|
|
@@ -749,17 +907,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
749
907
|
if (!agent) {
|
|
750
908
|
const available = agents.map(a => a.name).join(", ") || "none";
|
|
751
909
|
return {
|
|
752
|
-
content: [
|
|
753
|
-
|
|
754
|
-
type: "text",
|
|
755
|
-
text: `Unknown agent "${agentName}". Available: ${available}`,
|
|
756
|
-
},
|
|
757
|
-
],
|
|
758
|
-
details: {
|
|
759
|
-
projectAgentsDir,
|
|
760
|
-
results: [],
|
|
761
|
-
totalDurationMs: 0,
|
|
762
|
-
},
|
|
910
|
+
content: [{ type: "text", text: `Unknown agent "${agentName}". Available: ${available}` }],
|
|
911
|
+
details: { projectAgentsDir, results: [], totalDurationMs: 0 },
|
|
763
912
|
};
|
|
764
913
|
}
|
|
765
914
|
|
|
@@ -774,11 +923,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
774
923
|
text: `Agent "${agentName}" is disabled in settings. Enable it via /agents, or use a different agent type.${enabled.length > 0 ? ` Available: ${enabled.join(", ")}` : ""}`,
|
|
775
924
|
},
|
|
776
925
|
],
|
|
777
|
-
details: {
|
|
778
|
-
projectAgentsDir,
|
|
779
|
-
results: [],
|
|
780
|
-
totalDurationMs: 0,
|
|
781
|
-
},
|
|
926
|
+
details: { projectAgentsDir, results: [], totalDurationMs: 0 },
|
|
782
927
|
};
|
|
783
928
|
}
|
|
784
929
|
|
|
@@ -812,43 +957,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
812
957
|
});
|
|
813
958
|
const thinkingLevelOverride = effectiveAgent.thinkingLevel;
|
|
814
959
|
|
|
815
|
-
// Output schema priority:
|
|
816
|
-
// task
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
: (effectiveAgent.output ?? this.session.outputSchema);
|
|
820
|
-
|
|
821
|
-
// Handle empty or missing tasks
|
|
822
|
-
if (!params.tasks || params.tasks.length === 0) {
|
|
823
|
-
return {
|
|
824
|
-
content: [
|
|
825
|
-
{
|
|
826
|
-
type: "text",
|
|
827
|
-
text: contextEnabled
|
|
828
|
-
? "No tasks provided. Use: { agent, context?, tasks: [{ id, description, assignment }, ...] }"
|
|
829
|
-
: "No tasks provided. Use: { agent, tasks: [{ id, description, assignment }, ...] }",
|
|
830
|
-
},
|
|
831
|
-
],
|
|
832
|
-
details: {
|
|
833
|
-
projectAgentsDir,
|
|
834
|
-
results: [],
|
|
835
|
-
totalDurationMs: 0,
|
|
836
|
-
},
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const tasks = params.tasks;
|
|
841
|
-
const taskIdProblem = validateTaskIds(tasks);
|
|
842
|
-
if (taskIdProblem) {
|
|
843
|
-
return {
|
|
844
|
-
content: [{ type: "text", text: taskIdProblem }],
|
|
845
|
-
details: {
|
|
846
|
-
projectAgentsDir,
|
|
847
|
-
results: [],
|
|
848
|
-
totalDurationMs: 0,
|
|
849
|
-
},
|
|
850
|
-
};
|
|
851
|
-
}
|
|
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;
|
|
852
964
|
|
|
853
965
|
let repoRoot: string | null = null;
|
|
854
966
|
let baseline: WorktreeBaseline | null = null;
|
|
@@ -859,17 +971,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
859
971
|
} catch (err) {
|
|
860
972
|
const message = err instanceof Error ? err.message : String(err);
|
|
861
973
|
return {
|
|
862
|
-
content: [
|
|
863
|
-
|
|
864
|
-
type: "text",
|
|
865
|
-
text: `Isolated task execution requires a git repository. ${message}`,
|
|
866
|
-
},
|
|
867
|
-
],
|
|
868
|
-
details: {
|
|
869
|
-
projectAgentsDir,
|
|
870
|
-
results: [],
|
|
871
|
-
totalDurationMs: Date.now() - startTime,
|
|
872
|
-
},
|
|
974
|
+
content: [{ type: "text", text: `Isolated task execution requires a git repository. ${message}` }],
|
|
975
|
+
details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
|
|
873
976
|
};
|
|
874
977
|
}
|
|
875
978
|
}
|
|
@@ -902,23 +1005,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
902
1005
|
localProtocolOptions,
|
|
903
1006
|
);
|
|
904
1007
|
|
|
905
|
-
// Initialize progress tracking
|
|
906
|
-
const progressMap = new Map<number, AgentProgress>();
|
|
907
|
-
|
|
908
|
-
// Update callback
|
|
909
|
-
const emitProgress = () => {
|
|
910
|
-
const progress = Array.from(progressMap.values()).sort((a, b) => a.index - b.index);
|
|
911
|
-
onUpdate?.({
|
|
912
|
-
content: [{ type: "text", text: `Running ${params.tasks.length} agents...` }],
|
|
913
|
-
details: {
|
|
914
|
-
projectAgentsDir,
|
|
915
|
-
results: [],
|
|
916
|
-
totalDurationMs: Date.now() - startTime,
|
|
917
|
-
progress,
|
|
918
|
-
},
|
|
919
|
-
});
|
|
920
|
-
};
|
|
921
|
-
|
|
922
1008
|
try {
|
|
923
1009
|
// Check self-recursion prevention
|
|
924
1010
|
if (this.#blockedAgent && agentName === this.#blockedAgent) {
|
|
@@ -929,11 +1015,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
929
1015
|
text: `Cannot spawn ${this.#blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
|
|
930
1016
|
},
|
|
931
1017
|
],
|
|
932
|
-
details: {
|
|
933
|
-
projectAgentsDir,
|
|
934
|
-
results: [],
|
|
935
|
-
totalDurationMs: Date.now() - startTime,
|
|
936
|
-
},
|
|
1018
|
+
details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
|
|
937
1019
|
};
|
|
938
1020
|
}
|
|
939
1021
|
|
|
@@ -950,36 +1032,21 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
950
1032
|
const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
|
|
951
1033
|
return {
|
|
952
1034
|
content: [{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${allowed}` }],
|
|
953
|
-
details: {
|
|
954
|
-
projectAgentsDir,
|
|
955
|
-
results: [],
|
|
956
|
-
totalDurationMs: Date.now() - startTime,
|
|
957
|
-
},
|
|
1035
|
+
details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
|
|
958
1036
|
};
|
|
959
1037
|
}
|
|
960
1038
|
|
|
961
|
-
// Write parent conversation context for subagents. When IRC is available,
|
|
962
|
-
// subagents should ask live peers instead of reading a stale markdown dump.
|
|
963
1039
|
await fs.mkdir(effectiveArtifactsDir, { recursive: true });
|
|
964
|
-
const shouldWriteConversationContext = this.session.settings.get("irc.enabled") !== true;
|
|
965
|
-
const compactContext = shouldWriteConversationContext ? this.session.getCompactContext?.() : undefined;
|
|
966
|
-
let contextFilePath: string | undefined;
|
|
967
|
-
if (compactContext) {
|
|
968
|
-
contextFilePath = path.join(effectiveArtifactsDir, "context.md");
|
|
969
|
-
await Bun.write(contextFilePath, compactContext);
|
|
970
|
-
}
|
|
971
1040
|
|
|
972
|
-
//
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
uniqueIds = preAllocatedIds;
|
|
1041
|
+
// Allocate a unique ID across the session to prevent artifact collisions
|
|
1042
|
+
let agentId: string;
|
|
1043
|
+
if (preAllocatedId) {
|
|
1044
|
+
agentId = preAllocatedId;
|
|
977
1045
|
} else {
|
|
978
1046
|
const outputManager =
|
|
979
1047
|
this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
|
|
980
|
-
|
|
1048
|
+
agentId = await outputManager.allocate(params.id?.trim() || generateTaskName());
|
|
981
1049
|
}
|
|
982
|
-
const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
|
|
983
1050
|
|
|
984
1051
|
const availableSkills = [...(this.session.skills ?? [])];
|
|
985
1052
|
// Resolve autoload skills from agent definition against available skills
|
|
@@ -996,84 +1063,102 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
996
1063
|
const parentEvalSessionId = this.session.getEvalSessionId?.() ?? undefined;
|
|
997
1064
|
const mcpManager = this.session.mcpManager ?? MCPManager.instance();
|
|
998
1065
|
|
|
999
|
-
//
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
+
},
|
|
1019
1094
|
});
|
|
1020
|
-
}
|
|
1095
|
+
};
|
|
1021
1096
|
emitProgress();
|
|
1022
1097
|
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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> => {
|
|
1028
1160
|
if (!isIsolated) {
|
|
1029
|
-
return runSubprocess(
|
|
1030
|
-
cwd: this.session.cwd,
|
|
1031
|
-
agent: effectiveAgent,
|
|
1032
|
-
task: renderSubagentUserPrompt(task.assignment, simpleMode),
|
|
1033
|
-
assignment: task.assignment.trim(),
|
|
1034
|
-
context: sharedContext,
|
|
1035
|
-
planReference,
|
|
1036
|
-
description: task.description,
|
|
1037
|
-
index,
|
|
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,72 +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
|
-
id: task.id,
|
|
1101
|
-
taskDepth,
|
|
1102
|
-
modelOverride,
|
|
1103
|
-
parentActiveModelPattern,
|
|
1104
|
-
thinkingLevel: thinkingLevelOverride,
|
|
1105
|
-
outputSchema: effectiveOutputSchema,
|
|
1106
|
-
sessionFile,
|
|
1107
|
-
persistArtifacts: !!artifactsDir,
|
|
1108
|
-
artifactsDir: effectiveArtifactsDir,
|
|
1109
|
-
contextFile: contextFilePath,
|
|
1110
|
-
enableLsp: subagentLspEnabled,
|
|
1111
|
-
signal: workerSignal ?? signal,
|
|
1112
|
-
eventBus: this.session.eventBus,
|
|
1113
|
-
onProgress: progress => {
|
|
1114
|
-
progressMap.set(index, { ...progress, recentTools: progress.recentTools.slice() });
|
|
1115
|
-
emitProgress();
|
|
1116
|
-
},
|
|
1117
|
-
authStorage: this.session.authStorage,
|
|
1118
|
-
modelRegistry: this.session.modelRegistry,
|
|
1119
|
-
settings: this.session.settings,
|
|
1120
|
-
mcpManager,
|
|
1121
|
-
contextFiles,
|
|
1122
|
-
skills: availableSkills,
|
|
1123
|
-
autoloadSkills: resolvedAutoloadSkills,
|
|
1124
|
-
workspaceTree: this.session.workspaceTree,
|
|
1125
|
-
promptTemplates,
|
|
1126
|
-
rules: this.session.rules,
|
|
1127
|
-
localProtocolOptions,
|
|
1128
|
-
parentArtifactManager,
|
|
1129
|
-
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
1130
|
-
parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
|
|
1131
|
-
parentTelemetry: this.session.getTelemetry?.(),
|
|
1132
|
-
parentEvalSessionId,
|
|
1180
|
+
preloadedExtensionPaths: undefined,
|
|
1181
|
+
preloadedCustomToolPaths: undefined,
|
|
1133
1182
|
});
|
|
1134
1183
|
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
1135
1184
|
try {
|
|
1136
|
-
const commitMsg =
|
|
1137
|
-
commitStyle === "ai" && this.session.modelRegistry
|
|
1138
|
-
? async (diff: string) => {
|
|
1139
|
-
return generateCommitMessage(
|
|
1140
|
-
diff,
|
|
1141
|
-
this.session.modelRegistry!,
|
|
1142
|
-
this.session.settings,
|
|
1143
|
-
this.session.getSessionId?.() ?? undefined,
|
|
1144
|
-
);
|
|
1145
|
-
}
|
|
1146
|
-
: undefined;
|
|
1147
1185
|
const commitResult = await commitToBranch(
|
|
1148
1186
|
isolationDir,
|
|
1149
1187
|
taskBaseline,
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1188
|
+
agentId,
|
|
1189
|
+
params.description,
|
|
1190
|
+
buildCommitMessageFn(),
|
|
1153
1191
|
);
|
|
1154
1192
|
return {
|
|
1155
1193
|
...result,
|
|
@@ -1158,7 +1196,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1158
1196
|
};
|
|
1159
1197
|
} catch (mergeErr) {
|
|
1160
1198
|
// Agent succeeded but branch commit failed — clean up stale branch
|
|
1161
|
-
const branchName = `omp/task/${
|
|
1199
|
+
const branchName = `omp/task/${agentId}`;
|
|
1162
1200
|
await git.branch.tryDelete(repoRoot, branchName);
|
|
1163
1201
|
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
1164
1202
|
return { ...result, error: `Merge failed: ${msg}` };
|
|
@@ -1167,7 +1205,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1167
1205
|
if (result.exitCode === 0) {
|
|
1168
1206
|
try {
|
|
1169
1207
|
const delta = await captureDeltaPatch(isolationDir, taskBaseline);
|
|
1170
|
-
const patchPath = path.join(effectiveArtifactsDir, `${
|
|
1208
|
+
const patchPath = path.join(effectiveArtifactsDir, `${agentId}.patch`);
|
|
1171
1209
|
await Bun.write(patchPath, delta.rootPatch);
|
|
1172
1210
|
return {
|
|
1173
1211
|
...result,
|
|
@@ -1182,21 +1220,21 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1182
1220
|
return result;
|
|
1183
1221
|
} catch (err) {
|
|
1184
1222
|
const message = err instanceof Error ? err.message : String(err);
|
|
1185
|
-
const assignment = task.assignment.trim();
|
|
1186
1223
|
return {
|
|
1187
|
-
index,
|
|
1188
|
-
id:
|
|
1224
|
+
index: 0,
|
|
1225
|
+
id: agentId,
|
|
1189
1226
|
agent: agent.name,
|
|
1190
1227
|
agentSource: agent.source,
|
|
1191
|
-
task: renderSubagentUserPrompt(assignment
|
|
1228
|
+
task: renderSubagentUserPrompt(assignment),
|
|
1192
1229
|
assignment,
|
|
1193
|
-
description:
|
|
1230
|
+
description: params.description,
|
|
1194
1231
|
exitCode: 1,
|
|
1195
1232
|
output: "",
|
|
1196
1233
|
stderr: message,
|
|
1197
1234
|
truncated: false,
|
|
1198
1235
|
durationMs: Date.now() - taskStart,
|
|
1199
1236
|
tokens: 0,
|
|
1237
|
+
requests: 0,
|
|
1200
1238
|
modelOverride,
|
|
1201
1239
|
error: message,
|
|
1202
1240
|
};
|
|
@@ -1207,147 +1245,68 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1207
1245
|
}
|
|
1208
1246
|
};
|
|
1209
1247
|
|
|
1210
|
-
|
|
1211
|
-
const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
|
|
1212
|
-
tasksWithUniqueIds,
|
|
1213
|
-
maxConcurrency,
|
|
1214
|
-
runTask,
|
|
1215
|
-
signal,
|
|
1216
|
-
);
|
|
1217
|
-
|
|
1218
|
-
// Fill in skipped tasks (undefined entries from abort) with placeholder results
|
|
1219
|
-
const results: SingleResult[] = partialResults.map((result, index) => {
|
|
1220
|
-
if (result !== undefined) {
|
|
1221
|
-
return result;
|
|
1222
|
-
}
|
|
1223
|
-
const task = tasksWithUniqueIds[index];
|
|
1224
|
-
const assignment = task.assignment.trim();
|
|
1225
|
-
return {
|
|
1226
|
-
index,
|
|
1227
|
-
id: task.id,
|
|
1228
|
-
agent: agentName,
|
|
1229
|
-
agentSource: agent.source,
|
|
1230
|
-
task: renderSubagentUserPrompt(assignment, simpleMode),
|
|
1231
|
-
assignment,
|
|
1232
|
-
description: task.description,
|
|
1233
|
-
exitCode: 1,
|
|
1234
|
-
output: "",
|
|
1235
|
-
stderr: "Skipped (cancelled before start)",
|
|
1236
|
-
truncated: false,
|
|
1237
|
-
durationMs: 0,
|
|
1238
|
-
tokens: 0,
|
|
1239
|
-
modelOverride,
|
|
1240
|
-
error: "Cancelled before start",
|
|
1241
|
-
aborted: true,
|
|
1242
|
-
abortReason: "Cancelled before start",
|
|
1243
|
-
};
|
|
1244
|
-
});
|
|
1245
|
-
|
|
1246
|
-
// Aggregate usage from executor results (already accumulated incrementally)
|
|
1247
|
-
const aggregatedUsage = createUsageTotals();
|
|
1248
|
-
let hasAggregatedUsage = false;
|
|
1249
|
-
for (const result of results) {
|
|
1250
|
-
if (result.usage) {
|
|
1251
|
-
addUsageTotals(aggregatedUsage, result.usage);
|
|
1252
|
-
hasAggregatedUsage = true;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
// Collect output paths (artifacts already written by executor in real-time)
|
|
1257
|
-
const outputPaths: string[] = [];
|
|
1258
|
-
const patchPaths: string[] = [];
|
|
1259
|
-
for (const result of results) {
|
|
1260
|
-
if (result.outputPath) {
|
|
1261
|
-
outputPaths.push(result.outputPath);
|
|
1262
|
-
}
|
|
1263
|
-
if (result.patchPath) {
|
|
1264
|
-
patchPaths.push(result.patchPath);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1248
|
+
const result = await runTask();
|
|
1267
1249
|
|
|
1268
1250
|
let mergeSummary = "";
|
|
1269
1251
|
let changesApplied: boolean | null = null;
|
|
1270
1252
|
let hadAnyChanges = false;
|
|
1271
|
-
let
|
|
1253
|
+
let mergedBranchForNestedPatches = false;
|
|
1272
1254
|
if (isIsolated && repoRoot) {
|
|
1273
1255
|
try {
|
|
1274
1256
|
if (mergeMode === "branch") {
|
|
1275
|
-
|
|
1276
|
-
const branchEntries = results
|
|
1277
|
-
.filter(r => r.branchName && r.exitCode === 0 && !r.aborted)
|
|
1278
|
-
.map(r => ({ branchName: r.branchName!, taskId: r.id, description: r.description }));
|
|
1279
|
-
|
|
1280
|
-
if (branchEntries.length === 0) {
|
|
1257
|
+
if (!result.branchName || result.exitCode !== 0 || result.aborted) {
|
|
1281
1258
|
changesApplied = true;
|
|
1282
|
-
hadAnyChanges = false;
|
|
1283
1259
|
mergeSummary = "\n\nNo changes to apply.";
|
|
1284
1260
|
} else {
|
|
1285
|
-
const mergeResult = await mergeTaskBranches(repoRoot,
|
|
1286
|
-
|
|
1261
|
+
const mergeResult = await mergeTaskBranches(repoRoot, [
|
|
1262
|
+
{ branchName: result.branchName, taskId: result.id, description: result.description },
|
|
1263
|
+
]);
|
|
1264
|
+
mergedBranchForNestedPatches = mergeResult.merged.includes(result.branchName);
|
|
1287
1265
|
changesApplied = mergeResult.failed.length === 0;
|
|
1288
1266
|
hadAnyChanges = changesApplied && mergeResult.merged.length > 0;
|
|
1289
1267
|
|
|
1290
1268
|
if (changesApplied) {
|
|
1291
1269
|
mergeSummary = hadAnyChanges
|
|
1292
|
-
? `\n\nMerged
|
|
1270
|
+
? `\n\nMerged branch: ${result.branchName}`
|
|
1293
1271
|
: "\n\nNo changes to apply.";
|
|
1294
1272
|
} else {
|
|
1295
|
-
const mergedPart =
|
|
1296
|
-
mergeResult.merged.length > 0 ? `Merged: ${mergeResult.merged.join(", ")}.\n` : "";
|
|
1297
|
-
const failedPart = `Failed: ${mergeResult.failed.join(", ")}.`;
|
|
1298
1273
|
const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
|
|
1299
|
-
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>`;
|
|
1300
1275
|
}
|
|
1301
1276
|
if (mergeResult.stashConflict) {
|
|
1302
1277
|
mergeSummary += `\n\n<system-notification>${mergeResult.stashConflict}</system-notification>`;
|
|
1303
1278
|
}
|
|
1304
|
-
}
|
|
1305
1279
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1280
|
+
// Clean up the merged branch (keep failed ones for manual resolution)
|
|
1281
|
+
if (changesApplied) {
|
|
1282
|
+
await cleanupTaskBranches(repoRoot, [result.branchName]);
|
|
1283
|
+
}
|
|
1310
1284
|
}
|
|
1311
1285
|
} else {
|
|
1312
|
-
// Patch mode: apply
|
|
1313
|
-
// aborted
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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) {
|
|
1318
1293
|
changesApplied = false;
|
|
1319
1294
|
hadAnyChanges = false;
|
|
1320
1295
|
} else {
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
patchPath,
|
|
1324
|
-
size: (await fs.stat(patchPath)).size,
|
|
1325
|
-
})),
|
|
1326
|
-
);
|
|
1327
|
-
const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
|
|
1328
|
-
if (nonEmptyPatches.length === 0) {
|
|
1296
|
+
const patchText = await Bun.file(result.patchPath).text();
|
|
1297
|
+
if (!patchText.trim()) {
|
|
1329
1298
|
changesApplied = true;
|
|
1330
1299
|
hadAnyChanges = false;
|
|
1331
1300
|
} else {
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
)
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
} else {
|
|
1342
|
-
changesApplied = await git.patch.canApplyText(repoRoot, combinedPatch);
|
|
1343
|
-
if (changesApplied) {
|
|
1344
|
-
try {
|
|
1345
|
-
await git.patch.applyText(repoRoot, combinedPatch);
|
|
1346
|
-
hadAnyChanges = true;
|
|
1347
|
-
} catch {
|
|
1348
|
-
changesApplied = false;
|
|
1349
|
-
hadAnyChanges = false;
|
|
1350
|
-
}
|
|
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;
|
|
1351
1310
|
}
|
|
1352
1311
|
}
|
|
1353
1312
|
}
|
|
@@ -1358,10 +1317,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1358
1317
|
} else {
|
|
1359
1318
|
const notification =
|
|
1360
1319
|
"<system-notification>Patches were not applied and must be handled manually.</system-notification>";
|
|
1361
|
-
const patchList =
|
|
1362
|
-
patchPaths.length > 0
|
|
1363
|
-
? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
|
|
1364
|
-
: "";
|
|
1320
|
+
const patchList = result.patchPath ? `\n\nPatch artifact:\n- ${result.patchPath}` : "";
|
|
1365
1321
|
mergeSummary = `\n\n${notification}${patchList}`;
|
|
1366
1322
|
}
|
|
1367
1323
|
}
|
|
@@ -1375,34 +1331,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1375
1331
|
|
|
1376
1332
|
// Apply nested repo patches (separate from parent git)
|
|
1377
1333
|
if (isIsolated && repoRoot && (mergeMode === "branch" || changesApplied !== false)) {
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
}
|
|
1386
|
-
if (!r.branchName || !mergedBranchesForNestedPatches) {
|
|
1387
|
-
return false;
|
|
1388
|
-
}
|
|
1389
|
-
return mergedBranchesForNestedPatches.has(r.branchName);
|
|
1390
|
-
})
|
|
1391
|
-
.flatMap(r => r.nestedPatches!);
|
|
1392
|
-
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) {
|
|
1393
1341
|
try {
|
|
1394
|
-
|
|
1395
|
-
commitStyle === "ai" && this.session.modelRegistry
|
|
1396
|
-
? async (diff: string) => {
|
|
1397
|
-
return generateCommitMessage(
|
|
1398
|
-
diff,
|
|
1399
|
-
this.session.modelRegistry!,
|
|
1400
|
-
this.session.settings,
|
|
1401
|
-
this.session.getSessionId?.() ?? undefined,
|
|
1402
|
-
);
|
|
1403
|
-
}
|
|
1404
|
-
: undefined;
|
|
1405
|
-
await applyNestedPatches(repoRoot, allNestedPatches, commitMsg);
|
|
1342
|
+
await applyNestedPatches(repoRoot, nestedPatches, buildCommitMessageFn());
|
|
1406
1343
|
} catch {
|
|
1407
1344
|
// Nested patch failures are non-fatal to the parent merge
|
|
1408
1345
|
mergeSummary +=
|
|
@@ -1411,58 +1348,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1411
1348
|
}
|
|
1412
1349
|
}
|
|
1413
1350
|
|
|
1414
|
-
// Build final output - match plugin format
|
|
1415
|
-
const cancelledCount = results.filter(r => r.aborted).length;
|
|
1416
|
-
const successCount = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted).length;
|
|
1417
|
-
const totalDuration = Date.now() - startTime;
|
|
1418
|
-
|
|
1419
|
-
const summaries = results.map(r => {
|
|
1420
|
-
const status = r.aborted
|
|
1421
|
-
? "cancelled"
|
|
1422
|
-
: r.exitCode === 0 && r.error
|
|
1423
|
-
? "merge failed"
|
|
1424
|
-
: r.exitCode === 0
|
|
1425
|
-
? "completed"
|
|
1426
|
-
: `failed (exit ${r.exitCode})`;
|
|
1427
|
-
const output = r.output.trim() || r.stderr.trim() || "(no output)";
|
|
1428
|
-
const outputCharCount = r.outputMeta?.charCount ?? output.length;
|
|
1429
|
-
const fullOutputThreshold = 5000;
|
|
1430
|
-
let preview = output;
|
|
1431
|
-
let truncated = false;
|
|
1432
|
-
if (outputCharCount > fullOutputThreshold) {
|
|
1433
|
-
const slice = output.slice(0, fullOutputThreshold);
|
|
1434
|
-
const lastNewline = slice.lastIndexOf("\n");
|
|
1435
|
-
preview = lastNewline >= 0 ? slice.slice(0, lastNewline) : slice;
|
|
1436
|
-
truncated = true;
|
|
1437
|
-
}
|
|
1438
|
-
return {
|
|
1439
|
-
agent: r.agent,
|
|
1440
|
-
status,
|
|
1441
|
-
id: r.id,
|
|
1442
|
-
preview,
|
|
1443
|
-
truncated,
|
|
1444
|
-
meta: r.outputMeta
|
|
1445
|
-
? {
|
|
1446
|
-
lineCount: r.outputMeta.lineCount,
|
|
1447
|
-
charSize: formatBytes(r.outputMeta.charCount),
|
|
1448
|
-
}
|
|
1449
|
-
: undefined,
|
|
1450
|
-
};
|
|
1451
|
-
});
|
|
1452
|
-
|
|
1453
|
-
const outputIds = results.filter(r => !r.aborted || r.output.trim()).map(r => `agent://${r.id}`);
|
|
1454
|
-
const summary = prompt.render(taskSummaryTemplate, {
|
|
1455
|
-
successCount,
|
|
1456
|
-
totalCount: results.length,
|
|
1457
|
-
cancelledCount,
|
|
1458
|
-
hasCancelledNote: aborted && cancelledCount > 0,
|
|
1459
|
-
duration: formatDuration(totalDuration),
|
|
1460
|
-
summaries,
|
|
1461
|
-
outputIds,
|
|
1462
|
-
agentName,
|
|
1463
|
-
mergeSummary,
|
|
1464
|
-
});
|
|
1465
|
-
|
|
1466
1351
|
// Cleanup temp directory if used
|
|
1467
1352
|
const shouldCleanupTempArtifacts =
|
|
1468
1353
|
tempArtifactsDir && (!isIsolated || changesApplied === true || changesApplied === null);
|
|
@@ -1470,25 +1355,65 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1470
1355
|
await fs.rm(tempArtifactsDir, { recursive: true, force: true });
|
|
1471
1356
|
}
|
|
1472
1357
|
|
|
1473
|
-
return
|
|
1474
|
-
content: [{ type: "text", text: summary }],
|
|
1475
|
-
details: {
|
|
1476
|
-
projectAgentsDir,
|
|
1477
|
-
results: results,
|
|
1478
|
-
totalDurationMs: totalDuration,
|
|
1479
|
-
usage: hasAggregatedUsage ? aggregatedUsage : undefined,
|
|
1480
|
-
outputPaths,
|
|
1481
|
-
},
|
|
1482
|
-
};
|
|
1358
|
+
return this.#buildResultPayload(result, projectAgentsDir, Date.now() - startTime, mergeSummary);
|
|
1483
1359
|
} catch (err) {
|
|
1484
1360
|
return {
|
|
1485
1361
|
content: [{ type: "text", text: `Task execution failed: ${err}` }],
|
|
1486
|
-
details: {
|
|
1487
|
-
projectAgentsDir,
|
|
1488
|
-
results: [],
|
|
1489
|
-
totalDurationMs: Date.now() - startTime,
|
|
1490
|
-
},
|
|
1362
|
+
details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
|
|
1491
1363
|
};
|
|
1492
1364
|
}
|
|
1493
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
|
+
}
|
|
1494
1419
|
}
|