@dreki-gg/pi-subagent 0.7.0 → 0.8.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/.fallowrc.json +6 -0
- package/CHANGELOG.md +16 -0
- package/README.md +4 -13
- package/docs/plans/01_structured-handoffs-and-synthesis.plan.md +14 -14
- package/docs/plans/02_clean-context-review-loop.plan.md +12 -12
- package/docs/plans/03_parallel-recon-single-writer-docs.plan.md +4 -4
- package/docs/plans/04_manager-workflow.plan.md +10 -10
- package/docs/plans/05_advisor-routing.plan.md +22 -22
- package/extensions/subagent/agent-runner-types.ts +1 -1
- package/extensions/subagent/agent-runner.ts +40 -152
- package/extensions/subagent/agents.ts +31 -77
- package/extensions/subagent/index.ts +83 -354
- package/extensions/subagent/spawn-utils.ts +198 -0
- package/extensions/subagent/synthesis.ts +1 -51
- package/package.json +8 -8
- package/skills/write-an-agent/SKILL.md +5 -5
- /package/{agents → prompts}/advisor.md +0 -0
- /package/{agents → prompts}/bug-prover.md +0 -0
- /package/{agents → prompts}/docs-scout.md +0 -0
- /package/{agents → prompts}/manager.md +0 -0
- /package/{agents → prompts}/planner.md +0 -0
- /package/{agents → prompts}/reviewer.md +0 -0
- /package/{agents → prompts}/scout.md +0 -0
- /package/{agents → prompts}/ux-designer.md +0 -0
- /package/{agents → prompts}/validator.md +0 -0
- /package/{agents → prompts}/worker.md +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for spawning pi subagent processes.
|
|
3
|
+
* Used by both the `subagent` tool (index.ts) and the `/run-agent` command (agent-runner.ts).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { withFileMutationQueue } from '@earendil-works/pi-coding-agent';
|
|
11
|
+
import type { Message } from '@earendil-works/pi-ai';
|
|
12
|
+
import type { UsageStats } from './agent-runner-types.js';
|
|
13
|
+
|
|
14
|
+
export function emptyUsage(): UsageStats {
|
|
15
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
19
|
+
const currentScript = process.argv[1];
|
|
20
|
+
|
|
21
|
+
// Bun standalone binaries set argv[1] to a virtual FS path (e.g. /$bunfs/root/pi)
|
|
22
|
+
// that only resolves inside the running Bun process. Skip it — the binary IS the entry point.
|
|
23
|
+
const isBunVirtualPath = currentScript?.startsWith('/$bunfs/');
|
|
24
|
+
|
|
25
|
+
if (currentScript && !isBunVirtualPath && fs.existsSync(currentScript)) {
|
|
26
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
30
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
31
|
+
if (!isGenericRuntime) {
|
|
32
|
+
// Standalone binary (e.g. pi compiled with Bun) — invoke directly
|
|
33
|
+
return { command: process.execPath, args };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { command: 'pi', args };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function writePromptToTempFile(
|
|
40
|
+
agentName: string,
|
|
41
|
+
prompt: string,
|
|
42
|
+
): Promise<{ dir: string; filePath: string }> {
|
|
43
|
+
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pi-subagent-'));
|
|
44
|
+
const safeName = agentName.replace(/[^\w.-]+/g, '_');
|
|
45
|
+
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
46
|
+
await withFileMutationQueue(filePath, async () => {
|
|
47
|
+
await fs.promises.writeFile(filePath, prompt, { encoding: 'utf-8', mode: 0o600 });
|
|
48
|
+
});
|
|
49
|
+
return { dir: tmpDir, filePath };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cleanupTempFiles(
|
|
53
|
+
tmpPromptPath: string | null,
|
|
54
|
+
tmpPromptDir: string | null,
|
|
55
|
+
): void {
|
|
56
|
+
if (tmpPromptPath) try { fs.unlinkSync(tmpPromptPath); } catch { /* ignore */ }
|
|
57
|
+
if (tmpPromptDir) try { fs.rmdirSync(tmpPromptDir); } catch { /* ignore */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SpawnPiAgentOptions {
|
|
61
|
+
cwd: string;
|
|
62
|
+
agentName: string;
|
|
63
|
+
task: string;
|
|
64
|
+
systemPrompt?: string;
|
|
65
|
+
model?: string;
|
|
66
|
+
thinking?: string;
|
|
67
|
+
tools?: string[];
|
|
68
|
+
signal?: AbortSignal;
|
|
69
|
+
onMessage?: (msg: Message) => void;
|
|
70
|
+
onToolResult?: (msg: Message) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SpawnPiAgentResult {
|
|
74
|
+
exitCode: number;
|
|
75
|
+
messages: Message[];
|
|
76
|
+
stderr: string;
|
|
77
|
+
wasAborted: boolean;
|
|
78
|
+
usage: UsageStats;
|
|
79
|
+
model?: string;
|
|
80
|
+
stopReason?: string;
|
|
81
|
+
errorMessage?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Spawn a pi process for a subagent and collect results.
|
|
86
|
+
* This is the shared core that both the tool and the /run-agent command use.
|
|
87
|
+
*/
|
|
88
|
+
export async function spawnPiAgent(options: SpawnPiAgentOptions): Promise<SpawnPiAgentResult> {
|
|
89
|
+
const args: string[] = ['--mode', 'json', '-p', '--no-session'];
|
|
90
|
+
if (options.model) args.push('--model', options.model);
|
|
91
|
+
if (options.thinking) args.push('--thinking', options.thinking);
|
|
92
|
+
if (options.tools && options.tools.length > 0) args.push('--tools', options.tools.join(','));
|
|
93
|
+
|
|
94
|
+
let tmpPromptDir: string | null = null;
|
|
95
|
+
let tmpPromptPath: string | null = null;
|
|
96
|
+
|
|
97
|
+
const result: SpawnPiAgentResult = {
|
|
98
|
+
exitCode: 0,
|
|
99
|
+
messages: [],
|
|
100
|
+
stderr: '',
|
|
101
|
+
wasAborted: false,
|
|
102
|
+
usage: emptyUsage(),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
if (options.systemPrompt?.trim()) {
|
|
107
|
+
const tmp = await writePromptToTempFile(options.agentName, options.systemPrompt);
|
|
108
|
+
tmpPromptDir = tmp.dir;
|
|
109
|
+
tmpPromptPath = tmp.filePath;
|
|
110
|
+
args.push('--append-system-prompt', tmpPromptPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
args.push(`Task: ${options.task}`);
|
|
114
|
+
|
|
115
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
116
|
+
const invocation = getPiInvocation(args);
|
|
117
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
118
|
+
cwd: options.cwd,
|
|
119
|
+
shell: false,
|
|
120
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
121
|
+
});
|
|
122
|
+
let buffer = '';
|
|
123
|
+
|
|
124
|
+
const processLine = (line: string) => {
|
|
125
|
+
if (!line.trim()) return;
|
|
126
|
+
let event: any;
|
|
127
|
+
try {
|
|
128
|
+
event = JSON.parse(line);
|
|
129
|
+
} catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (event.type === 'message_end' && event.message) {
|
|
134
|
+
const msg = event.message as Message;
|
|
135
|
+
result.messages.push(msg);
|
|
136
|
+
|
|
137
|
+
if (msg.role === 'assistant') {
|
|
138
|
+
result.usage.turns++;
|
|
139
|
+
const usage = msg.usage;
|
|
140
|
+
if (usage) {
|
|
141
|
+
result.usage.input += usage.input || 0;
|
|
142
|
+
result.usage.output += usage.output || 0;
|
|
143
|
+
result.usage.cacheRead += usage.cacheRead || 0;
|
|
144
|
+
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
145
|
+
result.usage.cost += usage.cost?.total || 0;
|
|
146
|
+
result.usage.contextTokens = usage.totalTokens || 0;
|
|
147
|
+
}
|
|
148
|
+
if (!result.model && msg.model) result.model = msg.model;
|
|
149
|
+
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
150
|
+
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
options.onMessage?.(msg);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (event.type === 'tool_result_end' && event.message) {
|
|
157
|
+
result.messages.push(event.message as Message);
|
|
158
|
+
options.onToolResult?.(event.message as Message);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
proc.stdout.on('data', (data: Buffer) => {
|
|
163
|
+
buffer += data.toString();
|
|
164
|
+
const lines = buffer.split('\n');
|
|
165
|
+
buffer = lines.pop() || '';
|
|
166
|
+
for (const line of lines) processLine(line);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
170
|
+
result.stderr += data.toString();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
proc.on('close', (code: number | null) => {
|
|
174
|
+
if (buffer.trim()) processLine(buffer);
|
|
175
|
+
resolve(code ?? 0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
proc.on('error', () => resolve(1));
|
|
179
|
+
|
|
180
|
+
if (options.signal) {
|
|
181
|
+
const killProc = () => {
|
|
182
|
+
result.wasAborted = true;
|
|
183
|
+
proc.kill('SIGTERM');
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (!proc.killed) proc.kill('SIGKILL');
|
|
186
|
+
}, 5000);
|
|
187
|
+
};
|
|
188
|
+
if (options.signal.aborted) killProc();
|
|
189
|
+
else options.signal.addEventListener('abort', killProc, { once: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
result.exitCode = exitCode;
|
|
194
|
+
return result;
|
|
195
|
+
} finally {
|
|
196
|
+
cleanupTempFiles(tmpPromptPath, tmpPromptDir);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -1,42 +1,7 @@
|
|
|
1
|
-
import type { ExtensionCommandContext } from '@
|
|
1
|
+
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
|
|
3
3
|
const MAX_MESSAGES = 40;
|
|
4
4
|
|
|
5
|
-
const SYNTHESIS_INSTRUCTION = `Synthesize the current conversation into a clear task specification for a team of subagents that will execute it.
|
|
6
|
-
|
|
7
|
-
Your synthesis MUST capture:
|
|
8
|
-
|
|
9
|
-
1. **Goal** — What are we building, changing, or investigating?
|
|
10
|
-
2. **Decisions made** — Every locked choice, preference, or constraint the user confirmed.
|
|
11
|
-
3. **Constraints** — What was explicitly rejected and why.
|
|
12
|
-
4. **Architecture** — The agreed structure, file layout, data flow, or integration shape.
|
|
13
|
-
5. **Open questions** — Anything flagged but not resolved yet.
|
|
14
|
-
6. **Intent** — The user's real motivation and the nuance behind the decisions.
|
|
15
|
-
|
|
16
|
-
Be specific and actionable. Do NOT summarize vaguely. Include concrete names, paths, tools, APIs, and patterns that were discussed.
|
|
17
|
-
|
|
18
|
-
The output should be usable by agents who have NOT seen this conversation.
|
|
19
|
-
|
|
20
|
-
Format:
|
|
21
|
-
|
|
22
|
-
## Goal
|
|
23
|
-
<one paragraph>
|
|
24
|
-
|
|
25
|
-
## Decisions
|
|
26
|
-
<bulleted list of every locked decision>
|
|
27
|
-
|
|
28
|
-
## Constraints
|
|
29
|
-
<bulleted list of what was rejected and why>
|
|
30
|
-
|
|
31
|
-
## Architecture
|
|
32
|
-
<concrete structure, files, data flow>
|
|
33
|
-
|
|
34
|
-
## Open Questions
|
|
35
|
-
<anything unresolved>
|
|
36
|
-
|
|
37
|
-
## Intent
|
|
38
|
-
<the user's real motivation>`;
|
|
39
|
-
|
|
40
5
|
export function extractRecentConversation(ctx: ExtensionCommandContext): string {
|
|
41
6
|
const entries = ctx.sessionManager.getBranch();
|
|
42
7
|
const messages: string[] = [];
|
|
@@ -61,18 +26,3 @@ export function extractRecentConversation(ctx: ExtensionCommandContext): string
|
|
|
61
26
|
|
|
62
27
|
return messages.slice(-MAX_MESSAGES).join('\n\n---\n\n');
|
|
63
28
|
}
|
|
64
|
-
|
|
65
|
-
export function buildSynthesisPrompt(conversation: string, explicitTask?: string): string {
|
|
66
|
-
const parts = [SYNTHESIS_INSTRUCTION];
|
|
67
|
-
|
|
68
|
-
if (explicitTask) {
|
|
69
|
-
parts.push(`\nThe user explicitly specified the task as:\n${explicitTask}`);
|
|
70
|
-
parts.push(
|
|
71
|
-
'\nUse this as the primary goal, but enrich it with relevant context from the conversation.',
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
parts.push(`\n\n## Conversation\n\n${conversation}`);
|
|
76
|
-
|
|
77
|
-
return parts.join('\n');
|
|
78
|
-
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreki-gg/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Subagent tool and direct agent runs for pi — isolated agents, parallel scouts, manager workflows, and bundled
|
|
3
|
+
"version": "0.8.0",
|
|
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"
|
|
7
7
|
],
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"skills": [
|
|
27
27
|
"./skills"
|
|
28
28
|
],
|
|
29
|
-
"
|
|
30
|
-
"./
|
|
29
|
+
"prompts": [
|
|
30
|
+
"./prompts"
|
|
31
31
|
]
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"typescript": "^6.0.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
40
|
+
"@earendil-works/pi-ai": "*",
|
|
41
|
+
"@earendil-works/pi-agent-core": "*",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
43
|
+
"@earendil-works/pi-tui": "*",
|
|
44
44
|
"typebox": "*"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: write-an-agent
|
|
3
|
-
description: Writes or refines concise pi subagent definitions for this repo, keeping each agent under 100 lines with a sharp role, tool policy, and output contract. Use when creating, rewriting, or tightening agent prompts in `.pi/
|
|
3
|
+
description: Writes or refines concise pi subagent definitions for this repo, keeping each agent under 100 lines with a sharp role, tool policy, and output contract. Use when creating, rewriting, or tightening agent prompts in `.pi/prompts/` or `packages/subagent/prompts/`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Write an Agent
|
|
@@ -8,8 +8,8 @@ description: Writes or refines concise pi subagent definitions for this repo, ke
|
|
|
8
8
|
Goal: produce a high-signal agent file in fewer than 100 lines.
|
|
9
9
|
|
|
10
10
|
## Use this for
|
|
11
|
-
- new repo-local
|
|
12
|
-
- bundled package
|
|
11
|
+
- new repo-local agent prompts in `.pi/prompts/`
|
|
12
|
+
- bundled package agent prompts in `packages/subagent/prompts/`
|
|
13
13
|
- tightening bloated prompts into a crisp reusable worker
|
|
14
14
|
|
|
15
15
|
## Design rules
|
|
@@ -53,8 +53,8 @@ Output:
|
|
|
53
53
|
7. Count lines. Trim anything decorative.
|
|
54
54
|
|
|
55
55
|
## Repo conventions
|
|
56
|
-
- Repo-local overrides belong in `.pi/
|
|
57
|
-
- Bundled reusable
|
|
56
|
+
- Repo-local overrides belong in `.pi/prompts/`.
|
|
57
|
+
- Bundled reusable agent prompts belong in `packages/subagent/prompts/`.
|
|
58
58
|
- For this repo, default roles are `scout`, `docs-scout`, `planner`, `worker`, `reviewer`.
|
|
59
59
|
- Prefer local pi docs before external docs when writing `docs-scout`.
|
|
60
60
|
- Mention validation commands in planner/worker/reviewer when package behavior changes.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|