@oh-my-pi/pi-coding-agent 3.20.1 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +107 -8
  2. package/docs/custom-tools.md +3 -3
  3. package/docs/extensions.md +226 -220
  4. package/docs/hooks.md +2 -2
  5. package/docs/sdk.md +50 -53
  6. package/examples/custom-tools/README.md +2 -17
  7. package/examples/extensions/README.md +76 -74
  8. package/examples/extensions/todo.ts +2 -5
  9. package/examples/hooks/custom-compaction.ts +2 -4
  10. package/examples/hooks/handoff.ts +1 -1
  11. package/examples/hooks/qna.ts +1 -1
  12. package/examples/sdk/02-custom-model.ts +1 -1
  13. package/examples/sdk/README.md +7 -11
  14. package/package.json +6 -6
  15. package/src/cli/args.ts +9 -6
  16. package/src/cli/file-processor.ts +1 -1
  17. package/src/cli/list-models.ts +1 -1
  18. package/src/core/agent-session.ts +16 -5
  19. package/src/core/auth-storage.ts +1 -1
  20. package/src/core/compaction/branch-summarization.ts +2 -2
  21. package/src/core/compaction/compaction.ts +2 -2
  22. package/src/core/compaction/utils.ts +1 -1
  23. package/src/core/custom-tools/types.ts +1 -1
  24. package/src/core/custom-tools/wrapper.ts +0 -1
  25. package/src/core/extensions/index.ts +1 -6
  26. package/src/core/extensions/runner.ts +1 -1
  27. package/src/core/extensions/types.ts +1 -1
  28. package/src/core/extensions/wrapper.ts +1 -8
  29. package/src/core/file-mentions.ts +5 -8
  30. package/src/core/hooks/runner.ts +2 -2
  31. package/src/core/hooks/types.ts +1 -1
  32. package/src/core/messages.ts +1 -1
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/model-resolver.ts +1 -1
  35. package/src/core/sdk.ts +64 -105
  36. package/src/core/session-manager.ts +18 -22
  37. package/src/core/settings-manager.ts +66 -1
  38. package/src/core/slash-commands.ts +12 -5
  39. package/src/core/system-prompt.ts +49 -36
  40. package/src/core/title-generator.ts +2 -2
  41. package/src/core/tools/ask.ts +98 -4
  42. package/src/core/tools/bash-interceptor.ts +11 -4
  43. package/src/core/tools/bash.ts +121 -5
  44. package/src/core/tools/context.ts +7 -0
  45. package/src/core/tools/edit-diff.ts +73 -24
  46. package/src/core/tools/edit.ts +221 -34
  47. package/src/core/tools/exa/render.ts +4 -16
  48. package/src/core/tools/find.ts +149 -5
  49. package/src/core/tools/gemini-image.ts +279 -56
  50. package/src/core/tools/git.ts +17 -3
  51. package/src/core/tools/grep.ts +185 -5
  52. package/src/core/tools/index.test.ts +180 -0
  53. package/src/core/tools/index.ts +96 -242
  54. package/src/core/tools/ls.ts +133 -5
  55. package/src/core/tools/lsp/index.ts +32 -29
  56. package/src/core/tools/lsp/render.ts +21 -22
  57. package/src/core/tools/notebook.ts +112 -4
  58. package/src/core/tools/output.ts +175 -15
  59. package/src/core/tools/read.ts +127 -25
  60. package/src/core/tools/render-utils.ts +241 -0
  61. package/src/core/tools/renderers.ts +40 -828
  62. package/src/core/tools/review.ts +26 -25
  63. package/src/core/tools/rulebook.ts +11 -3
  64. package/src/core/tools/task/agents.ts +28 -7
  65. package/src/core/tools/task/discovery.ts +0 -6
  66. package/src/core/tools/task/executor.ts +264 -254
  67. package/src/core/tools/task/index.ts +48 -208
  68. package/src/core/tools/task/render.ts +26 -11
  69. package/src/core/tools/task/types.ts +7 -12
  70. package/src/core/tools/task/worker-protocol.ts +17 -0
  71. package/src/core/tools/task/worker.ts +238 -0
  72. package/src/core/tools/truncate.ts +27 -1
  73. package/src/core/tools/web-fetch.ts +25 -49
  74. package/src/core/tools/web-search/index.ts +132 -46
  75. package/src/core/tools/web-search/providers/anthropic.ts +7 -2
  76. package/src/core/tools/web-search/providers/exa.ts +2 -1
  77. package/src/core/tools/web-search/providers/perplexity.ts +6 -1
  78. package/src/core/tools/web-search/render.ts +6 -4
  79. package/src/core/tools/web-search/types.ts +13 -0
  80. package/src/core/tools/write.ts +96 -14
  81. package/src/core/voice.ts +1 -1
  82. package/src/discovery/helpers.test.ts +1 -1
  83. package/src/index.ts +5 -16
  84. package/src/main.ts +5 -5
  85. package/src/modes/interactive/components/assistant-message.ts +1 -1
  86. package/src/modes/interactive/components/custom-message.ts +1 -1
  87. package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
  88. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  89. package/src/modes/interactive/components/footer.ts +1 -1
  90. package/src/modes/interactive/components/hook-message.ts +1 -1
  91. package/src/modes/interactive/components/model-selector.ts +1 -1
  92. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  93. package/src/modes/interactive/components/settings-defs.ts +49 -0
  94. package/src/modes/interactive/components/status-line.ts +1 -1
  95. package/src/modes/interactive/components/tool-execution.ts +93 -538
  96. package/src/modes/interactive/interactive-mode.ts +19 -7
  97. package/src/modes/interactive/theme/theme.ts +4 -4
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +1 -1
  100. package/src/modes/rpc/rpc-types.ts +1 -1
  101. package/src/prompts/system-prompt.md +4 -0
  102. package/src/prompts/task.md +0 -7
  103. package/src/prompts/tools/gemini-image.md +5 -1
  104. package/src/prompts/tools/output.md +6 -2
  105. package/src/prompts/tools/task.md +68 -0
  106. package/src/prompts/tools/web-fetch.md +1 -0
  107. package/src/prompts/tools/web-search.md +2 -0
  108. package/src/utils/image-convert.ts +8 -2
  109. package/src/utils/image-magick.ts +247 -0
  110. package/src/utils/image-resize.ts +53 -13
  111. package/examples/custom-tools/question/index.ts +0 -84
  112. package/examples/custom-tools/subagent/README.md +0 -172
  113. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  114. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  115. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  116. package/examples/custom-tools/subagent/agents.ts +0 -156
  117. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  118. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  119. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  120. package/examples/custom-tools/subagent/index.ts +0 -1002
  121. package/examples/sdk/05-tools.ts +0 -94
  122. package/examples/sdk/12-full-control.ts +0 -95
  123. package/src/prompts/browser.md +0 -71
