@oh-my-pi/pi-coding-agent 1.341.0 → 2.1.1337

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 (158) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +10 -9
  5. package/src/bun-imports.d.ts +16 -0
  6. package/src/cli/args.ts +5 -6
  7. package/src/cli/file-processor.ts +3 -3
  8. package/src/cli/list-models.ts +2 -2
  9. package/src/cli/plugin-cli.ts +1 -1
  10. package/src/cli/session-picker.ts +2 -2
  11. package/src/cli/update-cli.ts +273 -0
  12. package/src/cli.ts +1 -1
  13. package/src/config.ts +23 -75
  14. package/src/core/agent-session.ts +158 -16
  15. package/src/core/auth-storage.ts +2 -3
  16. package/src/core/bash-executor.ts +50 -10
  17. package/src/core/compaction/branch-summarization.ts +5 -5
  18. package/src/core/compaction/compaction.ts +3 -3
  19. package/src/core/compaction/index.ts +3 -3
  20. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  21. package/src/core/custom-commands/index.ts +15 -0
  22. package/src/core/custom-commands/loader.ts +232 -0
  23. package/src/core/custom-commands/types.ts +112 -0
  24. package/src/core/custom-tools/index.ts +3 -3
  25. package/src/core/custom-tools/loader.ts +10 -8
  26. package/src/core/custom-tools/types.ts +11 -6
  27. package/src/core/custom-tools/wrapper.ts +2 -1
  28. package/src/core/exec.ts +22 -12
  29. package/src/core/export-html/index.ts +38 -123
  30. package/src/core/export-html/template.css +0 -7
  31. package/src/core/export-html/template.html +3 -4
  32. package/src/core/export-html/template.macro.ts +24 -0
  33. package/src/core/file-mentions.ts +54 -0
  34. package/src/core/hooks/index.ts +5 -5
  35. package/src/core/hooks/loader.ts +21 -16
  36. package/src/core/hooks/runner.ts +6 -6
  37. package/src/core/hooks/tool-wrapper.ts +2 -2
  38. package/src/core/hooks/types.ts +12 -15
  39. package/src/core/index.ts +6 -6
  40. package/src/core/logger.ts +112 -0
  41. package/src/core/mcp/client.ts +3 -3
  42. package/src/core/mcp/config.ts +1 -1
  43. package/src/core/mcp/index.ts +12 -12
  44. package/src/core/mcp/loader.ts +2 -2
  45. package/src/core/mcp/manager.ts +6 -6
  46. package/src/core/mcp/tool-bridge.ts +3 -3
  47. package/src/core/mcp/transports/http.ts +1 -1
  48. package/src/core/mcp/transports/index.ts +2 -2
  49. package/src/core/mcp/transports/stdio.ts +1 -1
  50. package/src/core/messages.ts +22 -0
  51. package/src/core/model-registry.ts +2 -2
  52. package/src/core/model-resolver.ts +2 -2
  53. package/src/core/plugins/doctor.ts +1 -1
  54. package/src/core/plugins/index.ts +6 -6
  55. package/src/core/plugins/installer.ts +4 -4
  56. package/src/core/plugins/loader.ts +4 -9
  57. package/src/core/plugins/manager.ts +5 -5
  58. package/src/core/plugins/paths.ts +3 -3
  59. package/src/core/sdk.ts +77 -35
  60. package/src/core/session-manager.ts +6 -6
  61. package/src/core/settings-manager.ts +16 -3
  62. package/src/core/skills.ts +5 -5
  63. package/src/core/slash-commands.ts +60 -45
  64. package/src/core/system-prompt.ts +6 -6
  65. package/src/core/title-generator.ts +2 -2
  66. package/src/core/tools/bash.ts +32 -155
  67. package/src/core/tools/context.ts +2 -2
  68. package/src/core/tools/edit-diff.ts +3 -3
  69. package/src/core/tools/edit.ts +18 -5
  70. package/src/core/tools/exa/company.ts +3 -3
  71. package/src/core/tools/exa/index.ts +16 -17
  72. package/src/core/tools/exa/linkedin.ts +3 -3
  73. package/src/core/tools/exa/mcp-client.ts +9 -9
  74. package/src/core/tools/exa/render.ts +5 -5
  75. package/src/core/tools/exa/researcher.ts +3 -3
  76. package/src/core/tools/exa/search.ts +6 -5
  77. package/src/core/tools/exa/types.ts +5 -6
  78. package/src/core/tools/exa/websets.ts +3 -3
  79. package/src/core/tools/find.ts +3 -3
  80. package/src/core/tools/grep.ts +3 -3
  81. package/src/core/tools/index.ts +48 -34
  82. package/src/core/tools/ls.ts +4 -4
  83. package/src/core/tools/lsp/client.ts +161 -90
  84. package/src/core/tools/lsp/config.ts +1 -1
  85. package/src/core/tools/lsp/edits.ts +2 -2
  86. package/src/core/tools/lsp/index.ts +15 -13
  87. package/src/core/tools/lsp/render.ts +2 -2
  88. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  89. package/src/core/tools/lsp/utils.ts +1 -1
  90. package/src/core/tools/notebook.ts +1 -1
  91. package/src/core/tools/output.ts +175 -0
  92. package/src/core/tools/read.ts +7 -7
  93. package/src/core/tools/renderers.ts +92 -13
  94. package/src/core/tools/review.ts +268 -0
  95. package/src/core/tools/task/agents.ts +22 -38
  96. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  97. package/src/core/tools/task/commands.ts +31 -10
  98. package/src/core/tools/task/discovery.ts +2 -2
  99. package/src/core/tools/task/executor.ts +145 -28
  100. package/src/core/tools/task/index.ts +78 -30
  101. package/src/core/tools/task/model-resolver.ts +30 -20
  102. package/src/core/tools/task/parallel.ts +1 -1
  103. package/src/core/tools/task/render.ts +219 -30
  104. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  105. package/src/core/tools/task/types.ts +36 -2
  106. package/src/core/tools/web-fetch.ts +5 -3
  107. package/src/core/tools/web-search/auth.ts +1 -1
  108. package/src/core/tools/web-search/index.ts +17 -15
  109. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  110. package/src/core/tools/web-search/providers/exa.ts +3 -5
  111. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  112. package/src/core/tools/web-search/render.ts +3 -3
  113. package/src/core/tools/write.ts +4 -4
  114. package/src/index.ts +29 -18
  115. package/src/main.ts +50 -33
  116. package/src/migrations.ts +3 -3
  117. package/src/modes/index.ts +5 -5
  118. package/src/modes/interactive/components/armin.ts +1 -1
  119. package/src/modes/interactive/components/assistant-message.ts +1 -1
  120. package/src/modes/interactive/components/bash-execution.ts +4 -4
  121. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  122. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  123. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  124. package/src/modes/interactive/components/diff.ts +1 -1
  125. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  126. package/src/modes/interactive/components/footer.ts +5 -5
  127. package/src/modes/interactive/components/hook-editor.ts +2 -2
  128. package/src/modes/interactive/components/hook-input.ts +2 -2
  129. package/src/modes/interactive/components/hook-message.ts +3 -3
  130. package/src/modes/interactive/components/hook-selector.ts +2 -2
  131. package/src/modes/interactive/components/model-selector.ts +281 -59
  132. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  133. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  134. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  135. package/src/modes/interactive/components/session-selector.ts +4 -4
  136. package/src/modes/interactive/components/settings-defs.ts +1 -1
  137. package/src/modes/interactive/components/settings-selector.ts +5 -5
  138. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  139. package/src/modes/interactive/components/theme-selector.ts +2 -2
  140. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  141. package/src/modes/interactive/components/tool-execution.ts +26 -8
  142. package/src/modes/interactive/components/tree-selector.ts +3 -3
  143. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  144. package/src/modes/interactive/components/user-message.ts +1 -1
  145. package/src/modes/interactive/components/welcome.ts +2 -2
  146. package/src/modes/interactive/interactive-mode.ts +86 -42
  147. package/src/modes/interactive/theme/theme.ts +15 -17
  148. package/src/modes/print-mode.ts +4 -3
  149. package/src/modes/rpc/rpc-client.ts +4 -4
  150. package/src/modes/rpc/rpc-mode.ts +22 -12
  151. package/src/modes/rpc/rpc-types.ts +3 -3
  152. package/src/utils/changelog.ts +2 -2
  153. package/src/utils/clipboard.ts +1 -1
  154. package/src/utils/shell-snapshot.ts +218 -0
  155. package/src/utils/shell.ts +93 -13
  156. package/src/utils/tools-manager.ts +1 -1
  157. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  158. package/src/core/tools/exa/logger.ts +0 -56
