@dreki-gg/pi-subagent 0.8.2 → 0.8.4

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # @dreki-gg/pi-subagent
2
2
 
3
+ ## 0.8.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [`376864c`](https://github.com/dreki-gg/pi-extensions/commit/376864c37cefa47530363b47055311269c1724a8) Thanks [@jalbarrang](https://github.com/jalbarrang)! - feat(subagent): show live tool-aware status in working message during /run-agent
8
+
9
+ Previously `/run-agent` only showed a transient notification, leaving users with no visibility into what the background agent was doing (especially after a fork-at session switch that visually looks like a reload). Now the working message updates in real-time as the agent works:
10
+
11
+ - `scout · starting...`
12
+ - `scout · reading …/src/utils.ts`
13
+ - `scout · $ bun test --filter...`
14
+ - `scout · editing …/config.json`
15
+ - `scout · thinking...`
16
+
17
+ Added `onToolExecutionStart` callback to `spawnPiAgent` and `runAgent` to surface `tool_execution_start` events from the JSON stream.
18
+
19
+ ## 0.8.3
20
+
21
+ ### Patch Changes
22
+
23
+ - [`6f0b219`](https://github.com/dreki-gg/pi-extensions/commit/6f0b219ac357ce1607a7a8211fd1c66bd35c62f1) Thanks [@jalbarrang](https://github.com/jalbarrang)! - fix(subagent): resolve TDZ crash when onMessage/onToolResult callbacks fire before spawnResult is assigned
24
+
25
+ Previously, `runSingleAgent` declared `const spawnResult = await spawnPiAgent({...})` and referenced `spawnResult` inside the `onMessage`/`onToolResult` callbacks. Since callbacks fire during the await (before the const is assigned), this caused a `ReferenceError: Cannot access 'spawnResult' before initialization`. Now uses `let` with a guard to safely accumulate messages when the result is not yet available.
26
+
3
27
  ## 0.8.2
4
28
 
5
29
  ### Patch Changes
@@ -1,7 +1,7 @@
1
1
  import type { ResolvedPaths } from '@earendil-works/pi-coding-agent';
2
2
  import { discoverAgents, type AgentScope } from './agents.js';
3
3
  import type { AgentResult } from './agent-runner-types.js';
4
- import { emptyUsage, spawnPiAgent } from './spawn-utils.js';
4
+ import { emptyUsage, spawnPiAgent, type ToolExecutionStartEvent } from './spawn-utils.js';
5
5
 
6
6
  export type OnPhaseUpdate = (phaseName: string, agentName: string, result: AgentResult) => void;
7
7
 
@@ -11,6 +11,7 @@ export interface RunAgentOptions {
11
11
  model?: string;
12
12
  thinking?: string;
13
13
  onUpdate?: OnPhaseUpdate;
14
+ onToolExecutionStart?: (event: ToolExecutionStartEvent) => void;
14
15
  phaseName?: string;
15
16
  signal?: AbortSignal;
16
17
  resolvedPaths?: ResolvedPaths;
@@ -76,6 +77,7 @@ export async function runAgent(
76
77
  options.onUpdate(options.phaseName ?? 'unknown', agentName, { ...result });
77
78
  }
78
79
  },
80
+ onToolExecutionStart: options.onToolExecutionStart,
79
81
  });
80
82
 
81
83
  result.exitCode = spawnResult.exitCode;
@@ -27,26 +27,77 @@ import {
27
27
  SettingsManager,
28
28
  } from '@earendil-works/pi-coding-agent';
29
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';
32
30
  import {
33
- type AgentConfig,
34
- type AgentScope,
35
- type AgentSource,
36
- discoverAgents,
37
- } from './agents.js';
31
+ Box,
32
+ Container,
33
+ Markdown,
34
+ Spacer,
35
+ Text,
36
+ type AutocompleteItem,
37
+ } from '@earendil-works/pi-tui';
38
+ import { Type } from 'typebox';
39
+ import { type AgentConfig, type AgentScope, type AgentSource, discoverAgents } from './agents.js';
38
40
  import { formatRunAgentUsage, parseRunAgentArgs } from './run-agent-args.js';
39
41
  import { runAgent } from './agent-runner.js';
40
42
  import { extractRecentConversation } from './synthesis.js';
41
43
  import type { AgentResult } from './agent-runner-types.js';
42
44
  import { getFinalText } from './agent-result-utils.js';
