@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.67

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/docs/sdk.md +1 -1
  3. package/package.json +5 -5
  4. package/scripts/generate-template.ts +6 -6
  5. package/src/cli/args.ts +3 -0
  6. package/src/core/agent-session.ts +39 -0
  7. package/src/core/bash-executor.ts +3 -3
  8. package/src/core/cursor/exec-bridge.ts +95 -88
  9. package/src/core/custom-commands/bundled/review/index.ts +142 -145
  10. package/src/core/custom-commands/bundled/wt/index.ts +68 -66
  11. package/src/core/custom-commands/loader.ts +4 -6
  12. package/src/core/custom-tools/index.ts +2 -2
  13. package/src/core/custom-tools/loader.ts +66 -61
  14. package/src/core/custom-tools/types.ts +4 -4
  15. package/src/core/custom-tools/wrapper.ts +61 -25
  16. package/src/core/event-bus.ts +19 -47
  17. package/src/core/extensions/index.ts +8 -4
  18. package/src/core/extensions/loader.ts +160 -120
  19. package/src/core/extensions/types.ts +4 -4
  20. package/src/core/extensions/wrapper.ts +149 -100
  21. package/src/core/hooks/index.ts +1 -1
  22. package/src/core/hooks/tool-wrapper.ts +96 -70
  23. package/src/core/hooks/types.ts +1 -2
  24. package/src/core/index.ts +1 -0
  25. package/src/core/mcp/index.ts +6 -2
  26. package/src/core/mcp/json-rpc.ts +88 -0
  27. package/src/core/mcp/loader.ts +22 -4
  28. package/src/core/mcp/manager.ts +202 -48
  29. package/src/core/mcp/tool-bridge.ts +143 -55
  30. package/src/core/mcp/tool-cache.ts +122 -0
  31. package/src/core/python-executor.ts +3 -9
  32. package/src/core/sdk.ts +33 -32
  33. package/src/core/session-manager.ts +30 -0
  34. package/src/core/settings-manager.ts +54 -1
  35. package/src/core/ssh/ssh-executor.ts +6 -84
  36. package/src/core/streaming-output.ts +107 -53
  37. package/src/core/tools/ask.ts +92 -93
  38. package/src/core/tools/bash.ts +103 -94
  39. package/src/core/tools/calculator.ts +41 -26
  40. package/src/core/tools/complete.ts +76 -66
  41. package/src/core/tools/context.ts +22 -24
  42. package/src/core/tools/exa/index.ts +1 -1
  43. package/src/core/tools/exa/mcp-client.ts +56 -101
  44. package/src/core/tools/find.ts +250 -253
  45. package/src/core/tools/git.ts +39 -33
  46. package/src/core/tools/grep.ts +440 -427
  47. package/src/core/tools/index.ts +63 -61
  48. package/src/core/tools/ls.ts +119 -114
  49. package/src/core/tools/lsp/clients/biome-client.ts +5 -7
  50. package/src/core/tools/lsp/clients/index.ts +4 -4
  51. package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
  52. package/src/core/tools/lsp/config.ts +2 -2
  53. package/src/core/tools/lsp/index.ts +604 -578
  54. package/src/core/tools/notebook.ts +121 -119
  55. package/src/core/tools/output.ts +163 -147
  56. package/src/core/tools/patch/applicator.ts +1100 -0
  57. package/src/core/tools/patch/diff.ts +362 -0
  58. package/src/core/tools/patch/fuzzy.ts +647 -0
  59. package/src/core/tools/patch/index.ts +430 -0
  60. package/src/core/tools/patch/normalize.ts +220 -0
  61. package/src/core/tools/patch/normative.ts +73 -0
  62. package/src/core/tools/patch/parser.ts +528 -0
  63. package/src/core/tools/patch/shared.ts +257 -0
  64. package/src/core/tools/patch/types.ts +244 -0
  65. package/src/core/tools/python.ts +139 -136
  66. package/src/core/tools/read.ts +239 -216
  67. package/src/core/tools/render-utils.ts +196 -77
  68. package/src/core/tools/renderers.ts +6 -2
  69. package/src/core/tools/ssh.ts +99 -80
  70. package/src/core/tools/task/executor.ts +11 -7
  71. package/src/core/tools/task/index.ts +352 -343
  72. package/src/core/tools/task/worker.ts +13 -23
  73. package/src/core/tools/todo-write.ts +74 -59
  74. package/src/core/tools/web-fetch.ts +54 -47
  75. package/src/core/tools/web-search/index.ts +27 -16
  76. package/src/core/tools/write.ts +108 -47
  77. package/src/core/ttsr.ts +106 -152
  78. package/src/core/voice.ts +49 -39
  79. package/src/index.ts +16 -12
  80. package/src/lib/worktree/index.ts +1 -9
  81. package/src/modes/interactive/components/diff.ts +15 -8
  82. package/src/modes/interactive/components/settings-defs.ts +42 -0
  83. package/src/modes/interactive/components/tool-execution.ts +46 -8
  84. package/src/modes/interactive/controllers/event-controller.ts +6 -19
  85. package/src/modes/interactive/controllers/input-controller.ts +1 -1
  86. package/src/modes/interactive/utils/ui-helpers.ts +5 -1
  87. package/src/modes/rpc/rpc-mode.ts +99 -81
  88. package/src/prompts/tools/patch.md +76 -0
  89. package/src/prompts/tools/read.md +1 -1
  90. package/src/prompts/tools/{edit.md → replace.md} +1 -0
  91. package/src/utils/shell.ts +0 -40
  92. package/src/core/tools/edit-diff.ts +0 -574
  93. package/src/core/tools/edit.ts +0 -345
