@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.
- package/CHANGELOG.md +86 -0
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +10 -9
- package/src/bun-imports.d.ts +16 -0
- package/src/cli/args.ts +5 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli/update-cli.ts +273 -0
- package/src/cli.ts +1 -1
- package/src/config.ts +23 -75
- package/src/core/agent-session.ts +158 -16
- package/src/core/auth-storage.ts +2 -3
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +38 -123
- package/src/core/export-html/template.css +0 -7
- package/src/core/export-html/template.html +3 -4
- package/src/core/export-html/template.macro.ts +24 -0
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +2 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +77 -35
- package/src/core/session-manager.ts +6 -6
- package/src/core/settings-manager.ts +16 -3
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/bash.ts +32 -155
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +3 -3
- package/src/core/tools/edit.ts +18 -5
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +3 -3
- package/src/core/tools/index.ts +48 -34
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +161 -90
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +15 -13
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +22 -38
- package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
- package/src/core/tools/task/commands.ts +31 -10
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +30 -20
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +4 -4
- package/src/index.ts +29 -18
- package/src/main.ts +50 -33
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +281 -59
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +4 -4
- package/src/modes/interactive/components/settings-defs.ts +1 -1
- package/src/modes/interactive/components/settings-selector.ts +5 -5
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +26 -8
- package/src/modes/interactive/components/tree-selector.ts +3 -3
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +86 -42
- package/src/modes/interactive/theme/theme.ts +15 -17
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +22 -12
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- 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
|
|
17
|
-
import type { AgentDefinition, AgentSource } from "./types
|
|
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 {
|
|
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
|
-
|
|
21
|
+
PI_BLOCKED_AGENT_ENV,
|
|
20
22
|
type SingleResult,
|
|
21
|
-
} from "./types
|
|
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 (
|
|
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[
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
397
|
+
// Wait for readline to finish BEFORE resolving
|
|
320
398
|
const exitCode = await new Promise<number>((resolve) => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir
|
|
19
|
-
import { discoverAgents, getAgent } from "./discovery
|
|
20
|
-
import { runSubprocess } from "./executor
|
|
21
|
-
import { mapWithConcurrencyLimit } from "./parallel
|
|
22
|
-
import { formatDuration, renderCall, renderResult } from "./render
|
|
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
|
|
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
|
|
40
|
-
export { discoverCommands, expandCommand, getCommand } from "./commands
|
|
41
|
-
export { discoverAgents, getAgent } from "./discovery
|
|
42
|
-
export type { AgentDefinition, AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types
|
|
43
|
-
export { taskSchema } from "./types
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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/
|
|
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
|
|
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
|
-
|
|
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,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
// configured is in "provider/modelId" format, find in available models
|
|
113
|
+
return availableModels.find((m) => m.toLowerCase() === configured.toLowerCase());
|
|
114
|
+
}
|
|
112
115
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
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
|
|