43
45
  import { buildHandoffFromResult, renderHandoffForPrompt } from './handoffs.js';
44
- import { emptyUsage, spawnPiAgent } from './spawn-utils.js';
46
+ import { emptyUsage, spawnPiAgent, type ToolExecutionStartEvent } from './spawn-utils.js';
45
47
 
46
48
  const MAX_PARALLEL_TASKS = 8;
47
49
  const MAX_CONCURRENCY = 4;
48
50
  const COLLAPSED_ITEM_COUNT = 10;
49
51
 
52
+ function describeToolExecution(event: ToolExecutionStartEvent): string {
53
+ const shortenPath = (p: string) => {
54
+ const home = os.homedir();
55
+ const shortened = p.startsWith(home) ? `~${p.slice(home.length)}` : p;
56
+ // Show only the last 2 path segments for brevity
57
+ const parts = shortened.split('/');
58
+ return parts.length > 3 ? `…/${parts.slice(-2).join('/')}` : shortened;
59
+ };
60
+
61
+ const args = event.args;
62
+ switch (event.toolName) {
63
+ case 'bash': {
64
+ const cmd = (args.command as string) || '';
65
+ const preview = cmd.length > 50 ? `${cmd.slice(0, 50)}…` : cmd;
66
+ return `$ ${preview}`;
67
+ }
68
+ case 'read': {
69
+ const p = ((args.file_path || args.path) as string) || '';
70
+ return `reading ${shortenPath(p)}`;
71
+ }
72
+ case 'write': {
73
+ const p = ((args.file_path || args.path) as string) || '';
74
+ return `writing ${shortenPath(p)}`;
75
+ }
76
+ case 'edit': {
77
+ const p = ((args.file_path || args.path) as string) || '';
78
+ return `editing ${shortenPath(p)}`;
79
+ }
80
+ case 'grep': {
81
+ const pattern = (args.pattern as string) || '';
82
+ return `grep /${pattern}/`;
83
+ }
84
+ case 'find': {
85
+ const pattern = (args.pattern as string) || '*';
86
+ return `find ${pattern}`;
87
+ }
88
+ case 'ls':
89
+ return `ls ${shortenPath((args.path as string) || '.')}`;
90
+ case 'web_search':
91
+ return `searching: ${(args.query as string)?.slice(0, 40) || '…'}`;
92
+ case 'web_visit':
93
+ return 'visiting page';
94
+ case 'subagent':
95
+ return `spawning ${(args.agent as string) || 'subagent'}`;
96
+ default:
97
+ return event.toolName;
98
+ }
99
+ }
100
+
50
101
  function formatTokens(count: number): string {
51
102
  if (count < 1000) return count.toString();
52
103
  if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
@@ -247,7 +298,6 @@ async function mapWithConcurrencyLimit<TIn, TOut>(
247
298
  return results;
248
299
  }
249
300
 
250
-
251
301
  type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
252
302
 
253
303
  async function runSingleAgent(
@@ -303,7 +353,8 @@ async function runSingleAgent(
303
353
  }
304
354
  };
305
355
 
306
- const spawnResult = await spawnPiAgent({
356
+ let spawnResult: Awaited<ReturnType<typeof spawnPiAgent>> | undefined;
357
+ spawnResult = await spawnPiAgent({
307
358
  cwd: cwd ?? defaultCwd,
308
359
  agentName: agent.name,
309
360
  task,
@@ -313,15 +364,23 @@ async function runSingleAgent(
313
364
  tools: agent.tools,
314
365
  signal,
315
366
  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;
367
+ if (!spawnResult) {
368
+ currentResult.messages = [...currentResult.messages, msg];
369
+ } else {
370
+ currentResult.messages = spawnResult.messages;
371
+ currentResult.usage = spawnResult.usage;
372
+ currentResult.model = spawnResult.model ?? currentResult.model;
373
+ currentResult.stopReason = spawnResult.stopReason;
374
+ currentResult.errorMessage = spawnResult.errorMessage;
375
+ }
321
376
  emitUpdate();
322
377
  },
323
- onToolResult: () => {
324
- currentResult.messages = spawnResult.messages;
378
+ onToolResult: (msg) => {
379
+ if (!spawnResult) {
380
+ currentResult.messages = [...currentResult.messages, msg];
381
+ } else {
382
+ currentResult.messages = spawnResult.messages;
383
+ }
325
384
  emitUpdate();
326
385
  },
327
386
  });
@@ -373,13 +432,19 @@ const SubagentParams = Type.Object({
373
432
  Type.String({ description: 'Optional default model override for the run or all steps/tasks' }),
374
433
  ),
375
434
  thinking: Type.Optional(
376
- Type.String({ description: 'Optional default reasoning level override for the run or all steps/tasks' }),
435
+ Type.String({
436
+ description: 'Optional default reasoning level override for the run or all steps/tasks',
437
+ }),
377
438
  ),
378
439
  tasks: Type.Optional(
379
- Type.Array(TaskItem, { description: 'Array of {agent, task, model?, thinking?} for parallel execution' }),
440
+ Type.Array(TaskItem, {
441
+ description: 'Array of {agent, task, model?, thinking?} for parallel execution',
442
+ }),
380
443
  ),
381
444
  chain: Type.Optional(
382
- Type.Array(ChainItem, { description: 'Array of {agent, task, model?, thinking?} for sequential execution' }),
445
+ Type.Array(ChainItem, {
446
+ description: 'Array of {agent, task, model?, thinking?} for sequential execution',
447
+ }),
383
448
  ),
384
449
  agentScope: Type.Optional(AgentScopeSchema),
385
450
  confirmProjectAgents: Type.Optional(
@@ -424,9 +489,7 @@ export default function (pi: ExtensionAPI) {
424
489
  if (!trimmedEnd) return replacement;
425
490
  if (/\s$/.test(argumentText)) return `${trimmedEnd} ${replacement}`;
426
491
  const lastSpace = trimmedEnd.lastIndexOf(' ');
427
- return lastSpace === -1
428
- ? replacement
429
- : `${trimmedEnd.slice(0, lastSpace + 1)}${replacement}`;
492
+ return lastSpace === -1 ? replacement : `${trimmedEnd.slice(0, lastSpace + 1)}${replacement}`;
430
493
  }
431
494
 
432
495
  function buildArgumentCompletions(
@@ -456,9 +519,9 @@ export default function (pi: ExtensionAPI) {
456
519
  return discoverAgents(autocompleteCwd, scope, resolvedPaths).agents;
457
520
  }
458
521
 
459
- async function getAutocompleteModels(scope: AgentScope): Promise<
460
- Array<{ value: string; label?: string; description?: string }>
461
- > {
522
+ async function getAutocompleteModels(
523
+ scope: AgentScope,
524
+ ): Promise<Array<{ value: string; label?: string; description?: string }>> {
462
525
  const items = new Map<string, { value: string; label?: string; description?: string }>();
463
526
 
464
527
  try {
@@ -1238,7 +1301,9 @@ export default function (pi: ExtensionAPI) {
1238
1301
  title,
1239
1302
  modeLine,
1240
1303
  theme.fg('muted', `Task: ${details.task}`),
1241
- failed && details.errorMessage ? theme.fg('error', `Error: ${details.errorMessage}`) : previewLines,
1304
+ failed && details.errorMessage
1305
+ ? theme.fg('error', `Error: ${details.errorMessage}`)
1306
+ : previewLines,
1242
1307
  usageLine ? theme.fg('dim', usageLine) : undefined,
1243
1308
  theme.fg('muted', '(Ctrl+O to expand)'),
1244
1309
  ]
@@ -1347,7 +1412,10 @@ export default function (pi: ExtensionAPI) {
1347
1412
  { value: '--model', description: 'Override the agent model for this run' },
1348
1413
  { value: '--thinking', description: 'Override the reasoning level for this run' },
1349
1414
  { value: '--reasoning-level', description: 'Alias for --thinking' },
1350
- { value: '--yes-project-agents', description: 'Skip the project-agent trust confirmation prompt' },
1415
+ {
1416
+ value: '--yes-project-agents',
1417
+ description: 'Skip the project-agent trust confirmation prompt',
1418
+ },
1351
1419
  ...agents.map((agent) => ({
1352
1420
  value: agent.name,
1353
1421
  label: agent.name,
@@ -1363,7 +1431,8 @@ export default function (pi: ExtensionAPI) {
1363
1431
  return;
1364
1432
  }
1365
1433
 
1366
- const { agentName, explicitTask, agentScope, confirmProjectAgents, model, thinking } = parsed.options;
1434
+ const { agentName, explicitTask, agentScope, confirmProjectAgents, model, thinking } =
1435
+ parsed.options;
1367
1436
  if (!agentName) {
1368
1437
  ctx.ui.notify(formatRunAgentUsage(), 'warning');
1369
1438
  return;
@@ -1394,7 +1463,10 @@ export default function (pi: ExtensionAPI) {
1394
1463
  const conversation = extractRecentConversation(ctx);
1395
1464
  const task = buildRunAgentTask(conversation, explicitTask);
1396
1465
  if (!task) {
1397
- ctx.ui.notify(`No conversation context and no task specified.\n\n${formatRunAgentUsage()}`, 'warning');
1466
+ ctx.ui.notify(
1467
+ `No conversation context and no task specified.\n\n${formatRunAgentUsage()}`,
1468
+ 'warning',
1469
+ );
1398
1470
  return;
1399
1471
  }
1400
1472
 
@@ -1416,13 +1488,29 @@ export default function (pi: ExtensionAPI) {
1416
1488
  `Running agent: ${agent.name} [${agent.source}${forked ? ', forked' : ', inline'}${selectedModel ? `, model=${selectedModel}` : ''}${selectedThinking ? `, thinking=${selectedThinking}` : ''}]`,
1417
1489
  'info',
1418
1490
  );
1491
+ let lastToolDescription = '';
1492
+ const updateWorkingStatus = (toolDesc?: string) => {
1493
+ if (toolDesc) lastToolDescription = toolDesc;
1494
+ const parts = [agent.name];
1495
+ if (lastToolDescription) parts.push(lastToolDescription);
1496
+ commandUi.setWorkingMessage(parts.join(' · '));
1497
+ };
1498
+ updateWorkingStatus('starting...');
1419
1499
 
1420
1500
  const result = await runAgent(commandCwd, agent.name, task, {
1421
1501
  agentScope,
1422
1502
  resolvedPaths,
1423
1503
  model,
1424
1504
  thinking,
1505
+ onUpdate: () => {
1506
+ // When a turn completes and the agent is thinking about next steps
1507
+ updateWorkingStatus('thinking...');
1508
+ },
1509
+ onToolExecutionStart: (event) => {
1510
+ updateWorkingStatus(describeToolExecution(event));
1511
+ },
1425
1512
  });
1513
+ commandUi.setWorkingMessage();
1426
1514
  const failed = result.exitCode !== 0 || result.stopReason === 'error';
1427
1515
  const output = getFinalText(result).trim();
1428
1516
 
@@ -1486,5 +1574,4 @@ export default function (pi: ExtensionAPI) {
1486
1574
  await runAndReport(ctx.cwd, ctx.ui, (message) => pi.sendMessage(message), false);
1487
1575
  },
1488
1576
  });
1489
-
1490
1577
  }
@@ -57,6 +57,12 @@ function cleanupTempFiles(
57
57
  if (tmpPromptDir) try { fs.rmdirSync(tmpPromptDir); } catch { /* ignore */ }
58
58
  }
59
59
 
60
+ export interface ToolExecutionStartEvent {
61
+ toolCallId: string;
62
+ toolName: string;
63
+ args: Record<string, unknown>;
64
+ }
65
+
60
66
  export interface SpawnPiAgentOptions {
61
67
  cwd: string;
62
68
  agentName: string;
@@ -68,6 +74,7 @@ export interface SpawnPiAgentOptions {
68
74
  signal?: AbortSignal;
69
75
  onMessage?: (msg: Message) => void;
70
76
  onToolResult?: (msg: Message) => void;
77
+ onToolExecutionStart?: (event: ToolExecutionStartEvent) => void;
71
78
  }
72
79
 
73
80
  export interface SpawnPiAgentResult {
@@ -158,6 +165,14 @@ export async function spawnPiAgent(options: SpawnPiAgentOptions): Promise<SpawnP
158
165
  result.messages.push(event.message as Message);
159
166
  options.onToolResult?.(event.message as Message);
160
167
  }
168
+
169
+ if (event.type === 'tool_execution_start' && event.toolName) {
170
+ options.onToolExecutionStart?.({
171
+ toolCallId: event.toolCallId ?? '',
172
+ toolName: event.toolName,
173
+ args: event.args ?? {},
174
+ });
175
+ }
161
176
  };
162
177
 
163
178
  proc.stdout.on('data', (data: Buffer) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-subagent",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Subagent tool and direct agent runs for pi — isolated agents, parallel scouts, manager workflows, and bundled prompts",
5
5
  "keywords": [
6
6
  "pi-package"