@dreki-gg/pi-subagent 0.8.3 → 0.9.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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @dreki-gg/pi-subagent
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`99bc4e7`](https://github.com/dreki-gg/pi-extensions/commit/99bc4e7abb9f468ebd6705a56ed1c2a801dd466e) Thanks [@jalbarrang](https://github.com/jalbarrang)! - Add `list_agents` tool and `/create-agent` command to help the LLM discover available agents before spawning and scaffold new project-local agent prompts.
8
+
9
+ ## 0.8.4
10
+
11
+ ### Patch Changes
12
+
13
+ - [`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
14
+
15
+ 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:
16
+
17
+ - `scout · starting...`
18
+ - `scout · reading …/src/utils.ts`
19
+ - `scout · $ bun test --filter...`
20
+ - `scout · editing …/config.json`
21
+ - `scout · thinking...`
22
+
23
+ Added `onToolExecutionStart` callback to `spawnPiAgent` and `runAgent` to surface `tool_execution_start` events from the JSON stream.
24
+
3
25
  ## 0.8.3
4
26
 
5
27
  ### 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;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * /create-agent command — scaffolds a new project-local agent prompt in .pi/prompts/.
3
+ */
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
8
+
9
+ const TEMPLATE = `---
10
+ name: {{name}}
11
+ description: {{description}}
12
+ ---
13
+
14
+ You are the **{{name}}** agent.
15
+
16
+ ## Role
17
+
18
+ {{description}}
19
+
20
+ ## Guidelines
21
+
22
+ - Be concise and focused on your assigned task.
23
+ - Use available tools to gather context before acting.
24
+ - Report your findings or output clearly.
25
+ `;
26
+
27
+ function slugify(name: string): string {
28
+ return name
29
+ .toLowerCase()
30
+ .replace(/[^a-z0-9]+/g, '-')
31
+ .replace(/^-|-$/g, '');
32
+ }
33
+
34
+ export function registerCreateAgentCommand(pi: ExtensionAPI) {
35
+ pi.registerCommand('create-agent', {
36
+ description:
37
+ 'Create a new project-local agent prompt in .pi/prompts/. Usage: /create-agent <name> [description]',
38
+
39
+ handler: async (args, ctx) => {
40
+ if (!args?.trim()) {
41
+ ctx.ui.notify(
42
+ 'Usage: /create-agent <name> [description]\n\nCreates a new agent prompt file in .pi/prompts/',
43
+ 'warning',
44
+ );
45
+ return;
46
+ }
47
+
48
+ const tokens = args.trim().split(/\s+/);
49
+ const name = slugify(tokens[0]);
50
+ const description = tokens.slice(1).join(' ') || `A project-local ${name} agent.`;
51
+
52
+ if (!name) {
53
+ ctx.ui.notify('Invalid agent name.', 'error');
54
+ return;
55
+ }
56
+
57
+ const promptsDir = path.join(ctx.cwd, '.pi', 'prompts');
58
+ const filePath = path.join(promptsDir, `${name}.md`);
59
+
60
+ if (fs.existsSync(filePath)) {
61
+ ctx.ui.notify(
62
+ `Agent "${name}" already exists at ${filePath}`,
63
+ 'warning',
64
+ );
65
+ return;
66
+ }
67
+
68
+ // Create directory if needed
69
+ fs.mkdirSync(promptsDir, { recursive: true });
70
+
71
+ // Write the template
72
+ const content = TEMPLATE
73
+ .replace(/\{\{name\}\}/g, name)
74
+ .replace(/\{\{description\}\}/g, description);
75
+
76
+ fs.writeFileSync(filePath, content, 'utf-8');
77
+
78
+ ctx.ui.notify(
79
+ `Created agent "${name}" at ${path.relative(ctx.cwd, filePath)}`,
80
+ 'info',
81
+ );
82
+
83
+ // Send a follow-up so the LLM can help refine the prompt
84
+ pi.sendUserMessage(
85
+ [
86
+ `I just created a new project-local agent prompt at \`${path.relative(ctx.cwd, filePath)}\`.`,
87
+ '',
88
+ `Agent name: **${name}**`,
89
+ `Description: ${description}`,
90
+ '',
91
+ 'The file has a basic template. You can now read and refine the agent prompt to fit your needs.',
92
+ 'Remember to use `agentScope: "both"` or `agentScope: "project"` when spawning this agent.',
93
+ ].join('\n'),
94
+ { deliverAs: 'followUp' },
95
+ );
96
+ },
97
+ });
98
+ }
@@ -43,12 +43,63 @@ import { extractRecentConversation } from './synthesis.js';
43
43
  import type { AgentResult } from './agent-runner-types.js';
44
44
  import { getFinalText } from './agent-result-utils.js';
45
45
  import { buildHandoffFromResult, renderHandoffForPrompt } from './handoffs.js';
46
- import { emptyUsage, spawnPiAgent } from './spawn-utils.js';
46
+ import { emptyUsage, spawnPiAgent, type ToolExecutionStartEvent } from './spawn-utils.js';
47
+ import { registerListAgentsTool } from './list-agents.js';
48
+ import { registerCreateAgentCommand } from './create-agent.js';
47
49
 
