@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.
@@ -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 '@sinclair/typebox';
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
- formatDelegateUsage,
47
- formatRunAgentUsage,
48
- parseDelegateArgs,
49
- parseRunAgentArgs,
50
- } from './delegate-args.js';
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 (agent.model) args.push('--model', agent.model);
348
- if (agent.thinking) args.push('--thinking', agent.thinking);
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: agent.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
- previousOutput = getFinalOutput(result.messages);
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
- // ── Delegate commands ──────────────────────────────────────────────────────
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
- let forked = false;
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 forkCompat = ctx.fork as unknown as (
1529
- entryId: string,
1530
- options?: { position: 'at' },
1531
- ) => Promise<{ cancelled: boolean }>;
1532
- const forkResult = await forkCompat(leafId, { position: 'at' });
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
- // Step 4: Execute phases
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
  }