@dreki-gg/pi-subagent 0.6.0 → 0.8.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.
- package/.fallowrc.json +6 -0
- package/CHANGELOG.md +37 -0
- package/README.md +82 -70
- package/docs/orchestration-principles.md +132 -0
- package/docs/plans/01_structured-handoffs-and-synthesis.plan.md +376 -0
- package/docs/plans/02_clean-context-review-loop.plan.md +240 -0
- package/docs/plans/03_parallel-recon-single-writer-docs.plan.md +199 -0
- package/docs/plans/04_manager-workflow.plan.md +248 -0
- package/docs/plans/05_advisor-routing.plan.md +244 -0
- package/extensions/subagent/agent-result-utils.ts +12 -0
- package/extensions/subagent/agent-runner-types.ts +23 -0
- package/extensions/subagent/agent-runner.ts +90 -0
- package/extensions/subagent/agents.ts +31 -77
- package/extensions/subagent/handoffs.ts +273 -0
- package/extensions/subagent/index.ts +359 -581
- package/extensions/subagent/run-agent-args.ts +90 -0
- package/extensions/subagent/{delegate-executor.ts → spawn-utils.ts} +64 -87
- package/extensions/subagent/synthesis.ts +1 -51
- package/package.json +7 -10
- package/prompts/advisor.md +37 -0
- package/prompts/bug-prover.md +42 -0
- package/{agents → prompts}/docs-scout.md +2 -2
- package/prompts/manager.md +52 -0
- package/{agents → prompts}/planner.md +16 -1
- package/prompts/reviewer.md +93 -0
- package/{agents → prompts}/scout.md +5 -1
- package/prompts/validator.md +42 -0
- package/prompts/worker.md +61 -0
- package/skills/spawn-subagents/SKILL.md +80 -8
- package/skills/write-an-agent/SKILL.md +5 -5
- package/agents/reviewer.md +0 -37
- package/agents/worker.md +0 -26
- package/extensions/subagent/delegate-args.ts +0 -145
- package/extensions/subagent/delegate-types.ts +0 -62
- package/extensions/subagent/delegate-ui.ts +0 -132
- package/extensions/subagent/workflows.ts +0 -150
- package/prompts/implement-and-review.md +0 -10
- package/prompts/implement.md +0 -10
- package/prompts/scout-and-plan.md +0 -9
- /package/{agents → prompts}/ux-designer.md +0 -0
|
@@ -12,61 +12,36 @@
|
|
|
12
12
|
* Uses JSON mode to capture structured output from subagents.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { spawn } from 'node:child_process';
|
|
16
|
-
import { existsSync } from 'node:fs';
|
|
17
|
-
import * as fs from 'node:fs';
|
|
18
|
-
import { copyFile, mkdir, readdir, unlink } from 'node:fs/promises';
|
|
19
15
|
import * as os from 'node:os';
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import type { Message } from '@mariozechner/pi-ai';
|
|
24
|
-
import { StringEnum } from '@mariozechner/pi-ai';
|
|
16
|
+
import type { AgentToolResult } from '@earendil-works/pi-agent-core';
|
|
17
|
+
import type { Message } from '@earendil-works/pi-ai';
|
|
18
|
+
import { StringEnum } from '@earendil-works/pi-ai';
|
|
25
19
|
import {
|
|
26
20
|
type ExtensionAPI,
|
|
27
21
|
type ExtensionCommandContext,
|
|
22
|
+
AuthStorage,
|
|
28
23
|
DefaultPackageManager,
|
|
29
24
|
getAgentDir,
|
|
30
25
|
getMarkdownTheme,
|
|
26
|
+
ModelRegistry,
|
|
31
27
|
SettingsManager,
|
|
32
|
-
|
|
33
|
-
} from '@
|
|
34
|
-
import type
|
|
35
|
-
import {
|
|
36
|
-
import { Type } from '@sinclair/typebox';
|
|
28
|
+
} from '@earendil-works/pi-coding-agent';
|
|
29
|
+
import type { ResolvedPaths } from '@earendil-works/pi-coding-agent';
|
|
30
|
+
import { Box, Container, Markdown, Spacer, Text, type AutocompleteItem } from '@earendil-works/pi-tui';
|
|
31
|
+
import { Type } from 'typebox';
|
|
37
32
|
import {
|
|
38
33
|
type AgentConfig,
|
|
39
34
|
type AgentScope,
|
|
40
35
|
type AgentSource,
|
|
41
|
-
bundledAgentsDir,
|
|
42
36
|
discoverAgents,
|
|
43
|
-
discoverAgentsWithPackages,
|
|
44
37
|
} from './agents.js';
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
} from './
|
|
51
|
-
import {
|
|
52
|
-
import { buildSynthesisPrompt, extractRecentConversation } from './synthesis.js';
|
|
53
|
-
import type {
|
|
54
|
-
AgentResult,
|
|
55
|
-
DelegateState,
|
|
56
|
-
PhaseDefinition,
|
|
57
|
-
PhaseResult,
|
|
58
|
-
UsageStats as DelegateUsageStats,
|
|
59
|
-
} from './delegate-types.js';
|
|
60
|
-
import {
|
|
61
|
-
aggregateUsage,
|
|
62
|
-
confirmPlan,
|
|
63
|
-
confirmSynthesis,
|
|
64
|
-
formatFullSummary,
|
|
65
|
-
formatPhaseHeader,
|
|
66
|
-
getFinalText,
|
|
67
|
-
pickWorkflow,
|
|
68
|
-
} from './delegate-ui.js';
|
|
69
|
-
import { getWorkflow, suggestWorkflow } from './workflows.js';
|
|
38
|
+
import { formatRunAgentUsage, parseRunAgentArgs } from './run-agent-args.js';
|
|
39
|
+
import { runAgent } from './agent-runner.js';
|
|
40
|
+
import { extractRecentConversation } from './synthesis.js';
|
|
41
|
+
import type { AgentResult } from './agent-runner-types.js';
|
|
42
|
+
import { getFinalText } from './agent-result-utils.js';
|
|
43
|
+
import { buildHandoffFromResult, renderHandoffForPrompt } from './handoffs.js';
|
|
44
|
+
import { emptyUsage, spawnPiAgent } from './spawn-utils.js';
|
|
70
45
|
|
|
71
46
|
const MAX_PARALLEL_TASKS = 8;
|
|
72
47
|
const MAX_CONCURRENCY = 4;
|
|
@@ -218,7 +193,7 @@ interface SingleResult {
|
|
|
218
193
|
interface SubagentDetails {
|
|
219
194
|
mode: 'single' | 'parallel' | 'chain';
|
|
220
195
|
agentScope: AgentScope;
|
|
221
|
-
|
|
196
|
+
projectPromptsDir: string | null;
|
|
222
197
|
results: SingleResult[];
|
|
223
198
|
}
|
|
224
199
|
|
|
@@ -272,39 +247,6 @@ async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
|
272
247
|
return results;
|
|
273
248
|
}
|
|
274
249
|
|
|
275
|
-
async function writePromptToTempFile(
|
|
276
|
-
agentName: string,
|
|
277
|
-
prompt: string,
|
|
278
|
-
): Promise<{ dir: string; filePath: string }> {
|
|
279
|
-
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pi-subagent-'));
|
|
280
|
-
const safeName = agentName.replace(/[^\w.-]+/g, '_');
|
|
281
|
-
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
282
|
-
await withFileMutationQueue(filePath, async () => {
|
|
283
|
-
await fs.promises.writeFile(filePath, prompt, { encoding: 'utf-8', mode: 0o600 });
|
|
284
|
-
});
|
|
285
|
-
return { dir: tmpDir, filePath };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
289
|
-
const currentScript = process.argv[1];
|
|
290
|
-
|
|
291
|
-
// Bun standalone binaries set argv[1] to a virtual FS path (e.g. /$bunfs/root/pi)
|
|
292
|
-
// that only resolves inside the running Bun process. Skip it — the binary IS the entry point.
|
|
293
|
-
const isBunVirtualPath = currentScript?.startsWith('/$bunfs/');
|
|
294
|
-
|
|
295
|
-
if (currentScript && !isBunVirtualPath && fs.existsSync(currentScript)) {
|
|
296
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const execName = path.basename(process.execPath).toLowerCase();
|
|
300
|
-
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
301
|
-
if (!isGenericRuntime) {
|
|
302
|
-
// Standalone binary (e.g. pi compiled with Bun) — invoke directly
|
|
303
|
-
return { command: process.execPath, args };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return { command: 'pi', args };
|
|
307
|
-
}
|
|
308
250
|
|
|
309
251
|
type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
310
252
|
|
|
@@ -314,6 +256,8 @@ async function runSingleAgent(
|
|
|
314
256
|
agentName: string,
|
|
315
257
|
task: string,
|
|
316
258
|
cwd: string | undefined,
|
|
259
|
+
modelOverride: string | undefined,
|
|
260
|
+
thinkingOverride: string | undefined,
|
|
317
261
|
step: number | undefined,
|
|
318
262
|
signal: AbortSignal | undefined,
|
|
319
263
|
onUpdate: OnUpdateCallback | undefined,
|
|
@@ -330,26 +274,13 @@ async function runSingleAgent(
|
|
|
330
274
|
exitCode: 1,
|
|
331
275
|
messages: [],
|
|
332
276
|
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
333
|
-
usage:
|
|
334
|
-
input: 0,
|
|
335
|
-
output: 0,
|
|
336
|
-
cacheRead: 0,
|
|
337
|
-
cacheWrite: 0,
|
|
338
|
-
cost: 0,
|
|
339
|
-
contextTokens: 0,
|
|
340
|
-
turns: 0,
|
|
341
|
-
},
|
|
277
|
+
usage: emptyUsage(),
|
|
342
278
|
step,
|
|
343
279
|
};
|
|
344
280
|
}
|
|
345
281
|
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
if (agent.thinking) args.push('--thinking', agent.thinking);
|
|
349
|
-
if (agent.tools && agent.tools.length > 0) args.push('--tools', agent.tools.join(','));
|
|
350
|
-
|
|
351
|
-
let tmpPromptDir: string | null = null;
|
|
352
|
-
let tmpPromptPath: string | null = null;
|
|
282
|
+
const selectedModel = modelOverride ?? agent.model;
|
|
283
|
+
const selectedThinking = thinkingOverride ?? agent.thinking;
|
|
353
284
|
|
|
354
285
|
const currentResult: SingleResult = {
|
|
355
286
|
agent: agentName,
|
|
@@ -358,16 +289,8 @@ async function runSingleAgent(
|
|
|
358
289
|
exitCode: 0,
|
|
359
290
|
messages: [],
|
|
360
291
|
stderr: '',
|
|
361
|
-
usage:
|
|
362
|
-
|
|
363
|
-
output: 0,
|
|
364
|
-
cacheRead: 0,
|
|
365
|
-
cacheWrite: 0,
|
|
366
|
-
cost: 0,
|
|
367
|
-
contextTokens: 0,
|
|
368
|
-
turns: 0,
|
|
369
|
-
},
|
|
370
|
-
model: agent.model,
|
|
292
|
+
usage: emptyUsage(),
|
|
293
|
+
model: selectedModel,
|
|
371
294
|
step,
|
|
372
295
|
};
|
|
373
296
|
|
|
@@ -380,124 +303,58 @@ async function runSingleAgent(
|
|
|
380
303
|
}
|
|
381
304
|
};
|
|
382
305
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
let event: any;
|
|
406
|
-
try {
|
|
407
|
-
event = JSON.parse(line);
|
|
408
|
-
} catch {
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (event.type === 'message_end' && event.message) {
|
|
413
|
-
const msg = event.message as Message;
|
|
414
|
-
currentResult.messages.push(msg);
|
|
415
|
-
|
|
416
|
-
if (msg.role === 'assistant') {
|
|
417
|
-
currentResult.usage.turns++;
|
|
418
|
-
const usage = msg.usage;
|
|
419
|
-
if (usage) {
|
|
420
|
-
currentResult.usage.input += usage.input || 0;
|
|
421
|
-
currentResult.usage.output += usage.output || 0;
|
|
422
|
-
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
423
|
-
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
424
|
-
currentResult.usage.cost += usage.cost?.total || 0;
|
|
425
|
-
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
426
|
-
}
|
|
427
|
-
if (!currentResult.model && msg.model) currentResult.model = msg.model;
|
|
428
|
-
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
429
|
-
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
|
430
|
-
}
|
|
431
|
-
emitUpdate();
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (event.type === 'tool_result_end' && event.message) {
|
|
435
|
-
currentResult.messages.push(event.message as Message);
|
|
436
|
-
emitUpdate();
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
proc.stdout.on('data', (data) => {
|
|
441
|
-
buffer += data.toString();
|
|
442
|
-
const lines = buffer.split('\n');
|
|
443
|
-
buffer = lines.pop() || '';
|
|
444
|
-
for (const line of lines) processLine(line);
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
proc.stderr.on('data', (data) => {
|
|
448
|
-
currentResult.stderr += data.toString();
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
proc.on('close', (code) => {
|
|
452
|
-
if (buffer.trim()) processLine(buffer);
|
|
453
|
-
resolve(code ?? 0);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
proc.on('error', () => {
|
|
457
|
-
resolve(1);
|
|
458
|
-
});
|
|
306
|
+
const spawnResult = await spawnPiAgent({
|
|
307
|
+
cwd: cwd ?? defaultCwd,
|
|
308
|
+
agentName: agent.name,
|
|
309
|
+
task,
|
|
310
|
+
systemPrompt: agent.systemPrompt,
|
|
311
|
+
model: selectedModel,
|
|
312
|
+
thinking: selectedThinking,
|
|
313
|
+
tools: agent.tools,
|
|
314
|
+
signal,
|
|
315
|
+
onMessage: (msg) => {
|
|
316
|
+
currentResult.messages = spawnResult.messages;
|
|
317
|
+
currentResult.usage = spawnResult.usage;
|
|
318
|
+
currentResult.model = spawnResult.model ?? currentResult.model;
|
|
319
|
+
currentResult.stopReason = spawnResult.stopReason;
|
|
320
|
+
currentResult.errorMessage = spawnResult.errorMessage;
|
|
321
|
+
emitUpdate();
|
|
322
|
+
},
|
|
323
|
+
onToolResult: () => {
|
|
324
|
+
currentResult.messages = spawnResult.messages;
|
|
325
|
+
emitUpdate();
|
|
326
|
+
},
|
|
327
|
+
});
|
|
459
328
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
};
|
|
468
|
-
if (signal.aborted) killProc();
|
|
469
|
-
else signal.addEventListener('abort', killProc, { once: true });
|
|
470
|
-
}
|
|
471
|
-
});
|
|
329
|
+
currentResult.exitCode = spawnResult.exitCode;
|
|
330
|
+
currentResult.messages = spawnResult.messages;
|
|
331
|
+
currentResult.stderr = spawnResult.stderr;
|
|
332
|
+
currentResult.usage = spawnResult.usage;
|
|
333
|
+
currentResult.model = spawnResult.model ?? currentResult.model;
|
|
334
|
+
currentResult.stopReason = spawnResult.stopReason;
|
|
335
|
+
currentResult.errorMessage = spawnResult.errorMessage;
|
|
472
336
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return currentResult;
|
|
476
|
-
} finally {
|
|
477
|
-
if (tmpPromptPath)
|
|
478
|
-
try {
|
|
479
|
-
fs.unlinkSync(tmpPromptPath);
|
|
480
|
-
} catch {
|
|
481
|
-
/* ignore */
|
|
482
|
-
}
|
|
483
|
-
if (tmpPromptDir)
|
|
484
|
-
try {
|
|
485
|
-
fs.rmdirSync(tmpPromptDir);
|
|
486
|
-
} catch {
|
|
487
|
-
/* ignore */
|
|
488
|
-
}
|
|
489
|
-
}
|
|
337
|
+
if (spawnResult.wasAborted) throw new Error('Subagent was aborted');
|
|
338
|
+
return currentResult;
|
|
490
339
|
}
|
|
491
340
|
|
|
492
341
|
const TaskItem = Type.Object({
|
|
493
342
|
agent: Type.String({ description: 'Name of the agent to invoke' }),
|
|
494
343
|
task: Type.String({ description: 'Task to delegate to the agent' }),
|
|
344
|
+
model: Type.Optional(Type.String({ description: 'Optional model override for this task' })),
|
|
345
|
+
thinking: Type.Optional(
|
|
346
|
+
Type.String({ description: 'Optional reasoning level override for this task' }),
|
|
347
|
+
),
|
|
495
348
|
cwd: Type.Optional(Type.String({ description: 'Working directory for the agent process' })),
|
|
496
349
|
});
|
|
497
350
|
|
|
498
351
|
const ChainItem = Type.Object({
|
|
499
352
|
agent: Type.String({ description: 'Name of the agent to invoke' }),
|
|
500
353
|
task: Type.String({ description: 'Task with optional {previous} placeholder for prior output' }),
|
|
354
|
+
model: Type.Optional(Type.String({ description: 'Optional model override for this step' })),
|
|
355
|
+
thinking: Type.Optional(
|
|
356
|
+
Type.String({ description: 'Optional reasoning level override for this step' }),
|
|
357
|
+
),
|
|
501
358
|
cwd: Type.Optional(Type.String({ description: 'Working directory for the agent process' })),
|
|
502
359
|
});
|
|
503
360
|
|
|
@@ -512,11 +369,17 @@ const SubagentParams = Type.Object({
|
|
|
512
369
|
Type.String({ description: 'Name of the agent to invoke (for single mode)' }),
|
|
513
370
|
),
|
|
514
371
|
task: Type.Optional(Type.String({ description: 'Task to delegate (for single mode)' })),
|
|
372
|
+
model: Type.Optional(
|
|
373
|
+
Type.String({ description: 'Optional default model override for the run or all steps/tasks' }),
|
|
374
|
+
),
|
|
375
|
+
thinking: Type.Optional(
|
|
376
|
+
Type.String({ description: 'Optional default reasoning level override for the run or all steps/tasks' }),
|
|
377
|
+
),
|
|
515
378
|
tasks: Type.Optional(
|
|
516
|
-
Type.Array(TaskItem, { description: 'Array of {agent, task} for parallel execution' }),
|
|
379
|
+
Type.Array(TaskItem, { description: 'Array of {agent, task, model?, thinking?} for parallel execution' }),
|
|
517
380
|
),
|
|
518
381
|
chain: Type.Optional(
|
|
519
|
-
Type.Array(ChainItem, { description: 'Array of {agent, task} for sequential execution' }),
|
|
382
|
+
Type.Array(ChainItem, { description: 'Array of {agent, task, model?, thinking?} for sequential execution' }),
|
|
520
383
|
),
|
|
521
384
|
agentScope: Type.Optional(AgentScopeSchema),
|
|
522
385
|
confirmProjectAgents: Type.Optional(
|
|
@@ -530,42 +393,120 @@ const SubagentParams = Type.Object({
|
|
|
530
393
|
),
|
|
531
394
|
});
|
|
532
395
|
|
|
533
|
-
|
|
534
|
-
* Try to resolve package paths including the agents resource.
|
|
535
|
-
* Returns null if the pi version does not support ResolvedPaths.agents.
|
|
536
|
-
*/
|
|
537
|
-
async function tryResolvePackagePaths(cwd: string): Promise<ResolvedPaths | undefined> {
|
|
396
|
+
async function resolvePackagePaths(cwd: string): Promise<ResolvedPaths | undefined> {
|
|
538
397
|
try {
|
|
539
398
|
const agentDir = getAgentDir();
|
|
540
399
|
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
541
400
|
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
|
542
|
-
|
|
543
|
-
// Check if this pi version has agents support
|
|
544
|
-
if ('agents' in resolved && Array.isArray((resolved as Record<string, unknown>).agents)) {
|
|
545
|
-
return resolved;
|
|
546
|
-
}
|
|
401
|
+
return await packageManager.resolve();
|
|
547
402
|
} catch {
|
|
548
|
-
|
|
403
|
+
return undefined;
|
|
549
404
|
}
|
|
550
|
-
return undefined;
|
|
551
405
|
}
|
|
552
406
|
|
|
553
407
|
export default function (pi: ExtensionAPI) {
|
|
408
|
+
let autocompleteCwd = process.cwd();
|
|
409
|
+
|
|
410
|
+
function parseArgumentText(argumentText: string): {
|
|
411
|
+
completedTokens: string[];
|
|
412
|
+
currentToken: string;
|
|
413
|
+
} {
|
|
414
|
+
const endsWithSpace = argumentText.length > 0 && /\s$/.test(argumentText);
|
|
415
|
+
const tokens = argumentText.trim().length > 0 ? argumentText.trim().split(/\s+/) : [];
|
|
416
|
+
return {
|
|
417
|
+
completedTokens: endsWithSpace ? tokens : tokens.slice(0, -1),
|
|
418
|
+
currentToken: endsWithSpace ? '' : (tokens.at(-1) ?? ''),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function replaceCurrentToken(argumentText: string, replacement: string): string {
|
|
423
|
+
const trimmedEnd = argumentText.replace(/\s+$/, '');
|
|
424
|
+
if (!trimmedEnd) return replacement;
|
|
425
|
+
if (/\s$/.test(argumentText)) return `${trimmedEnd} ${replacement}`;
|
|
426
|
+
const lastSpace = trimmedEnd.lastIndexOf(' ');
|
|
427
|
+
return lastSpace === -1
|
|
428
|
+
? replacement
|
|
429
|
+
: `${trimmedEnd.slice(0, lastSpace + 1)}${replacement}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function buildArgumentCompletions(
|
|
433
|
+
argumentText: string,
|
|
434
|
+
items: Array<{ value: string; label?: string; description?: string }>,
|
|
435
|
+
): AutocompleteItem[] | null {
|
|
436
|
+
const { currentToken } = parseArgumentText(argumentText);
|
|
437
|
+
const query = currentToken.toLowerCase();
|
|
438
|
+
const filtered = items.filter((item) => {
|
|
439
|
+
if (!query) return true;
|
|
440
|
+
return (
|
|
441
|
+
item.value.toLowerCase().startsWith(query) ||
|
|
442
|
+
item.label?.toLowerCase().includes(query) ||
|
|
443
|
+
item.description?.toLowerCase().includes(query)
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
if (filtered.length === 0) return null;
|
|
447
|
+
return filtered.map((item) => ({
|
|
448
|
+
value: replaceCurrentToken(argumentText, item.value),
|
|
449
|
+
label: item.label ?? item.value,
|
|
450
|
+
description: item.description,
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function getAutocompleteAgents(scope: AgentScope): Promise<AgentConfig[]> {
|
|
455
|
+
const resolvedPaths = await resolvePackagePaths(autocompleteCwd);
|
|
456
|
+
return discoverAgents(autocompleteCwd, scope, resolvedPaths).agents;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function getAutocompleteModels(scope: AgentScope): Promise<
|
|
460
|
+
Array<{ value: string; label?: string; description?: string }>
|
|
461
|
+
> {
|
|
462
|
+
const items = new Map<string, { value: string; label?: string; description?: string }>();
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const authStorage = AuthStorage.create();
|
|
466
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
467
|
+
const available = await modelRegistry.getAvailable();
|
|
468
|
+
for (const model of available) {
|
|
469
|
+
const value = `${model.provider}/${model.id}`;
|
|
470
|
+
items.set(value, {
|
|
471
|
+
value,
|
|
472
|
+
label: value,
|
|
473
|
+
description: 'Configured model with available credentials',
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
// Fall back to models referenced by discovered agents.
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const agents = await getAutocompleteAgents(scope);
|
|
481
|
+
for (const agent of agents) {
|
|
482
|
+
if (!agent.model) continue;
|
|
483
|
+
if (items.has(agent.model)) continue;
|
|
484
|
+
items.set(agent.model, {
|
|
485
|
+
value: agent.model,
|
|
486
|
+
label: agent.model,
|
|
487
|
+
description: `Default model on agent ${agent.name}`,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return [...items.values()];
|
|
492
|
+
}
|
|
493
|
+
|
|
554
494
|
pi.registerTool({
|
|
555
495
|
name: 'subagent',
|
|
556
496
|
label: 'Subagent',
|
|
557
497
|
description: [
|
|
558
498
|
'Delegate tasks to specialized subagents with isolated context.',
|
|
559
499
|
'Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).',
|
|
560
|
-
'
|
|
561
|
-
'
|
|
500
|
+
'Optional per-run or per-step model and reasoning-level overrides are supported via model/thinking.',
|
|
501
|
+
'Default agent scope is "user" (from ~/.pi/agent/prompts).',
|
|
502
|
+
'To enable project-local agents in .pi/prompts, set agentScope: "both" (or "project").',
|
|
562
503
|
].join(' '),
|
|
563
504
|
parameters: SubagentParams,
|
|
564
505
|
|
|
565
506
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
566
507
|
const agentScope: AgentScope = params.agentScope ?? 'user';
|
|
567
|
-
const resolvedPaths = await
|
|
568
|
-
const discovery =
|
|
508
|
+
const resolvedPaths = await resolvePackagePaths(ctx.cwd);
|
|
509
|
+
const discovery = discoverAgents(ctx.cwd, agentScope, resolvedPaths);
|
|
569
510
|
const agents = discovery.agents;
|
|
570
511
|
const confirmProjectAgents = params.confirmProjectAgents ?? true;
|
|
571
512
|
|
|
@@ -579,7 +520,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
579
520
|
(results: SingleResult[]): SubagentDetails => ({
|
|
580
521
|
mode,
|
|
581
522
|
agentScope,
|
|
582
|
-
|
|
523
|
+
projectPromptsDir: discovery.projectPromptsDir,
|
|
583
524
|
results,
|
|
584
525
|
});
|
|
585
526
|
|
|
@@ -612,7 +553,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
612
553
|
|
|
613
554
|
if (projectAgentsRequested.length > 0) {
|
|
614
555
|
const names = projectAgentsRequested.map((a) => a.name).join(', ');
|
|
615
|
-
const dir = discovery.
|
|
556
|
+
const dir = discovery.projectPromptsDir ?? '(unknown)';
|
|
616
557
|
const ok = await ctx.ui.confirm(
|
|
617
558
|
'Run project-local agents?',
|
|
618
559
|
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
@@ -648,12 +589,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
648
589
|
}
|
|
649
590
|
: undefined;
|
|
650
591
|
|
|
592
|
+
if (ctx.hasUI) {
|
|
593
|
+
ctx.ui.setWorkingMessage(`Chain step ${i + 1}/${params.chain.length}: ${step.agent}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
651
596
|
const result = await runSingleAgent(
|
|
652
597
|
ctx.cwd,
|
|
653
598
|
agents,
|
|
654
599
|
step.agent,
|
|
655
600
|
taskWithContext,
|
|
656
601
|
step.cwd,
|
|
602
|
+
step.model ?? params.model,
|
|
603
|
+
step.thinking ?? params.thinking,
|
|
657
604
|
i + 1,
|
|
658
605
|
signal,
|
|
659
606
|
chainUpdate,
|
|
@@ -682,7 +629,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
682
629
|
isError: true,
|
|
683
630
|
};
|
|
684
631
|
}
|
|
685
|
-
|
|
632
|
+
const finalOutput = getFinalOutput(result.messages) || '(no output)';
|
|
633
|
+
previousOutput = renderHandoffForPrompt(
|
|
634
|
+
buildHandoffFromResult({
|
|
635
|
+
agent: step.agent,
|
|
636
|
+
step: i + 1,
|
|
637
|
+
task: step.task,
|
|
638
|
+
output: finalOutput,
|
|
639
|
+
}),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
if (ctx.hasUI) {
|
|
643
|
+
ctx.ui.setWorkingMessage();
|
|
686
644
|
}
|
|
687
645
|
return {
|
|
688
646
|
content: [
|
|
@@ -747,6 +705,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
747
705
|
}
|
|
748
706
|
};
|
|
749
707
|
|
|
708
|
+
if (ctx.hasUI) {
|
|
709
|
+
ctx.ui.setWorkingMessage(`Running ${params.tasks.length} agents in parallel`);
|
|
710
|
+
}
|
|
711
|
+
|
|
750
712
|
const results = await mapWithConcurrencyLimit(
|
|
751
713
|
params.tasks,
|
|
752
714
|
MAX_CONCURRENCY,
|
|
@@ -757,6 +719,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
757
719
|
t.agent,
|
|
758
720
|
t.task,
|
|
759
721
|
t.cwd,
|
|
722
|
+
t.model ?? params.model,
|
|
723
|
+
t.thinking ?? params.thinking,
|
|
760
724
|
undefined,
|
|
761
725
|
signal,
|
|
762
726
|
// Per-task update callback
|
|
@@ -774,6 +738,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
774
738
|
},
|
|
775
739
|
);
|
|
776
740
|
|
|
741
|
+
if (ctx.hasUI) {
|
|
742
|
+
ctx.ui.setWorkingMessage();
|
|
743
|
+
}
|
|
744
|
+
|
|
777
745
|
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
778
746
|
const summaries = results.map((r) => {
|
|
779
747
|
const output = getFinalOutput(r.messages);
|
|
@@ -791,17 +759,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
791
759
|
}
|
|
792
760
|
|
|
793
761
|
if (params.agent && params.task) {
|
|
762
|
+
if (ctx.hasUI) {
|
|
763
|
+
ctx.ui.setWorkingMessage(`Running agent: ${params.agent}`);
|
|
764
|
+
}
|
|
765
|
+
|
|
794
766
|
const result = await runSingleAgent(
|
|
795
767
|
ctx.cwd,
|
|
796
768
|
agents,
|
|
797
769
|
params.agent,
|
|
798
770
|
params.task,
|
|
799
771
|
params.cwd,
|
|
772
|
+
params.model,
|
|
773
|
+
params.thinking,
|
|
800
774
|
undefined,
|
|
801
775
|
signal,
|
|
802
776
|
onUpdate,
|
|
803
777
|
makeDetails('single'),
|
|
804
778
|
);
|
|
779
|
+
if (ctx.hasUI) {
|
|
780
|
+
ctx.ui.setWorkingMessage();
|
|
781
|
+
}
|
|
782
|
+
|
|
805
783
|
const isError =
|
|
806
784
|
result.exitCode !== 0 || result.stopReason === 'error' || result.stopReason === 'aborted';
|
|
807
785
|
if (isError) {
|
|
@@ -1164,31 +1142,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1164
1142
|
},
|
|
1165
1143
|
});
|
|
1166
1144
|
|
|
1167
|
-
// ──
|
|
1168
|
-
|
|
1169
|
-
function emptyDelegateUsage(): DelegateUsageStats {
|
|
1170
|
-
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
function combineParallelOutputs(results: AgentResult[]): string {
|
|
1174
|
-
return results
|
|
1175
|
-
.map((r) => {
|
|
1176
|
-
const output = getFinalText(r);
|
|
1177
|
-
return `## ${r.agent}\n${output}`;
|
|
1178
|
-
})
|
|
1179
|
-
.join('\n\n');
|
|
1180
|
-
}
|
|
1145
|
+
// ── Command helpers ───────────────────────────────────────────────────────
|
|
1181
1146
|
|
|
1182
1147
|
async function confirmProjectAgentsIfNeeded(
|
|
1183
1148
|
ctx: ExtensionCommandContext,
|
|
1184
1149
|
agents: AgentConfig[],
|
|
1185
|
-
|
|
1150
|
+
projectPromptsDir: string | null,
|
|
1186
1151
|
confirmProjectAgents: boolean,
|
|
1187
1152
|
): Promise<boolean> {
|
|
1188
1153
|
if (!ctx.hasUI || !confirmProjectAgents || agents.length === 0) return true;
|
|
1189
1154
|
|
|
1190
1155
|
const names = Array.from(new Set(agents.map((agent) => agent.name))).join(', ');
|
|
1191
|
-
const dir =
|
|
1156
|
+
const dir = projectPromptsDir ?? '(unknown)';
|
|
1192
1157
|
return ctx.ui.confirm(
|
|
1193
1158
|
'Run project-local agents?',
|
|
1194
1159
|
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
@@ -1303,192 +1268,109 @@ export default function (pi: ExtensionAPI) {
|
|
|
1303
1268
|
return box;
|
|
1304
1269
|
});
|
|
1305
1270
|
|
|
1306
|
-
async
|
|
1307
|
-
cwd
|
|
1308
|
-
|
|
1309
|
-
synthesis: string,
|
|
1310
|
-
previousOutput: string,
|
|
1311
|
-
ctx: ExtensionCommandContext,
|
|
1312
|
-
agentScope: AgentScope,
|
|
1313
|
-
resolvedPaths?: ResolvedPaths,
|
|
1314
|
-
): Promise<{ results: AgentResult[]; output: string; aborted: boolean }> {
|
|
1315
|
-
const task = phase.taskTemplate
|
|
1316
|
-
.replace(/\{synthesis\}/g, synthesis)
|
|
1317
|
-
.replace(/\{previous\}/g, previousOutput);
|
|
1318
|
-
|
|
1319
|
-
ctx.ui.notify(
|
|
1320
|
-
`${formatPhaseHeader(phase.name, 'running')} ${phase.kind === 'parallel' ? `(${phase.agents.length} agents)` : phase.agents[0]}`,
|
|
1321
|
-
'info',
|
|
1322
|
-
);
|
|
1323
|
-
|
|
1324
|
-
let results: AgentResult[];
|
|
1325
|
-
if (phase.kind === 'parallel') {
|
|
1326
|
-
results = await runParallel(cwd, phase.agents, task, {
|
|
1327
|
-
agentScope,
|
|
1328
|
-
onUpdate: (_phaseName, _agentName, _result) => {
|
|
1329
|
-
// Streaming updates happen per-message
|
|
1330
|
-
},
|
|
1331
|
-
phaseName: phase.name,
|
|
1332
|
-
resolvedPaths,
|
|
1333
|
-
});
|
|
1334
|
-
} else {
|
|
1335
|
-
const result = await runAgent(cwd, phase.agents[0], task, {
|
|
1336
|
-
agentScope,
|
|
1337
|
-
phaseName: phase.name,
|
|
1338
|
-
resolvedPaths,
|
|
1339
|
-
});
|
|
1340
|
-
results = [result];
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
const hasError = results.some((r) => r.exitCode !== 0 || r.stopReason === 'error');
|
|
1344
|
-
if (hasError) {
|
|
1345
|
-
const failedAgents = results
|
|
1346
|
-
.filter((r) => r.exitCode !== 0 || r.stopReason === 'error')
|
|
1347
|
-
.map((r) => `${r.agent}: ${r.errorMessage || r.stderr || '(unknown error)'}`)
|
|
1348
|
-
.join('\n');
|
|
1349
|
-
|
|
1350
|
-
ctx.ui.notify(`${formatPhaseHeader(phase.name, 'failed')}\n${failedAgents}`, 'error');
|
|
1351
|
-
return { results, output: combineParallelOutputs(results), aborted: true };
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
const output = combineParallelOutputs(results);
|
|
1355
|
-
ctx.ui.notify(formatPhaseHeader(phase.name, 'done'), 'info');
|
|
1356
|
-
|
|
1357
|
-
if (phase.requiresConfirmation && ctx.hasUI) {
|
|
1358
|
-
const planText = output;
|
|
1359
|
-
const proceed = await confirmPlan(ctx, planText);
|
|
1360
|
-
if (!proceed) {
|
|
1361
|
-
ctx.ui.notify('Delegate aborted by user after plan review.', 'warning');
|
|
1362
|
-
return { results, output, aborted: true };
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
return { results, output, aborted: false };
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
async function listMdFiles(dir: string): Promise<Set<string>> {
|
|
1370
|
-
if (!existsSync(dir)) return new Set();
|
|
1371
|
-
try {
|
|
1372
|
-
const entries = await readdir(dir);
|
|
1373
|
-
return new Set(entries.filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, '')));
|
|
1374
|
-
} catch {
|
|
1375
|
-
return new Set();
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1271
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
1272
|
+
autocompleteCwd = ctx.cwd;
|
|
1273
|
+
});
|
|
1378
1274
|
|
|
1379
|
-
pi.registerCommand('
|
|
1380
|
-
description:
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const isUser = user.has(name);
|
|
1395
|
-
if (isUser && isBundled) {
|
|
1396
|
-
lines.push(`- **${name}** — user override (bundled version available, \`/delegate-agents reset ${name}\` to restore)`);
|
|
1397
|
-
} else if (isUser) {
|
|
1398
|
-
lines.push(`- **${name}** — user-only`);
|
|
1399
|
-
} else {
|
|
1400
|
-
lines.push(`- **${name}** — bundled`);
|
|
1275
|
+
pi.registerCommand('run-agent', {
|
|
1276
|
+
description:
|
|
1277
|
+
'Run a single agent directly — honors agent sessionStrategy metadata such as fork-at',
|
|
1278
|
+
getArgumentCompletions: async (argumentText) => {
|
|
1279
|
+
const { completedTokens } = parseArgumentText(argumentText);
|
|
1280
|
+
let agentScope: AgentScope = 'user';
|
|
1281
|
+
let expectsScopeValue = false;
|
|
1282
|
+
let expectsThinkingValue = false;
|
|
1283
|
+
let expectsModelValue = false;
|
|
1284
|
+
let agentName: string | undefined;
|
|
1285
|
+
|
|
1286
|
+
for (const token of completedTokens) {
|
|
1287
|
+
if (expectsScopeValue) {
|
|
1288
|
+
if (token === 'user' || token === 'project' || token === 'both') {
|
|
1289
|
+
agentScope = token;
|
|
1401
1290
|
}
|
|
1291
|
+
expectsScopeValue = false;
|
|
1292
|
+
continue;
|
|
1402
1293
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
if (action === 'reset') {
|
|
1408
|
-
const name = parts[1];
|
|
1409
|
-
if (!name) {
|
|
1410
|
-
ctx.ui.notify('Usage: /delegate-agents reset <name|--all>', 'warning');
|
|
1411
|
-
return;
|
|
1294
|
+
if (expectsThinkingValue) {
|
|
1295
|
+
expectsThinkingValue = false;
|
|
1296
|
+
continue;
|
|
1412
1297
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
const bundled = await listMdFiles(bundledAgentsDir);
|
|
1417
|
-
let count = 0;
|
|
1418
|
-
for (const n of user) {
|
|
1419
|
-
if (bundled.has(n)) {
|
|
1420
|
-
await unlink(join(userDir, `${n}.md`));
|
|
1421
|
-
count++;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
ctx.ui.notify(count > 0 ? `Reset ${count} agent(s) to bundled versions.` : 'No user overrides to reset.', 'info');
|
|
1425
|
-
return;
|
|
1298
|
+
if (expectsModelValue) {
|
|
1299
|
+
expectsModelValue = false;
|
|
1300
|
+
continue;
|
|
1426
1301
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
if (!existsSync(userFile)) {
|
|
1432
|
-
ctx.ui.notify(`No user override for "${name}" — already using bundled version.`, 'info');
|
|
1433
|
-
return;
|
|
1302
|
+
if (token === '--scope') {
|
|
1303
|
+
expectsScopeValue = true;
|
|
1304
|
+
continue;
|
|
1434
1305
|
}
|
|
1435
|
-
if (
|
|
1436
|
-
|
|
1437
|
-
|
|
1306
|
+
if (token === '--thinking' || token === '--reasoning-level') {
|
|
1307
|
+
expectsThinkingValue = true;
|
|
1308
|
+
continue;
|
|
1438
1309
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (action === 'edit') {
|
|
1446
|
-
const name = parts[1];
|
|
1447
|
-
if (!name) {
|
|
1448
|
-
ctx.ui.notify('Usage: /delegate-agents edit <name>', 'warning');
|
|
1449
|
-
return;
|
|
1310
|
+
if (token === '--model') {
|
|
1311
|
+
expectsModelValue = true;
|
|
1312
|
+
continue;
|
|
1450
1313
|
}
|
|
1314
|
+
if (token === '--yes-project-agents') continue;
|
|
1315
|
+
if (token.startsWith('--')) continue;
|
|
1316
|
+
if (!agentName) agentName = token;
|
|
1317
|
+
}
|
|
1451
1318
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
if (!existsSync(bundledFile)) {
|
|
1460
|
-
ctx.ui.notify(`No bundled agent named "${name}".`, 'warning');
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1319
|
+
if (expectsScopeValue) {
|
|
1320
|
+
return buildArgumentCompletions(argumentText, [
|
|
1321
|
+
{ value: 'user', description: 'Only user/global agents from ~/.pi/agent/prompts' },
|
|
1322
|
+
{ value: 'project', description: 'Only project-local agents from .pi/prompts' },
|
|
1323
|
+
{ value: 'both', description: 'User/global agents plus project-local agents' },
|
|
1324
|
+
]);
|
|
1325
|
+
}
|
|
1463
1326
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1327
|
+
if (expectsThinkingValue) {
|
|
1328
|
+
return buildArgumentCompletions(argumentText, [
|
|
1329
|
+
{ value: 'off', description: 'Disable reasoning if the model supports it' },
|
|
1330
|
+
{ value: 'minimal', description: 'Minimal reasoning effort' },
|
|
1331
|
+
{ value: 'low', description: 'Lower reasoning effort' },
|
|
1332
|
+
{ value: 'medium', description: 'Balanced reasoning effort' },
|
|
1333
|
+
{ value: 'high', description: 'Higher reasoning effort' },
|
|
1334
|
+
{ value: 'xhigh', description: 'Maximum reasoning effort on supported models' },
|
|
1335
|
+
]);
|
|
1468
1336
|
}
|
|
1469
1337
|
|
|
1470
|
-
|
|
1338
|
+
if (expectsModelValue) {
|
|
1339
|
+
const models = await getAutocompleteModels(agentScope);
|
|
1340
|
+
return buildArgumentCompletions(argumentText, models);
|
|
1341
|
+
}
|
|
1342
|
+
if (agentName) return null;
|
|
1343
|
+
|
|
1344
|
+
const agents = await getAutocompleteAgents(agentScope);
|
|
1345
|
+
return buildArgumentCompletions(argumentText, [
|
|
1346
|
+
{ value: '--scope', description: 'Choose which agent directories are searched' },
|
|
1347
|
+
{ value: '--model', description: 'Override the agent model for this run' },
|
|
1348
|
+
{ value: '--thinking', description: 'Override the reasoning level for this run' },
|
|
1349
|
+
{ value: '--reasoning-level', description: 'Alias for --thinking' },
|
|
1350
|
+
{ value: '--yes-project-agents', description: 'Skip the project-agent trust confirmation prompt' },
|
|
1351
|
+
...agents.map((agent) => ({
|
|
1352
|
+
value: agent.name,
|
|
1353
|
+
label: agent.name,
|
|
1354
|
+
description: `${agent.source} — ${agent.description}`,
|
|
1355
|
+
})),
|
|
1356
|
+
]);
|
|
1471
1357
|
},
|
|
1472
|
-
});
|
|
1473
|
-
|
|
1474
|
-
pi.registerCommand('run-agent', {
|
|
1475
|
-
description:
|
|
1476
|
-
'Run a single agent directly — honors agent sessionStrategy metadata such as fork-at',
|
|
1477
1358
|
handler: async (args, ctx) => {
|
|
1359
|
+
autocompleteCwd = ctx.cwd;
|
|
1478
1360
|
const parsed = parseRunAgentArgs(args);
|
|
1479
1361
|
if (!parsed.ok) {
|
|
1480
1362
|
ctx.ui.notify(parsed.error, 'warning');
|
|
1481
1363
|
return;
|
|
1482
1364
|
}
|
|
1483
1365
|
|
|
1484
|
-
const { agentName, explicitTask, agentScope, confirmProjectAgents } = parsed.options;
|
|
1366
|
+
const { agentName, explicitTask, agentScope, confirmProjectAgents, model, thinking } = parsed.options;
|
|
1485
1367
|
if (!agentName) {
|
|
1486
1368
|
ctx.ui.notify(formatRunAgentUsage(), 'warning');
|
|
1487
1369
|
return;
|
|
1488
1370
|
}
|
|
1489
1371
|
|
|
1490
|
-
const resolvedPaths = await
|
|
1491
|
-
const discovery =
|
|
1372
|
+
const resolvedPaths = await resolvePackagePaths(ctx.cwd);
|
|
1373
|
+
const discovery = discoverAgents(ctx.cwd, agentScope, resolvedPaths);
|
|
1492
1374
|
const agent = discovery.agents.find((candidate) => candidate.name === agentName);
|
|
1493
1375
|
|
|
1494
1376
|
if (!agent) {
|
|
@@ -1501,7 +1383,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1501
1383
|
const approved = await confirmProjectAgentsIfNeeded(
|
|
1502
1384
|
ctx,
|
|
1503
1385
|
requestedProjectAgents,
|
|
1504
|
-
discovery.
|
|
1386
|
+
discovery.projectPromptsDir,
|
|
1505
1387
|
confirmProjectAgents,
|
|
1506
1388
|
);
|
|
1507
1389
|
if (!approved) {
|
|
@@ -1516,7 +1398,65 @@ export default function (pi: ExtensionAPI) {
|
|
|
1516
1398
|
return;
|
|
1517
1399
|
}
|
|
1518
1400
|
|
|
1519
|
-
|
|
1401
|
+
const runAndReport = async (
|
|
1402
|
+
commandCwd: string,
|
|
1403
|
+
commandUi: typeof ctx.ui,
|
|
1404
|
+
sendSummary: (message: {
|
|
1405
|
+
customType: 'run-agent-summary';
|
|
1406
|
+
content: string;
|
|
1407
|
+
display: true;
|
|
1408
|
+
details: RunAgentSummaryDetails;
|
|
1409
|
+
}) => Promise<void> | void,
|
|
1410
|
+
forked: boolean,
|
|
1411
|
+
) => {
|
|
1412
|
+
const selectedModel = model ?? agent.model;
|
|
1413
|
+
const selectedThinking = thinking ?? agent.thinking;
|
|
1414
|
+
|
|
1415
|
+
commandUi.notify(
|
|
1416
|
+
`Running agent: ${agent.name} [${agent.source}${forked ? ', forked' : ', inline'}${selectedModel ? `, model=${selectedModel}` : ''}${selectedThinking ? `, thinking=${selectedThinking}` : ''}]`,
|
|
1417
|
+
'info',
|
|
1418
|
+
);
|
|
1419
|
+
|
|
1420
|
+
const result = await runAgent(commandCwd, agent.name, task, {
|
|
1421
|
+
agentScope,
|
|
1422
|
+
resolvedPaths,
|
|
1423
|
+
model,
|
|
1424
|
+
thinking,
|
|
1425
|
+
});
|
|
1426
|
+
const failed = result.exitCode !== 0 || result.stopReason === 'error';
|
|
1427
|
+
const output = getFinalText(result).trim();
|
|
1428
|
+
|
|
1429
|
+
commandUi.notify(
|
|
1430
|
+
failed ? `Agent "${agent.name}" failed.` : `Agent "${agent.name}" complete.`,
|
|
1431
|
+
failed ? 'error' : 'info',
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
await sendSummary({
|
|
1435
|
+
customType: 'run-agent-summary',
|
|
1436
|
+
content: formatRunAgentSummary({
|
|
1437
|
+
agent,
|
|
1438
|
+
agentScope,
|
|
1439
|
+
task,
|
|
1440
|
+
result,
|
|
1441
|
+
forked,
|
|
1442
|
+
}),
|
|
1443
|
+
display: true,
|
|
1444
|
+
details: {
|
|
1445
|
+
agent: agent.name,
|
|
1446
|
+
agentSource: agent.source,
|
|
1447
|
+
agentScope,
|
|
1448
|
+
sessionStrategy: agent.sessionStrategy ?? 'inline',
|
|
1449
|
+
forked,
|
|
1450
|
+
exitCode: result.exitCode,
|
|
1451
|
+
stopReason: result.stopReason,
|
|
1452
|
+
errorMessage: result.errorMessage,
|
|
1453
|
+
usage: result.usage,
|
|
1454
|
+
task,
|
|
1455
|
+
output,
|
|
1456
|
+
} satisfies RunAgentSummaryDetails,
|
|
1457
|
+
});
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1520
1460
|
if (agent.sessionStrategy === 'fork-at') {
|
|
1521
1461
|
const leafId = ctx.sessionManager.getLeafId?.();
|
|
1522
1462
|
if (!leafId) {
|
|
@@ -1525,188 +1465,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
1525
1465
|
'warning',
|
|
1526
1466
|
);
|
|
1527
1467
|
} else {
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1468
|
+
const forkResult = await ctx.fork(leafId, {
|
|
1469
|
+
position: 'at',
|
|
1470
|
+
withSession: async (replacementCtx) => {
|
|
1471
|
+
await runAndReport(
|
|
1472
|
+
replacementCtx.cwd,
|
|
1473
|
+
replacementCtx.ui,
|
|
1474
|
+
(message) => replacementCtx.sendMessage(message),
|
|
1475
|
+
true,
|
|
1476
|
+
);
|
|
1477
|
+
},
|
|
1478
|
+
});
|
|
1533
1479
|
if (forkResult.cancelled) {
|
|
1534
1480
|
ctx.ui.notify(`Run cancelled — unable to fork session for "${agent.name}".`, 'info');
|
|
1535
|
-
return;
|
|
1536
1481
|
}
|
|
1537
|
-
forked = true;
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
ctx.ui.notify(
|
|
1542
|
-
`Running agent: ${agent.name} [${agent.source}${forked ? ', forked' : ', inline'}]`,
|
|
1543
|
-
'info',
|
|
1544
|
-
);
|
|
1545
|
-
|
|
1546
|
-
const result = await runAgent(ctx.cwd, agent.name, task, { agentScope, resolvedPaths });
|
|
1547
|
-
const failed = result.exitCode !== 0 || result.stopReason === 'error';
|
|
1548
|
-
const output = getFinalText(result).trim();
|
|
1549
|
-
|
|
1550
|
-
ctx.ui.notify(
|
|
1551
|
-
failed ? `Agent "${agent.name}" failed.` : `Agent "${agent.name}" complete.`,
|
|
1552
|
-
failed ? 'error' : 'info',
|
|
1553
|
-
);
|
|
1554
|
-
|
|
1555
|
-
pi.sendMessage({
|
|
1556
|
-
customType: 'run-agent-summary',
|
|
1557
|
-
content: formatRunAgentSummary({
|
|
1558
|
-
agent,
|
|
1559
|
-
agentScope,
|
|
1560
|
-
task,
|
|
1561
|
-
result,
|
|
1562
|
-
forked,
|
|
1563
|
-
}),
|
|
1564
|
-
display: true,
|
|
1565
|
-
details: {
|
|
1566
|
-
agent: agent.name,
|
|
1567
|
-
agentSource: agent.source,
|
|
1568
|
-
agentScope,
|
|
1569
|
-
sessionStrategy: agent.sessionStrategy ?? 'inline',
|
|
1570
|
-
forked,
|
|
1571
|
-
exitCode: result.exitCode,
|
|
1572
|
-
stopReason: result.stopReason,
|
|
1573
|
-
errorMessage: result.errorMessage,
|
|
1574
|
-
usage: result.usage,
|
|
1575
|
-
task,
|
|
1576
|
-
output,
|
|
1577
|
-
} satisfies RunAgentSummaryDetails,
|
|
1578
|
-
});
|
|
1579
|
-
},
|
|
1580
|
-
});
|
|
1581
|
-
|
|
1582
|
-
pi.registerCommand('delegate', {
|
|
1583
|
-
description:
|
|
1584
|
-
'Orchestrate subagent workflows — supports --scope, --workflow, and project-local agents',
|
|
1585
|
-
handler: async (args, ctx) => {
|
|
1586
|
-
const parsed = parseDelegateArgs(args);
|
|
1587
|
-
if (!parsed.ok) {
|
|
1588
|
-
ctx.ui.notify(parsed.error, 'warning');
|
|
1589
|
-
return;
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
const { explicitTask, agentScope, workflowId, confirmProjectAgents } = parsed.options;
|
|
1593
|
-
const resolvedPaths = await tryResolvePackagePaths(ctx.cwd);
|
|
1594
|
-
|
|
1595
|
-
// Step 1: Synthesize
|
|
1596
|
-
const conversation = extractRecentConversation(ctx);
|
|
1597
|
-
if (!conversation.trim() && !explicitTask) {
|
|
1598
|
-
ctx.ui.notify(`No conversation context and no task specified.\n\n${formatDelegateUsage()}`, 'warning');
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
const prompt = buildSynthesisPrompt(conversation, explicitTask);
|
|
1603
|
-
ctx.ui.notify('⏳ Synthesizing task from conversation...', 'info');
|
|
1604
|
-
|
|
1605
|
-
const synthResult = await runAgent(ctx.cwd, 'planner', prompt, { agentScope, resolvedPaths });
|
|
1606
|
-
const synthesis = getFinalText(synthResult);
|
|
1607
|
-
|
|
1608
|
-
if (!synthesis.trim()) {
|
|
1609
|
-
ctx.ui.notify('Failed to synthesize task from conversation.', 'error');
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
// Step 2: Confirm synthesis
|
|
1614
|
-
if (ctx.hasUI) {
|
|
1615
|
-
const synthOk = await confirmSynthesis(ctx, synthesis);
|
|
1616
|
-
if (!synthOk) {
|
|
1617
|
-
ctx.ui.notify('Delegate cancelled.', 'info');
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
// Step 3: Pick workflow
|
|
1623
|
-
const workflow = workflowId
|
|
1624
|
-
? getWorkflow(workflowId)
|
|
1625
|
-
: ctx.hasUI
|
|
1626
|
-
? await pickWorkflow(ctx, synthesis)
|
|
1627
|
-
: getWorkflow(suggestWorkflow(synthesis));
|
|
1628
|
-
if (!workflow) {
|
|
1629
|
-
ctx.ui.notify('Delegate cancelled — no workflow selected.', 'info');
|
|
1630
|
-
return;
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
if (agentScope === 'project' || agentScope === 'both') {
|
|
1634
|
-
const discovery = discoverAgentsWithPackages(ctx.cwd, agentScope, resolvedPaths);
|
|
1635
|
-
const requestedProjectAgents = workflow.phases
|
|
1636
|
-
.flatMap((phase) => phase.agents)
|
|
1637
|
-
.map((name) => discovery.agents.find((agent) => agent.name === name))
|
|
1638
|
-
.filter((agent): agent is AgentConfig => agent?.source === 'project');
|
|
1639
|
-
|
|
1640
|
-
const ok = await confirmProjectAgentsIfNeeded(
|
|
1641
|
-
ctx,
|
|
1642
|
-
requestedProjectAgents,
|
|
1643
|
-
discovery.projectAgentsDir,
|
|
1644
|
-
confirmProjectAgents,
|
|
1645
|
-
);
|
|
1646
|
-
if (!ok) {
|
|
1647
|
-
ctx.ui.notify('Delegate cancelled — project-local agents not approved.', 'info');
|
|
1648
1482
|
return;
|
|
1649
1483
|
}
|
|
1650
1484
|
}
|
|
1651
1485
|
|
|
1652
|
-
|
|
1653
|
-
const state: DelegateState = {
|
|
1654
|
-
synthesis,
|
|
1655
|
-
workflow,
|
|
1656
|
-
phases: [],
|
|
1657
|
-
totalUsage: emptyDelegateUsage(),
|
|
1658
|
-
};
|
|
1659
|
-
|
|
1660
|
-
let previousOutput = '';
|
|
1661
|
-
|
|
1662
|
-
ctx.ui.notify(`Starting workflow: ${workflow.label} [${agentScope}]`, 'info');
|
|
1663
|
-
|
|
1664
|
-
for (const phaseDef of workflow.phases) {
|
|
1665
|
-
const phaseResult = await executePhase(
|
|
1666
|
-
ctx.cwd,
|
|
1667
|
-
phaseDef,
|
|
1668
|
-
synthesis,
|
|
1669
|
-
previousOutput,
|
|
1670
|
-
ctx,
|
|
1671
|
-
agentScope,
|
|
1672
|
-
resolvedPaths,
|
|
1673
|
-
);
|
|
1674
|
-
|
|
1675
|
-
const phase: PhaseResult = {
|
|
1676
|
-
name: phaseDef.name,
|
|
1677
|
-
kind: phaseDef.kind,
|
|
1678
|
-
agents: phaseResult.results,
|
|
1679
|
-
};
|
|
1680
|
-
|
|
1681
|
-
state.phases.push(phase);
|
|
1682
|
-
const phaseUsage = aggregateUsage(phaseResult.results);
|
|
1683
|
-
state.totalUsage.input += phaseUsage.input;
|
|
1684
|
-
state.totalUsage.output += phaseUsage.output;
|
|
1685
|
-
state.totalUsage.cacheRead += phaseUsage.cacheRead;
|
|
1686
|
-
state.totalUsage.cacheWrite += phaseUsage.cacheWrite;
|
|
1687
|
-
state.totalUsage.cost += phaseUsage.cost;
|
|
1688
|
-
state.totalUsage.turns += phaseUsage.turns;
|
|
1689
|
-
|
|
1690
|
-
if (phaseResult.aborted) break;
|
|
1691
|
-
|
|
1692
|
-
previousOutput = phaseResult.output;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
// Step 5: Show full summary
|
|
1696
|
-
const summary = formatFullSummary(state);
|
|
1697
|
-
ctx.ui.notify(`Delegate complete — ${workflow.label}`, 'info');
|
|
1698
|
-
|
|
1699
|
-
pi.sendMessage({
|
|
1700
|
-
customType: 'delegate-summary',
|
|
1701
|
-
content: summary,
|
|
1702
|
-
display: true,
|
|
1703
|
-
details: {
|
|
1704
|
-
workflow: workflow.id,
|
|
1705
|
-
agentScope,
|
|
1706
|
-
phaseCount: state.phases.length,
|
|
1707
|
-
totalUsage: state.totalUsage,
|
|
1708
|
-
},
|
|
1709
|
-
});
|
|
1486
|
+
await runAndReport(ctx.cwd, ctx.ui, (message) => pi.sendMessage(message), false);
|
|
1710
1487
|
},
|
|
1711
1488
|
});
|
|
1489
|
+
|
|
1712
1490
|
}
|