@@ -13,8 +13,8 @@
13
13
  import * as fs from "node:fs";
14
14
  import * as os from "node:os";
15
15
  import * as path from "node:path";
16
- import { loadBundledAgents } from "./agents.js";
17
- import type { AgentDefinition, AgentSource } from "./types.js";
16
+ import { loadBundledAgents } from "./agents";
17
+ import type { AgentDefinition, AgentSource } from "./types";
18
18
 
19
19
  /** Result of agent discovery */
20
20
  export interface DiscoveryResult {
@@ -10,15 +10,17 @@ import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import * as readline from "node:readline";
13
- import { resolveModelPattern } from "./model-resolver.js";
13
+ import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
14
+ import { resolveModelPattern } from "./model-resolver";
15
+ import { subprocessToolRegistry } from "./subprocess-tool-registry";
14
16
  import {
15
17
  type AgentDefinition,
16
18
  type AgentProgress,
17
19
  MAX_OUTPUT_BYTES,
18
20
  MAX_OUTPUT_LINES,
19
- PI_NO_SUBAGENTS_ENV,
21
+ PI_BLOCKED_AGENT_ENV,
20
22
  type SingleResult,
21
- } from "./types.js";
23
+ } from "./types";
22
24
 
23
25
  /** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
24
26
  const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
@@ -166,6 +168,23 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
166
168
  // Build full task with context
167
169
  const fullTask = context ? `${context}\n\n${task}` : task;
168
170
 
171
+ // Set up artifact paths and write input file upfront if artifacts dir provided
172
+ let artifactPaths: { inputPath: string; outputPath: string; jsonlPath: string } | undefined;
173
+ let subtaskSessionFile: string | undefined;
174
+
175
+ if (options.artifactsDir) {
176
+ ensureArtifactsDir(options.artifactsDir);
177
+ artifactPaths = getArtifactPaths(options.artifactsDir, agent.name, index);
178
+ subtaskSessionFile = artifactPaths.jsonlPath;
179
+
180
+ // Write input file immediately (real-time visibility)
181
+ try {
182
+ fs.writeFileSync(artifactPaths.inputPath, fullTask, "utf-8");
183
+ } catch {
184
+ // Non-fatal, continue without input artifact
185
+ }
186
+ }
187
+
169
188
  // Build args
170
189
  const args: string[] = ["--mode", "json", "--non-interactive"];
171
190
 
@@ -183,8 +202,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
183
202
  args.push("--model", resolvedModel);
184
203
  }
185
204
 
186
- // Add session options
187
- if (options.sessionFile) {
205
+ // Add session options - use subtask-specific session file for real-time streaming
206
+ if (subtaskSessionFile) {
207
+ args.push("--session", subtaskSessionFile);
208
+ } else if (options.sessionFile) {
188
209
  args.push("--session", options.sessionFile);
189
210
  } else {
190
211
  args.push("--no-session");
@@ -193,10 +214,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
193
214
  // Add task as prompt
194
215
  args.push("--prompt", fullTask);
195
216
 
196
- // Set up environment
217
+ // Set up environment - block same-agent recursion unless explicitly recursive
197
218
  const env = { ...process.env };
198
219
  if (!agent.recursive) {
199
- env[PI_NO_SUBAGENTS_ENV] = "1";
220
+ env[PI_BLOCKED_AGENT_ENV] = agent.name;
200
221
  }
201
222
 
202
223
  // Spawn subprocess
@@ -211,6 +232,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
211
232
  let stderr = "";
212
233
  let finalOutput = "";
213
234
  let resolved = false;
235
+ let pendingTermination = false; // Set when shouldTerminate fires, wait for message_end
214
236
  const jsonlEvents: string[] = [];
215
237
 
216
238
  // Handle abort signal
@@ -242,7 +264,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
242
264
  progress.currentToolStartMs = now;
243
265
  break;
244
266
 
245
- case "tool_execution_end":
267
+ case "tool_execution_end": {
246
268
  if (progress.currentTool) {
247
269
  progress.recentTools.unshift({
248
270
  tool: progress.currentTool,
@@ -257,24 +279,73 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
257
279
  progress.currentTool = undefined;
258
280
  progress.currentToolArgs = undefined;
259
281
  progress.currentToolStartMs = undefined;
282
+
283
+ // Check for registered subprocess tool handler
284
+ const handler = subprocessToolRegistry.getHandler(event.toolName);
285
+ if (handler) {
286
+ // Extract data using handler
287
+ if (handler.extractData) {
288
+ const data = handler.extractData({
289
+ toolName: event.toolName,
290
+ toolCallId: event.toolCallId,
291
+ args: event.args,
292
+ result: event.result,
293
+ isError: event.isError,
294
+ });
295
+ if (data !== undefined) {
296
+ progress.extractedToolData = progress.extractedToolData || {};
297
+ progress.extractedToolData[event.toolName] = progress.extractedToolData[event.toolName] || [];
298
+ progress.extractedToolData[event.toolName].push(data);
299
+ }
300
+ }
301
+
302
+ // Check if handler wants to terminate subprocess
303
+ if (
304
+ handler.shouldTerminate?.({
305
+ toolName: event.toolName,
306
+ toolCallId: event.toolCallId,
307
+ args: event.args,
308
+ result: event.result,
309
+ isError: event.isError,
310
+ })
311
+ ) {
312
+ // Don't kill immediately - wait for message_end to get token counts
313
+ pendingTermination = true;
314
+ // Safety timeout in case message_end never arrives
315
+ setTimeout(() => {
316
+ if (!resolved) {
317
+ resolved = true;
318
+ proc.kill("SIGTERM");
319
+ }
320
+ }, 2000);
321
+ }
322
+ }
323
+ break;
324
+ }
325
+
326
+ case "message_update": {
327
+ // Extract text for progress display only (replace, don't accumulate)
328
+ const updateContent = event.message?.content || event.content;
329
+ if (updateContent && Array.isArray(updateContent)) {
330
+ const allText: string[] = [];
331
+ for (const block of updateContent) {
332
+ if (block.type === "text" && block.text) {
333
+ const lines = block.text.split("\n").filter((l: string) => l.trim());
334
+ allText.push(...lines);
335
+ }
336
+ }
337
+ // Show last 8 lines from current state (not accumulated)
338
+ progress.recentOutput = allText.slice(-8).reverse();
339
+ }
260
340
  break;
341
+ }
261
342
 
262
- case "message_update":
263
343
  case "message_end": {
264
- // Extract text content for recent output (prefer message.content, fallback to event.content)
344
+ // Extract final text content from completed message
265
345
  const messageContent = event.message?.content || event.content;
266
346
  if (messageContent && Array.isArray(messageContent)) {
267
347
  for (const block of messageContent) {
268
348
  if (block.type === "text" && block.text) {
269
- const lines = block.text.split("\n").filter((l: string) => l.trim());
270
- for (const l of lines) {
271
- if (!progress.recentOutput.includes(l)) {
272
- progress.recentOutput.unshift(l);
273
- if (progress.recentOutput.length > 8) {
274
- progress.recentOutput.pop();
275
- }
276
- }
277
- }
278
349
  output += block.text;
279
350
  }
280
351
  }
@@ -282,7 +353,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
282
353
  // Extract usage (prefer message.usage, fallback to event.usage)
283
354
  const messageUsage = event.message?.usage || event.usage;
284
355
  if (messageUsage) {
285
- progress.tokens = (messageUsage.input_tokens || 0) + (messageUsage.output_tokens || 0);
356
+ // Accumulate tokens across messages (not overwrite)
357
+ progress.tokens += (messageUsage.input_tokens || 0) + (messageUsage.output_tokens || 0);
358
+ }
359
+ // If pending termination, now we have tokens - terminate
360
+ if (pendingTermination && !resolved) {
361
+ resolved = true;
362
+ proc.kill("SIGTERM");
286
363
  }
287
364
  break;
288
365
  }
@@ -304,7 +381,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
304
381
  }
305
382
 
306
383
  progress.durationMs = now - startTime;
307
- onProgress?.(progress);
384
+ // Clone progress object before passing to callback to prevent mutation during render
385
+ onProgress?.({ ...progress });
308
386
  } catch {
309
387
  // Ignore non-JSON lines
310
388
  }
@@ -316,16 +394,35 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
316
394
  stderr += stderrDecoder.decode(chunk, { stream: true });
317
395
  });
318
396
 
319
- // Wait for process to exit
397
+ // Wait for readline to finish BEFORE resolving
320
398
  const exitCode = await new Promise<number>((resolve) => {
321
- proc.on("close", (code) => {
322
- resolved = true;
323
- resolve(code ?? 1);
399
+ let code: number | null = null;
400
+ let rlClosed = false;
401
+ let procClosed = false;
402
+
403
+ const maybeResolve = () => {
404
+ if (rlClosed && procClosed) {
405
+ resolved = true;
406
+ resolve(code ?? 1);
407
+ }
408
+ };
409
+
410
+ rl.on("close", () => {
411
+ rlClosed = true;
412
+ maybeResolve();
413
+ });
414
+
415
+ proc.on("close", (c) => {
416
+ code = c;
417
+ procClosed = true;
418
+ maybeResolve();
324
419
  });
420
+
325
421
  proc.on("error", (err) => {
326
- resolved = true;
327
422
  stderr += `\nProcess error: ${err.message}`;
328
- resolve(1);
423
+ code = 1;
424
+ procClosed = true;
425
+ maybeResolve();
329
426
  });
330
427
  });
331
428
 
@@ -344,8 +441,24 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
344
441
  const rawOutput = finalOutput || output;
345
442
  const { text: truncatedOutput, truncated } = truncateOutput(rawOutput);
346
443
 
444
+ // Write output artifact (input and jsonl already written in real-time)
445
+ // Compute output metadata for Output tool integration
446
+ let outputMeta: { lineCount: number; charCount: number } | undefined;
447
+ if (artifactPaths) {
448
+ try {
449
+ fs.writeFileSync(artifactPaths.outputPath, rawOutput, "utf-8");
450
+ outputMeta = {
451
+ lineCount: rawOutput.split("\n").length,
452
+ charCount: rawOutput.length,
453
+ };
454
+ } catch {
455
+ // Non-fatal
456
+ }
457
+ }
458
+
347
459
  // Update final progress
348
- progress.status = exitCode === 0 ? "completed" : "failed";
460
+ const wasAborted = signal?.aborted ?? false;
461
+ progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
349
462
  progress.durationMs = Date.now() - startTime;
350
463
  onProgress?.(progress);
351
464
 
@@ -362,6 +475,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
362
475
  tokens: progress.tokens,
363
476
  modelOverride,
364
477
  error: exitCode !== 0 && stderr ? stderr : undefined,
478
+ aborted: wasAborted,
365
479
  jsonlEvents,
480
+ artifactPaths,
481
+ extractedToolData: progress.extractedToolData,
482
+ outputMeta,
366
483
  };
367
484
  }
@@ -14,33 +14,50 @@
14
14
  */
