@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.6
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 +62 -0
- package/dist/cli.js +3799 -3729
- package/dist/types/async/job-manager.d.ts +8 -0
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +19 -9
- package/dist/types/config/settings.d.ts +3 -2
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +36 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
- package/dist/types/modes/components/ask-dialog.d.ts +27 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +100 -0
- package/dist/types/modes/components/model-hub.d.ts +52 -0
- package/dist/types/modes/components/pause-screen.d.ts +43 -0
- package/dist/types/modes/components/session-selector.d.ts +13 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -13
- package/dist/types/task/index.d.ts +12 -11
- package/dist/types/task/label.d.ts +4 -0
- package/dist/types/task/repair-args.d.ts +8 -8
- package/dist/types/task/types.d.ts +31 -56
- package/dist/types/tools/ask.d.ts +12 -0
- package/dist/types/tools/conflict-detect.d.ts +17 -1
- package/dist/types/tools/job.d.ts +16 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/async/job-manager.ts +9 -0
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/commit/agentic/tools/analyze-file.ts +2 -3
- package/src/config/settings-schema.ts +18 -7
- package/src/config/settings.ts +13 -4
- package/src/discovery/helpers.ts +3 -4
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/extensions/types.ts +46 -0
- package/src/irc/bus.ts +61 -20
- package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
- package/src/modes/components/advisor-config.ts +32 -22
- package/src/modes/components/ask-dialog.ts +888 -0
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-browser.ts +769 -0
- package/src/modes/components/model-hub.ts +2002 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/session-selector.ts +299 -42
- package/src/modes/components/tool-execution.ts +2 -0
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/event-controller.ts +8 -2
- package/src/modes/controllers/extension-ui-controller.ts +252 -5
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +160 -97
- package/src/modes/controllers/tan-command-controller.ts +1 -1
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +8 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +50 -24
- package/src/prompts/agents/scout.md +0 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -5
- package/src/prompts/system/subagent-yield-reminder.md +10 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/task-summary.md +3 -0
- package/src/prompts/tools/task.md +17 -18
- package/src/session/agent-session.ts +186 -49
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/session/session-context.test.ts +73 -0
- package/src/session/session-context.ts +43 -26
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +159 -46
- package/src/task/index.ts +377 -239
- package/src/task/label.ts +38 -0
- package/src/task/render.ts +74 -22
- package/src/task/repair-args.ts +20 -31
- package/src/task/spawn-policy.test.ts +4 -4
- package/src/task/types.ts +46 -66
- package/src/tools/ask.ts +233 -40
- package/src/tools/conflict-detect.ts +102 -5
- package/src/tools/index.ts +1 -0
- package/src/tools/irc.ts +20 -11
- package/src/tools/job.ts +158 -18
- package/src/tools/write.ts +70 -6
- package/src/vibe/runtime.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +30 -13
- package/dist/types/modes/components/model-selector.d.ts +0 -37
- package/dist/types/tools/bash-command-fixup.d.ts +0 -3
- package/src/modes/components/model-selector.ts +0 -1291
- package/src/tools/bash-command-fixup.ts +0 -4
package/src/task/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type:
|
|
|
30
30
|
import { truncateForPrompt } from "../tools/approval";
|
|
31
31
|
import { isIrcEnabled } from "../tools/irc";
|
|
32
32
|
import { formatBytes, formatDuration } from "../tools/render-utils";
|
|
33
|
-
import {
|
|
33
|
+
import { resolveSpawnPolicy } from "./spawn-policy";
|
|
34
34
|
import {
|
|
35
35
|
type AgentDefinition,
|
|
36
36
|
type AgentProgress,
|
|
@@ -200,16 +200,17 @@ function renderDescription(
|
|
|
200
200
|
name: agent.name,
|
|
201
201
|
description: agent.description,
|
|
202
202
|
readOnly: isReadOnlyAgent(agent),
|
|
203
|
+
blocking: agent.blocking === true,
|
|
203
204
|
}));
|
|
204
205
|
return prompt.render(taskDescriptionTemplate, {
|
|
205
206
|
agents: renderedAgents,
|
|
206
207
|
spawningDisabled,
|
|
207
208
|
defaultAgent: spawnPolicy.defaultAgent,
|
|
208
|
-
defaultAgentIsGeneric: spawnPolicy.defaultAgent === DEFAULT_SPAWN_AGENT,
|
|
209
209
|
allowedAgentsText: spawnPolicy.allowedPromptText,
|
|
210
210
|
isolationEnabled,
|
|
211
211
|
batchEnabled,
|
|
212
212
|
asyncEnabled,
|
|
213
|
+
hasBlockingAgents: renderedAgents.some(agent => agent.blocking),
|
|
213
214
|
ircEnabled,
|
|
214
215
|
});
|
|
215
216
|
}
|
|
@@ -234,92 +235,90 @@ function validateShapeParams(batchEnabled: boolean, params: TaskParams): string
|
|
|
234
235
|
if (!batchEnabled) {
|
|
235
236
|
const disallowed = (["tasks", "context"] as const).filter(field => params[field] !== undefined);
|
|
236
237
|
if (disallowed.length > 0) {
|
|
237
|
-
return `task.batch is disabled, so the task tool does not accept ${disallowed.map(f => `\`${f}\``).join(" or ")}. Spawn one agent per call with \`
|
|
238
|
+
return `task.batch is disabled, so the task tool does not accept ${disallowed.map(f => `\`${f}\``).join(" or ")}. Spawn one agent per call with \`task\`, or enable the task.batch setting.`;
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
return undefined;
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
/**
|
|
244
|
-
* Validate the spawn parameter contract against the wire shapes.
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* setting (internal callers, stale transcripts). Returns a problem
|
|
253
|
-
* description, or undefined when valid.
|
|
245
|
+
* Validate the spawn parameter contract against the wire shapes. With
|
|
246
|
+
* `task.batch` the model-facing shape is `{ context, tasks[] }` — `tasks`
|
|
247
|
+
* non-empty with per-item `task` instructions and unique names, `context`
|
|
248
|
+
* non-empty, no top-level `task` alongside. The flat `{ agent?, ...item }`
|
|
249
|
+
* form stays accepted at runtime under either setting (internal callers, stale
|
|
250
|
+
* transcripts). Missing `agent` values resolve against the session spawn
|
|
251
|
+
* policy later, in `spawnParamsFor`. Returns a problem description, or
|
|
252
|
+
* undefined when valid.
|
|
254
253
|
*/
|
|
255
254
|
function validateSpawnParams(params: TaskParams, batchEnabled: boolean): string | undefined {
|
|
256
|
-
const
|
|
257
|
-
if (!agent) {
|
|
258
|
-
return "Missing `agent`. Provide an agent type to spawn.";
|
|
259
|
-
}
|
|
260
|
-
const hasAssignment = typeof params.assignment === "string" && params.assignment.trim() !== "";
|
|
255
|
+
const hasTask = typeof params.task === "string" && params.task.trim() !== "";
|
|
261
256
|
const tasks = params.tasks;
|
|
262
257
|
if (batchEnabled && tasks !== undefined) {
|
|
263
258
|
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
264
|
-
return "Missing `tasks`. Provide at least one task item ({
|
|
259
|
+
return "Missing `tasks`. Provide at least one task item ({ name?, agent?, task }).";
|
|
265
260
|
}
|
|
266
|
-
if (
|
|
267
|
-
return "Top-level `
|
|
261
|
+
if (hasTask) {
|
|
262
|
+
return "Top-level `task` is not part of the batch shape. Put the work in `tasks[]` items.";
|
|
268
263
|
}
|
|
269
264
|
for (let i = 0; i < tasks.length; i++) {
|
|
270
265
|
const item = tasks[i];
|
|
271
|
-
if (!item || typeof item.
|
|
272
|
-
return `Task ${i + 1}${item?.
|
|
266
|
+
if (!item || typeof item.task !== "string" || item.task.trim() === "") {
|
|
267
|
+
return `Task ${i + 1}${item?.name ? ` (\`${item.name}\`)` : ""} is missing \`task\`. Every task needs complete, self-contained instructions.`;
|
|
273
268
|
}
|
|
274
269
|
}
|
|
275
270
|
const seen = new Map<string, string>();
|
|
276
271
|
for (const item of tasks) {
|
|
277
|
-
const
|
|
278
|
-
if (!
|
|
279
|
-
const key =
|
|
272
|
+
const name = item.name?.trim();
|
|
273
|
+
if (!name) continue;
|
|
274
|
+
const key = name.toLowerCase();
|
|
280
275
|
const existing = seen.get(key);
|
|
281
276
|
if (existing !== undefined) {
|
|
282
|
-
return `Duplicate task
|
|
277
|
+
return `Duplicate task name ${existing === name ? `\`${name}\`` : `\`${existing}\` / \`${name}\``}. Provided names must be unique within a call (case-insensitive).`;
|
|
283
278
|
}
|
|
284
|
-
seen.set(key,
|
|
279
|
+
seen.set(key, name);
|
|
285
280
|
}
|
|
286
281
|
if (typeof params.context !== "string" || params.context.trim() === "") {
|
|
287
282
|
return "Missing `context`. Provide the shared background for this batch — goal, constraints, and any contract the tasks share.";
|
|
288
283
|
}
|
|
289
284
|
return undefined;
|
|
290
285
|
}
|
|
291
|
-
if (!
|
|
286
|
+
if (!hasTask) {
|
|
292
287
|
return batchEnabled
|
|
293
288
|
? "Missing `tasks`. Provide a `tasks` array (one subagent per item) with a shared `context`."
|
|
294
|
-
: "Missing `
|
|
289
|
+
: "Missing `task`. Provide complete, self-contained instructions for the agent.";
|
|
295
290
|
}
|
|
296
291
|
return undefined;
|
|
297
292
|
}
|
|
298
293
|
|
|
299
294
|
/**
|
|
300
295
|
* Normalize a validated call into its spawn list: the `tasks[]` batch when
|
|
301
|
-
* provided, otherwise the single top-level spawn.
|
|
296
|
+
* provided, otherwise the single top-level spawn. The flat form's `isolated`
|
|
297
|
+
* flag is only materialized when the caller sent one — `#runSpawn`
|
|
298
|
+
* distinguishes an absent key from an explicit value.
|
|
302
299
|
*/
|
|
303
300
|
function resolveSpawnItems(params: TaskParams): TaskItem[] {
|
|
304
301
|
if (Array.isArray(params.tasks) && params.tasks.length > 0) {
|
|
305
302
|
return params.tasks;
|
|
306
303
|
}
|
|
307
|
-
|
|
304
|
+
const item: TaskItem = { name: params.name, agent: params.agent, task: params.task };
|
|
305
|
+
if ("isolated" in params) item.isolated = params.isolated;
|
|
306
|
+
return [item];
|
|
308
307
|
}
|
|
309
308
|
|
|
310
309
|
/**
|
|
311
310
|
* Per-spawn params handed to the executor path: top-level call fields with the
|
|
312
|
-
* item's identity substituted in. `
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
311
|
+
* item's identity substituted in. Each spawn's `agent` resolves here —
|
|
312
|
+
* the item's own value, else `defaultAgent` from the session spawn policy.
|
|
313
|
+
* `tasks` never leaks into a spawn; the shared `context` rides along
|
|
314
|
+
* unchanged. Keys are only materialized when present — `#runSpawn`
|
|
315
|
+
* distinguishes an absent `isolated` from an explicit one. The item's
|
|
316
|
+
* `isolated` (batch form) wins over the top-level flag (flat form).
|
|
316
317
|
*/
|
|
317
|
-
function spawnParamsFor(params: TaskParams, item: TaskItem): TaskParams {
|
|
318
|
-
const spawn: TaskParams = { agent:
|
|
319
|
-
if (item.
|
|
320
|
-
if (item.
|
|
321
|
-
if (item.role !== undefined) spawn.role = item.role;
|
|
322
|
-
if (item.assignment !== undefined) spawn.assignment = item.assignment;
|
|
318
|
+
function spawnParamsFor(params: TaskParams, item: TaskItem, defaultAgent: string): TaskParams {
|
|
319
|
+
const spawn: TaskParams = { agent: item.agent?.trim() || defaultAgent };
|
|
320
|
+
if (item.name !== undefined) spawn.name = item.name;
|
|
321
|
+
if (item.task !== undefined) spawn.task = item.task;
|
|
323
322
|
if (params.context !== undefined) spawn.context = params.context;
|
|
324
323
|
if (item.isolated !== undefined) {
|
|
325
324
|
spawn.isolated = item.isolated;
|
|
@@ -329,33 +328,83 @@ function spawnParamsFor(params: TaskParams, item: TaskItem): TaskParams {
|
|
|
329
328
|
return spawn;
|
|
330
329
|
}
|
|
331
330
|
|
|
332
|
-
/**
|
|
331
|
+
/** One sync-executed spawn: its item, position in the original call, and (for mixed calls) a pre-claimed agent id. */
|
|
332
|
+
interface SyncSpawnRef {
|
|
333
|
+
item: TaskItem;
|
|
334
|
+
index: number;
|
|
335
|
+
preAllocatedId?: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Merged view of a sync spawn set's payloads: joined text plus flattened results/usage/paths. */
|
|
339
|
+
interface MergedSyncPayloads {
|
|
340
|
+
contentParts: string[];
|
|
341
|
+
results: SingleResult[];
|
|
342
|
+
usage?: Usage;
|
|
343
|
+
outputPaths?: string[];
|
|
344
|
+
projectAgentsDir: string | null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Merge per-spawn sync payloads into one result view. `index` is each spawn's
|
|
349
|
+
* position in the original call so batch rows keep stable ordering; a missing
|
|
350
|
+
* payload (cancelled before start) becomes an explanatory content line.
|
|
351
|
+
*/
|
|
352
|
+
function mergeSyncPayloads(
|
|
353
|
+
spawns: SyncSpawnRef[],
|
|
354
|
+
payloads: (AgentToolResult<TaskToolDetails> | undefined)[],
|
|
355
|
+
): MergedSyncPayloads {
|
|
356
|
+
const results: SingleResult[] = [];
|
|
357
|
+
const contentParts: string[] = [];
|
|
358
|
+
const outputPaths: string[] = [];
|
|
359
|
+
const usageTotals = createUsageTotals();
|
|
360
|
+
let hasUsage = false;
|
|
361
|
+
let projectAgentsDir: string | null = null;
|
|
362
|
+
for (let position = 0; position < spawns.length; position++) {
|
|
363
|
+
const payload = payloads[position];
|
|
364
|
+
const { item, index } = spawns[position];
|
|
365
|
+
if (!payload) {
|
|
366
|
+
contentParts.push(`Task ${item.name?.trim() || `#${index + 1}`}: cancelled before start.`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
projectAgentsDir ??= payload.details?.projectAgentsDir ?? null;
|
|
370
|
+
const text = payload.content.find(part => part.type === "text")?.text;
|
|
371
|
+
if (text) contentParts.push(text);
|
|
372
|
+
for (const result of payload.details?.results ?? []) {
|
|
373
|
+
results.push({ ...result, index });
|
|
374
|
+
if (result.usage) {
|
|
375
|
+
addUsageTotals(usageTotals, result.usage);
|
|
376
|
+
hasUsage = true;
|
|
377
|
+
}
|
|
378
|
+
if (result.outputPath) outputPaths.push(result.outputPath);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
contentParts,
|
|
383
|
+
results,
|
|
384
|
+
usage: hasUsage ? usageTotals : undefined,
|
|
385
|
+
outputPaths: outputPaths.length > 0 ? outputPaths : undefined,
|
|
386
|
+
projectAgentsDir,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Generic worker agent types; several in one call usually means a more specific type exists. */
|
|
333
391
|
const GENERIC_SPAWN_AGENTS: ReadonlySet<string> = new Set(["task", "sonic"]);
|
|
334
392
|
|
|
335
393
|
/**
|
|
336
394
|
* Advisory — never a rejection — nudging the spawner toward tailored
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
395
|
+
* specific agent types when one call resolves ≥2 items to a generic
|
|
396
|
+
* `task`/`sonic` worker and the spawner still holds spawn capacity
|
|
397
|
+
* (DepthCapacity: it currently has the `task` tool). `agentNames` are the
|
|
398
|
+
* per-item resolved agent types. Returns undefined when no nudge applies.
|
|
341
399
|
*/
|
|
342
|
-
export function buildSpecializationAdvisory(
|
|
343
|
-
agentName: string | undefined,
|
|
344
|
-
items: TaskItem[],
|
|
345
|
-
depthCapacity: boolean,
|
|
346
|
-
): string | undefined {
|
|
400
|
+
export function buildSpecializationAdvisory(agentNames: string[], depthCapacity: boolean): string | undefined {
|
|
347
401
|
if (!depthCapacity) return undefined;
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
350
|
-
const generic = agentName !== undefined && GENERIC_SPAWN_AGENTS.has(agentName);
|
|
351
|
-
const cloned = items.length >= 2 && rolelessCount === items.length;
|
|
352
|
-
if (!generic && !cloned) return undefined;
|
|
353
|
-
const label = agentName ?? "task";
|
|
402
|
+
const generics = agentNames.filter(name => GENERIC_SPAWN_AGENTS.has(name));
|
|
403
|
+
if (generics.length < 2) return undefined;
|
|
354
404
|
return (
|
|
355
|
-
`Tip: spawned ${
|
|
356
|
-
`
|
|
357
|
-
|
|
358
|
-
`rather than cloning one generic worker.`
|
|
405
|
+
`Tip: this call spawned ${generics.length} generic \`${generics[0]}\` workers. ` +
|
|
406
|
+
`Check the agent list for a closer specialist type — e.g. read-only research belongs on ` +
|
|
407
|
+
`\`agent: "scout"\`, which runs on a faster model.`
|
|
359
408
|
);
|
|
360
409
|
}
|
|
361
410
|
|
|
@@ -379,14 +428,15 @@ export function buildCoordinationAdvisory(
|
|
|
379
428
|
|
|
380
429
|
/**
|
|
381
430
|
* Compose the non-blocking advisory appended to a `task` result: the
|
|
382
|
-
* specialization nudge
|
|
383
|
-
* call (`willRunAsync`) — the
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
431
|
+
* specialization nudge (from the per-item resolved agent types), plus — only
|
|
432
|
+
* when some spawns keep running after this call (`willRunAsync`) — the
|
|
433
|
+
* coordination suggestion over those still-live spawns (`items`). Coordination
|
|
434
|
+
* is gated on async because a sync spawn has already finished by the time the
|
|
435
|
+
* call returns, so a "coordinate while they run" hint would misfire. Returns
|
|
436
|
+
* undefined when neither applies.
|
|
387
437
|
*/
|
|
388
438
|
export function composeSpawnAdvisory(args: {
|
|
389
|
-
|
|
439
|
+
agents: string[];
|
|
390
440
|
items: TaskItem[];
|
|
391
441
|
depthCapacity: boolean;
|
|
392
442
|
ircEnabled: boolean;
|
|
@@ -394,7 +444,7 @@ export function composeSpawnAdvisory(args: {
|
|
|
394
444
|
}): string | undefined {
|
|
395
445
|
return (
|
|
396
446
|
[
|
|
397
|
-
buildSpecializationAdvisory(args.
|
|
447
|
+
buildSpecializationAdvisory(args.agents, args.depthCapacity),
|
|
398
448
|
args.willRunAsync ? buildCoordinationAdvisory(args.items, args.depthCapacity, args.ircEnabled) : undefined,
|
|
399
449
|
]
|
|
400
450
|
.filter(Boolean)
|
|
@@ -456,14 +506,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
456
506
|
if (typeof params.agent === "string") {
|
|
457
507
|
lines.push(`Agent: ${truncateForPrompt(params.agent)}`);
|
|
458
508
|
}
|
|
459
|
-
if (typeof params.
|
|
460
|
-
lines.push(`
|
|
461
|
-
}
|
|
462
|
-
if (typeof params.id === "string" && params.id.trim()) {
|
|
463
|
-
lines.push(`Task: ${truncateForPrompt(params.id)}`);
|
|
509
|
+
if (typeof params.name === "string" && params.name.trim()) {
|
|
510
|
+
lines.push(`Name: ${truncateForPrompt(params.name)}`);
|
|
464
511
|
}
|
|
465
|
-
if (typeof params.
|
|
466
|
-
lines.push(`
|
|
512
|
+
if (typeof params.task === "string") {
|
|
513
|
+
lines.push(`Task:\n${truncateForPrompt(params.task)}`);
|
|
467
514
|
}
|
|
468
515
|
if (typeof params.context === "string" && params.context.trim()) {
|
|
469
516
|
lines.push(`Context:\n${truncateForPrompt(params.context)}`);
|
|
@@ -471,14 +518,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
471
518
|
const tasks = Array.isArray(params.tasks) ? params.tasks : [];
|
|
472
519
|
const firstTask = tasks[0];
|
|
473
520
|
if (firstTask) {
|
|
474
|
-
if (typeof firstTask.
|
|
475
|
-
lines.push(`
|
|
521
|
+
if (typeof firstTask.name === "string" && firstTask.name.trim()) {
|
|
522
|
+
lines.push(`Name: ${truncateForPrompt(firstTask.name)}`);
|
|
476
523
|
}
|
|
477
|
-
if (typeof firstTask.
|
|
478
|
-
lines.push(`
|
|
524
|
+
if (typeof firstTask.agent === "string" && firstTask.agent.trim()) {
|
|
525
|
+
lines.push(`Agent: ${truncateForPrompt(firstTask.agent)}`);
|
|
479
526
|
}
|
|
480
|
-
if (typeof firstTask.
|
|
481
|
-
lines.push(`
|
|
527
|
+
if (typeof firstTask.task === "string") {
|
|
528
|
+
lines.push(`Task:\n${truncateForPrompt(firstTask.task)}`);
|
|
482
529
|
}
|
|
483
530
|
if (tasks.length > 1) {
|
|
484
531
|
lines.push(`+${tasks.length - 1} more task${tasks.length === 2 ? "" : "s"}`);
|
|
@@ -570,15 +617,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
570
617
|
signal?: AbortSignal,
|
|
571
618
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
572
619
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
573
|
-
const
|
|
574
|
-
// Schema defaults
|
|
575
|
-
// transcripts can bypass arktype.
|
|
576
|
-
//
|
|
620
|
+
const params = repairTaskParams(rawParams as TaskParams);
|
|
621
|
+
// Schema defaults fill `agent` for model calls, but internal callers
|
|
622
|
+
// and stale transcripts can bypass arktype. `spawnParamsFor` resolves each
|
|
623
|
+
// item's agent type against the session's actual default agent.
|
|
577
624
|
const defaultAgent = resolveSpawnPolicy(this.session.getSessionSpawns()).defaultAgent;
|
|
578
|
-
const params =
|
|
579
|
-
typeof repaired.agent === "string" && repaired.agent.trim() !== ""
|
|
580
|
-
? repaired
|
|
581
|
-
: { ...repaired, agent: defaultAgent };
|
|
582
625
|
const batchEnabled = this.#isBatchEnabled();
|
|
583
626
|
const validationError = validateShapeParams(batchEnabled, params) ?? validateSpawnParams(params, batchEnabled);
|
|
584
627
|
if (validationError) {
|
|
@@ -586,23 +629,31 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
586
629
|
}
|
|
587
630
|
|
|
588
631
|
const spawnItems = resolveSpawnItems(params);
|
|
589
|
-
const
|
|
632
|
+
const resolvedAgents = spawnItems.map(item => item.agent?.trim() || defaultAgent);
|
|
633
|
+
// Execution mode is per item: an item whose agent type declares
|
|
634
|
+
// `blocking: true` runs inline on this turn (the parent waits on its
|
|
635
|
+
// result); every other item becomes a background job when async
|
|
636
|
+
// execution is available.
|
|
637
|
+
const itemBlocking = resolvedAgents.map(
|
|
638
|
+
name => this.#discoveredAgents.find(agent => agent.name === name)?.blocking === true,
|
|
639
|
+
);
|
|
590
640
|
const asyncEnabled = this.session.settings.get("async.enabled");
|
|
591
641
|
const manager = asyncEnabled ? this.session.asyncJobManager : undefined;
|
|
642
|
+
const asyncItems = manager ? spawnItems.filter((_, index) => !itemBlocking[index]) : [];
|
|
592
643
|
const depthCapacity = canSpawnAtDepth(
|
|
593
644
|
this.session.settings.get("task.maxRecursionDepth") ?? 2,
|
|
594
645
|
this.session.taskDepth ?? 0,
|
|
595
646
|
);
|
|
596
647
|
const ircEnabled = isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0);
|
|
597
|
-
// Coordination only makes sense
|
|
598
|
-
// call returns (async).
|
|
599
|
-
// so a "coordinate while they run" hint would misfire.
|
|
600
|
-
const willRunAsync =
|
|
648
|
+
// Coordination only makes sense for spawns that keep running after this
|
|
649
|
+
// call returns (the async subset). Blocking items have already completed
|
|
650
|
+
// by then, so a "coordinate while they run" hint would misfire.
|
|
651
|
+
const willRunAsync = asyncItems.length > 0;
|
|
601
652
|
const advisory = this.session.suppressSpawnAdvisory
|
|
602
653
|
? undefined
|
|
603
654
|
: composeSpawnAdvisory({
|
|
604
|
-
|
|
605
|
-
items:
|
|
655
|
+
agents: resolvedAgents,
|
|
656
|
+
items: asyncItems,
|
|
606
657
|
depthCapacity,
|
|
607
658
|
ircEnabled,
|
|
608
659
|
willRunAsync,
|
|
@@ -623,39 +674,49 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
623
674
|
if (!appended) content.push({ type: "text", text: advisory });
|
|
624
675
|
return { ...result, content };
|
|
625
676
|
};
|
|
626
|
-
if (!
|
|
677
|
+
if (!manager || asyncItems.length === 0) {
|
|
627
678
|
// Sync fallback: async execution disabled, orphaned host that never
|
|
628
|
-
// wired a job manager, or
|
|
679
|
+
// wired a job manager, or every item's agent type declares
|
|
629
680
|
// `blocking: true`. The session-scoped semaphore still bounds fan-out
|
|
630
681
|
// across parallel task calls.
|
|
631
|
-
if (asyncEnabled && !
|
|
682
|
+
if (asyncEnabled && !this.session.asyncJobManager) {
|
|
632
683
|
logger.warn("task: no AsyncJobManager registered; falling back to sync execution");
|
|
633
684
|
}
|
|
634
|
-
return withAdvisory(
|
|
685
|
+
return withAdvisory(
|
|
686
|
+
await this.#executeSyncFanout(toolCallId, params, spawnItems, defaultAgent, signal, onUpdate),
|
|
687
|
+
);
|
|
635
688
|
}
|
|
636
689
|
|
|
637
690
|
// Resolve agent ids up front so the immediate result can name them.
|
|
638
691
|
const outputManager =
|
|
639
692
|
this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
|
|
640
|
-
const
|
|
641
|
-
const
|
|
642
|
-
|
|
693
|
+
const callStartedAt = Date.now();
|
|
694
|
+
const spawns: Array<{
|
|
695
|
+
agentId: string;
|
|
696
|
+
item: TaskItem;
|
|
697
|
+
index: number;
|
|
698
|
+
blocking: boolean;
|
|
699
|
+
progress: AgentProgress;
|
|
700
|
+
}> = [];
|
|
643
701
|
for (let index = 0; index < spawnItems.length; index++) {
|
|
644
702
|
const item = spawnItems[index];
|
|
645
|
-
const
|
|
646
|
-
const
|
|
703
|
+
const agentType = resolvedAgents[index];
|
|
704
|
+
const agentSource = this.#discoveredAgents.find(agent => agent.name === agentType)?.source ?? "bundled";
|
|
705
|
+
const agentId = await outputManager.allocate(item.name?.trim() || generateTaskName());
|
|
706
|
+
const assignment = (item.task ?? "").trim();
|
|
647
707
|
spawns.push({
|
|
648
708
|
agentId,
|
|
649
709
|
item,
|
|
710
|
+
index,
|
|
711
|
+
blocking: itemBlocking[index],
|
|
650
712
|
progress: {
|
|
651
713
|
index,
|
|
652
714
|
id: agentId,
|
|
653
|
-
agent:
|
|
715
|
+
agent: agentType,
|
|
654
716
|
agentSource,
|
|
655
717
|
status: "pending",
|
|
656
718
|
task: renderSubagentUserPrompt(assignment),
|
|
657
719
|
assignment,
|
|
658
|
-
description: item.description,
|
|
659
720
|
recentTools: [],
|
|
660
721
|
recentOutput: [],
|
|
661
722
|
toolCount: 0,
|
|
@@ -666,35 +727,44 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
666
727
|
},
|
|
667
728
|
});
|
|
668
729
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
//
|
|
674
|
-
|
|
730
|
+
const asyncSpawns = spawns.filter(spawn => !spawn.blocking);
|
|
731
|
+
const syncSpawns = spawns.filter(spawn => spawn.blocking);
|
|
732
|
+
const agentLabel = [...new Set(asyncSpawns.map(spawn => spawn.progress.agent))].join(", ");
|
|
733
|
+
|
|
734
|
+
// Aggregate state for the one tool call. Async spawns report into the
|
|
735
|
+
// shared progress snapshot through their jobs: the async half stays
|
|
736
|
+
// "running" until every job settles, then turns "failed" if any spawn
|
|
737
|
+
// failed. Blocking spawns run inline below and land in `results` before
|
|
738
|
+
// the call returns, so post-return job updates never drop them.
|
|
675
739
|
let settledCount = 0;
|
|
676
740
|
let failedCount = 0;
|
|
677
|
-
let primaryJobId =
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
741
|
+
let primaryJobId = asyncSpawns[0].agentId;
|
|
742
|
+
const syncResults: SingleResult[] = [];
|
|
743
|
+
let syncUsage: Usage | undefined;
|
|
744
|
+
let syncOutputPaths: string[] | undefined;
|
|
745
|
+
let syncProjectAgentsDir: string | null = null;
|
|
746
|
+
const buildAsyncDetails = (): TaskToolDetails => ({
|
|
747
|
+
projectAgentsDir: syncProjectAgentsDir,
|
|
748
|
+
results: [...syncResults],
|
|
749
|
+
totalDurationMs: Date.now() - callStartedAt,
|
|
750
|
+
usage: syncUsage,
|
|
751
|
+
outputPaths: syncOutputPaths,
|
|
682
752
|
progress: spawns.map(spawn => ({ ...spawn.progress })),
|
|
683
753
|
async: {
|
|
684
|
-
state:
|
|
685
|
-
jobId:
|
|
754
|
+
state: settledCount < asyncSpawns.length ? "running" : failedCount > 0 ? "failed" : "completed",
|
|
755
|
+
jobId: primaryJobId,
|
|
686
756
|
type: "task",
|
|
687
757
|
},
|
|
688
758
|
});
|
|
689
759
|
|
|
690
|
-
const started: Array<{ agentId: string; jobId: string
|
|
760
|
+
const started: Array<{ agentId: string; jobId: string }> = [];
|
|
691
761
|
const failedSchedules: string[] = [];
|
|
692
|
-
for (const spawn of
|
|
762
|
+
for (const spawn of asyncSpawns) {
|
|
693
763
|
try {
|
|
694
764
|
const jobId = this.#registerSpawnJob({
|
|
695
765
|
manager,
|
|
696
766
|
toolCallId,
|
|
697
|
-
spawnParams: spawnParamsFor(params, spawn.item),
|
|
767
|
+
spawnParams: spawnParamsFor(params, spawn.item, defaultAgent),
|
|
698
768
|
agentId: spawn.agentId,
|
|
699
769
|
progress: spawn.progress,
|
|
700
770
|
ircEnabled,
|
|
@@ -706,7 +776,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
706
776
|
},
|
|
707
777
|
});
|
|
708
778
|
if (started.length === 0) primaryJobId = jobId;
|
|
709
|
-
started.push({ agentId: spawn.agentId, jobId
|
|
779
|
+
started.push({ agentId: spawn.agentId, jobId });
|
|
710
780
|
} catch (error) {
|
|
711
781
|
const message = error instanceof Error ? error.message : String(error);
|
|
712
782
|
failedSchedules.push(`${spawn.agentId}: ${message}`);
|
|
@@ -716,64 +786,129 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
716
786
|
}
|
|
717
787
|
}
|
|
718
788
|
|
|
719
|
-
if (started.length === 0) {
|
|
789
|
+
if (started.length === 0 && syncSpawns.length === 0) {
|
|
720
790
|
return {
|
|
721
791
|
content: [
|
|
722
792
|
{
|
|
723
793
|
type: "text",
|
|
724
|
-
text: `Failed to start background task job${
|
|
794
|
+
text: `Failed to start background task job${failedSchedules.length === 1 ? "" : "s"}: ${failedSchedules.join("; ")}`,
|
|
725
795
|
},
|
|
726
796
|
],
|
|
727
797
|
details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
|
|
728
798
|
};
|
|
729
799
|
}
|
|
730
800
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
801
|
+
const scheduleFailureSummary =
|
|
802
|
+
failedSchedules.length > 0
|
|
803
|
+
? ` Failed to schedule ${failedSchedules.length} spawn${failedSchedules.length === 1 ? "" : "s"}: ${failedSchedules.join("; ")}.`
|
|
804
|
+
: "";
|
|
805
|
+
const coordinationHint =
|
|
806
|
+
started.length === 1
|
|
807
|
+
? ircEnabled
|
|
808
|
+
? `DM \`${started[0].agentId}\` via \`irc\` to coordinate while it runs; use \`job\` only to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task.`
|
|
809
|
+
: `Use \`job\` to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task.`
|
|
810
|
+
: ircEnabled
|
|
811
|
+
? `DM these ids via \`irc\` to coordinate while they run; use \`job\` only to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task.`
|
|
812
|
+
: `Use \`job\` to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task by id.`;
|
|
813
|
+
|
|
814
|
+
if (syncSpawns.length === 0) {
|
|
815
|
+
if (spawns.length === 1) {
|
|
816
|
+
const { agentId, jobId } = started[0];
|
|
817
|
+
onUpdate?.({
|
|
818
|
+
content: [{ type: "text", text: `Spawned agent \`${agentId}\`...` }],
|
|
819
|
+
details: buildAsyncDetails(),
|
|
820
|
+
});
|
|
821
|
+
return withAdvisory({
|
|
822
|
+
content: [
|
|
823
|
+
{
|
|
824
|
+
type: "text",
|
|
825
|
+
text: `Spawned agent \`${agentId}\` (job \`${jobId}\`). The result will be delivered when it yields. ${coordinationHint}`,
|
|
826
|
+
},
|
|
827
|
+
],
|
|
828
|
+
details: buildAsyncDetails(),
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
const startedListing = started.map(({ agentId, jobId }) => `- \`${agentId}\` (job \`${jobId}\`)`).join("\n");
|
|
737
832
|
onUpdate?.({
|
|
738
|
-
content: [{ type: "text", text: `Spawned
|
|
739
|
-
details: buildAsyncDetails(
|
|
833
|
+
content: [{ type: "text", text: `Spawned ${started.length} agents...` }],
|
|
834
|
+
details: buildAsyncDetails(),
|
|
740
835
|
});
|
|
741
836
|
return withAdvisory({
|
|
742
837
|
content: [
|
|
743
838
|
{
|
|
744
839
|
type: "text",
|
|
745
|
-
text: `Spawned
|
|
840
|
+
text: `Spawned ${started.length} background agents using ${agentLabel}.${scheduleFailureSummary} Each result will be delivered when that agent yields.\n${startedListing}\n${coordinationHint}`,
|
|
746
841
|
},
|
|
747
842
|
],
|
|
748
|
-
details: buildAsyncDetails(
|
|
843
|
+
details: buildAsyncDetails(),
|
|
749
844
|
});
|
|
750
845
|
}
|
|
751
846
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
failedSchedules.length > 0
|
|
757
|
-
? ` Failed to schedule ${failedSchedules.length} spawn${failedSchedules.length === 1 ? "" : "s"}: ${failedSchedules.join("; ")}.`
|
|
758
|
-
: "";
|
|
759
|
-
const startedListing = started
|
|
760
|
-
.map(({ agentId, jobId, description }) => {
|
|
761
|
-
const prefix = `- \`${agentId}\` (job \`${jobId}\`)`;
|
|
762
|
-
return description ? `${prefix} — ${description}` : prefix;
|
|
763
|
-
})
|
|
764
|
-
.join("\n");
|
|
847
|
+
// Mixed call: the async jobs above already run detached; the blocking
|
|
848
|
+
// subset runs inline and gates the call's return — exactly what each
|
|
849
|
+
// agent type declares (`blocking: true` = the parent waits on it).
|
|
850
|
+
const syncLabel = syncSpawns.map(spawn => `\`${spawn.agentId}\``).join(", ");
|
|
765
851
|
onUpdate?.({
|
|
766
|
-
content: [{ type: "text", text: `Spawned ${started.length} agents...` }],
|
|
767
|
-
details: buildAsyncDetails("running", primaryJobId),
|
|
768
|
-
});
|
|
769
|
-
return withAdvisory({
|
|
770
852
|
content: [
|
|
771
853
|
{
|
|
772
854
|
type: "text",
|
|
773
|
-
text: `
|
|
855
|
+
text: `Running ${syncLabel} inline; ${started.length} background agent${started.length === 1 ? "" : "s"} spawned...`,
|
|
774
856
|
},
|
|
775
857
|
],
|
|
776
|
-
details: buildAsyncDetails(
|
|
858
|
+
details: buildAsyncDetails(),
|
|
859
|
+
});
|
|
860
|
+
const payloads = await this.#runSyncSpawns({
|
|
861
|
+
toolCallId,
|
|
862
|
+
params,
|
|
863
|
+
defaultAgent,
|
|
864
|
+
signal,
|
|
865
|
+
spawns: syncSpawns.map(spawn => ({ item: spawn.item, index: spawn.index, preAllocatedId: spawn.agentId })),
|
|
866
|
+
onItemProgress: onUpdate
|
|
867
|
+
? (index, progress) => {
|
|
868
|
+
const spawn = spawns[index];
|
|
869
|
+
if (spawn) spawn.progress = { ...progress, index };
|
|
870
|
+
onUpdate({
|
|
871
|
+
content: [{ type: "text", text: `Running ${syncLabel} inline...` }],
|
|
872
|
+
details: buildAsyncDetails(),
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
: undefined,
|
|
876
|
+
});
|
|
877
|
+
const merged = mergeSyncPayloads(
|
|
878
|
+
syncSpawns.map(spawn => ({ item: spawn.item, index: spawn.index })),
|
|
879
|
+
payloads,
|
|
880
|
+
);
|
|
881
|
+
syncResults.push(...merged.results);
|
|
882
|
+
syncUsage = merged.usage;
|
|
883
|
+
syncOutputPaths = merged.outputPaths;
|
|
884
|
+
syncProjectAgentsDir = merged.projectAgentsDir;
|
|
885
|
+
// Settle the inline spawns' progress rows from their merged results so
|
|
886
|
+
// post-return job updates carry final statuses, not the last snapshot.
|
|
887
|
+
for (let position = 0; position < syncSpawns.length; position++) {
|
|
888
|
+
const spawn = syncSpawns[position];
|
|
889
|
+
const result = merged.results.find(r => r.id === spawn.agentId);
|
|
890
|
+
if (result) {
|
|
891
|
+
spawn.progress.status = result.aborted
|
|
892
|
+
? "aborted"
|
|
893
|
+
: result.exitCode === 0 && !result.error
|
|
894
|
+
? "completed"
|
|
895
|
+
: "failed";
|
|
896
|
+
spawn.progress.durationMs = result.durationMs;
|
|
897
|
+
} else {
|
|
898
|
+
spawn.progress.status = payloads[position] ? "failed" : "aborted";
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const spawnedSummary =
|
|
903
|
+
started.length > 0
|
|
904
|
+
? `Spawned ${started.length} background agent${started.length === 1 ? "" : "s"}.${scheduleFailureSummary} Each result will be delivered when that agent yields.\n${started.map(({ agentId, jobId }) => `- \`${agentId}\` (job \`${jobId}\`)`).join("\n")}\n${coordinationHint}`
|
|
905
|
+
: scheduleFailureSummary.trim();
|
|
906
|
+
const text = [merged.contentParts.join("\n\n"), spawnedSummary]
|
|
907
|
+
.filter(section => section.trim().length > 0)
|
|
908
|
+
.join("\n\n");
|
|
909
|
+
return withAdvisory({
|
|
910
|
+
content: [{ type: "text", text: text.length > 0 ? text : "No results." }],
|
|
911
|
+
details: buildAsyncDetails(),
|
|
777
912
|
});
|
|
778
913
|
}
|
|
779
914
|
|
|
@@ -790,7 +925,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
790
925
|
agentId: string;
|
|
791
926
|
progress: AgentProgress;
|
|
792
927
|
ircEnabled: boolean;
|
|
793
|
-
buildDetails: (
|
|
928
|
+
buildDetails: () => TaskToolDetails;
|
|
794
929
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>;
|
|
795
930
|
onSettled?: (failed: boolean) => void;
|
|
796
931
|
}): string {
|
|
@@ -798,6 +933,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
798
933
|
options;
|
|
799
934
|
const buildFollowUpHint = (aborted: boolean): string => {
|
|
800
935
|
if (aborted) {
|
|
936
|
+
const status = AgentRegistry.global().get(agentId)?.status;
|
|
937
|
+
if (status === "idle" || status === "parked") {
|
|
938
|
+
const followUp = ircEnabled ? "message it via `irc` to resume; " : "";
|
|
939
|
+
return `\n\n${agentId} was stopped but is still resumable — ${followUp}transcript at history://${agentId}`;
|
|
940
|
+
}
|
|
801
941
|
return `\n\n${agentId} was aborted — transcript at history://${agentId}`;
|
|
802
942
|
}
|
|
803
943
|
const followUp = ircEnabled ? "message it via `irc` to follow up; " : "";
|
|
@@ -806,7 +946,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
806
946
|
return manager.register(
|
|
807
947
|
"task",
|
|
808
948
|
agentId,
|
|
809
|
-
async ({
|
|
949
|
+
async ({ signal: runSignal, reportProgress, markRunning }) => {
|
|
810
950
|
const startedAt = Date.now();
|
|
811
951
|
const semaphore = this.#getSpawnSemaphore();
|
|
812
952
|
let semaphoreHeld = false;
|
|
@@ -840,10 +980,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
840
980
|
try {
|
|
841
981
|
markRunning();
|
|
842
982
|
progress.status = "running";
|
|
843
|
-
await reportProgress(
|
|
844
|
-
`Running background task ${agentId}...`,
|
|
845
|
-
buildDetails("running", ownJobId) as unknown as Record<string, unknown>,
|
|
846
|
-
);
|
|
983
|
+
await reportProgress(`Running background task ${agentId}...`);
|
|
847
984
|
const result = await this.#executeSync(
|
|
848
985
|
toolCallId,
|
|
849
986
|
spawnParams,
|
|
@@ -873,14 +1010,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
873
1010
|
const statusText = resultFailed
|
|
874
1011
|
? `Background task ${agentId} failed.`
|
|
875
1012
|
: `Background task ${agentId} complete.`;
|
|
876
|
-
await reportProgress(
|
|
877
|
-
statusText,
|
|
878
|
-
buildDetails(resultFailed ? "failed" : "completed", ownJobId) as unknown as Record<string, unknown>,
|
|
879
|
-
);
|
|
880
|
-
onUpdate?.({
|
|
881
|
-
content: [{ type: "text", text: statusText }],
|
|
882
|
-
details: buildDetails(resultFailed ? "failed" : "completed", ownJobId),
|
|
883
|
-
});
|
|
1013
|
+
await reportProgress(statusText);
|
|
884
1014
|
const deliveryText = `${finalText}${buildFollowUpHint(singleResult?.aborted === true)}`;
|
|
885
1015
|
if (resultFailed) {
|
|
886
1016
|
// Mark the job itself failed; the failed agent stays interrogable.
|
|
@@ -895,11 +1025,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
895
1025
|
progress.durationMs = Math.max(0, Date.now() - startedAt);
|
|
896
1026
|
onSettled?.(true);
|
|
897
1027
|
const statusText = `Background task ${agentId} failed.`;
|
|
898
|
-
await reportProgress(statusText
|
|
899
|
-
onUpdate?.({
|
|
900
|
-
content: [{ type: "text", text: statusText }],
|
|
901
|
-
details: buildDetails("failed", ownJobId),
|
|
902
|
-
});
|
|
1028
|
+
await reportProgress(statusText);
|
|
903
1029
|
const message = error instanceof Error ? error.message : String(error);
|
|
904
1030
|
const hint = AgentRegistry.global().get(agentId) ? buildFollowUpHint(false) : "";
|
|
905
1031
|
throw new TaskJobError(`${message}${hint}`);
|
|
@@ -909,38 +1035,39 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
909
1035
|
},
|
|
910
1036
|
{
|
|
911
1037
|
id: agentId,
|
|
1038
|
+
agentId,
|
|
912
1039
|
queued: true,
|
|
913
1040
|
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
914
|
-
onProgress:
|
|
915
|
-
|
|
916
|
-
onUpdate?.({ content: [{ type: "text", text }], details: progressDetails });
|
|
1041
|
+
onProgress: text => {
|
|
1042
|
+
onUpdate?.({ content: [{ type: "text", text }], details: buildDetails() });
|
|
917
1043
|
},
|
|
918
1044
|
},
|
|
919
1045
|
);
|
|
920
1046
|
}
|
|
921
1047
|
|
|
922
1048
|
/**
|
|
923
|
-
* Sync
|
|
924
|
-
* every spawn to completion inline and merge the
|
|
925
|
-
* single tool result. The session-scoped
|
|
926
|
-
* across parallel task calls.
|
|
1049
|
+
* Sync fan-out (async unavailable, or every item's agent type is
|
|
1050
|
+
* `blocking: true`): run every spawn to completion inline and merge the
|
|
1051
|
+
* per-spawn payloads into a single tool result. The session-scoped
|
|
1052
|
+
* semaphore still bounds concurrency across parallel task calls.
|
|
927
1053
|
*/
|
|
928
1054
|
async #executeSyncFanout(
|
|
929
1055
|
toolCallId: string,
|
|
930
1056
|
params: TaskParams,
|
|
931
1057
|
spawnItems: TaskItem[],
|
|
1058
|
+
defaultAgent: string,
|
|
932
1059
|
signal?: AbortSignal,
|
|
933
1060
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
934
1061
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
935
|
-
const semaphore = this.#getSpawnSemaphore();
|
|
936
1062
|
if (spawnItems.length === 1) {
|
|
1063
|
+
const semaphore = this.#getSpawnSemaphore();
|
|
937
1064
|
const invokedAt = Date.now();
|
|
938
1065
|
await semaphore.acquire(signal);
|
|
939
1066
|
const acquiredAt = Date.now();
|
|
940
1067
|
try {
|
|
941
1068
|
return await this.#executeSync(
|
|
942
1069
|
toolCallId,
|
|
943
|
-
spawnParamsFor(params, spawnItems[0]),
|
|
1070
|
+
spawnParamsFor(params, spawnItems[0], defaultAgent),
|
|
944
1071
|
signal,
|
|
945
1072
|
onUpdate,
|
|
946
1073
|
undefined,
|
|
@@ -969,30 +1096,75 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
969
1096
|
});
|
|
970
1097
|
};
|
|
971
1098
|
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1099
|
+
const payloads = await this.#runSyncSpawns({
|
|
1100
|
+
toolCallId,
|
|
1101
|
+
params,
|
|
1102
|
+
defaultAgent,
|
|
1103
|
+
signal,
|
|
1104
|
+
spawns: spawnItems.map((item, index) => ({ item, index })),
|
|
1105
|
+
onItemProgress: onUpdate
|
|
1106
|
+
? (index, progress) => {
|
|
1107
|
+
latestProgress.set(index, { ...progress, index });
|
|
1108
|
+
emitCombined();
|
|
1109
|
+
}
|
|
1110
|
+
: undefined,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const merged = mergeSyncPayloads(
|
|
1114
|
+
spawnItems.map((item, index) => ({ item, index })),
|
|
1115
|
+
payloads,
|
|
1116
|
+
);
|
|
1117
|
+
return {
|
|
1118
|
+
content: [{ type: "text", text: merged.contentParts.join("\n\n") }],
|
|
1119
|
+
details: {
|
|
1120
|
+
projectAgentsDir: merged.projectAgentsDir,
|
|
1121
|
+
results: merged.results,
|
|
1122
|
+
totalDurationMs: Date.now() - startTime,
|
|
1123
|
+
usage: merged.usage,
|
|
1124
|
+
outputPaths: merged.outputPaths,
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Run a set of spawns to completion inline, bounded by the session spawn
|
|
1131
|
+
* semaphore. `preAllocatedId` reuses an id claimed up front (mixed calls);
|
|
1132
|
+
* `index` is each item's position in the original call so progress rows and
|
|
1133
|
+
* merged results keep stable ordering. Per-item progress snapshots flow
|
|
1134
|
+
* through `onItemProgress`. Returns per-spawn payloads in input order;
|
|
1135
|
+
* `undefined` marks a spawn cancelled before it started.
|
|
1136
|
+
*/
|
|
1137
|
+
async #runSyncSpawns(args: {
|
|
1138
|
+
toolCallId: string;
|
|
1139
|
+
params: TaskParams;
|
|
1140
|
+
defaultAgent: string;
|
|
1141
|
+
spawns: SyncSpawnRef[];
|
|
1142
|
+
signal?: AbortSignal;
|
|
1143
|
+
onItemProgress?: (index: number, progress: AgentProgress) => void;
|
|
1144
|
+
}): Promise<(AgentToolResult<TaskToolDetails> | undefined)[]> {
|
|
1145
|
+
const { toolCallId, params, defaultAgent, spawns, signal, onItemProgress } = args;
|
|
1146
|
+
const semaphore = this.#getSpawnSemaphore();
|
|
1147
|
+
const { results } = await mapWithConcurrencyLimit(
|
|
1148
|
+
spawns,
|
|
1149
|
+
spawns.length,
|
|
1150
|
+
async (spawn, _position, workerSignal) => {
|
|
976
1151
|
const invokedAt = Date.now();
|
|
977
1152
|
await semaphore.acquire(workerSignal);
|
|
978
1153
|
const acquiredAt = Date.now();
|
|
979
1154
|
try {
|
|
980
|
-
const itemOnUpdate: AgentToolUpdateCallback<TaskToolDetails> | undefined =
|
|
1155
|
+
const itemOnUpdate: AgentToolUpdateCallback<TaskToolDetails> | undefined = onItemProgress
|
|
981
1156
|
? update => {
|
|
982
1157
|
const progress = update.details?.progress?.[0];
|
|
983
|
-
if (progress)
|
|
984
|
-
latestProgress.set(index, { ...progress, index });
|
|
985
|
-
emitCombined();
|
|
986
|
-
}
|
|
1158
|
+
if (progress) onItemProgress(spawn.index, progress);
|
|
987
1159
|
}
|
|
988
1160
|
: undefined;
|
|
989
1161
|
return await this.#executeSync(
|
|
990
1162
|
toolCallId,
|
|
991
|
-
spawnParamsFor(params, item),
|
|
1163
|
+
spawnParamsFor(params, spawn.item, defaultAgent),
|
|
992
1164
|
workerSignal,
|
|
993
1165
|
itemOnUpdate,
|
|
994
|
-
|
|
995
|
-
index,
|
|
1166
|
+
spawn.preAllocatedId,
|
|
1167
|
+
spawn.index,
|
|
996
1168
|
false,
|
|
997
1169
|
{ invokedAt, acquiredAt },
|
|
998
1170
|
);
|
|
@@ -1002,42 +1174,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1002
1174
|
},
|
|
1003
1175
|
signal,
|
|
1004
1176
|
);
|
|
1005
|
-
|
|
1006
|
-
const results: SingleResult[] = [];
|
|
1007
|
-
const contentParts: string[] = [];
|
|
1008
|
-
const outputPaths: string[] = [];
|
|
1009
|
-
const usageTotals = createUsageTotals();
|
|
1010
|
-
let hasUsage = false;
|
|
1011
|
-
let projectAgentsDir: string | null = null;
|
|
1012
|
-
for (let index = 0; index < spawnItems.length; index++) {
|
|
1013
|
-
const payload = payloads[index];
|
|
1014
|
-
if (!payload) {
|
|
1015
|
-
contentParts.push(`Task ${spawnItems[index].id?.trim() || `#${index + 1}`}: cancelled before start.`);
|
|
1016
|
-
continue;
|
|
1017
|
-
}
|
|
1018
|
-
projectAgentsDir ??= payload.details?.projectAgentsDir ?? null;
|
|
1019
|
-
const text = payload.content.find(part => part.type === "text")?.text;
|
|
1020
|
-
if (text) contentParts.push(text);
|
|
1021
|
-
for (const result of payload.details?.results ?? []) {
|
|
1022
|
-
results.push({ ...result, index });
|
|
1023
|
-
if (result.usage) {
|
|
1024
|
-
addUsageTotals(usageTotals, result.usage);
|
|
1025
|
-
hasUsage = true;
|
|
1026
|
-
}
|
|
1027
|
-
if (result.outputPath) outputPaths.push(result.outputPath);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
return {
|
|
1032
|
-
content: [{ type: "text", text: contentParts.join("\n\n") }],
|
|
1033
|
-
details: {
|
|
1034
|
-
projectAgentsDir,
|
|
1035
|
-
results,
|
|
1036
|
-
totalDurationMs: Date.now() - startTime,
|
|
1037
|
-
usage: hasUsage ? usageTotals : undefined,
|
|
1038
|
-
outputPaths: outputPaths.length > 0 ? outputPaths : undefined,
|
|
1039
|
-
},
|
|
1040
|
-
};
|
|
1177
|
+
return results;
|
|
1041
1178
|
}
|
|
1042
1179
|
|
|
1043
1180
|
/**
|
|
@@ -1074,7 +1211,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1074
1211
|
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
|
|
1075
1212
|
const agentName = params.agent ?? "";
|
|
1076
1213
|
const sharedContext = this.#isBatchEnabled() ? params.context?.trim() || undefined : undefined;
|
|
1077
|
-
const assignment = (params.
|
|
1214
|
+
const assignment = (params.task ?? "").trim();
|
|
1078
1215
|
const isolationMode = this.session.settings.get("task.isolation.mode");
|
|
1079
1216
|
const isolationRequested = "isolated" in params ? params.isolated === true : false;
|
|
1080
1217
|
const isIsolated = isolationMode !== "none" && isolationRequested;
|
|
@@ -1228,7 +1365,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1228
1365
|
} else {
|
|
1229
1366
|
const outputManager =
|
|
1230
1367
|
this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
|
|
1231
|
-
agentId = await outputManager.allocate(params.
|
|
1368
|
+
agentId = await outputManager.allocate(params.name?.trim() || generateTaskName());
|
|
1232
1369
|
}
|
|
1233
1370
|
|
|
1234
1371
|
const availableSkills = [...(this.session.skills ?? [])];
|
|
@@ -1263,7 +1400,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1263
1400
|
cost: 0,
|
|
1264
1401
|
durationMs: 0,
|
|
1265
1402
|
modelOverride,
|
|
1266
|
-
description: params.description,
|
|
1267
1403
|
};
|
|
1268
1404
|
const emitProgress = () => {
|
|
1269
1405
|
onUpdate?.({
|
|
@@ -1287,8 +1423,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1287
1423
|
assignment,
|
|
1288
1424
|
context: sharedContext,
|
|
1289
1425
|
planReference,
|
|
1290
|
-
description: params.description,
|
|
1291
|
-
role: params.role,
|
|
1292
1426
|
index: spawnIndex,
|
|
1293
1427
|
parentToolCallId: toolCallId,
|
|
1294
1428
|
detached,
|
|
@@ -1356,7 +1490,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1356
1490
|
agentId,
|
|
1357
1491
|
mergeMode,
|
|
1358
1492
|
artifactsDir: effectiveArtifactsDir,
|
|
1359
|
-
description: params.description,
|
|
1360
1493
|
buildCommitMessage: buildCommitMessageFn,
|
|
1361
1494
|
buildFailureResult: err => {
|
|
1362
1495
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1367,7 +1500,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1367
1500
|
agentSource: agent.source,
|
|
1368
1501
|
task: renderSubagentUserPrompt(assignment),
|
|
1369
1502
|
assignment,
|
|
1370
|
-
description: params.description,
|
|
1371
1503
|
exitCode: 1,
|
|
1372
1504
|
output: "",
|
|
1373
1505
|
stderr: message,
|
|
@@ -1447,11 +1579,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1447
1579
|
preview = lastNewline >= 0 ? slice.slice(0, lastNewline) : slice;
|
|
1448
1580
|
truncated = true;
|
|
1449
1581
|
}
|
|
1582
|
+
// A stopped-but-adopted agent (soft-budget stop) stays messageable; tell
|
|
1583
|
+
// the parent so it can resume via irc instead of redoing the work.
|
|
1584
|
+
const refStatus = AgentRegistry.global().get(result.id)?.status;
|
|
1585
|
+
const resumable = result.aborted && (refStatus === "idle" || refStatus === "parked");
|
|
1450
1586
|
const summary = prompt.render(taskSummaryTemplate, {
|
|
1451
1587
|
agentName: result.agent,
|
|
1452
1588
|
id: result.id,
|
|
1453
1589
|
status,
|
|
1454
1590
|
duration: formatDuration(totalDurationMs),
|
|
1591
|
+
abortReason: result.aborted ? result.abortReason : undefined,
|
|
1592
|
+
resumable,
|
|
1455
1593
|
preview,
|
|
1456
1594
|
truncated,
|
|
1457
1595
|
meta: result.outputMeta
|