@@ -13,7 +13,7 @@
13
13
  * - Session artifacts for debugging
14
14
  */
15
15
 
16
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
16
+ import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
17
17
  import type { Usage } from "@oh-my-pi/pi-ai";
18
18
  import type { Theme } from "../../../modes/interactive/theme/theme";
19
19
  import taskDescriptionTemplate from "../../../prompts/tools/task.md" with { type: "text" };
@@ -105,387 +105,396 @@ async function buildDescription(cwd: string): Promise<string> {
105
105
  });
106
106
  }
107
107
 
108
- /**
109
- * Create the task tool configured for a specific session.
110
- */
111
- export async function createTaskTool(
112
- session: ToolSession,
113
- ): Promise<AgentTool<typeof taskSchema, TaskToolDetails, Theme>> {
114
- // Check for same-agent blocking (allows other agent types)
115
- const blockedAgent = process.env.OMP_BLOCKED_AGENT;
108
+ // ═══════════════════════════════════════════════════════════════════════════
109
+ // Tool Class
110
+ // ═══════════════════════════════════════════════════════════════════════════
116
111
 
117
- // Build description upfront
118
- const description = await buildDescription(session.cwd);
112
+ type TaskParams = {
113
+ agent: string;
114
+ context?: string;
115
+ model?: string;
116
+ output?: unknown;
117
+ tasks: Array<{ id: string; task: string; description: string }>;
118
+ };
119
119
 
120
- return {
121
- name: "task",
122
- label: "Task",
123
- description,
124
- parameters: taskSchema,
125
- renderCall,
126
- renderResult,
127
- execute: async (_toolCallId, params, signal, onUpdate) => {
128
- const startTime = Date.now();
129
- const { agents, projectAgentsDir } = await discoverAgents(session.cwd);
130
- const { agent: agentName, context, model, output: outputSchema } = params;
131
-
132
- const isDefaultModelAlias = (value: string | undefined): boolean => {
133
- if (!value) return true;
134
- const normalized = value.trim().toLowerCase();
135
- return normalized === "default" || normalized === "pi/default" || normalized === "omp/default";
120
+ /**
121
+ * Task tool - Delegate tasks to specialized agents.
122
+ *
123
+ * Requires async initialization to discover available agents.
124
+ * Use `TaskTool.create(session)` to instantiate.
125
+ */
126
+ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, Theme> {
127
+ public readonly name = "task";
128
+ public readonly label = "Task";
129
+ public readonly description: string;
130
+ public readonly parameters = taskSchema;
131
+ public readonly renderCall = renderCall;
132
+ public readonly renderResult = renderResult;
133
+
134
+ private readonly session: ToolSession;
135
+ private readonly blockedAgent: string | undefined;
136
+
137
+ private constructor(session: ToolSession, description: string) {
138
+ this.session = session;
139
+ this.description = description;
140
+ this.blockedAgent = process.env.OMP_BLOCKED_AGENT;
141
+ }
142
+
143
+ /**
144
+ * Create a TaskTool instance with async agent discovery.
145
+ */
146
+ public static async create(session: ToolSession): Promise<TaskTool> {
147
+ const description = await buildDescription(session.cwd);
148
+ return new TaskTool(session, description);
149
+ }
150
+
151
+ public async execute(
152
+ _toolCallId: string,
153
+ params: TaskParams,
154
+ signal?: AbortSignal,
155
+ onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
156
+ ): Promise<AgentToolResult<TaskToolDetails>> {
157
+ const startTime = Date.now();
158
+ const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
159
+ const { agent: agentName, context, model, output: outputSchema } = params;
160
+
161
+ const isDefaultModelAlias = (value: string | undefined): boolean => {
162
+ if (!value) return true;
163
+ const normalized = value.trim().toLowerCase();
164
+ return normalized === "default" || normalized === "pi/default" || normalized === "omp/default";
165
+ };
166
+
167
+ // Validate agent exists
168
+ const agent = getAgent(agents, agentName);
169
+ if (!agent) {
170
+ const available = agents.map((a) => a.name).join(", ") || "none";
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: `Unknown agent "${agentName}". Available: ${available}`,
176
+ },
177
+ ],
178
+ details: {
179
+ projectAgentsDir,
180
+ results: [],
181
+ totalDurationMs: 0,
182
+ },
136
183
  };
137
-
138
- // Validate agent exists
139
- const agent = getAgent(agents, agentName);
140
- if (!agent) {
141
- const available = agents.map((a) => a.name).join(", ") || "none";
142
- return {
143
- content: [
144
- {
145
- type: "text",
146
- text: `Unknown agent "${agentName}". Available: ${available}`,
147
- },
148
- ],
149
- details: {
150
- projectAgentsDir,
151
- results: [],
152
- totalDurationMs: 0,
184
+ }
185
+
186
+ const shouldInheritSessionModel = model === undefined && isDefaultModelAlias(agent.model);
187
+ const sessionModel = shouldInheritSessionModel ? this.session.getActiveModelString?.() : undefined;
188
+ const modelOverride = model ?? sessionModel ?? this.session.getModelString?.();
189
+ const thinkingLevelOverride = agent.thinkingLevel;
190
+
191
+ // Output schema priority: agent frontmatter > params > inherited from parent session
192
+ const schemaOverridden = outputSchema !== undefined && agent.output !== undefined;
193
+ const effectiveOutputSchema = agent.output ?? outputSchema ?? this.session.outputSchema;
194
+
195
+ // Handle empty or missing tasks
196
+ if (!params.tasks || params.tasks.length === 0) {
197
+ return {
198
+ content: [
199
+ {
200
+ type: "text",
201
+ text: `No tasks provided. Use: { agent, context, tasks: [{id, task, description}, ...] }`,
153
202
  },
154
- };
155
- }
156
-
157
- const shouldInheritSessionModel = model === undefined && isDefaultModelAlias(agent.model);
158
- const sessionModel = shouldInheritSessionModel ? session.getActiveModelString?.() : undefined;
159
- const modelOverride = model ?? sessionModel ?? session.getModelString?.();
160
- const thinkingLevelOverride = agent.thinkingLevel;
203
+ ],
204
+ details: {
205
+ projectAgentsDir,
206
+ results: [],
207
+ totalDurationMs: 0,
208
+ },
209
+ };
210
+ }
211
+
212
+ // Validate task count
213
+ if (params.tasks.length > MAX_PARALLEL_TASKS) {
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: `Too many tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
219
+ },
220
+ ],
221
+ details: {
222
+ projectAgentsDir,
223
+ results: [],
224
+ totalDurationMs: 0,
225
+ },
226
+ };
227
+ }
161
228
 
162
- // Output schema priority: agent frontmatter > params > inherited from parent session
163
- const schemaOverridden = outputSchema !== undefined && agent.output !== undefined;
164
- const effectiveOutputSchema = agent.output ?? outputSchema ?? session.outputSchema;
229
+ const tasks = params.tasks;
230
+ const missingTaskIndexes: number[] = [];
231
+ const idIndexes = new Map<string, number[]>();
165
232
 
166
- // Handle empty or missing tasks
167
- if (!params.tasks || params.tasks.length === 0) {
168
- return {
169
- content: [
170
- {
171
- type: "text",
172
- text: `No tasks provided. Use: { agent, context, tasks: [{id, task, description}, ...] }`,
173
- },
174
- ],
175
- details: {
176
- projectAgentsDir,
177
- results: [],
178
- totalDurationMs: 0,
179
- },
180
- };
233
+ for (let i = 0; i < tasks.length; i++) {
234
+ const id = tasks[i]?.id;
235
+ if (typeof id !== "string" || id.trim() === "") {
236
+ missingTaskIndexes.push(i);
237
+ continue;
238
+ }
239
+ const normalizedId = id.toLowerCase();
240
+ const indexes = idIndexes.get(normalizedId);
241
+ if (indexes) {
242
+ indexes.push(i);
243
+ } else {
244
+ idIndexes.set(normalizedId, [i]);
181
245
  }
246
+ }
247
+
248
+ const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
249
+ for (const [normalizedId, indexes] of idIndexes.entries()) {
250
+ if (indexes.length > 1) {
251
+ duplicateIds.push({
252
+ id: tasks[indexes[0]]?.id ?? normalizedId,
253
+ indexes,
254
+ });
255
+ }
256
+ }
182
257
 
183
- // Validate task count
184
- if (params.tasks.length > MAX_PARALLEL_TASKS) {
258
+ if (missingTaskIndexes.length > 0 || duplicateIds.length > 0) {
259
+ const problems: string[] = [];
260
+ if (missingTaskIndexes.length > 0) {
261
+ problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
262
+ }
263
+ if (duplicateIds.length > 0) {
264
+ const details = duplicateIds.map((entry) => `${entry.id} (indexes ${entry.indexes.join(", ")})`).join("; ");
265
+ problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
266
+ }
267
+ return {
268
+ content: [{ type: "text", text: `Invalid tasks: ${problems.join(". ")}` }],
269
+ details: {
270
+ projectAgentsDir,
271
+ results: [],
272
+ totalDurationMs: 0,
273
+ },
274
+ };
275
+ }
276
+
277
+ // Derive artifacts directory
278
+ const sessionFile = this.session.getSessionFile();
279
+ const artifactsDir = sessionFile ? getArtifactsDir(sessionFile) : null;
280
+ const tempArtifactsDir = artifactsDir ? null : createTempArtifactsDir();
281
+ const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
282
+
283
+ // Initialize progress tracking
284
+ const progressMap = new Map<number, AgentProgress>();
285
+
286
+ // Update callback
287
+ const emitProgress = () => {
288
+ const progress = Array.from(progressMap.values()).sort((a, b) => a.index - b.index);
289
+ onUpdate?.({
290
+ content: [{ type: "text", text: `Running ${params.tasks.length} agents...` }],
291
+ details: {
292
+ projectAgentsDir,
293
+ results: [],
294
+ totalDurationMs: Date.now() - startTime,
295
+ progress,
296
+ },
297
+ });
298
+ };
299
+
300
+ try {
301
+ // Check self-recursion prevention
302
+ if (this.blockedAgent && agentName === this.blockedAgent) {
185
303
  return {
186
304
  content: [
187
305
  {
188
306
  type: "text",
189
- text: `Too many tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
307
+ text: `Cannot spawn ${this.blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
190
308
  },
191
309
  ],
192
310
  details: {
193
311
  projectAgentsDir,
194
312
  results: [],
195
- totalDurationMs: 0,
313
+ totalDurationMs: Date.now() - startTime,
196
314
  },
197
315
  };
198
316
  }
199
317
 
200
- const tasks = params.tasks;
201
- const missingTaskIndexes: number[] = [];
202
- const idIndexes = new Map<string, number[]>();
203
-
204
- for (let i = 0; i < tasks.length; i++) {
205
- const id = tasks[i]?.id;
206
- if (typeof id !== "string" || id.trim() === "") {
207
- missingTaskIndexes.push(i);
208
- continue;
209
- }
210
- const normalizedId = id.toLowerCase();
211
- const indexes = idIndexes.get(normalizedId);
212
- if (indexes) {
213
- indexes.push(i);
214
- } else {
215
- idIndexes.set(normalizedId, [i]);
216
- }
217
- }
218
-
219
- const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
220
- for (const [normalizedId, indexes] of idIndexes.entries()) {
221
- if (indexes.length > 1) {
222
- duplicateIds.push({
223
- id: tasks[indexes[0]]?.id ?? normalizedId,
224
- indexes,
225
- });
226
- }
227
- }
318
+ // Check spawn restrictions from parent
319
+ const parentSpawns = this.session.getSessionSpawns() ?? "*";
320
+ const allowedSpawns = parentSpawns.split(",").map((s) => s.trim());
321
+ const isSpawnAllowed = (): boolean => {
322
+ if (parentSpawns === "") return false; // Empty = deny all
323
+ if (parentSpawns === "*") return true; // Wildcard = allow all
324
+ return allowedSpawns.includes(agentName);
325
+ };
228
326
 
229
- if (missingTaskIndexes.length > 0 || duplicateIds.length > 0) {
230
- const problems: string[] = [];
231
- if (missingTaskIndexes.length > 0) {
232
- problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
233
- }
234
- if (duplicateIds.length > 0) {
235
- const details = duplicateIds
236
- .map((entry) => `${entry.id} (indexes ${entry.indexes.join(", ")})`)
237
- .join("; ");
238
- problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
239
- }
327
+ if (!isSpawnAllowed()) {
328
+ const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
240
329
  return {
241
- content: [{ type: "text", text: `Invalid tasks: ${problems.join(". ")}` }],
242
- details: {
243
- projectAgentsDir,
244
- results: [],
245
- totalDurationMs: 0,
246
- },
247
- };
248
- }
249
-
250
- // Derive artifacts directory
251
- const sessionFile = session.getSessionFile();
252
- const artifactsDir = sessionFile ? getArtifactsDir(sessionFile) : null;
253
- const tempArtifactsDir = artifactsDir ? null : createTempArtifactsDir();
254
- const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
255
-
256
- // Initialize progress tracking
257
- const progressMap = new Map<number, AgentProgress>();
258
-
259
- // Update callback
260
- const emitProgress = () => {
261
- const progress = Array.from(progressMap.values()).sort((a, b) => a.index - b.index);
262
- onUpdate?.({
263
- content: [{ type: "text", text: `Running ${params.tasks.length} agents...` }],
330
+ content: [{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${allowed}` }],
264
331
  details: {
265
332
  projectAgentsDir,
266
333
  results: [],
267
334
  totalDurationMs: Date.now() - startTime,
268
- progress,
269
335
  },
270
- });
271
- };
272
-
273
- try {
274
- // Check self-recursion prevention
275
- if (blockedAgent && agentName === blockedAgent) {
276
- return {
277
- content: [
278
- {
279
- type: "text",
280
- text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
281
- },
282
- ],
283
- details: {
284
- projectAgentsDir,
285
- results: [],
286
- totalDurationMs: Date.now() - startTime,
287
- },
288
- };
289
- }
290
-
291
- // Check spawn restrictions from parent
292
- const parentSpawns = session.getSessionSpawns() ?? "*";
293
- const allowedSpawns = parentSpawns.split(",").map((s) => s.trim());
294
- const isSpawnAllowed = (): boolean => {
295
- if (parentSpawns === "") return false; // Empty = deny all
296
- if (parentSpawns === "*") return true; // Wildcard = allow all
297
- return allowedSpawns.includes(agentName);
298
336
  };
337
+ }
299
338
 