15
15
 
16
16
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
17
- import type { Theme } from "../../../modes/interactive/theme/theme.js";
18
- import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir, writeArtifacts } from "./artifacts.js";
19
- import { discoverAgents, getAgent } from "./discovery.js";
20
- import { runSubprocess } from "./executor.js";
21
- import { mapWithConcurrencyLimit } from "./parallel.js";
22
- import { formatDuration, renderCall, renderResult } from "./render.js";
17
+ import type { Theme } from "../../../modes/interactive/theme/theme";
18
+ import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir } from "./artifacts";
19
+ import { discoverAgents, getAgent } from "./discovery";
20
+ import { runSubprocess } from "./executor";
21
+ import { mapWithConcurrencyLimit } from "./parallel";
22
+ import { formatDuration, renderCall, renderResult } from "./render";
23
23
  import {
24
24
  type AgentProgress,
25
25
  MAX_AGENTS_IN_DESCRIPTION,
26
26
  MAX_CONCURRENCY,
27
27
  MAX_PARALLEL_TASKS,
28
+ PI_BLOCKED_AGENT_ENV,
28
29
  PI_NO_SUBAGENTS_ENV,
29
30
  type TaskToolDetails,
30
31
  taskSchema,
31
- } from "./types.js";
32
+ } from "./types";
33
+
34
+ // Import review tools for side effects (registers subprocess tool handlers)
35
+ import "../review";
36
+
37
+ /** Format byte count for display */
38
+ function formatBytes(bytes: number): string {
39
+ if (bytes < 1024) return `${bytes}B`;
40
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
41
+ return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
42
+ }
32
43
 
