pi-subagentura 1.0.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/package.json +44 -0
- package/subagent.ts +590 -0
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-subagentura",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sub-agent engine extension for Pi — spawn in-process sub-agents via the SDK",
|
|
5
|
+
"main": "subagent.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi",
|
|
10
|
+
"subagent",
|
|
11
|
+
"agent",
|
|
12
|
+
"multi-agent",
|
|
13
|
+
"swarm",
|
|
14
|
+
"crew"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"files": [
|
|
18
|
+
"subagent.ts",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"bun": ">=1.0.0"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"pi": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"./subagent.ts"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"pack:check": "npm pack --dry-run"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
39
|
+
"@mariozechner/pi-agent-core": "*",
|
|
40
|
+
"@mariozechner/pi-ai": "*",
|
|
41
|
+
"@mariozechner/pi-tui": "*",
|
|
42
|
+
"typebox": "*"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/subagent.ts
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-Engine Extension - Spawn in-process sub-agents via the SDK
|
|
3
|
+
*
|
|
4
|
+
* Two tools:
|
|
5
|
+
* - subagent_with_context: Inherits full conversation history + task + persona
|
|
6
|
+
* - subagent_isolated: Fresh context window, task + optional persona only
|
|
7
|
+
*
|
|
8
|
+
* Both inherit the current model by default. Persona is an optional argument.
|
|
9
|
+
* Runs in the same process — no subprocess overhead, live streaming output.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
AuthStorage,
|
|
14
|
+
createAgentSession,
|
|
15
|
+
ModelRegistry,
|
|
16
|
+
SessionManager,
|
|
17
|
+
type ExtensionAPI,
|
|
18
|
+
type AgentSession,
|
|
19
|
+
type Theme,
|
|
20
|
+
convertToLlm,
|
|
21
|
+
serializeConversation,
|
|
22
|
+
} from "@mariozechner/pi-coding-agent";
|
|
23
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
24
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
25
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
26
|
+
import { Text, truncateToWidth } from "@mariozechner/pi-tui";
|
|
27
|
+
import { Type } from "typebox";
|
|
28
|
+
|
|
29
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
interface SubagentResult {
|
|
32
|
+
output: string;
|
|
33
|
+
usage: {
|
|
34
|
+
input: number;
|
|
35
|
+
output: number;
|
|
36
|
+
cacheRead: number;
|
|
37
|
+
cacheWrite: number;
|
|
38
|
+
cost: number;
|
|
39
|
+
turns: number;
|
|
40
|
+
};
|
|
41
|
+
model?: string;
|
|
42
|
+
isError: boolean;
|
|
43
|
+
errorMessage?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SubagentLiveStatus {
|
|
47
|
+
turn: number;
|
|
48
|
+
activeTool?: { name: string; args: Record<string, unknown> };
|
|
49
|
+
output: string;
|
|
50
|
+
usage: SubagentResult["usage"];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveModel(
|
|
54
|
+
modelId: string | undefined,
|
|
55
|
+
defaultModel: Model | undefined,
|
|
56
|
+
): Model | undefined {
|
|
57
|
+
if (!modelId) return defaultModel;
|
|
58
|
+
|
|
59
|
+
// "provider/id" format
|
|
60
|
+
if (modelId.includes("/")) {
|
|
61
|
+
const [provider, id] = modelId.split("/", 2);
|
|
62
|
+
return getModel(provider, id) ?? defaultModel;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Bare id — search known providers
|
|
66
|
+
for (const provider of [
|
|
67
|
+
"anthropic",
|
|
68
|
+
"openai",
|
|
69
|
+
"google",
|
|
70
|
+
"deepseek",
|
|
71
|
+
"openrouter",
|
|
72
|
+
]) {
|
|
73
|
+
const found = getModel(provider, modelId);
|
|
74
|
+
if (found) return found;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return defaultModel;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatTokens(count: number): string {
|
|
81
|
+
if (count < 1000) return count.toString();
|
|
82
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
83
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
84
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatUsage(u: SubagentResult["usage"], model?: string): string {
|
|
88
|
+
const parts: string[] = [];
|
|
89
|
+
if (u.turns) parts.push(`${u.turns} turn${u.turns > 1 ? "s" : ""}`);
|
|
90
|
+
if (u.input) parts.push(`↑${formatTokens(u.input)}`);
|
|
91
|
+
if (u.output) parts.push(`↓${formatTokens(u.output)}`);
|
|
92
|
+
if (u.cacheRead) parts.push(`R${formatTokens(u.cacheRead)}`);
|
|
93
|
+
if (u.cacheWrite) parts.push(`W${formatTokens(u.cacheWrite)}`);
|
|
94
|
+
if (u.cost) parts.push(`$${u.cost.toFixed(4)}`);
|
|
95
|
+
if (model) parts.push(model);
|
|
96
|
+
return parts.join(" ");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildLiveUpdate(
|
|
100
|
+
status: SubagentLiveStatus,
|
|
101
|
+
model?: string,
|
|
102
|
+
): AgentToolResult {
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: status.output }],
|
|
105
|
+
details: {
|
|
106
|
+
status: "running",
|
|
107
|
+
subagentStatus: status,
|
|
108
|
+
model,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runSubagent(
|
|
114
|
+
task: string,
|
|
115
|
+
persona: string | undefined,
|
|
116
|
+
modelOverride: string | undefined,
|
|
117
|
+
cwd: string,
|
|
118
|
+
contextText: string | null,
|
|
119
|
+
signal: AbortSignal | undefined,
|
|
120
|
+
onUpdate: ((partial: AgentToolResult) => void) | undefined,
|
|
121
|
+
defaultModel: Model | undefined,
|
|
122
|
+
): Promise<SubagentResult> {
|
|
123
|
+
|
|
124
|
+
const authStorage = AuthStorage.create();
|
|
125
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
126
|
+
|
|
127
|
+
const targetModel = resolveModel(modelOverride, defaultModel);
|
|
128
|
+
const modelLabel = targetModel
|
|
129
|
+
? `${targetModel.provider}/${targetModel.id}`
|
|
130
|
+
: undefined;
|
|
131
|
+
|
|
132
|
+
let session: AgentSession | undefined;
|
|
133
|
+
let handleAbort: (() => void) | undefined;
|
|
134
|
+
let unsubscribe: (() => void) | undefined;
|
|
135
|
+
|
|
136
|
+
const liveStatus: SubagentLiveStatus = {
|
|
137
|
+
turn: 0,
|
|
138
|
+
output: "",
|
|
139
|
+
usage: {
|
|
140
|
+
input: 0,
|
|
141
|
+
output: 0,
|
|
142
|
+
cacheRead: 0,
|
|
143
|
+
cacheWrite: 0,
|
|
144
|
+
cost: 0,
|
|
145
|
+
turns: 0,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Debounce activeTool updates to prevent flickering on fast tool calls.
|
|
150
|
+
// When a tool executes quickly (< DEBOUNCE_MS), we skip the setActiveTool
|
|
151
|
+
// render entirely, avoiding a brief height/width expansion that the user
|
|
152
|
+
// perceives as flickering.
|
|
153
|
+
const DEBOUNCE_MS = 150;
|
|
154
|
+
let activeToolTimer: ReturnType<typeof setTimeout> | undefined;
|
|
155
|
+
let pendingActiveTool: SubagentLiveStatus["activeTool"] = undefined;
|
|
156
|
+
|
|
157
|
+
function setActiveToolDebounced(tool: SubagentLiveStatus["activeTool"]) {
|
|
158
|
+
pendingActiveTool = tool;
|
|
159
|
+
if (activeToolTimer) {
|
|
160
|
+
clearTimeout(activeToolTimer);
|
|
161
|
+
activeToolTimer = undefined;
|
|
162
|
+
}
|
|
163
|
+
if (tool) {
|
|
164
|
+
// Starting a tool: wait DEBOUNCE_MS before showing it.
|
|
165
|
+
// If the tool finishes before the timer fires, clearActiveToolDebounced
|
|
166
|
+
// will cancel the timer and the activeTool line never appears.
|
|
167
|
+
activeToolTimer = setTimeout(() => {
|
|
168
|
+
activeToolTimer = undefined;
|
|
169
|
+
liveStatus.activeTool = pendingActiveTool;
|
|
170
|
+
onUpdate?.(buildLiveUpdate(liveStatus, modelLabel));
|
|
171
|
+
}, DEBOUNCE_MS);
|
|
172
|
+
} else {
|
|
173
|
+
// Clearing: apply immediately (no delay) so the UI stays responsive
|
|
174
|
+
// when a genuinely long tool finishes. But only if we had an activeTool
|
|
175
|
+
// that was already committed (not just pending).
|
|
176
|
+
if (liveStatus.activeTool) {
|
|
177
|
+
liveStatus.activeTool = undefined;
|
|
178
|
+
onUpdate?.(buildLiveUpdate(liveStatus, modelLabel));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let result: SubagentResult;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
session = (
|
|
187
|
+
await createAgentSession({
|
|
188
|
+
sessionManager: SessionManager.inMemory(),
|
|
189
|
+
authStorage,
|
|
190
|
+
modelRegistry,
|
|
191
|
+
model: targetModel,
|
|
192
|
+
cwd,
|
|
193
|
+
})
|
|
194
|
+
).session;
|
|
195
|
+
|
|
196
|
+
// Wire abort signal
|
|
197
|
+
if (signal) {
|
|
198
|
+
handleAbort = () => {
|
|
199
|
+
session!.abort().catch(() => {});
|
|
200
|
+
};
|
|
201
|
+
if (signal.aborted) {
|
|
202
|
+
handleAbort();
|
|
203
|
+
} else {
|
|
204
|
+
signal.addEventListener("abort", handleAbort);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Stream output and lifecycle events back to parent
|
|
209
|
+
unsubscribe = session.subscribe((event) => {
|
|
210
|
+
switch (event.type) {
|
|
211
|
+
case "turn_start": {
|
|
212
|
+
liveStatus.turn++;
|
|
213
|
+
liveStatus.usage.turns = liveStatus.turn;
|
|
214
|
+
// Reset output on each new turn so the live preview always shows
|
|
215
|
+
// only the current turn's text, not an accumulation of all turns.
|
|
216
|
+
liveStatus.output = "";
|
|
217
|
+
onUpdate?.(buildLiveUpdate(liveStatus, modelLabel));
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "tool_execution_start": {
|
|
221
|
+
setActiveToolDebounced({
|
|
222
|
+
name: event.toolName,
|
|
223
|
+
args: event.args as Record<string, unknown>,
|
|
224
|
+
});
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case "tool_execution_end": {
|
|
228
|
+
setActiveToolDebounced(undefined);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case "turn_end": {
|
|
232
|
+
// Cancel any pending activeTool timer and clear immediately
|
|
233
|
+
if (activeToolTimer) {
|
|
234
|
+
clearTimeout(activeToolTimer);
|
|
235
|
+
activeToolTimer = undefined;
|
|
236
|
+
}
|
|
237
|
+
liveStatus.activeTool = undefined;
|
|
238
|
+
onUpdate?.(buildLiveUpdate(liveStatus, modelLabel));
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "message_update": {
|
|
242
|
+
if (event.assistantMessageEvent.type === "text_delta") {
|
|
243
|
+
liveStatus.output += event.assistantMessageEvent.delta;
|
|
244
|
+
onUpdate?.(buildLiveUpdate(liveStatus, modelLabel));
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const personaPrefix = persona ? `${persona}\n\n` : "";
|
|
252
|
+
const finalPrompt = contextText
|
|
253
|
+
? `${personaPrefix}You are a sub-agent receiving the full conversation history below. Use it as context, then fulfill the task.\n\n## Conversation History\n${contextText}\n\n## Your Task\n${task}`
|
|
254
|
+
: `${personaPrefix}Task: ${task}`;
|
|
255
|
+
|
|
256
|
+
await session.prompt(finalPrompt);
|
|
257
|
+
|
|
258
|
+
// Extract final assistant output
|
|
259
|
+
const messages = session.agent.state.messages;
|
|
260
|
+
let finalOutput = liveStatus.output; // fallback to streamed
|
|
261
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
262
|
+
const msg = messages[i];
|
|
263
|
+
if (msg.role === "assistant") {
|
|
264
|
+
const textParts = msg.content
|
|
265
|
+
?.filter(
|
|
266
|
+
(c): c is { type: "text"; text: string } => c.type === "text",
|
|
267
|
+
)
|
|
268
|
+
.map((c) => c.text)
|
|
269
|
+
.join("\n");
|
|
270
|
+
if (textParts) {
|
|
271
|
+
finalOutput = textParts;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const usage = {
|
|
278
|
+
input: 0,
|
|
279
|
+
output: 0,
|
|
280
|
+
cacheRead: 0,
|
|
281
|
+
cacheWrite: 0,
|
|
282
|
+
cost: 0,
|
|
283
|
+
turns: 0,
|
|
284
|
+
};
|
|
285
|
+
for (const msg of messages) {
|
|
286
|
+
if (msg.role === "assistant" && msg.usage) {
|
|
287
|
+
usage.turns++;
|
|
288
|
+
usage.input += msg.usage.input;
|
|
289
|
+
usage.output += msg.usage.output;
|
|
290
|
+
usage.cacheRead += msg.usage.cacheRead;
|
|
291
|
+
usage.cacheWrite += msg.usage.cacheWrite;
|
|
292
|
+
usage.cost += msg.usage.cost.total;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
result = {
|
|
297
|
+
output: finalOutput || "(no output)",
|
|
298
|
+
usage,
|
|
299
|
+
model: session.model
|
|
300
|
+
? `${session.model.provider}/${session.model.id}`
|
|
301
|
+
: undefined,
|
|
302
|
+
isError: !!session.agent.state.errorMessage,
|
|
303
|
+
errorMessage: session.agent.state.errorMessage,
|
|
304
|
+
};
|
|
305
|
+
} catch (err) {
|
|
306
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
307
|
+
result = {
|
|
308
|
+
output: `Sub-agent crashed: ${msg}`,
|
|
309
|
+
usage: {
|
|
310
|
+
input: 0,
|
|
311
|
+
output: 0,
|
|
312
|
+
cacheRead: 0,
|
|
313
|
+
cacheWrite: 0,
|
|
314
|
+
cost: 0,
|
|
315
|
+
turns: 0,
|
|
316
|
+
},
|
|
317
|
+
model: undefined,
|
|
318
|
+
isError: true,
|
|
319
|
+
errorMessage: msg,
|
|
320
|
+
};
|
|
321
|
+
} finally {
|
|
322
|
+
if (activeToolTimer) {
|
|
323
|
+
clearTimeout(activeToolTimer);
|
|
324
|
+
activeToolTimer = undefined;
|
|
325
|
+
}
|
|
326
|
+
if (signal && handleAbort) signal.removeEventListener("abort", handleAbort);
|
|
327
|
+
if (unsubscribe) unsubscribe();
|
|
328
|
+
session?.dispose();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Rendering ────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
function renderSubagentCall(
|
|
337
|
+
args: Record<string, unknown>,
|
|
338
|
+
theme: Theme,
|
|
339
|
+
label: string,
|
|
340
|
+
) {
|
|
341
|
+
const task = String(args.task ?? "");
|
|
342
|
+
const taskPreview =
|
|
343
|
+
task.length > 60 ? `${task.slice(0, 57)}…` : task;
|
|
344
|
+
let text = theme.fg("toolTitle", theme.bold(`${label} `));
|
|
345
|
+
text += theme.fg("accent", taskPreview);
|
|
346
|
+
if (args.model) {
|
|
347
|
+
text += theme.fg("dim", ` @${args.model}`);
|
|
348
|
+
}
|
|
349
|
+
return new Text(text, 0, 0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function renderSubagentResult(
|
|
353
|
+
result: AgentToolResult,
|
|
354
|
+
{ expanded, isPartial }: { expanded: boolean; isPartial: boolean },
|
|
355
|
+
theme: Theme,
|
|
356
|
+
_context: unknown,
|
|
357
|
+
) {
|
|
358
|
+
if (isPartial) {
|
|
359
|
+
const status = result.details?.subagentStatus as
|
|
360
|
+
| SubagentLiveStatus
|
|
361
|
+
| undefined;
|
|
362
|
+
const model = result.details?.model as string | undefined;
|
|
363
|
+
|
|
364
|
+
let text = theme.fg("accent", "● ") + theme.fg("toolTitle", "Sub-agent working");
|
|
365
|
+
|
|
366
|
+
if (status) {
|
|
367
|
+
text += theme.fg("dim", ` — turn ${status.turn}`);
|
|
368
|
+
|
|
369
|
+
if (status.activeTool) {
|
|
370
|
+
let argsStr = "{…}";
|
|
371
|
+
try {
|
|
372
|
+
argsStr = JSON.stringify(status.activeTool.args).slice(0, 80);
|
|
373
|
+
} catch {
|
|
374
|
+
/* circular or otherwise unserializable */
|
|
375
|
+
}
|
|
376
|
+
text += `
|
|
377
|
+
${theme.fg("muted", "→")} ${theme.fg(
|
|
378
|
+
"toolTitle",
|
|
379
|
+
status.activeTool.name,
|
|
380
|
+
)} ${theme.fg("dim", argsStr)}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const usageStr = formatUsage(status.usage, model);
|
|
384
|
+
if (usageStr) {
|
|
385
|
+
text += `
|
|
386
|
+
${theme.fg("muted", usageStr)}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (status.output) {
|
|
390
|
+
const preview = status.output
|
|
391
|
+
.slice(0, 200)
|
|
392
|
+
.replace(/\s+/g, " ");
|
|
393
|
+
text += `
|
|
394
|
+
${theme.fg("dim", truncateToWidth(preview, 120))}`;
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
text += theme.fg("dim", "…");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return new Text(text, 0, 0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Final result
|
|
404
|
+
const text =
|
|
405
|
+
result.content.find(
|
|
406
|
+
(c): c is { type: "text"; text: string } => c.type === "text",
|
|
407
|
+
)?.text ?? "";
|
|
408
|
+
|
|
409
|
+
if (result.isError) {
|
|
410
|
+
if (!expanded) {
|
|
411
|
+
const preview = truncateToWidth(text.replace(/\s+/g, " "), 120);
|
|
412
|
+
return new Text(theme.fg("error", preview), 0, 0);
|
|
413
|
+
}
|
|
414
|
+
return new Text(theme.fg("error", text), 0, 0);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const usageStr = result.details?.usageSummary as string | undefined;
|
|
418
|
+
|
|
419
|
+
if (usageStr) {
|
|
420
|
+
const header = theme.fg("success", "✓ ") + theme.fg("muted", usageStr);
|
|
421
|
+
if (!expanded) {
|
|
422
|
+
return new Text(header, 0, 0);
|
|
423
|
+
}
|
|
424
|
+
return new Text(`${header}\n${text}`, 0, 0);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!expanded) {
|
|
428
|
+
const preview = truncateToWidth(text.replace(/\s+/g, " "), 120);
|
|
429
|
+
return new Text(theme.fg("dim", preview), 0, 0);
|
|
430
|
+
}
|
|
431
|
+
return new Text(text, 0, 0);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Schema ───────────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
const BaseParams = Type.Object({
|
|
437
|
+
task: Type.String({ description: "Task to delegate to the sub-agent" }),
|
|
438
|
+
persona: Type.Optional(
|
|
439
|
+
Type.String({
|
|
440
|
+
description:
|
|
441
|
+
"Optional persona / system prompt (e.g. 'You are a senior TypeScript reviewer')",
|
|
442
|
+
}),
|
|
443
|
+
),
|
|
444
|
+
model: Type.Optional(
|
|
445
|
+
Type.String({
|
|
446
|
+
description:
|
|
447
|
+
"Override model (e.g. 'anthropic/claude-sonnet-4-5'). Default: inherit from current session.",
|
|
448
|
+
}),
|
|
449
|
+
),
|
|
450
|
+
cwd: Type.Optional(
|
|
451
|
+
Type.String({
|
|
452
|
+
description: "Working directory (default: current cwd)",
|
|
453
|
+
}),
|
|
454
|
+
),
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// ── Extension ────────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
export default function (pi: ExtensionAPI) {
|
|
460
|
+
// ── Tool 1: inherits conversation history ────────────────────────
|
|
461
|
+
pi.registerTool({
|
|
462
|
+
name: "subagent_with_context",
|
|
463
|
+
label: "Sub-Agent (with context)",
|
|
464
|
+
description: [
|
|
465
|
+
"Spawn an in-process sub-agent that inherits the full conversation history.",
|
|
466
|
+
"The sub-agent sees everything discussed so far plus the new task.",
|
|
467
|
+
"Model is inherited by default. Streams output in real-time.",
|
|
468
|
+
].join(" "),
|
|
469
|
+
parameters: BaseParams,
|
|
470
|
+
|
|
471
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
472
|
+
// Gather conversation history
|
|
473
|
+
const branch = ctx.sessionManager.getBranch();
|
|
474
|
+
const messages = branch
|
|
475
|
+
.filter(
|
|
476
|
+
(e): e is typeof e & { type: "message" } => e.type === "message",
|
|
477
|
+
)
|
|
478
|
+
.map((e) => e.message);
|
|
479
|
+
|
|
480
|
+
if (messages.length === 0) {
|
|
481
|
+
return {
|
|
482
|
+
content: [
|
|
483
|
+
{ type: "text", text: "No conversation history to inherit." },
|
|
484
|
+
],
|
|
485
|
+
details: {},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const llmMessages = convertToLlm(messages);
|
|
490
|
+
const conversationText = serializeConversation(llmMessages);
|
|
491
|
+
|
|
492
|
+
const targetCwd = params.cwd ?? ctx.cwd;
|
|
493
|
+
const result = await runSubagent(
|
|
494
|
+
params.task,
|
|
495
|
+
params.persona,
|
|
496
|
+
params.model,
|
|
497
|
+
targetCwd,
|
|
498
|
+
conversationText,
|
|
499
|
+
signal,
|
|
500
|
+
onUpdate,
|
|
501
|
+
ctx.model,
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const usageStr = formatUsage(result.usage, result.model);
|
|
505
|
+
const details: Record<string, unknown> = {
|
|
506
|
+
contextMessages: messages.length,
|
|
507
|
+
usage: result.usage,
|
|
508
|
+
model: result.model,
|
|
509
|
+
usageSummary: usageStr,
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
content: [
|
|
514
|
+
{
|
|
515
|
+
type: "text",
|
|
516
|
+
text: result.isError
|
|
517
|
+
? `Sub-agent failed: ${result.errorMessage || result.output}`
|
|
518
|
+
: result.output,
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
details,
|
|
522
|
+
isError: result.isError,
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
renderCall(args, theme) {
|
|
527
|
+
return renderSubagentCall(args, theme, "subagent_with_context");
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
renderResult(result, options, theme, context) {
|
|
531
|
+
return renderSubagentResult(result, options, theme, context);
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// ── Tool 2: isolated, no conversation history ────────────────────
|
|
536
|
+
pi.registerTool({
|
|
537
|
+
name: "subagent_isolated",
|
|
538
|
+
label: "Sub-Agent (isolated)",
|
|
539
|
+
description: [
|
|
540
|
+
"Spawn an in-process sub-agent with a fresh, empty context window.",
|
|
541
|
+
"Only receives the task and optional persona. No conversation history.",
|
|
542
|
+
"Model is inherited by default. Streams output in real-time.",
|
|
543
|
+
].join(" "),
|
|
544
|
+
parameters: BaseParams,
|
|
545
|
+
|
|
546
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
547
|
+
const targetCwd = params.cwd ?? ctx.cwd;
|
|
548
|
+
|
|
549
|
+
const result = await runSubagent(
|
|
550
|
+
params.task,
|
|
551
|
+
params.persona,
|
|
552
|
+
params.model,
|
|
553
|
+
targetCwd,
|
|
554
|
+
null, // no context
|
|
555
|
+
signal,
|
|
556
|
+
onUpdate,
|
|
557
|
+
ctx.model,
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
const usageStr = formatUsage(result.usage, result.model);
|
|
561
|
+
const details: Record<string, unknown> = {
|
|
562
|
+
usage: result.usage,
|
|
563
|
+
model: result.model,
|
|
564
|
+
usageSummary: usageStr,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
content: [
|
|
569
|
+
{
|
|
570
|
+
type: "text",
|
|
571
|
+
text: result.isError
|
|
572
|
+
? `Sub-agent failed: ${result.errorMessage || result.output}`
|
|
573
|
+
: result.output,
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
details,
|
|
577
|
+
isError: result.isError,
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
renderCall(args, theme) {
|
|
582
|
+
return renderSubagentCall(args, theme, "subagent_isolated");
|
|
583
|
+
},
|
|
584
|
+
|
|
585
|
+
renderResult(result, options, theme, context) {
|
|
586
|
+
return renderSubagentResult(result, options, theme, context);
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
}
|