@dreki-gg/pi-subagent 0.6.0 → 0.7.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/CHANGELOG.md +21 -0
- package/README.md +78 -57
- package/agents/advisor.md +37 -0
- package/agents/bug-prover.md +42 -0
- package/agents/docs-scout.md +2 -2
- package/agents/manager.md +52 -0
- package/agents/planner.md +16 -1
- package/agents/reviewer.md +64 -8
- package/agents/scout.md +5 -1
- package/agents/validator.md +42 -0
- package/agents/worker.md +38 -3
- 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/{delegate-executor.ts → agent-runner.ts} +10 -29
- package/extensions/subagent/handoffs.ts +273 -0
- package/extensions/subagent/index.ts +337 -288
- package/extensions/subagent/run-agent-args.ts +90 -0
- package/package.json +3 -6
- package/skills/spawn-subagents/SKILL.md +80 -8
- 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
|
@@ -25,48 +25,30 @@ import { StringEnum } from '@mariozechner/pi-ai';
|
|
|
25
25
|
import {
|
|
26
26
|
type ExtensionAPI,
|
|
27
27
|
type ExtensionCommandContext,
|
|
28
|
+
AuthStorage,
|
|
28
29
|
DefaultPackageManager,
|
|
29
30
|
getAgentDir,
|
|
30
31
|
getMarkdownTheme,
|
|
32
|
+
ModelRegistry,
|
|
31
33
|
SettingsManager,
|
|
32
34
|
withFileMutationQueue,
|
|
33
35
|
} from '@mariozechner/pi-coding-agent';
|
|
34
36
|
import type { ResolvedPaths } from '@mariozechner/pi-coding-agent';
|
|
35
|
-
import { Box, Container, Markdown, Spacer, Text } from '@mariozechner/pi-tui';
|
|
36
|
-
import { Type } from '
|
|
37
|
+
import { Box, Container, Markdown, Spacer, Text, type AutocompleteItem } from '@mariozechner/pi-tui';
|
|
38
|
+
import { Type } from 'typebox';
|
|
37
39
|
import {
|
|
38
40
|
type AgentConfig,
|
|
39
41
|
type AgentScope,
|
|
40
42
|
type AgentSource,
|
|
41
43
|
bundledAgentsDir,
|
|
42
|
-
discoverAgents,
|
|
43
44
|
discoverAgentsWithPackages,
|
|
44
45
|
} from './agents.js';
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
} from './
|
|
51
|
-
import { runAgent, runParallel } from './delegate-executor.js';
|
|
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';
|
|
46
|
+
import { formatRunAgentUsage, parseRunAgentArgs } from './run-agent-args.js';
|
|
47
|
+
import { runAgent } from './agent-runner.js';
|
|
48
|
+
import { extractRecentConversation } from './synthesis.js';
|
|
49
|
+
import type { AgentResult } from './agent-runner-types.js';
|
|
50
|
+
import { getFinalText } from './agent-result-utils.js';
|
|
51
|
+
import { buildHandoffFromResult, renderHandoffForPrompt } from './handoffs.js';
|
|
70
52
|
|
|
71
53
|
const MAX_PARALLEL_TASKS = 8;
|
|
72
54
|
const MAX_CONCURRENCY = 4;
|
|
@@ -314,6 +296,8 @@ async function runSingleAgent(
|
|
|
314
296
|
agentName: string,
|
|
315
297
|
task: string,
|
|
316
298
|
cwd: string | undefined,
|
|
299
|
+
modelOverride: string | undefined,
|
|
300
|
+
thinkingOverride: string | undefined,
|
|
317
301
|
step: number | undefined,
|
|
318
302
|
signal: AbortSignal | undefined,
|
|
319
303
|
onUpdate: OnUpdateCallback | undefined,
|
|
@@ -343,9 +327,12 @@ async function runSingleAgent(
|
|
|
343
327
|
};
|
|
344
328
|
}
|
|
345
329
|
|
|
330
|
+
const selectedModel = modelOverride ?? agent.model;
|
|
331
|
+
const selectedThinking = thinkingOverride ?? agent.thinking;
|
|
332
|
+
|
|
346
333
|
const args: string[] = ['--mode', 'json', '-p', '--no-session'];
|
|
347
|
-
if (
|
|
348
|
-
if (
|
|
334
|
+
if (selectedModel) args.push('--model', selectedModel);
|
|
335
|
+
if (selectedThinking) args.push('--thinking', selectedThinking);
|
|
349
336
|
if (agent.tools && agent.tools.length > 0) args.push('--tools', agent.tools.join(','));
|
|
350
337
|
|
|
351
338
|
let tmpPromptDir: string | null = null;
|
|
@@ -367,7 +354,7 @@ async function runSingleAgent(
|
|
|
367
354
|
contextTokens: 0,
|
|
368
355
|
turns: 0,
|
|
369
356
|
},
|
|
370
|
-
model:
|
|
357
|
+
model: selectedModel,
|
|
371
358
|
step,
|
|
372
359
|
};
|
|
373
360
|
|
|
@@ -492,12 +479,20 @@ async function runSingleAgent(
|
|
|
492
479
|
const TaskItem = Type.Object({
|
|
493
480
|
agent: Type.String({ description: 'Name of the agent to invoke' }),
|
|
494
481
|
task: Type.String({ description: 'Task to delegate to the agent' }),
|
|
482
|
+
model: Type.Optional(Type.String({ description: 'Optional model override for this task' })),
|
|
483
|
+
thinking: Type.Optional(
|
|
484
|
+
Type.String({ description: 'Optional reasoning level override for this task' }),
|
|
485
|
+
),
|
|
495
486
|
cwd: Type.Optional(Type.String({ description: 'Working directory for the agent process' })),
|
|
496
487
|
});
|
|
497
488
|
|
|
498
489
|
const ChainItem = Type.Object({
|
|
499
490
|
agent: Type.String({ description: 'Name of the agent to invoke' }),
|
|
500
491
|
task: Type.String({ description: 'Task with optional {previous} placeholder for prior output' }),
|
|
492
|
+
model: Type.Optional(Type.String({ description: 'Optional model override for this step' })),
|
|
493
|
+
thinking: Type.Optional(
|
|
494
|
+
Type.String({ description: 'Optional reasoning level override for this step' }),
|
|
495
|
+
),
|
|
501
496
|
cwd: Type.Optional(Type.String({ description: 'Working directory for the agent process' })),
|
|
502
497
|
});
|
|
503
498
|
|
|
@@ -512,11 +507,17 @@ const SubagentParams = Type.Object({
|
|
|
512
507
|
Type.String({ description: 'Name of the agent to invoke (for single mode)' }),
|
|
513
508
|
),
|
|
514
509
|
task: Type.Optional(Type.String({ description: 'Task to delegate (for single mode)' })),
|
|
510
|
+
model: Type.Optional(
|
|
511
|
+
Type.String({ description: 'Optional default model override for the run or all steps/tasks' }),
|
|
512
|
+
),
|
|
513
|
+
thinking: Type.Optional(
|
|
514
|
+
Type.String({ description: 'Optional default reasoning level override for the run or all steps/tasks' }),
|
|
515
|
+
),
|
|
515
516
|
tasks: Type.Optional(
|
|
516
|
-
Type.Array(TaskItem, { description: 'Array of {agent, task} for parallel execution' }),
|
|
517
|
+
Type.Array(TaskItem, { description: 'Array of {agent, task, model?, thinking?} for parallel execution' }),
|
|
517
518
|
),
|
|
518
519
|
chain: Type.Optional(
|
|
519
|
-
Type.Array(ChainItem, { description: 'Array of {agent, task} for sequential execution' }),
|
|
520
|
+
Type.Array(ChainItem, { description: 'Array of {agent, task, model?, thinking?} for sequential execution' }),
|
|
520
521
|
),
|
|
521
522
|
agentScope: Type.Optional(AgentScopeSchema),
|
|
522
523
|
confirmProjectAgents: Type.Optional(
|
|
@@ -551,12 +552,99 @@ async function tryResolvePackagePaths(cwd: string): Promise<ResolvedPaths | unde
|
|
|
551
552
|
}
|
|
552
553
|
|
|
553
554
|
export default function (pi: ExtensionAPI) {
|
|
555
|
+
let autocompleteCwd = process.cwd();
|
|
556
|
+
|
|
557
|
+
function parseArgumentText(argumentText: string): {
|
|
558
|
+
completedTokens: string[];
|
|
559
|
+
currentToken: string;
|
|
560
|
+
} {
|
|
561
|
+
const endsWithSpace = argumentText.length > 0 && /\s$/.test(argumentText);
|
|
562
|
+
const tokens = argumentText.trim().length > 0 ? argumentText.trim().split(/\s+/) : [];
|
|
563
|
+
return {
|
|
564
|
+
completedTokens: endsWithSpace ? tokens : tokens.slice(0, -1),
|
|
565
|
+
currentToken: endsWithSpace ? '' : (tokens.at(-1) ?? ''),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function replaceCurrentToken(argumentText: string, replacement: string): string {
|
|
570
|
+
const trimmedEnd = argumentText.replace(/\s+$/, '');
|
|
571
|
+
if (!trimmedEnd) return replacement;
|
|
572
|
+
if (/\s$/.test(argumentText)) return `${trimmedEnd} ${replacement}`;
|
|
573
|
+
const lastSpace = trimmedEnd.lastIndexOf(' ');
|
|
574
|
+
return lastSpace === -1
|
|
575
|
+
? replacement
|
|
576
|
+
: `${trimmedEnd.slice(0, lastSpace + 1)}${replacement}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function buildArgumentCompletions(
|
|
580
|
+
argumentText: string,
|
|
581
|
+
items: Array<{ value: string; label?: string; description?: string }>,
|
|
582
|
+
): AutocompleteItem[] | null {
|
|
583
|
+
const { currentToken } = parseArgumentText(argumentText);
|
|
584
|
+
const query = currentToken.toLowerCase();
|
|
585
|
+
const filtered = items.filter((item) => {
|
|
586
|
+
if (!query) return true;
|
|
587
|
+
return (
|
|
588
|
+
item.value.toLowerCase().startsWith(query) ||
|
|
589
|
+
item.label?.toLowerCase().includes(query) ||
|
|
590
|
+
item.description?.toLowerCase().includes(query)
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
if (filtered.length === 0) return null;
|
|
594
|
+
return filtered.map((item) => ({
|
|
595
|
+
value: replaceCurrentToken(argumentText, item.value),
|
|
596
|
+
label: item.label ?? item.value,
|
|
597
|
+
description: item.description,
|
|
598
|
+
}));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function getAutocompleteAgents(scope: AgentScope): Promise<AgentConfig[]> {
|
|
602
|
+
const resolvedPaths = await tryResolvePackagePaths(autocompleteCwd);
|
|
603
|
+
return discoverAgentsWithPackages(autocompleteCwd, scope, resolvedPaths).agents;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function getAutocompleteModels(scope: AgentScope): Promise<
|
|
607
|
+
Array<{ value: string; label?: string; description?: string }>
|
|
608
|
+
> {
|
|
609
|
+
const items = new Map<string, { value: string; label?: string; description?: string }>();
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
const authStorage = AuthStorage.create();
|
|
613
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
614
|
+
const available = await modelRegistry.getAvailable();
|
|
615
|
+
for (const model of available) {
|
|
616
|
+
const value = `${model.provider}/${model.id}`;
|
|
617
|
+
items.set(value, {
|
|
618
|
+
value,
|
|
619
|
+
label: value,
|
|
620
|
+
description: 'Configured model with available credentials',
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
} catch {
|
|
624
|
+
// Fall back to models referenced by discovered agents.
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const agents = await getAutocompleteAgents(scope);
|
|
628
|
+
for (const agent of agents) {
|
|
629
|
+
if (!agent.model) continue;
|
|
630
|
+
if (items.has(agent.model)) continue;
|
|
631
|
+
items.set(agent.model, {
|
|
632
|
+
value: agent.model,
|
|
633
|
+
label: agent.model,
|
|
634
|
+
description: `Default model on agent ${agent.name}`,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return [...items.values()];
|
|
639
|
+
}
|
|
640
|
+
|
|
554
641
|
pi.registerTool({
|
|
555
642
|
name: 'subagent',
|
|
556
643
|
label: 'Subagent',
|
|
557
644
|
description: [
|
|
558
645
|
'Delegate tasks to specialized subagents with isolated context.',
|
|
559
646
|
'Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).',
|
|
647
|
+
'Optional per-run or per-step model and reasoning-level overrides are supported via model/thinking.',
|
|
560
648
|
'Default agent scope is "user" (from ~/.pi/agent/agents).',
|
|
561
649
|
'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").',
|
|
562
650
|
].join(' '),
|
|
@@ -654,6 +742,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
654
742
|
step.agent,
|
|
655
743
|
taskWithContext,
|
|
656
744
|
step.cwd,
|
|
745
|
+
step.model ?? params.model,
|
|
746
|
+
step.thinking ?? params.thinking,
|
|
657
747
|
i + 1,
|
|
658
748
|
signal,
|
|
659
749
|
chainUpdate,
|
|
@@ -682,7 +772,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
682
772
|
isError: true,
|
|
683
773
|
};
|
|
684
774
|
}
|
|
685
|
-
|
|
775
|
+
const finalOutput = getFinalOutput(result.messages) || '(no output)';
|
|
776
|
+
previousOutput = renderHandoffForPrompt(
|
|
777
|
+
buildHandoffFromResult({
|
|
778
|
+
agent: step.agent,
|
|
779
|
+
step: i + 1,
|
|
780
|
+
task: step.task,
|
|
781
|
+
output: finalOutput,
|
|
782
|
+
}),
|
|
783
|
+
);
|
|
686
784
|
}
|
|
687
785
|
return {
|
|
688
786
|
content: [
|
|
@@ -757,6 +855,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
757
855
|
t.agent,
|
|
758
856
|
t.task,
|
|
759
857
|
t.cwd,
|
|
858
|
+
t.model ?? params.model,
|
|
859
|
+
t.thinking ?? params.thinking,
|
|
760
860
|
undefined,
|
|
761
861
|
signal,
|
|
762
862
|
// Per-task update callback
|
|
@@ -797,6 +897,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
797
897
|
params.agent,
|
|
798
898
|
params.task,
|
|
799
899
|
params.cwd,
|
|
900
|
+
params.model,
|
|
901
|
+
params.thinking,
|
|
800
902
|
undefined,
|
|
801
903
|
signal,
|
|
802
904
|
onUpdate,
|
|
@@ -1164,20 +1266,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1164
1266
|
},
|
|
1165
1267
|
});
|
|
1166
1268
|
|
|
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
|
-
}
|
|
1269
|
+
// ── Command helpers ───────────────────────────────────────────────────────
|
|
1181
1270
|
|
|
1182
1271
|
async function confirmProjectAgentsIfNeeded(
|
|
1183
1272
|
ctx: ExtensionCommandContext,
|
|
@@ -1303,69 +1392,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1303
1392
|
return box;
|
|
1304
1393
|
});
|
|
1305
1394
|
|
|
1306
|
-
async function executePhase(
|
|
1307
|
-
cwd: string,
|
|
1308
|
-
phase: PhaseDefinition,
|
|
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
1395
|
async function listMdFiles(dir: string): Promise<Set<string>> {
|
|
1370
1396
|
if (!existsSync(dir)) return new Set();
|
|
1371
1397
|
try {
|
|
@@ -1376,9 +1402,55 @@ export default function (pi: ExtensionAPI) {
|
|
|
1376
1402
|
}
|
|
1377
1403
|
}
|
|
1378
1404
|
|
|
1405
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
1406
|
+
autocompleteCwd = ctx.cwd;
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1379
1409
|
pi.registerCommand('delegate-agents', {
|
|
1380
1410
|
description: 'List, customize, or reset delegate agents',
|
|
1411
|
+
getArgumentCompletions: async (argumentText) => {
|
|
1412
|
+
const userDir = join(getAgentDir(), 'agents');
|
|
1413
|
+
const { completedTokens } = parseArgumentText(argumentText);
|
|
1414
|
+
const action = completedTokens[0];
|
|
1415
|
+
|
|
1416
|
+
if (!action) {
|
|
1417
|
+
return buildArgumentCompletions(argumentText, [
|
|
1418
|
+
{ value: 'list', description: 'Show bundled and overridden agents' },
|
|
1419
|
+
{ value: 'reset', description: 'Restore bundled agents by removing user overrides' },
|
|
1420
|
+
{ value: 'edit', description: 'Copy a bundled agent into your user agents directory' },
|
|
1421
|
+
]);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (action === 'reset') {
|
|
1425
|
+
if (completedTokens.length > 1) return null;
|
|
1426
|
+
const bundled = await listMdFiles(bundledAgentsDir);
|
|
1427
|
+
const user = await listMdFiles(userDir);
|
|
1428
|
+
const resettable = [...user].filter((name) => bundled.has(name)).sort();
|
|
1429
|
+
return buildArgumentCompletions(argumentText, [
|
|
1430
|
+
{ value: '--all', description: 'Reset all user overrides that have bundled versions' },
|
|
1431
|
+
...resettable.map((name) => ({
|
|
1432
|
+
value: name,
|
|
1433
|
+
description: 'Reset this user override to the bundled version',
|
|
1434
|
+
})),
|
|
1435
|
+
]);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (action === 'edit') {
|
|
1439
|
+
if (completedTokens.length > 1) return null;
|
|
1440
|
+
const bundled = [...(await listMdFiles(bundledAgentsDir))].sort();
|
|
1441
|
+
return buildArgumentCompletions(
|
|
1442
|
+
argumentText,
|
|
1443
|
+
bundled.map((name) => ({
|
|
1444
|
+
value: name,
|
|
1445
|
+
description: 'Copy this bundled agent into your user agents directory',
|
|
1446
|
+
})),
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
return null;
|
|
1451
|
+
},
|
|
1381
1452
|
handler: async (args, ctx) => {
|
|
1453
|
+
autocompleteCwd = ctx.cwd;
|
|
1382
1454
|
const userDir = join(getAgentDir(), 'agents');
|
|
1383
1455
|
const parts = args?.trim().split(/\s+/) || [];
|
|
1384
1456
|
const action = parts[0] || 'list';
|
|
@@ -1474,14 +1546,95 @@ export default function (pi: ExtensionAPI) {
|
|
|
1474
1546
|
pi.registerCommand('run-agent', {
|
|
1475
1547
|
description:
|
|
1476
1548
|
'Run a single agent directly — honors agent sessionStrategy metadata such as fork-at',
|
|
1549
|
+
getArgumentCompletions: async (argumentText) => {
|
|
1550
|
+
const { completedTokens } = parseArgumentText(argumentText);
|
|
1551
|
+
let agentScope: AgentScope = 'user';
|
|
1552
|
+
let expectsScopeValue = false;
|
|
1553
|
+
let expectsThinkingValue = false;
|
|
1554
|
+
let expectsModelValue = false;
|
|
1555
|
+
let agentName: string | undefined;
|
|
1556
|
+
|
|
1557
|
+
for (const token of completedTokens) {
|
|
1558
|
+
if (expectsScopeValue) {
|
|
1559
|
+
if (token === 'user' || token === 'project' || token === 'both') {
|
|
1560
|
+
agentScope = token;
|
|
1561
|
+
}
|
|
1562
|
+
expectsScopeValue = false;
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
if (expectsThinkingValue) {
|
|
1566
|
+
expectsThinkingValue = false;
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
if (expectsModelValue) {
|
|
1570
|
+
expectsModelValue = false;
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
if (token === '--scope') {
|
|
1574
|
+
expectsScopeValue = true;
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
if (token === '--thinking' || token === '--reasoning-level') {
|
|
1578
|
+
expectsThinkingValue = true;
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
if (token === '--model') {
|
|
1582
|
+
expectsModelValue = true;
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
if (token === '--yes-project-agents') continue;
|
|
1586
|
+
if (token.startsWith('--')) continue;
|
|
1587
|
+
if (!agentName) agentName = token;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
if (expectsScopeValue) {
|
|
1591
|
+
return buildArgumentCompletions(argumentText, [
|
|
1592
|
+
{ value: 'user', description: 'Only user/global agents from ~/.pi/agent/agents' },
|
|
1593
|
+
{ value: 'project', description: 'Only project-local agents from .pi/agents' },
|
|
1594
|
+
{ value: 'both', description: 'User/global agents plus project-local agents' },
|
|
1595
|
+
]);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (expectsThinkingValue) {
|
|
1599
|
+
return buildArgumentCompletions(argumentText, [
|
|
1600
|
+
{ value: 'off', description: 'Disable reasoning if the model supports it' },
|
|
1601
|
+
{ value: 'minimal', description: 'Minimal reasoning effort' },
|
|
1602
|
+
{ value: 'low', description: 'Lower reasoning effort' },
|
|
1603
|
+
{ value: 'medium', description: 'Balanced reasoning effort' },
|
|
1604
|
+
{ value: 'high', description: 'Higher reasoning effort' },
|
|
1605
|
+
{ value: 'xhigh', description: 'Maximum reasoning effort on supported models' },
|
|
1606
|
+
]);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (expectsModelValue) {
|
|
1610
|
+
const models = await getAutocompleteModels(agentScope);
|
|
1611
|
+
return buildArgumentCompletions(argumentText, models);
|
|
1612
|
+
}
|
|
1613
|
+
if (agentName) return null;
|
|
1614
|
+
|
|
1615
|
+
const agents = await getAutocompleteAgents(agentScope);
|
|
1616
|
+
return buildArgumentCompletions(argumentText, [
|
|
1617
|
+
{ value: '--scope', description: 'Choose which agent directories are searched' },
|
|
1618
|
+
{ value: '--model', description: 'Override the agent model for this run' },
|
|
1619
|
+
{ value: '--thinking', description: 'Override the reasoning level for this run' },
|
|
1620
|
+
{ value: '--reasoning-level', description: 'Alias for --thinking' },
|
|
1621
|
+
{ value: '--yes-project-agents', description: 'Skip the project-agent trust confirmation prompt' },
|
|
1622
|
+
...agents.map((agent) => ({
|
|
1623
|
+
value: agent.name,
|
|
1624
|
+
label: agent.name,
|
|
1625
|
+
description: `${agent.source} — ${agent.description}`,
|
|
1626
|
+
})),
|
|
1627
|
+
]);
|
|
1628
|
+
},
|
|
1477
1629
|
handler: async (args, ctx) => {
|
|
1630
|
+
autocompleteCwd = ctx.cwd;
|
|
1478
1631
|
const parsed = parseRunAgentArgs(args);
|
|
1479
1632
|
if (!parsed.ok) {
|
|
1480
1633
|
ctx.ui.notify(parsed.error, 'warning');
|
|
1481
1634
|
return;
|
|
1482
1635
|
}
|
|
1483
1636
|
|
|
1484
|
-
const { agentName, explicitTask, agentScope, confirmProjectAgents } = parsed.options;
|
|
1637
|
+
const { agentName, explicitTask, agentScope, confirmProjectAgents, model, thinking } = parsed.options;
|
|
1485
1638
|
if (!agentName) {
|
|
1486
1639
|
ctx.ui.notify(formatRunAgentUsage(), 'warning');
|
|
1487
1640
|
return;
|
|
@@ -1516,7 +1669,65 @@ export default function (pi: ExtensionAPI) {
|
|
|
1516
1669
|
return;
|
|
1517
1670
|
}
|
|
1518
1671
|
|
|
1519
|
-
|
|
1672
|
+
const runAndReport = async (
|
|
1673
|
+
commandCwd: string,
|
|
1674
|
+
commandUi: typeof ctx.ui,
|
|
1675
|
+
sendSummary: (message: {
|
|
1676
|
+
customType: 'run-agent-summary';
|
|
1677
|
+
content: string;
|
|
1678
|
+
display: true;
|
|
1679
|
+
details: RunAgentSummaryDetails;
|
|
1680
|
+
}) => Promise<void> | void,
|
|
1681
|
+
forked: boolean,
|
|
1682
|
+
) => {
|
|
1683
|
+
const selectedModel = model ?? agent.model;
|
|
1684
|
+
const selectedThinking = thinking ?? agent.thinking;
|
|
1685
|
+
|
|
1686
|
+
commandUi.notify(
|
|
1687
|
+
`Running agent: ${agent.name} [${agent.source}${forked ? ', forked' : ', inline'}${selectedModel ? `, model=${selectedModel}` : ''}${selectedThinking ? `, thinking=${selectedThinking}` : ''}]`,
|
|
1688
|
+
'info',
|
|
1689
|
+
);
|
|
1690
|
+
|
|
1691
|
+
const result = await runAgent(commandCwd, agent.name, task, {
|
|
1692
|
+
agentScope,
|
|
1693
|
+
resolvedPaths,
|
|
1694
|
+
model,
|
|
1695
|
+
thinking,
|
|
1696
|
+
});
|
|
1697
|
+
const failed = result.exitCode !== 0 || result.stopReason === 'error';
|
|
1698
|
+
const output = getFinalText(result).trim();
|
|
1699
|
+
|
|
1700
|
+
commandUi.notify(
|
|
1701
|
+
failed ? `Agent "${agent.name}" failed.` : `Agent "${agent.name}" complete.`,
|
|
1702
|
+
failed ? 'error' : 'info',
|
|
1703
|
+
);
|
|
1704
|
+
|
|
1705
|
+
await sendSummary({
|
|
1706
|
+
customType: 'run-agent-summary',
|
|
1707
|
+
content: formatRunAgentSummary({
|
|
1708
|
+
agent,
|
|
1709
|
+
agentScope,
|
|
1710
|
+
task,
|
|
1711
|
+
result,
|
|
1712
|
+
forked,
|
|
1713
|
+
}),
|
|
1714
|
+
display: true,
|
|
1715
|
+
details: {
|
|
1716
|
+
agent: agent.name,
|
|
1717
|
+
agentSource: agent.source,
|
|
1718
|
+
agentScope,
|
|
1719
|
+
sessionStrategy: agent.sessionStrategy ?? 'inline',
|
|
1720
|
+
forked,
|
|
1721
|
+
exitCode: result.exitCode,
|
|
1722
|
+
stopReason: result.stopReason,
|
|
1723
|
+
errorMessage: result.errorMessage,
|
|
1724
|
+
usage: result.usage,
|
|
1725
|
+
task,
|
|
1726
|
+
output,
|
|
1727
|
+
} satisfies RunAgentSummaryDetails,
|
|
1728
|
+
});
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1520
1731
|
if (agent.sessionStrategy === 'fork-at') {
|
|
1521
1732
|
const leafId = ctx.sessionManager.getLeafId?.();
|
|
1522
1733
|
if (!leafId) {
|
|
@@ -1525,188 +1736,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
1525
1736
|
'warning',
|
|
1526
1737
|
);
|
|
1527
1738
|
} else {
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1739
|
+
const forkResult = await ctx.fork(leafId, {
|
|
1740
|
+
position: 'at',
|
|
1741
|
+
withSession: async (replacementCtx) => {
|
|
1742
|
+
await runAndReport(
|
|
1743
|
+
replacementCtx.cwd,
|
|
1744
|
+
replacementCtx.ui,
|
|
1745
|
+
(message) => replacementCtx.sendMessage(message),
|
|
1746
|
+
true,
|
|
1747
|
+
);
|
|
1748
|
+
},
|
|
1749
|
+
});
|
|
1533
1750
|
if (forkResult.cancelled) {
|
|
1534
1751
|
ctx.ui.notify(`Run cancelled — unable to fork session for "${agent.name}".`, 'info');
|
|
1535
|
-
return;
|
|
1536
1752
|
}
|
|
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
1753
|
return;
|
|
1649
1754
|
}
|
|
1650
1755
|
}
|
|
1651
1756
|
|
|
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
|
-
});
|
|
1757
|
+
await runAndReport(ctx.cwd, ctx.ui, (message) => pi.sendMessage(message), false);
|
|
1710
1758
|
},
|
|
1711
1759
|
});
|
|
1760
|
+
|
|
1712
1761
|
}
|