300
- if (!isSpawnAllowed()) {
301
- const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
302
- return {
303
- content: [{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${allowed}` }],
304
- details: {
305
- projectAgentsDir,
306
- results: [],
307
- totalDurationMs: Date.now() - startTime,
308
- },
309
- };
310
- }
311
-
312
- // Build full prompts with context prepended
313
- const tasksWithContext = tasks.map((t) => ({
314
- task: context ? `${context}\n\n${t.task}` : t.task,
339
+ // Build full prompts with context prepended
340
+ const tasksWithContext = tasks.map((t) => ({
341
+ task: context ? `${context}\n\n${t.task}` : t.task,
342
+ description: t.description,
343
+ taskId: t.id,
344
+ }));
345
+
346
+ // Initialize progress for all tasks
347
+ for (let i = 0; i < tasksWithContext.length; i++) {
348
+ const t = tasksWithContext[i];
349
+ progressMap.set(i, {
350
+ index: i,
351
+ taskId: t.taskId,
352
+ agent: agentName,
353
+ agentSource: agent.source,
354
+ status: "pending",
355
+ task: t.task,
356
+ recentTools: [],
357
+ recentOutput: [],
358
+ toolCount: 0,
359
+ tokens: 0,
360
+ durationMs: 0,
361
+ modelOverride,
315
362
  description: t.description,
316
- taskId: t.id,
317
- }));
318
-
319
- // Initialize progress for all tasks
320
- for (let i = 0; i < tasksWithContext.length; i++) {
321
- const t = tasksWithContext[i];
322
- progressMap.set(i, {
323
- index: i,
324
- taskId: t.taskId,
325
- agent: agentName,
326
- agentSource: agent.source,
327
- status: "pending",
328
- task: t.task,
329
- recentTools: [],
330
- recentOutput: [],
331
- toolCount: 0,
332
- tokens: 0,
333
- durationMs: 0,
334
- modelOverride,
335
- description: t.description,
336
- });
337
- }
338
- emitProgress();
339
-
340
- // Execute in parallel with concurrency limit
341
- const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
342
- tasksWithContext,
343
- MAX_CONCURRENCY,
344
- async (task, index) => {
345
- return runSubprocess({
346
- cwd: session.cwd,
347
- agent,
348
- task: task.task,
349
- description: task.description,
350
- index,
351
- taskId: task.taskId,
352
- context: undefined, // Already prepended above
353
- modelOverride,
354
- thinkingLevel: thinkingLevelOverride,
355
- outputSchema: effectiveOutputSchema,
356
- sessionFile,
357
- persistArtifacts: !!artifactsDir,
358
- artifactsDir: effectiveArtifactsDir,
359
- enableLsp: false,
360
- signal,
361
- eventBus: undefined,
362
- onProgress: (progress) => {
363
- progressMap.set(index, structuredClone(progress));
364
- emitProgress();
365
- },
366
- authStorage: session.authStorage,
367
- modelRegistry: session.modelRegistry,
368
- settingsManager: session.settingsManager,
369
- mcpManager: session.mcpManager,
370
- });
371
- },
372
- signal,
373
- );
374
-
375
- // Fill in skipped tasks (undefined entries from abort) with placeholder results
376
- const results: SingleResult[] = partialResults.map((result, index) => {
377
- if (result !== undefined) return result;
378
- const task = tasksWithContext[index];
379
- return {
380
- index,
381
- taskId: task.taskId,
382
- agent: agentName,
383
- agentSource: agent.source,
363
+ });
364
+ }
365
+ emitProgress();
366
+
367
+ // Execute in parallel with concurrency limit
368
+ const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
369
+ tasksWithContext,
370
+ MAX_CONCURRENCY,
371
+ async (task, index) => {
372
+ return runSubprocess({
373
+ cwd: this.session.cwd,
374
+ agent,
384
375
  task: task.task,
385
376
  description: task.description,
386
- exitCode: 1,
387
- output: "",
388
- stderr: "Skipped (cancelled before start)",
389
- truncated: false,
390
- durationMs: 0,
391
- tokens: 0,
377
+ index,
378
+ taskId: task.taskId,
379
+ context: undefined, // Already prepended above
392
380
  modelOverride,
393
- error: "Skipped",
394
- aborted: true,
395
- };
396
- });
397
-
398
- // Aggregate usage from executor results (already accumulated incrementally)
399
- const aggregatedUsage = createUsageTotals();
400
- let hasAggregatedUsage = false;
401
- for (const result of results) {
402
- if (result.usage) {
403
- addUsageTotals(aggregatedUsage, result.usage);
404
- hasAggregatedUsage = true;
405
- }
381
+ thinkingLevel: thinkingLevelOverride,
382
+ outputSchema: effectiveOutputSchema,
383
+ sessionFile,
384
+ persistArtifacts: !!artifactsDir,
385
+ artifactsDir: effectiveArtifactsDir,
386
+ enableLsp: false,
387
+ signal,
388
+ eventBus: undefined,
389
+ onProgress: (progress) => {
390
+ progressMap.set(index, structuredClone(progress));
391
+ emitProgress();
392
+ },
393
+ authStorage: this.session.authStorage,
394
+ modelRegistry: this.session.modelRegistry,
395
+ settingsManager: this.session.settingsManager,
396
+ mcpManager: this.session.mcpManager,
397
+ });
398
+ },
399
+ signal,
400
+ );
401
+
402
+ // Fill in skipped tasks (undefined entries from abort) with placeholder results
403
+ const results: SingleResult[] = partialResults.map((result, index) => {
404
+ if (result !== undefined) return result;
405
+ const task = tasksWithContext[index];
406
+ return {
407
+ index,
408
+ taskId: task.taskId,
409
+ agent: agentName,
410
+ agentSource: agent.source,
411
+ task: task.task,
412
+ description: task.description,
413
+ exitCode: 1,
414
+ output: "",
415
+ stderr: "Skipped (cancelled before start)",
416
+ truncated: false,
417
+ durationMs: 0,
418
+ tokens: 0,
419
+ modelOverride,
420
+ error: "Skipped",
421
+ aborted: true,
422
+ };
423
+ });
424
+
425
+ // Aggregate usage from executor results (already accumulated incrementally)
426
+ const aggregatedUsage = createUsageTotals();
427
+ let hasAggregatedUsage = false;
428
+ for (const result of results) {
429
+ if (result.usage) {
430
+ addUsageTotals(aggregatedUsage, result.usage);
431
+ hasAggregatedUsage = true;
406
432
  }
433
+ }
407
434
 
408
- // Collect output paths (artifacts already written by executor in real-time)
409
- const outputPaths: string[] = [];
410
- for (const result of results) {
411
- if (result.artifactPaths) {
412
- outputPaths.push(result.artifactPaths.outputPath);
413
- }
435
+ // Collect output paths (artifacts already written by executor in real-time)
436
+ const outputPaths: string[] = [];
437
+ for (const result of results) {
438
+ if (result.artifactPaths) {
439
+ outputPaths.push(result.artifactPaths.outputPath);
414
440
  }
441
+ }
415
442
 
416
- // Build final output - match plugin format
417
- const successCount = results.filter((r) => r.exitCode === 0).length;
418
- const cancelledCount = results.filter((r) => r.aborted).length;
419
- const totalDuration = Date.now() - startTime;
420
-
421
- const summaries = results.map((r) => {
422
- const status = r.aborted ? "cancelled" : r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
423
- const output = r.output.trim() || r.stderr.trim() || "(no output)";
424
- const preview = output.split("\n").slice(0, 5).join("\n");
425
- const meta = r.outputMeta
426
- ? ` [${r.outputMeta.lineCount} lines, ${formatBytes(r.outputMeta.charCount)}]`
427
- : "";
428
- return `[${r.agent}] ${status}${meta} ${r.taskId}\n${preview}`;
429
- });
430
-
431
- const outputIds = results.filter((r) => !r.aborted || r.output.trim()).map((r) => r.taskId);
432
- const outputHint =
433
- outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : "";
434
- const schemaNote = schemaOverridden
435
- ? `\n\nNote: Agent '${agentName}' has a fixed output schema; your 'output' parameter was ignored.\nRequired schema: ${JSON.stringify(agent.output)}`
443
+ // Build final output - match plugin format
444
+ const successCount = results.filter((r) => r.exitCode === 0).length;
445
+ const cancelledCount = results.filter((r) => r.aborted).length;
446
+ const totalDuration = Date.now() - startTime;
447
+
448
+ const summaries = results.map((r) => {
449
+ const status = r.aborted ? "cancelled" : r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
450
+ const output = r.output.trim() || r.stderr.trim() || "(no output)";
451
+ const preview = output.split("\n").slice(0, 5).join("\n");
452
+ const meta = r.outputMeta
453
+ ? ` [${r.outputMeta.lineCount} lines, ${formatBytes(r.outputMeta.charCount)}]`
436
454
  : "";
437
- const cancelledNote = aborted && cancelledCount > 0 ? ` (${cancelledCount} cancelled)` : "";
438
- const summary = `${successCount}/${results.length} succeeded${cancelledNote} [${formatDuration(
439
- totalDuration,
440
- )}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}${schemaNote}`;
441
-
442
- // Cleanup temp directory if used
443
- if (tempArtifactsDir) {
444
- await cleanupTempDir(tempArtifactsDir);
445
- }
446
-
447
- return {
448
- content: [{ type: "text", text: summary }],
449
- details: {
450
- projectAgentsDir,
451
- results: results,
452
- totalDurationMs: totalDuration,
453
- usage: hasAggregatedUsage ? aggregatedUsage : undefined,
454
- outputPaths,
455
- },
456
- };
457
- } catch (err) {
458
- // Cleanup temp directory on error
459
- if (tempArtifactsDir) {
460
- await cleanupTempDir(tempArtifactsDir);
461
- }
455
+ return `[${r.agent}] ${status}${meta} ${r.taskId}\n${preview}`;
456
+ });
457
+
458
+ const outputIds = results.filter((r) => !r.aborted || r.output.trim()).map((r) => r.taskId);
459
+ const outputHint =
460
+ outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : "";
461
+ const schemaNote = schemaOverridden
462
+ ? `\n\nNote: Agent '${agentName}' has a fixed output schema; your 'output' parameter was ignored.\nRequired schema: ${JSON.stringify(agent.output)}`
463
+ : "";
464
+ const cancelledNote = aborted && cancelledCount > 0 ? ` (${cancelledCount} cancelled)` : "";
465
+ const summary = `${successCount}/${results.length} succeeded${cancelledNote} [${formatDuration(
466
+ totalDuration,
467
+ )}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}${schemaNote}`;
468
+
469
+ // Cleanup temp directory if used
470
+ if (tempArtifactsDir) {
471
+ await cleanupTempDir(tempArtifactsDir);
472
+ }
462
473
 
463
- return {
464
- content: [{ type: "text", text: `Task execution failed: ${err}` }],
465
- details: {
466
- projectAgentsDir,
467
- results: [],
468
- totalDurationMs: Date.now() - startTime,
469
- },
470
- };
474
+ return {
475
+ content: [{ type: "text", text: summary }],
476
+ details: {
477
+ projectAgentsDir,
478
+ results: results,
479
+ totalDurationMs: totalDuration,
480
+ usage: hasAggregatedUsage ? aggregatedUsage : undefined,
481
+ outputPaths,
482
+ },
483
+ };
484
+ } catch (err) {
485
+ // Cleanup temp directory on error
486
+ if (tempArtifactsDir) {
487
+ await cleanupTempDir(tempArtifactsDir);
471
488
  }
472
- },
473
- };
474
- }
475
489
 
476
- // Default task tool - returns a placeholder tool
477
- // Real implementations should use createTaskTool(session) to initialize the tool
478
- export const taskTool: AgentTool<typeof taskSchema, TaskToolDetails, Theme> = {
479
- name: "task",
480
- label: "Task",
481
- description: "Launch a new agent to handle complex, multi-step tasks autonomously.",
482
- parameters: taskSchema,
483
- execute: async () => ({
484
- content: [{ type: "text", text: "Task tool not properly initialized. Use createTaskTool(session) instead." }],
485
- details: {
486
- projectAgentsDir: null,
487
- results: [],
488
- totalDurationMs: 0,
489
- },
490
- }),
491
- };
490
+ return {
491
+ content: [{ type: "text", text: `Task execution failed: ${err}` }],
492
+ details: {
493
+ projectAgentsDir,
494
+ results: [],
495
+ totalDurationMs: Date.now() - startTime,
496
+ },
497
+ };
498
+ }
499
+ }
500
+ }