48
50
  const MAX_PARALLEL_TASKS = 8;
49
51
  const MAX_CONCURRENCY = 4;
50
52
  const COLLAPSED_ITEM_COUNT = 10;
51
53
 
54
+ function describeToolExecution(event: ToolExecutionStartEvent): string {
55
+ const shortenPath = (p: string) => {
56
+ const home = os.homedir();
57
+ const shortened = p.startsWith(home) ? `~${p.slice(home.length)}` : p;
58
+ // Show only the last 2 path segments for brevity
59
+ const parts = shortened.split('/');
60
+ return parts.length > 3 ? `…/${parts.slice(-2).join('/')}` : shortened;
61
+ };
62
+
63
+ const args = event.args;
64
+ switch (event.toolName) {
65
+ case 'bash': {
66
+ const cmd = (args.command as string) || '';
67
+ const preview = cmd.length > 50 ? `${cmd.slice(0, 50)}…` : cmd;
68
+ return `$ ${preview}`;
69
+ }
70
+ case 'read': {
71
+ const p = ((args.file_path || args.path) as string) || '';
72
+ return `reading ${shortenPath(p)}`;
73
+ }
74
+ case 'write': {
75
+ const p = ((args.file_path || args.path) as string) || '';
76
+ return `writing ${shortenPath(p)}`;
77
+ }
78
+ case 'edit': {
79
+ const p = ((args.file_path || args.path) as string) || '';
80
+ return `editing ${shortenPath(p)}`;
81
+ }
82
+ case 'grep': {
83
+ const pattern = (args.pattern as string) || '';
84
+ return `grep /${pattern}/`;
85
+ }
86
+ case 'find': {
87
+ const pattern = (args.pattern as string) || '*';
88
+ return `find ${pattern}`;
89
+ }
90
+ case 'ls':
91
+ return `ls ${shortenPath((args.path as string) || '.')}`;
92
+ case 'web_search':
93
+ return `searching: ${(args.query as string)?.slice(0, 40) || '…'}`;
94
+ case 'web_visit':
95
+ return 'visiting page';
96
+ case 'subagent':
97
+ return `spawning ${(args.agent as string) || 'subagent'}`;
98
+ default:
99
+ return event.toolName;
100
+ }
101
+ }
102
+
52
103
  function formatTokens(count: number): string {
53
104
  if (count < 1000) return count.toString();
54
105
  if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
@@ -505,6 +556,9 @@ export default function (pi: ExtensionAPI) {
505
556
  return [...items.values()];
506
557
  }
507
558
 
559
+ registerListAgentsTool(pi, resolvePackagePaths);
560
+ registerCreateAgentCommand(pi);
561
+
508
562
  pi.registerTool({
509
563
  name: 'subagent',
510
564
  label: 'Subagent',
@@ -515,6 +569,10 @@ export default function (pi: ExtensionAPI) {
515
569
  'Default agent scope is "user" (from ~/.pi/agent/prompts).',
516
570
  'To enable project-local agents in .pi/prompts, set agentScope: "both" (or "project").',
517
571
  ].join(' '),
572
+ promptGuidelines: [
573
+ 'If you are unsure which agents are available, call list_agents first before using the subagent tool.',
574
+ 'Use /create-agent to scaffold new project-local agent prompts when the user needs a custom agent.',
575
+ ],
518
576
  parameters: SubagentParams,
519
577
 
520
578
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
@@ -1439,13 +1497,29 @@ export default function (pi: ExtensionAPI) {
1439
1497
  `Running agent: ${agent.name} [${agent.source}${forked ? ', forked' : ', inline'}${selectedModel ? `, model=${selectedModel}` : ''}${selectedThinking ? `, thinking=${selectedThinking}` : ''}]`,
1440
1498
  'info',
1441
1499
  );
1500
+ let lastToolDescription = '';
1501
+ const updateWorkingStatus = (toolDesc?: string) => {
1502
+ if (toolDesc) lastToolDescription = toolDesc;
1503
+ const parts = [agent.name];
1504
+ if (lastToolDescription) parts.push(lastToolDescription);
1505
+ commandUi.setWorkingMessage(parts.join(' · '));
1506
+ };
1507
+ updateWorkingStatus('starting...');
1442
1508
 
1443
1509
  const result = await runAgent(commandCwd, agent.name, task, {
1444
1510
  agentScope,
1445
1511
  resolvedPaths,
1446
1512
  model,
1447
1513
  thinking,
1514
+ onUpdate: () => {
1515
+ // When a turn completes and the agent is thinking about next steps
1516
+ updateWorkingStatus('thinking...');
1517
+ },
1518
+ onToolExecutionStart: (event) => {
1519
+ updateWorkingStatus(describeToolExecution(event));
1520
+ },
1448
1521
  });
1522
+ commandUi.setWorkingMessage();
1449
1523
  const failed = result.exitCode !== 0 || result.stopReason === 'error';
1450
1524
  const output = getFinalText(result).trim();
1451
1525
 
@@ -0,0 +1,92 @@
1
+ /**
2
+ * list_agents tool — lets the LLM discover available agents before spawning.
3
+ */
4
+
5
+ import type { ExtensionAPI, ResolvedPaths } from '@earendil-works/pi-coding-agent';
6
+ import { Type } from 'typebox';
7
+ import { type AgentScope, discoverAgents } from './agents.js';
8
+ import { StringEnum } from '@earendil-works/pi-ai';
9
+
10
+ export function registerListAgentsTool(
11
+ pi: ExtensionAPI,
12
+ resolvePackagePaths: (cwd: string) => Promise<ResolvedPaths | undefined>,
13
+ ) {
14
+ const AgentScopeSchema = StringEnum(['user', 'project', 'both'] as const, {
15
+ description:
16
+ 'Which agent directories to search. Default: "user". Use "both" to include project-local agents.',
17
+ default: 'user',
18
+ });
19
+
20
+ pi.registerTool({
21
+ name: 'list_agents',
22
+ label: 'List Agents',
23
+ description:
24
+ 'List available subagent prompts. Call this before spawning agents if you are unsure which agents exist.',
25
+ promptSnippet: 'List available subagent prompts to discover what agents can be spawned',
26
+ promptGuidelines: [
27
+ 'Call list_agents before using the subagent tool when you are not certain which agents are available.',
28
+ 'list_agents returns agent names, descriptions, sources, and capabilities.',
29
+ ],
30
+ parameters: Type.Object({
31
+ agentScope: Type.Optional(AgentScopeSchema),
32
+ }),
33
+
34
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
35
+ const agentScope: AgentScope = params.agentScope ?? 'user';
36
+ const resolvedPaths = await resolvePackagePaths(ctx.cwd);
37
+ const discovery = discoverAgents(ctx.cwd, agentScope, resolvedPaths);
38
+
39
+ if (discovery.agents.length === 0) {
40
+ const dirs = agentScope === 'user'
41
+ ? '~/.pi/agent/prompts'
42
+ : agentScope === 'project'
43
+ ? '.pi/prompts'
44
+ : '~/.pi/agent/prompts and .pi/prompts';
45
+ return {
46
+ content: [
47
+ {
48
+ type: 'text',
49
+ text: `No agents found in scope "${agentScope}". Check ${dirs} for agent prompt files.`,
50
+ },
51
+ ],
52
+ details: { agents: [], scope: agentScope },
53
+ };
54
+ }
55
+
56
+ const lines: string[] = [
57
+ `Available agents (scope: ${agentScope}):`,
58
+ '',
59
+ ];
60
+
61
+ for (const agent of discovery.agents) {
62
+ lines.push(`### ${agent.name}`);
63
+ lines.push(`- **Source**: ${agent.source}`);
64
+ lines.push(`- **Description**: ${agent.description}`);
65
+ if (agent.model) lines.push(`- **Default model**: ${agent.model}`);
66
+ if (agent.thinking) lines.push(`- **Default thinking**: ${agent.thinking}`);
67
+ if (agent.tools) lines.push(`- **Tools**: ${agent.tools.join(', ')}`);
68
+ if (agent.sessionStrategy) lines.push(`- **Session strategy**: ${agent.sessionStrategy}`);
69
+ lines.push('');
70
+ }
71
+
72
+ if (discovery.projectPromptsDir) {
73
+ lines.push(`Project prompts directory: ${discovery.projectPromptsDir}`);
74
+ }
75
+
76
+ return {
77
+ content: [{ type: 'text', text: lines.join('\n') }],
78
+ details: {
79
+ agents: discovery.agents.map((a) => ({
80
+ name: a.name,
81
+ description: a.description,
82
+ source: a.source,
83
+ model: a.model,
84
+ tools: a.tools,
85
+ })),
86
+ scope: agentScope,
87
+ projectPromptsDir: discovery.projectPromptsDir,
88
+ },
89
+ };
90
+ },
91
+ });
92
+ }
@@ -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.3",
3
+ "version": "0.9.0",
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"
@@ -114,6 +114,12 @@ Avoid:
114
114
  - calling `advisor` when `planner` or `reviewer` already has enough signal to proceed alone
115
115
  - sending speculative review findings back for fixes before they are validated when evidence is needed
116
116
 
117
+ ## 8. Agent discovery and creation
118
+ Use `list_agents` before spawning when you're unsure which agents exist.
119
+ Use `/create-agent <name> [description]` to scaffold a new project-local agent in `.pi/prompts/` when the user needs a custom agent that doesn't exist yet.
120
+ - After creating, read and refine the prompt file to fit the use case.
121
+ - Remember to use `agentScope: "both"` or `"project"` to include project-local agents.
122
+
117
123
  Execution rules:
118
124
  - Use `manager` when the task is too large for one prompt but still needs coherent decisions.
119
125
  - Use `advisor` for targeted second opinions on tricky or high-risk cases.