@@ -13,9 +13,10 @@
13
13
  * - Session artifacts for debugging
14
14
  */
15
15
 
16
+ import type { Usage } from "@mariozechner/pi-ai";
16
17
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
17
- import type { Usage } from "@oh-my-pi/pi-ai";
18
18
  import type { Theme } from "../../../modes/interactive/theme/theme";
19
+ import taskDescriptionTemplate from "../../../prompts/tools/task.md" with { type: "text" };
19
20
  import { formatDuration } from "../render-utils";
20
21
  import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir } from "./artifacts";
21
22
  import { discoverAgents, getAgent } from "./discovery";
@@ -27,15 +28,13 @@ import {
27
28
  MAX_AGENTS_IN_DESCRIPTION,
28
29
  MAX_CONCURRENCY,
29
30
  MAX_PARALLEL_TASKS,
30
- OMP_BLOCKED_AGENT_ENV,
31
- OMP_NO_SUBAGENTS_ENV,
32
- OMP_SPAWNS_ENV,
33
31
  type TaskToolDetails,
34
32
  taskSchema,
35
33
  } from "./types";
36
34
 
37
- // Import review tools for side effects (registers subprocess tool handlers)
35
+ // Import review tools for side effects (registers subagent tool handlers)
38
36
  import "../review";