33
44
  /** Session context interface */
34
45
  interface SessionContext {
35
46
  getSessionFile: () => string | null;
36
47
  }
37
48
 
49
+ /** Task tool options */
50
+ interface TaskToolOptions {
51
+ /** Set of available tool names (for cross-tool awareness) */
52
+ availableTools?: Set<string>;
53
+ }
54
+
38
55
  // Re-export types and utilities
39
- export { loadBundledAgents as BUNDLED_AGENTS } from "./agents.js";
40
- export { discoverCommands, expandCommand, getCommand } from "./commands.js";
41
- export { discoverAgents, getAgent } from "./discovery.js";
42
- export type { AgentDefinition, AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types.js";
43
- export { taskSchema } from "./types.js";
56
+ export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
57
+ export { discoverCommands, expandCommand, getCommand } from "./commands";
58
+ export { discoverAgents, getAgent } from "./discovery";
59
+ export type { AgentDefinition, AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
60
+ export { taskSchema } from "./types";
44
61
 
45
62
  /**
46
63
  * Build dynamic tool description listing available agents.
@@ -168,8 +185,10 @@ function buildDescription(cwd: string): string {
168
185
  export function createTaskTool(
169
186
  cwd: string,
170
187
  sessionContext?: SessionContext,
188
+ options?: TaskToolOptions,
171
189
  ): AgentTool<typeof taskSchema, TaskToolDetails, Theme> {
172
- // Check if subagents are inhibited (recursion prevention)
190
+ const hasOutputTool = options?.availableTools?.has("output") ?? false;
191
+ // Check if subagents are completely inhibited (legacy recursion prevention)
173
192
  if (process.env[PI_NO_SUBAGENTS_ENV]) {
174
193
  return {
175
194
  name: "task",
@@ -187,6 +206,9 @@ export function createTaskTool(
187
206
  };
188
207
  }
189
208
 
209
+ // Check for same-agent blocking (allows other agent types)
210
+ const blockedAgent = process.env[PI_BLOCKED_AGENT_ENV];
211
+
190
212
  return {
191
213
  name: "task",
192
214
  label: "Task",
@@ -258,7 +280,31 @@ export function createTaskTool(
258
280
  };
259
281
 
260
282
  try {
261
- const tasks = params.tasks;
283
+ let tasks = params.tasks;
284
+ let skippedSelfRecursion = 0;
285
+
286
+ // Filter out blocked agent (self-recursion prevention)
287
+ if (blockedAgent) {
288
+ const blockedTasks = tasks.filter((t) => t.agent === blockedAgent);
289
+ tasks = tasks.filter((t) => t.agent !== blockedAgent);
290
+ skippedSelfRecursion = blockedTasks.length;
291
+
292
+ if (skippedSelfRecursion > 0 && tasks.length === 0) {
293
+ return {
294
+ content: [
295
+ {
296
+ type: "text",
297
+ text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
298
+ },
299
+ ],
300
+ details: {
301
+ projectAgentsDir,
302
+ results: [],
303
+ totalDurationMs: Date.now() - startTime,
304
+ },
305
+ };
306
+ }
307
+ }
262
308
 
263
309
  // Validate all agents exist
264
310
  for (const task of tasks) {
@@ -316,40 +362,42 @@ export function createTaskTool(
316
362
  artifactsDir: effectiveArtifactsDir,
317
363
  signal,
318
364
  onProgress: (progress) => {
319
- progressMap.set(index, progress);
365
+ progressMap.set(index, structuredClone(progress));
320
366
  emitProgress();
321
367
  },
322
368
  });
323
369
  });
324
370
 
325
- // Write artifacts
371
+ // Collect output paths (artifacts already written by executor in real-time)
326
372
  const outputPaths: string[] = [];
327
373
  for (const result of results) {
328
- const fullTask = context ? `${context}\n\n${result.task}` : result.task;
329
- const paths = await writeArtifacts(
330
- effectiveArtifactsDir,
331
- result.agent,
332
- result.index,
333
- fullTask,
334
- result.output,
335
- result.jsonlEvents,
336
- );
337
- outputPaths.push(paths.outputPath);
338
- result.artifactPaths = paths;
374
+ if (result.artifactPaths) {
375
+ outputPaths.push(result.artifactPaths.outputPath);
376
+ }
339
377
  }
340
378
 
341
379
  // Build final output - match plugin format
342
380
  const successCount = results.filter((r) => r.exitCode === 0).length;
343
381
  const totalDuration = Date.now() - startTime;
344
382
 
345
- const summaries = results.map((r, i) => {
383
+ const summaries = results.map((r) => {
346
384
  const status = r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
347
385
  const output = r.output.trim() || r.stderr.trim() || "(no output)";
348
386
  const preview = output.split("\n").slice(0, 5).join("\n");
349
- return `[${r.agent}] ${status} ${outputPaths[i]}\n${preview}`;
387
+ // Include output metadata and ID; include path only if Output tool unavailable (for Read fallback)
388
+ const outputId = `${r.agent}_${r.index}`;
389
+ const meta = r.outputMeta
390
+ ? ` [${r.outputMeta.lineCount} lines, ${formatBytes(r.outputMeta.charCount)}]`
391
+ : "";
392
+ const pathInfo = !hasOutputTool && r.artifactPaths?.outputPath ? ` (${r.artifactPaths.outputPath})` : "";
393
+ return `[${r.agent}] ${status}${meta} → ${outputId}${pathInfo}\n${preview}`;
350
394
  });
351
395
 
352
- const summary = `${successCount}/${results.length} succeeded [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}`;
396
+ const skippedNote =
397
+ skippedSelfRecursion > 0
398
+ ? ` (${skippedSelfRecursion} ${blockedAgent} task${skippedSelfRecursion > 1 ? "s" : ""} skipped - self-recursion blocked)`
399
+ : "";
400
+ const summary = `${successCount}/${results.length} succeeded${skippedNote} [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}`;
353
401
 
354
402
  // Cleanup temp directory if used
355
403
  if (tempArtifactsDir) {
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Model resolution with fuzzy pattern matching.
3
3
  *
4
+ * Returns models in "provider/modelId" format for use with --model flag.
5
+ *
4
6
  * Supports:
5
- * - Exact match: "claude-opus-4-5"
6
- * - Fuzzy match: "opus" → "claude-opus-4-5"
7
+ * - Exact match: "gpt-5.2" → "p-openai/gpt-5.2"
8
+ * - Fuzzy match: "opus" → "p-anthropic/claude-opus-4-5"
7
9
  * - Comma fallback: "gpt, opus" → tries gpt first, then opus
8
10
  * - "default" → undefined (use system default)
9
- * - "pi/default" → configured default model from settings
10
- * - "pi/smol" → configured smol model from settings
11
+ * - "pi/slow" → configured slow model from settings
11
12
  */
12
13
 
13
14
  import { spawnSync } from "node:child_process";
@@ -21,7 +22,7 @@ const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
21
22
  /** Windows shell option for spawn/spawnSync */
22
23
  const PI_SHELL_OPT = process.platform === "win32";
23
24
 
24
- /** Cache for available models */
25
+ /** Cache for available models (provider/modelId format) */
25
26
  let cachedModels: string[] | null = null;
26
27
 
27
28
  /** Cache expiry time (5 minutes) */
@@ -31,6 +32,7 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
31
32
 
32
33
  /**
33
34
  * Get available models from `pi --list-models`.
35
+ * Returns models in "provider/modelId" format.
34
36
  * Caches the result for performance.
35
37
  */
36
38
  export function getAvailableModels(): string[] {
@@ -52,13 +54,14 @@ export function getAvailableModels(): string[] {
52
54
  return cachedModels;
53
55
  }
54
56
 
55
- // Parse output: skip header line, extract model column
57
+ // Parse output: skip header line, extract provider/model
56
58
  const lines = result.stdout.trim().split("\n");
57
59
  cachedModels = lines
58
60
  .slice(1) // Skip header
59
61
  .map((line) => {
60
62
  const parts = line.trim().split(/\s+/);
61
- return parts[1]; // Model name is second column
63
+ // Format: provider/modelId
64
+ return parts[0] && parts[1] ? `${parts[0]}/${parts[1]}` : "";
62
65
  })
63
66
  .filter(Boolean);
64
67
 
@@ -106,23 +109,26 @@ function resolvePiAlias(role: string, availableModels: string[]): string | undef
106
109
  const configured = roles[role] || roles[role.toLowerCase()];
107
110
  if (!configured) return undefined;
108
111
 
109
- // configured is in "provider/modelId" format, extract just the modelId for matching
110
- const slashIdx = configured.indexOf("/");
111
- if (slashIdx <= 0) return undefined;
112
+ // configured is in "provider/modelId" format, find in available models
113
+ return availableModels.find((m) => m.toLowerCase() === configured.toLowerCase());
114
+ }
112
115
 
113
- const modelId = configured.slice(slashIdx + 1);
114
- // Find in available models
115
- return availableModels.find((m) => m.toLowerCase() === modelId.toLowerCase());
116
+ /**
117
+ * Extract model ID from "provider/modelId" format.
118
+ */
119
+ function getModelId(fullModel: string): string {
120
+ const slashIdx = fullModel.indexOf("/");
121
+ return slashIdx > 0 ? fullModel.slice(slashIdx + 1) : fullModel;
116
122
  }
117
123
 
118
124
  /**
119
- * Resolve a fuzzy model pattern to an actual model name.
125
+ * Resolve a fuzzy model pattern to "provider/modelId" format.
120
126
  *
121
127
  * Supports comma-separated patterns (e.g., "gpt, opus") - tries each in order.
122
128
  * Returns undefined if pattern is "default", undefined, or no match found.
123
129
  *
124
130
  * @param pattern - Model pattern to resolve
125
- * @param availableModels - Optional pre-fetched list of available models
131
+ * @param availableModels - Optional pre-fetched list of available models (in provider/modelId format)
126
132
  */
127
133
  export function resolveModelPattern(pattern: string | undefined, availableModels?: string[]): string | undefined {
128
134
  if (!pattern || pattern === "default") {
@@ -150,12 +156,16 @@ export function resolveModelPattern(pattern: string | undefined, availableModels
150
156
  continue; // Role not configured, try next pattern
151
157
  }
152
158
 
153
- // Try exact match first
154
- const exactMatch = models.find((m) => m.toLowerCase() === p.toLowerCase());
155
- if (exactMatch) return exactMatch;
159
+ // Try exact match on full provider/modelId
160
+ const exactFull = models.find((m) => m.toLowerCase() === p.toLowerCase());
161
+ if (exactFull) return exactFull;
162
+
163
+ // Try exact match on model ID only
164
+ const exactId = models.find((m) => getModelId(m).toLowerCase() === p.toLowerCase());
165
+ if (exactId) return exactId;
156
166
 
157
- // Try fuzzy match (substring)
158
- const fuzzyMatch = models.find((m) => m.toLowerCase().includes(p.toLowerCase()));
167
+ // Try fuzzy match on model ID (substring)
168
+ const fuzzyMatch = models.find((m) => getModelId(m).toLowerCase().includes(p.toLowerCase()));
159
169
  if (fuzzyMatch) return fuzzyMatch;
160
170
  }
161
171
 
@@ -2,7 +2,7 @@
2
2
  * Parallel execution with concurrency control.
3
3
  */
4
4
 
5
- import { MAX_CONCURRENCY } from "./types.js";
5
+ import { MAX_CONCURRENCY } from "./types";
6
6
 
7
7
  /**
8
8
  * Execute items with a concurrency limit using a worker pool pattern.