@d3ara1n/pi-subagent 0.2.0 → 0.3.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/README.md +3 -3
- package/package.json +2 -1
- package/src/index.ts +67 -37
- package/src/spawn.ts +43 -2
- package/src/types.ts +16 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ This means:
|
|
|
44
44
|
## TUI Display
|
|
45
45
|
|
|
46
46
|
- **During execution**: Shows role, elapsed time, turn count, and live tool calls
|
|
47
|
-
- **Collapsed result**: `✓ explorer ·
|
|
47
|
+
- **Collapsed result**: `✓ explorer · Found login, registration, and token logic` + recent tool calls + usage stats
|
|
48
48
|
- **Expanded result** (Ctrl+O): Full task text, all tool calls, final output as rendered Markdown, and usage details
|
|
49
49
|
|
|
50
50
|
## Requirements
|
|
@@ -56,7 +56,7 @@ This means:
|
|
|
56
56
|
## Installation
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
pi
|
|
59
|
+
pi install @d3ara1n/pi-subagent
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
## Configuration
|
|
@@ -70,7 +70,7 @@ Edit `~/.pi/agent/settings.json`:
|
|
|
70
70
|
"timeoutMs": 300000,
|
|
71
71
|
|
|
72
72
|
// Summary generation — uses a lightweight model to create
|
|
73
|
-
// a one-line
|
|
73
|
+
// a one-line summary for the TUI display
|
|
74
74
|
"summary": {
|
|
75
75
|
"role": "utility", // pi-model-roles role for summarization
|
|
76
76
|
"enabled": true // set false to disable
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Role-based subagent orchestration for pi — delegates tasks to specialized pi child processes with configurable model roles",
|
|
5
6
|
"main": "src/index.ts",
|
|
6
7
|
"keywords": [
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
|
15
15
|
import { Type } from "typebox";
|
|
16
16
|
import type { ModelRolesAPI } from "@d3ara1n/pi-model-roles";
|
|
17
17
|
import { getModelRolesAPI } from "@d3ara1n/pi-model-roles";
|
|
18
|
-
import type { SubagentConfig, SubagentDetails, SubagentResult, SubagentRole } from "./types.ts";
|
|
18
|
+
import type { SubagentConfig, SubagentDetails, SubagentResult, SubagentRole, ToolStatus, ActivityEntry } from "./types.ts";
|
|
19
19
|
import { DEFAULT_CONFIG } from "./types.ts";
|
|
20
20
|
import { loadSubagentConfig } from "./config.ts";
|
|
21
21
|
import { BUILTIN_ROLES } from "./roles.ts";
|
|
@@ -44,21 +44,16 @@ function formatUsageStats(usage: SubagentResult["usage"], model?: string): strin
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
type DisplayItem =
|
|
47
|
-
| { type: "
|
|
48
|
-
| { type: "
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
items.push({ type: "toolCall", name: part.name, args: part.arguments ?? {} });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return items;
|
|
47
|
+
| { type: "toolCall"; name: string; args: Record<string, any>; status?: ToolStatus }
|
|
48
|
+
| { type: "thinking"; status?: ToolStatus };
|
|
49
|
+
|
|
50
|
+
/** Map the real-time activity log into renderable display items (in order). */
|
|
51
|
+
function buildDisplayItems(activityLog: ActivityEntry[]): DisplayItem[] {
|
|
52
|
+
return activityLog.map((a) =>
|
|
53
|
+
a.kind === "thinking"
|
|
54
|
+
? { type: "thinking", status: a.status }
|
|
55
|
+
: { type: "toolCall", name: a.toolName ?? "?", args: a.args ?? {}, status: a.status },
|
|
56
|
+
);
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
function shortenPath(p: string): string {
|
|
@@ -126,6 +121,35 @@ function formatToolCall(
|
|
|
126
121
|
}
|
|
127
122
|
}
|
|
128
123
|
|
|
124
|
+
/** Per-tool-call visual styling: prefix glyph + color function keyed by status. */
|
|
125
|
+
function statusStyle(
|
|
126
|
+
status: ToolStatus | undefined,
|
|
127
|
+
fg: (color: string, text: string) => string,
|
|
128
|
+
): { prefix: string; color: (c: string, text: string) => string } {
|
|
129
|
+
switch (status) {
|
|
130
|
+
case "running":
|
|
131
|
+
return { prefix: fg("accent", "\u25CF "), color: fg };
|
|
132
|
+
case "failed":
|
|
133
|
+
return { prefix: fg("error", "\u2717 "), color: (_c, text) => fg("error", text) };
|
|
134
|
+
case "done":
|
|
135
|
+
default:
|
|
136
|
+
return { prefix: fg("dim", "\u2192 "), color: (_c, text) => fg("dim", text) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Render a thinking-block row: diamond glyph + label, colored by status.
|
|
141
|
+
* Running = hollow diamond (unformed thought); done = solid diamond (settled). */
|
|
142
|
+
function formatThinking(
|
|
143
|
+
status: ToolStatus | undefined,
|
|
144
|
+
fg: (color: string, text: string) => string,
|
|
145
|
+
): string {
|
|
146
|
+
if (status === "running") {
|
|
147
|
+
return fg("accent", "\u25C7 thinking");
|
|
148
|
+
}
|
|
149
|
+
// done (or unknown) — dim past tense, solid diamond
|
|
150
|
+
return fg("dim", "\u25C6 thought");
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
function renderDisplayItems(
|
|
130
154
|
items: DisplayItem[],
|
|
131
155
|
limit: number | undefined,
|
|
@@ -136,11 +160,11 @@ function renderDisplayItems(
|
|
|
136
160
|
let text = "";
|
|
137
161
|
if (skipped > 0) text += fg("muted", `... ${skipped} earlier items\n`);
|
|
138
162
|
for (const item of toShow) {
|
|
139
|
-
if (item.type === "
|
|
140
|
-
|
|
141
|
-
text += `${fg("toolOutput", preview.length > 120 ? preview.slice(0, 120) + "..." : preview)}\n`;
|
|
163
|
+
if (item.type === "thinking") {
|
|
164
|
+
text += `${formatThinking(item.status, fg)}\n`;
|
|
142
165
|
} else {
|
|
143
|
-
|
|
166
|
+
const { prefix, color } = statusStyle(item.status, fg);
|
|
167
|
+
text += `${prefix}${formatToolCall(item.name, item.args, color)}\n`;
|
|
144
168
|
}
|
|
145
169
|
}
|
|
146
170
|
return text.trimEnd();
|
|
@@ -187,8 +211,8 @@ async function generateSummary(
|
|
|
187
211
|
resolved.model,
|
|
188
212
|
{
|
|
189
213
|
systemPrompt:
|
|
190
|
-
"Summarize the following agent output in one concise
|
|
191
|
-
messages: [{ role: "user", content: summaryInput }],
|
|
214
|
+
"Summarize the following agent output in one concise sentence (max 60 characters). Respond in the same language as the input. Focus on what was accomplished, not how. Output only the summary, no preamble.",
|
|
215
|
+
messages: [{ role: "user", content: summaryInput, timestamp: Date.now() }],
|
|
192
216
|
},
|
|
193
217
|
{
|
|
194
218
|
maxTokens: 100,
|
|
@@ -338,6 +362,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
338
362
|
text: `Unknown subagent role: ${params.role}. Available: ${Object.keys(availableRoles).join(", ")}`,
|
|
339
363
|
},
|
|
340
364
|
],
|
|
365
|
+
details: undefined as any,
|
|
341
366
|
};
|
|
342
367
|
}
|
|
343
368
|
|
|
@@ -348,6 +373,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
348
373
|
} catch {
|
|
349
374
|
return {
|
|
350
375
|
content: [{ type: "text", text: "pi-model-roles is not initialized. Cannot resolve model for subagent." }],
|
|
376
|
+
details: undefined as any,
|
|
351
377
|
};
|
|
352
378
|
}
|
|
353
379
|
|
|
@@ -355,6 +381,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
355
381
|
if (!resolved.model) {
|
|
356
382
|
return {
|
|
357
383
|
content: [{ type: "text", text: `Role "${roleDef.role}" could not be resolved. Model not available.` }],
|
|
384
|
+
details: undefined as any,
|
|
358
385
|
};
|
|
359
386
|
}
|
|
360
387
|
|
|
@@ -371,6 +398,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
371
398
|
output: "",
|
|
372
399
|
stderr: "",
|
|
373
400
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
401
|
+
activityLog: [],
|
|
374
402
|
};
|
|
375
403
|
onUpdate({
|
|
376
404
|
content: [{ type: "text", text: `${params.role}: running...` }],
|
|
@@ -407,6 +435,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
407
435
|
},
|
|
408
436
|
model: partial.model,
|
|
409
437
|
stopReason: partial.stopReason,
|
|
438
|
+
activityLog: partial.activityLog ?? [],
|
|
410
439
|
};
|
|
411
440
|
const statusText = `${params.role} ${elapsed}s ${liveResult.usage.turns} turn${liveResult.usage.turns !== 1 ? "s" : ""}`;
|
|
412
441
|
onUpdate({
|
|
@@ -511,7 +540,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
511
540
|
} else {
|
|
512
541
|
icon = theme.fg("success", "\u2713");
|
|
513
542
|
}
|
|
514
|
-
const displayItems =
|
|
543
|
+
const displayItems = buildDisplayItems(r.activityLog);
|
|
515
544
|
const finalOutput = getFinalOutput(r.messages);
|
|
516
545
|
const mdTheme = getMarkdownTheme();
|
|
517
546
|
|
|
@@ -532,20 +561,21 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
532
561
|
}
|
|
533
562
|
|
|
534
563
|
container.addChild(new Spacer(1));
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
564
|
+
const activity = displayItems.filter((item) => item.type === "toolCall" || item.type === "thinking");
|
|
565
|
+
if (activity.length === 0) {
|
|
537
566
|
const runningLabel = isRunning ? "(waiting for first event...)" : "(none)";
|
|
538
567
|
container.addChild(new Text(theme.fg("muted", runningLabel), 0, 0));
|
|
539
568
|
} else {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
569
|
+
const fg = theme.fg.bind(theme) as (color: string, text: string) => string;
|
|
570
|
+
for (const item of activity) {
|
|
571
|
+
if (item.type === "thinking") {
|
|
572
|
+
container.addChild(new Text(formatThinking(item.status, fg), 0, 0));
|
|
573
|
+
} else {
|
|
574
|
+
const { prefix, color } = statusStyle(item.status, fg);
|
|
575
|
+
container.addChild(
|
|
576
|
+
new Text(prefix + formatToolCall(item.name, item.args, color), 0, 0),
|
|
577
|
+
);
|
|
578
|
+
}
|
|
549
579
|
}
|
|
550
580
|
}
|
|
551
581
|
|
|
@@ -569,11 +599,11 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
569
599
|
|
|
570
600
|
if (isRunning) {
|
|
571
601
|
// Running: show recent tool calls only
|
|
572
|
-
const
|
|
573
|
-
if (
|
|
602
|
+
const activity = displayItems.filter((item) => item.type === "toolCall" || item.type === "thinking");
|
|
603
|
+
if (activity.length === 0) {
|
|
574
604
|
text += `\n${theme.fg("muted", "(running...)")}`;
|
|
575
605
|
} else {
|
|
576
|
-
const rendered = renderDisplayItems(
|
|
606
|
+
const rendered = renderDisplayItems(activity, 5, theme.fg.bind(theme) as (color: string, text: string) => string);
|
|
577
607
|
if (rendered) text += `\n${rendered}`;
|
|
578
608
|
}
|
|
579
609
|
} else {
|
package/src/spawn.ts
CHANGED
|
@@ -140,6 +140,7 @@ export async function spawnSubagent(
|
|
|
140
140
|
output: "",
|
|
141
141
|
stderr: "",
|
|
142
142
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
143
|
+
activityLog: [],
|
|
143
144
|
};
|
|
144
145
|
|
|
145
146
|
let tmpDir: string | null = null;
|
|
@@ -183,9 +184,12 @@ export async function spawnSubagent(
|
|
|
183
184
|
usage: { ...result.usage },
|
|
184
185
|
model: result.model,
|
|
185
186
|
stopReason: result.stopReason,
|
|
187
|
+
activityLog: result.activityLog.map((a) => ({ ...a })),
|
|
186
188
|
});
|
|
187
189
|
};
|
|
188
190
|
|
|
191
|
+
let thinkingCounter = 0;
|
|
192
|
+
|
|
189
193
|
const processLine = (line: string) => {
|
|
190
194
|
if (!line.trim()) return;
|
|
191
195
|
let event: any;
|
|
@@ -225,10 +229,47 @@ export async function spawnSubagent(
|
|
|
225
229
|
emitProgress();
|
|
226
230
|
}
|
|
227
231
|
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
// Activity log: track thinking blocks and tool calls in arrival order.
|
|
233
|
+
// Both update in place so the TUI reflects real-time state.
|
|
234
|
+
if (event.type === "tool_execution_start" && event.toolCallId) {
|
|
235
|
+
result.activityLog.push({
|
|
236
|
+
kind: "toolCall",
|
|
237
|
+
id: event.toolCallId,
|
|
238
|
+
status: "running",
|
|
239
|
+
toolName: event.toolName,
|
|
240
|
+
args: event.args ?? {},
|
|
241
|
+
});
|
|
242
|
+
emitProgress();
|
|
243
|
+
} else if (event.type === "tool_execution_end" && event.toolCallId) {
|
|
244
|
+
const entry = result.activityLog.find((a) => a.id === event.toolCallId);
|
|
245
|
+
if (entry) entry.status = event.isError ? "failed" : "done";
|
|
230
246
|
emitProgress();
|
|
231
247
|
}
|
|
248
|
+
|
|
249
|
+
// Thinking-block lifecycle: pi wraps thinking_start/end inside
|
|
250
|
+
// message_update.assistantMessageEvent. These arrive BEFORE message_end,
|
|
251
|
+
// so we can't rely on messages[] to show real-time thinking state —
|
|
252
|
+
// register them in the activity log directly.
|
|
253
|
+
const aev = event.assistantMessageEvent;
|
|
254
|
+
if (event.type === "message_update" && aev) {
|
|
255
|
+
if (aev.type === "thinking_start") {
|
|
256
|
+
result.activityLog.push({
|
|
257
|
+
kind: "thinking",
|
|
258
|
+
id: `thinking-${thinkingCounter++}`,
|
|
259
|
+
status: "running",
|
|
260
|
+
});
|
|
261
|
+
emitProgress();
|
|
262
|
+
} else if (aev.type === "thinking_end") {
|
|
263
|
+
// Mark the most recent still-running thinking block as done.
|
|
264
|
+
for (let i = result.activityLog.length - 1; i >= 0; i--) {
|
|
265
|
+
if (result.activityLog[i].kind === "thinking" && result.activityLog[i].status === "running") {
|
|
266
|
+
result.activityLog[i].status = "done";
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
emitProgress();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
232
273
|
};
|
|
233
274
|
|
|
234
275
|
// Build env with optional subagent allowlist and tmpdir for researcher role
|
package/src/types.ts
CHANGED
|
@@ -45,6 +45,20 @@ export interface SubagentRole {
|
|
|
45
45
|
fallbackRole?: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Status of an individual tool call within a subagent run. */
|
|
49
|
+
export type ToolStatus = "running" | "done" | "failed";
|
|
50
|
+
|
|
51
|
+
/** A single entry in the real-time activity log (thinking block or tool call). */
|
|
52
|
+
export interface ActivityEntry {
|
|
53
|
+
kind: "thinking" | "toolCall";
|
|
54
|
+
/** Synthetic id (thinking-N) or the toolCallId from the event stream. */
|
|
55
|
+
id: string;
|
|
56
|
+
status: ToolStatus;
|
|
57
|
+
/** Tool name + args (toolCall only). */
|
|
58
|
+
toolName?: string;
|
|
59
|
+
args?: Record<string, any>;
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
/** Usage statistics from a subagent execution. */
|
|
49
63
|
export interface SubagentUsage {
|
|
50
64
|
input: number;
|
|
@@ -104,6 +118,8 @@ export interface SubagentResult {
|
|
|
104
118
|
stopReason?: string;
|
|
105
119
|
/** Error message if failed */
|
|
106
120
|
errorMessage?: string;
|
|
121
|
+
/** Real-time activity log: thinking blocks and tool calls in arrival order. */
|
|
122
|
+
activityLog: ActivityEntry[];
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
/** TUI details structure passed via tool result details. */
|