37
+ import type { ToolSession } from "..";
39
38
 
40
39
  /** Format byte count for display */
41
40
  function formatBytes(bytes: number): string {
@@ -83,51 +82,6 @@ function addUsageTotals(target: Usage, usage: Partial<Usage>): void {
83
82
  target.cost.total += cost.total;
84
83
  }
85
84
 
86
- function parseSubagentUsage(events: string[] | undefined): Usage | undefined {
87
- if (!events || events.length === 0) return undefined;
88
-
89
- const totals = createUsageTotals();
90
- let hasUsage = false;
91
-
92
- for (const line of events) {
93
- let event: unknown;
94
- try {
95
- event = JSON.parse(line);
96
- } catch {
97
- continue;
98
- }
99
-
100
- if (!event || typeof event !== "object") continue;
101
- const record = event as Record<string, unknown>;
102
- if (record.type !== "message_end") continue;
103
-
104
- const message = record.message;
105
- if (!message || typeof message !== "object") continue;
106
- const msgRecord = message as Record<string, unknown>;
107
- if (msgRecord.role !== "assistant") continue;
108
- if (msgRecord.stopReason === "aborted" || msgRecord.stopReason === "error") continue;
109
-
110
- const usage = msgRecord.usage;
111
- if (!usage || typeof usage !== "object") continue;
112
-
113
- addUsageTotals(totals, usage as Partial<Usage>);
114
- hasUsage = true;
115
- }
116
-
117
- return hasUsage ? totals : undefined;
118
- }
119
-
120
- /** Session context interface */
121
- interface SessionContext {
122
- getSessionFile: () => string | null;
123
- }
124
-
125
- /** Task tool options */
126
- interface TaskToolOptions {
127
- /** Set of available tool names (for cross-tool awareness) */
128
- availableTools?: Set<string>;
129
- }
130
-
131
85
  // Re-export types and utilities
132
86
  export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
133
87
  export { discoverCommands, expandCommand, getCommand } from "./commands";
@@ -141,160 +95,45 @@ export { taskSchema } from "./types";
141
95
  async function buildDescription(cwd: string): Promise<string> {
142
96
  const { agents } = await discoverAgents(cwd);
143
97
 
144
- const lines: string[] = [];
145
-
146
- lines.push("Launch a new agent to handle complex, multi-step tasks autonomously.");
147
- lines.push("");
148
- lines.push(
149
- "The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.",
150
- );
151
- lines.push("");
152
- lines.push("Available agent types and the tools they have access to:");
153
-
98
+ // Build agents list
99
+ const agentLines: string[] = [];
154
100
  for (const agent of agents.slice(0, MAX_AGENTS_IN_DESCRIPTION)) {
155
101
  const tools = agent.tools?.join(", ") || "All tools";
156
- lines.push(`- ${agent.name}: ${agent.description} (Tools: ${tools})`);
102
+ agentLines.push(`- ${agent.name}: ${agent.description} (Tools: ${tools})`);
157
103
  }
158
104
  if (agents.length > MAX_AGENTS_IN_DESCRIPTION) {
159
- lines.push(` ...and ${agents.length - MAX_AGENTS_IN_DESCRIPTION} more agents`);
105
+ agentLines.push(` ...and ${agents.length - MAX_AGENTS_IN_DESCRIPTION} more agents`);
160
106
  }
161
107
 
162
- lines.push("");
163
- lines.push("When NOT to use the Task tool:");
164
- lines.push(
165
- "- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly",
166
- );
167
- lines.push(
168
- '- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly',
169
- );
170
- lines.push(
171
- "- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly",
172
- );
173
- lines.push("- Other tasks that are not related to the agent descriptions above");
174
- lines.push("");
175
- lines.push("");
176
- lines.push("Usage notes:");
177
- lines.push("- Always include a short description of the task in the task parameter");
178
- lines.push("- Launch multiple agents concurrently whenever possible, to maximize performance");
179
- lines.push(
180
- "- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.",
181
- );
182
- lines.push(
183
- "- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your task should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.",
184
- );
185
- lines.push(
186
- "- IMPORTANT: Agent results are intermediate data, not task completions. Use the agent's findings to continue executing the user's request. Do not treat agent reports as 'task complete' signals - they provide context for you to perform the actual work.",
187
- );
188
- lines.push("- The agent's outputs should generally be trusted");
189
- lines.push(
190
- "- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent",
191
- );
192
- lines.push(
193
- "- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.",
194
- );
195
- lines.push("");
196
- lines.push("Parameters:");
197
- lines.push(
198
- `- tasks: Array of {agent, task, description?, model?} - tasks to run in parallel (max ${MAX_PARALLEL_TASKS}, ${MAX_CONCURRENCY} concurrent)`,
199
- );
200
- lines.push(
201
- ' - model: (optional) Override the agent\'s default model with fuzzy matching (e.g., "sonnet", "codex", "5.2"). Supports comma-separated fallbacks: "gpt, opus" tries gpt first, then opus. Use "default" for omp\'s default model',
202
- );
203
- lines.push(
204
- "- context: (optional) Shared context string prepended to all task prompts - use this to avoid repeating instructions",
205
- );
206
- lines.push("");
207
- lines.push("Results are always written to {tempdir}/omp-task-{runId}/task_{agent}_{index}.md");
208
- lines.push("");
209
- lines.push("Example usage:");
210
- lines.push("");
211
- lines.push("<example_agent_descriptions>");
212
- lines.push('"code-reviewer": use this agent after you are done writing a significant piece of code');
213
- lines.push('"explore": use this agent for fast codebase exploration and research');
214
- lines.push("</example_agent_descriptions>");
215
- lines.push("");
216
- lines.push("<example>");
217
- lines.push('user: "Please write a function that checks if a number is prime"');
218
- lines.push("assistant: Sure let me write a function that checks if a number is prime");
219
- lines.push("assistant: I'm going to use the Write tool to write the following code:");
220
- lines.push("<code>");
221
- lines.push("function isPrime(n) {");
222
- lines.push(" if (n <= 1) return false");
223
- lines.push(" for (let i = 2; i * i <= n; i++) {");
224
- lines.push(" if (n % i === 0) return false");
225
- lines.push(" }");
226
- lines.push(" return true");
227
- lines.push("}");
228
- lines.push("</code>");
229
- lines.push("<commentary>");
230
- lines.push(
231
- "Since a significant piece of code was written and the task was completed, now use the code-reviewer agent to review the code",
232
- );
233
- lines.push("</commentary>");
234
- lines.push("assistant: Now let me use the code-reviewer agent to review the code");
235
- lines.push(
236
- 'assistant: Uses the Task tool: { tasks: [{ agent: "code-reviewer", task: "Review the isPrime function" }] }',
237
- );
238
- lines.push("</example>");
239
- lines.push("");
240
- lines.push("<example>");
241
- lines.push('user: "Find all TODO comments in the codebase"');
242
- lines.push("assistant: I'll use multiple explore agents to search different directories in parallel");
243
- lines.push("assistant: Uses the Task tool:");
244
- lines.push("{");
245
- lines.push(' "context": "Find all TODO comments. Return file:line:content format.",');
246
- lines.push(' "tasks": [');
247
- lines.push(' { "agent": "explore", "task": "Search in src/" },');
248
- lines.push(' { "agent": "explore", "task": "Search in lib/" },');
249
- lines.push(' { "agent": "explore", "task": "Search in tests/" }');
250
- lines.push(" ]");
251
- lines.push("}");
252
- lines.push("Results → {tempdir}/omp-task-{runId}/task_explore_*.md");
253
- lines.push("</example>");
254
-
255
- return lines.join("\n");
108
+ // Fill template placeholders
109
+ return taskDescriptionTemplate
110
+ .replace("{{AGENTS_LIST}}", agentLines.join("\n"))
111
+ .replace("{{MAX_PARALLEL_TASKS}}", String(MAX_PARALLEL_TASKS))
112
+ .replace("{{MAX_CONCURRENCY}}", String(MAX_CONCURRENCY));
256
113
  }
257
114
 
258
115
  /**
259
- * Create the task tool configured for a specific working directory.
116
+ * Create the task tool configured for a specific session.
260
117
  */
261
118
  export async function createTaskTool(
262
- cwd: string,
263
- sessionContext?: SessionContext,
264
- options?: TaskToolOptions,
119
+ session: ToolSession,
265
120
  ): Promise<AgentTool<typeof taskSchema, TaskToolDetails, Theme>> {
266
- const hasOutputTool = options?.availableTools?.has("output") ?? false;
267
- // Check if subagents are completely inhibited (legacy recursion prevention)
268
- if (process.env[OMP_NO_SUBAGENTS_ENV]) {
269
- return {
270
- name: "task",
271
- label: "Task",
272
- description: "Sub-agents disabled (recursion prevention)",
273
- parameters: taskSchema,
274
- execute: async () => ({
275
- content: [{ type: "text", text: "Sub-agents are disabled for this agent (recursion prevention)." }],
276
- details: {
277
- projectAgentsDir: null,
278
- results: [],
279
- totalDurationMs: 0,
280
- },
281
- }),
282
- };
283
- }
284
-
285
121
  // Check for same-agent blocking (allows other agent types)
286
- const blockedAgent = process.env[OMP_BLOCKED_AGENT_ENV];
122
+ const blockedAgent = process.env.OMP_BLOCKED_AGENT;
123
+
124
+ // Build description upfront
125
+ const description = await buildDescription(session.cwd);
287
126
 
288
127
  return {
289
128
  name: "task",
290
129
  label: "Task",
291
- description: await buildDescription(cwd),
130
+ description,
292
131
  parameters: taskSchema,
293
132
  renderCall,
294
133
  renderResult,
295
134
  execute: async (_toolCallId, params, signal, onUpdate) => {
296
135
  const startTime = Date.now();
297
- const { agents, projectAgentsDir } = await discoverAgents(cwd);
136
+ const { agents, projectAgentsDir } = await discoverAgents(session.cwd);
298
137
  const context = params.context;
299
138
 
300
139
  // Handle empty or missing tasks
@@ -333,7 +172,7 @@ export async function createTaskTool(
333
172
  }
334
173
 
335
174
  // Derive artifacts directory
336
- const sessionFile = sessionContext?.getSessionFile() ?? null;
175
+ const sessionFile = session.getSessionFile();
337
176
  const artifactsDir = sessionFile ? getArtifactsDir(sessionFile) : null;
338
177
  const tempArtifactsDir = artifactsDir ? null : createTempArtifactsDir();
339
178
  const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
@@ -398,13 +237,12 @@ export async function createTaskTool(
398
237
  }
399
238
 
400
239
  // Check spawn restrictions from parent
401
- const parentSpawns = process.env[OMP_SPAWNS_ENV];
240
+ const parentSpawns = session.getSessionSpawns() ?? "*";
241
+ const allowedSpawns = parentSpawns.split(",").map((s) => s.trim());
402
242
  const isSpawnAllowed = (agentName: string): boolean => {
403
- if (parentSpawns === undefined) return true; // Root = allow all
404
243
  if (parentSpawns === "") return false; // Empty = deny all
405
244
  if (parentSpawns === "*") return true; // Wildcard = allow all
406
- const allowed = new Set(parentSpawns.split(",").map((s) => s.trim()));
407
- return allowed.has(agentName);
245
+ return allowedSpawns.includes(agentName);
408
246
  };
409
247
 
410
248
  for (const task of tasks) {
@@ -453,7 +291,7 @@ export async function createTaskTool(
453
291
  const results = await mapWithConcurrencyLimit(tasksWithContext, MAX_CONCURRENCY, async (task, index) => {
454
292
  const agent = getAgent(agents, task.agent)!;
455
293
  return runSubprocess({
456
- cwd,
294
+ cwd: session.cwd,
457
295
  agent,
458
296
  task: task.task,
459
297
  description: task.description,
@@ -464,6 +302,7 @@ export async function createTaskTool(
464
302
  persistArtifacts: !!artifactsDir,
465
303
  artifactsDir: effectiveArtifactsDir,
466
304
  signal,
305
+ eventBus: undefined,
467
306
  onProgress: (progress) => {
468
307
  progressMap.set(index, structuredClone(progress));
469
308
  emitProgress();
@@ -471,48 +310,50 @@ export async function createTaskTool(
471
310
  });
472
311
  });
473
312
 
313
+ // Aggregate usage from executor results (already accumulated incrementally)
474
314
  const aggregatedUsage = createUsageTotals();
475
315
  let hasAggregatedUsage = false;
476
- const resultsWithUsage = results.map((result) => {
477
- const usage = parseSubagentUsage(result.jsonlEvents);
478
- if (usage) {
479
- addUsageTotals(aggregatedUsage, usage);
316
+ for (const result of results) {
317
+ if (result.usage) {
318
+ addUsageTotals(aggregatedUsage, result.usage);
480
319
  hasAggregatedUsage = true;
481
- return { ...result, usage };
482
320
  }
483
- return result;
484
- });
321
+ }
485
322
 
486
323
  // Collect output paths (artifacts already written by executor in real-time)
487
324
  const outputPaths: string[] = [];
488
- for (const result of resultsWithUsage) {
325
+ for (const result of results) {
489
326
  if (result.artifactPaths) {
490
327
  outputPaths.push(result.artifactPaths.outputPath);
491
328
  }
492
329
  }
493
330
 
494
331
  // Build final output - match plugin format
495
- const successCount = resultsWithUsage.filter((r) => r.exitCode === 0).length;
332
+ const successCount = results.filter((r) => r.exitCode === 0).length;
496
333
  const totalDuration = Date.now() - startTime;
497
334
 
498
- const summaries = resultsWithUsage.map((r) => {
335
+ const summaries = results.map((r) => {
499
336
  const status = r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
500
337
  const output = r.output.trim() || r.stderr.trim() || "(no output)";
501
338
  const preview = output.split("\n").slice(0, 5).join("\n");
502
- // Include output metadata and ID; include path only if Output tool unavailable (for Read fallback)
339
+ // Include output metadata and ID
503
340
  const outputId = `${r.agent}_${r.index}`;
504
341
  const meta = r.outputMeta
505
342
  ? ` [${r.outputMeta.lineCount} lines, ${formatBytes(r.outputMeta.charCount)}]`
506
343
  : "";
507
- const pathInfo = !hasOutputTool && r.artifactPaths?.outputPath ? ` (${r.artifactPaths.outputPath})` : "";
508
- return `[${r.agent}] ${status}${meta} ${outputId}${pathInfo}\n${preview}`;
344
+ return `[${r.agent}] ${status}${meta} ${outputId}\n${preview}`;
509
345
  });
510
346
 
511
347
  const skippedNote =
512
348
  skippedSelfRecursion > 0
513
349
  ? ` (${skippedSelfRecursion} ${blockedAgent} task${skippedSelfRecursion > 1 ? "s" : ""} skipped - self-recursion blocked)`
514
350
  : "";
515
- const summary = `${successCount}/${resultsWithUsage.length} succeeded${skippedNote} [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}`;
351
+ const outputIds = results.map((r) => `${r.agent}_${r.index}`);
352
+ const outputHint =
353
+ outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : "";
354
+ const summary = `${successCount}/${results.length} succeeded${skippedNote} [${formatDuration(
355
+ totalDuration,
356
+ )}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}`;
516
357
 
517
358
  // Cleanup temp directory if used
518
359
  if (tempArtifactsDir) {
@@ -523,7 +364,7 @@ export async function createTaskTool(
523
364
  content: [{ type: "text", text: summary }],
524
365
  details: {
525
366
  projectAgentsDir,
526
- results: resultsWithUsage,
367
+ results: results,
527
368
  totalDurationMs: totalDuration,
528
369
  usage: hasAggregatedUsage ? aggregatedUsage : undefined,
529
370
  outputPaths,
@@ -548,16 +389,15 @@ export async function createTaskTool(
548
389
  };
549
390
  }
550
391
 
551
- // Default task tool using process.cwd() - returns a placeholder sync tool
552
- // Real implementations should use createTaskTool() which properly initializes the tool
392
+ // Default task tool - returns a placeholder tool
393
+ // Real implementations should use createTaskTool(session) to initialize the tool
553
394
  export const taskTool: AgentTool<typeof taskSchema, TaskToolDetails, Theme> = {
554
395
  name: "task",
555
396
  label: "Task",
556
- description:
557
- "Launch a new agent to handle complex, multi-step tasks autonomously. (Agent discovery pending - use createTaskTool for full functionality)",
397
+ description: "Launch a new agent to handle complex, multi-step tasks autonomously.",
558
398
  parameters: taskSchema,
559
399
  execute: async () => ({
560
- content: [{ type: "text", text: "Task tool not properly initialized. Use createTaskTool(cwd) instead." }],
400
+ content: [{ type: "text", text: "Task tool not properly initialized. Use createTaskTool(session) instead." }],
561
401
  details: {
562
402
  projectAgentsDir: null,
563
403
  results: [],
@@ -181,19 +181,29 @@ function renderAgentProgress(
181
181
 
182
182
  lines.push(statusLine);
183
183
 
184
- // Current tool (if running)
185
- if (progress.status === "running" && progress.currentTool) {
186
- let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
187
- if (progress.currentToolArgs) {
188
- toolLine += `: ${theme.fg("dim", truncate(progress.currentToolArgs, 40, theme.format.ellipsis))}`;
189
- }
190
- if (progress.currentToolStartMs) {
191
- const elapsed = Date.now() - progress.currentToolStartMs;
192
- if (elapsed > 5000) {
193
- toolLine += `${theme.sep.dot}${theme.fg("warning", formatDuration(elapsed))}`;
184
+ // Current tool (if running) or most recent completed tool
185
+ if (progress.status === "running") {
186
+ if (progress.currentTool) {
187
+ let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
188
+ if (progress.currentToolArgs) {
189
+ toolLine += `: ${theme.fg("dim", truncate(progress.currentToolArgs, 40, theme.format.ellipsis))}`;
190
+ }
191
+ if (progress.currentToolStartMs) {
192
+ const elapsed = Date.now() - progress.currentToolStartMs;
193
+ if (elapsed > 5000) {
194
+ toolLine += `${theme.sep.dot}${theme.fg("warning", formatDuration(elapsed))}`;
195
+ }
194
196
  }
197
+ lines.push(toolLine);
198
+ } else if (progress.recentTools.length > 0) {
199
+ // Show most recent completed tool when idle between tools
200
+ const recent = progress.recentTools[0];
201
+ let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", recent.tool)}`;
202
+ if (recent.args) {
203
+ toolLine += `: ${theme.fg("dim", truncate(recent.args, 40, theme.format.ellipsis))}`;
204
+ }
205
+ lines.push(toolLine);
195
206
  }
196
- lines.push(toolLine);
197
207
  }
198
208
 
199
209
  // Render extracted tool data inline (e.g., review findings)
@@ -475,3 +485,8 @@ export function renderResult(
475
485
 
476
486
  return new Text(lines.join("\n"), 0, 0);
477
487
  }
488
+
489
+ export const taskToolRenderer = {
490
+ renderCall,
491
+ renderResult,
492
+ };
@@ -1,4 +1,4 @@
1
- import type { Usage } from "@oh-my-pi/pi-ai";
1
+ import type { Usage } from "@mariozechner/pi-ai";
2
2
  import { type Static, Type } from "@sinclair/typebox";
3
3
 
4
4
  /** Source of an agent definition */
@@ -19,14 +19,11 @@ export const MAX_OUTPUT_LINES = 5000;
19
19
  /** Maximum agents to show in description */
20
20
  export const MAX_AGENTS_IN_DESCRIPTION = 10;
21
21
 
22
- /** Environment variable to inhibit subagent spawning (legacy, still checked for backwards compat) */
23
- export const OMP_NO_SUBAGENTS_ENV = "OMP_NO_SUBAGENTS";
22
+ /** EventBus channel for raw subagent events */
23
+ export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
24
24
 
25
- /** Environment variable containing blocked agent name (self-recursion prevention) */
26
- export const OMP_BLOCKED_AGENT_ENV = "OMP_BLOCKED_AGENT";
27
-
28
- /** Environment variable containing allowed spawn list (propagated to subprocesses) */
29
- export const OMP_SPAWNS_ENV = "OMP_SPAWNS";
25
+ /** EventBus channel for aggregated subagent progress */
26
+ export const TASK_SUBAGENT_PROGRESS_CHANNEL = "task:subagent:progress";
30
27
 
31
28
  /** Single task item for parallel execution */
32
29
  export const taskItemSchema = Type.Object({
@@ -81,7 +78,6 @@ export interface AgentDefinition {
81
78
  tools?: string[];
82
79
  spawns?: string[] | "*";
83
80
  model?: string;
84
- recursive?: boolean;
85
81
  source: AgentSource;
86
82
  filePath?: string;
87
83
  }
@@ -123,10 +119,9 @@ export interface SingleResult {
123
119
  modelOverride?: string;
124
120
  error?: string;
125
121
  aborted?: boolean;
126
- jsonlEvents?: string[];
127
- artifactPaths?: { inputPath: string; outputPath: string; jsonlPath?: string };
128
- /** Aggregated usage from the subprocess, if available. */
122
+ /** Aggregated usage from the subprocess, accumulated incrementally from message_end events. */
129
123
  usage?: Usage;
124
+ artifactPaths?: { inputPath: string; outputPath: string; jsonlPath?: string };
130
125
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
131
126
  extractedToolData?: Record<string, unknown[]>;
132
127
  /** Output metadata for Output tool integration */
@@ -0,0 +1,17 @@
1
+ import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
2
+
3
+ export interface SubagentWorkerStartPayload {
4
+ cwd: string;
5
+ task: string;
6
+ systemPrompt: string;
7
+ model?: string;
8
+ toolNames?: string[];
9
+ sessionFile?: string | null;
10
+ spawnsEnv?: string;
11
+ }
12
+
13
+ export type SubagentWorkerRequest = { type: "start"; payload: SubagentWorkerStartPayload } | { type: "abort" };
14
+
15
+ export type SubagentWorkerResponse =
16
+ | { type: "event"; event: AgentEvent }
17
+ | { type: "done"; exitCode: number; durationMs: number; error?: string; aborted?: boolean };