@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 +22 -0
- package/extensions/subagent/agent-runner.ts +3 -1
- package/extensions/subagent/create-agent.ts +98 -0
- package/extensions/subagent/index.ts +75 -1
- package/extensions/subagent/list-agents.ts +92 -0
- package/extensions/subagent/spawn-utils.ts +15 -0
- package/package.json +1 -1
- package/skills/spawn-subagents/SKILL.md +6 -0
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
|
@@ -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.
|