@ian-pascoe/pi-minimal-subagents 0.1.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/LICENSE +21 -0
- package/README.md +129 -0
- package/package.json +51 -0
- package/src/index.ts +1 -0
- package/src/minimal-subagents-capabilities.ts +118 -0
- package/src/minimal-subagents-config.ts +217 -0
- package/src/minimal-subagents-context.ts +70 -0
- package/src/minimal-subagents-coordinator.ts +1230 -0
- package/src/minimal-subagents-extension.ts +279 -0
- package/src/minimal-subagents-fork-lifecycle.ts +36 -0
- package/src/minimal-subagents-registry.ts +219 -0
- package/src/minimal-subagents-rendering.ts +717 -0
- package/src/minimal-subagents-sessions.ts +702 -0
- package/src/minimal-subagents-shutdown.ts +29 -0
- package/src/minimal-subagents-tool-schemas.ts +66 -0
- package/src/minimal-subagents-tools.ts +285 -0
- package/src/minimal-subagents-types.ts +305 -0
- package/src/minimal-subagents-ui.ts +326 -0
- package/src/minimal-subagents-usage.ts +24 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
getMarkdownTheme,
|
|
4
|
+
keyHint,
|
|
5
|
+
type AgentToolResult,
|
|
6
|
+
type Theme,
|
|
7
|
+
type ThemeColor,
|
|
8
|
+
type ToolRenderResultOptions,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
Box,
|
|
12
|
+
Container,
|
|
13
|
+
Markdown,
|
|
14
|
+
sliceByColumn,
|
|
15
|
+
Spacer,
|
|
16
|
+
Text,
|
|
17
|
+
visibleWidth,
|
|
18
|
+
type Component,
|
|
19
|
+
} from "@earendil-works/pi-tui";
|
|
20
|
+
export type CoordinatorToolName =
|
|
21
|
+
| "subagent"
|
|
22
|
+
| "agent_message"
|
|
23
|
+
| "subagent_wait"
|
|
24
|
+
| "subagent_status"
|
|
25
|
+
| "subagent_cancel"
|
|
26
|
+
| "subagent_delete";
|
|
27
|
+
|
|
28
|
+
interface RenderableCoordinatorMessage {
|
|
29
|
+
content: unknown;
|
|
30
|
+
details?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CoordinatorMessageRenderOptions {
|
|
34
|
+
expanded: boolean;
|
|
35
|
+
outputPad: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SUBAGENT_STATUS_PRESENTATION: Record<string, { symbol: string; color: ThemeColor }> = {
|
|
39
|
+
running: { symbol: "◉", color: "accent" },
|
|
40
|
+
waiting: { symbol: "◌", color: "accent" },
|
|
41
|
+
completed: { symbol: "✓", color: "success" },
|
|
42
|
+
failed: { symbol: "×", color: "error" },
|
|
43
|
+
cancelled: { symbol: "■", color: "warning" },
|
|
44
|
+
interrupted: { symbol: "!", color: "warning" },
|
|
45
|
+
unavailable: { symbol: "!", color: "warning" },
|
|
46
|
+
idle: { symbol: "○", color: "dim" },
|
|
47
|
+
delivered: { symbol: "→", color: "accent" },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
52
|
+
? (value as Record<string, unknown>)
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asString(value: unknown): string | undefined {
|
|
57
|
+
return typeof value === "string" ? value : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function asStringArray(value: unknown): string[] {
|
|
61
|
+
return Array.isArray(value)
|
|
62
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
63
|
+
: [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function asNumber(value: unknown): number | undefined {
|
|
67
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function coordinatorMessageText(content: unknown): string {
|
|
71
|
+
if (typeof content === "string") return content;
|
|
72
|
+
if (!Array.isArray(content)) return "";
|
|
73
|
+
return content
|
|
74
|
+
.map((item) => {
|
|
75
|
+
const block = asRecord(item);
|
|
76
|
+
return block?.type === "text" ? (asString(block.text) ?? "") : "";
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function toolResultText(result: AgentToolResult<unknown>): string {
|
|
83
|
+
const text = result.content.find((item) => item.type === "text");
|
|
84
|
+
return text?.type === "text" ? text.text : "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Render the shared semantic symbol and color for one subagent status. */
|
|
88
|
+
export function renderSubagentStatusSymbol(theme: Theme, status: string): string {
|
|
89
|
+
const presentation = SUBAGENT_STATUS_PRESENTATION[status] ?? SUBAGENT_STATUS_PRESENTATION.idle!;
|
|
90
|
+
return theme.fg(presentation.color, presentation.symbol);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Render a subagent status label with the same semantic color as its symbol. */
|
|
94
|
+
export function renderSubagentStatusLabel(theme: Theme, status: string): string {
|
|
95
|
+
const presentation = SUBAGENT_STATUS_PRESENTATION[status] ?? SUBAGENT_STATUS_PRESENTATION.idle!;
|
|
96
|
+
return theme.fg(presentation.color, status);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderSubagentSeparator(theme: Theme): string {
|
|
100
|
+
return theme.fg("dim", " · ");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderSubagentSummary(
|
|
104
|
+
theme: Theme,
|
|
105
|
+
status: string,
|
|
106
|
+
agentId: string,
|
|
107
|
+
metrics: readonly string[] = [],
|
|
108
|
+
): string {
|
|
109
|
+
const identity = `${renderSubagentStatusSymbol(theme, status)} ${theme.fg("accent", theme.bold(agentId))}`;
|
|
110
|
+
return [
|
|
111
|
+
identity,
|
|
112
|
+
renderSubagentStatusLabel(theme, status),
|
|
113
|
+
...metrics.map((metric) => theme.fg("muted", metric)),
|
|
114
|
+
].join(renderSubagentSeparator(theme));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderLabelValue(theme: Theme, label: string, value: unknown): Text {
|
|
118
|
+
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
119
|
+
return new Text(`${theme.fg("muted", `${label}:`)} ${text ?? ""}`, 0, 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function appendSection(
|
|
123
|
+
container: Container,
|
|
124
|
+
theme: Theme,
|
|
125
|
+
label: string,
|
|
126
|
+
content: string | Component,
|
|
127
|
+
): void {
|
|
128
|
+
container.addChild(new Spacer(1));
|
|
129
|
+
container.addChild(new Text(theme.fg("muted", theme.bold(label)), 0, 0));
|
|
130
|
+
container.addChild(typeof content === "string" ? new Text(content, 0, 0) : content);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderFallbackToolResult(
|
|
134
|
+
result: AgentToolResult<unknown>,
|
|
135
|
+
theme: Theme,
|
|
136
|
+
isError: boolean,
|
|
137
|
+
): Component {
|
|
138
|
+
const content = toolResultText(result) || "(no output)";
|
|
139
|
+
return new Text(isError ? theme.fg("error", content) : content, 0, 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function collapsedExpansionHint(theme: Theme): string {
|
|
143
|
+
return theme.fg("dim", ` · ${keyHint("app.tools.expand", "to expand")}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Format milliseconds for compact subagent rows without losing sub-second durations. */
|
|
147
|
+
export function formatSubagentDuration(elapsedMs: number | undefined): string | undefined {
|
|
148
|
+
if (elapsedMs === undefined || !Number.isFinite(elapsedMs) || elapsedMs < 0) return undefined;
|
|
149
|
+
if (elapsedMs < 1_000) return `${Math.round(elapsedMs)}ms`;
|
|
150
|
+
const seconds = Math.floor(elapsedMs / 1_000);
|
|
151
|
+
if (seconds < 60) return `${seconds}s`;
|
|
152
|
+
const minutes = Math.floor(seconds / 60);
|
|
153
|
+
const remainingSeconds = seconds % 60;
|
|
154
|
+
if (minutes < 60) return `${minutes}m ${String(remainingSeconds).padStart(2, "0")}s`;
|
|
155
|
+
const hours = Math.floor(minutes / 60);
|
|
156
|
+
return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Format token counts as compact decimal values for transcript and widget summaries. */
|
|
160
|
+
export function formatSubagentTokenCount(tokens: number | undefined): string | undefined {
|
|
161
|
+
if (tokens === undefined || !Number.isFinite(tokens) || tokens < 0) return undefined;
|
|
162
|
+
if (tokens < 1_000) return String(Math.round(tokens));
|
|
163
|
+
if (tokens < 1_000_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
164
|
+
return `${(tokens / 1_000_000).toFixed(tokens < 10_000_000 ? 1 : 0)}m`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Collapse multiline task or message text into one terminal-friendly preview. */
|
|
168
|
+
export function formatSubagentPreview(content: string, maxWidth = 72): string {
|
|
169
|
+
const singleLine = content.replace(/\s+/g, " ").trim();
|
|
170
|
+
const boundedWidth = Math.max(1, maxWidth);
|
|
171
|
+
if (visibleWidth(singleLine) <= boundedWidth) return singleLine;
|
|
172
|
+
if (boundedWidth === 1) return "…";
|
|
173
|
+
return `${sliceByColumn(singleLine, 0, boundedWidth - 1, true).trimEnd()}…`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function currentSubagentPreviewWidth(reservedWidth: number): number {
|
|
177
|
+
return Math.max(12, Math.min(72, (process.stdout.columns || 100) - reservedWidth));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Format complete Pi usage metrics for expanded subagent output. */
|
|
181
|
+
export function formatSubagentUsage(usage: Usage | undefined): string | undefined {
|
|
182
|
+
const usageRecord = asRecord(usage);
|
|
183
|
+
if (!usageRecord) return undefined;
|
|
184
|
+
const values = [
|
|
185
|
+
`input ${formatSubagentTokenCount(asNumber(usageRecord.input)) ?? "0"}`,
|
|
186
|
+
`output ${formatSubagentTokenCount(asNumber(usageRecord.output)) ?? "0"}`,
|
|
187
|
+
`cache read ${formatSubagentTokenCount(asNumber(usageRecord.cacheRead)) ?? "0"}`,
|
|
188
|
+
`cache write ${formatSubagentTokenCount(asNumber(usageRecord.cacheWrite)) ?? "0"}`,
|
|
189
|
+
`total ${formatSubagentTokenCount(asNumber(usageRecord.totalTokens)) ?? "0"}`,
|
|
190
|
+
];
|
|
191
|
+
const totalCost = asNumber(asRecord(usageRecord.cost)?.total);
|
|
192
|
+
if (totalCost !== undefined && totalCost > 0) values.push(`cost $${totalCost.toFixed(4)}`);
|
|
193
|
+
return values.join(" · ");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
type CoordinatorToolCallRenderer = (args: Record<string, unknown>, theme: Theme) => Component;
|
|
197
|
+
|
|
198
|
+
function coordinatorToolCallTitle(theme: Theme, label: string): string {
|
|
199
|
+
return theme.fg("toolTitle", theme.bold(label));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function coordinatorToolCallPreview(theme: Theme, value: unknown): string {
|
|
203
|
+
return typeof value === "string" && value.length > 0
|
|
204
|
+
? ` · ${theme.fg("dim", `“${formatSubagentPreview(value, currentSubagentPreviewWidth(36))}”`)}`
|
|
205
|
+
: "";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderManagementToolCall(
|
|
209
|
+
label: string,
|
|
210
|
+
args: Record<string, unknown>,
|
|
211
|
+
theme: Theme,
|
|
212
|
+
): Component {
|
|
213
|
+
return new Text(
|
|
214
|
+
`${coordinatorToolCallTitle(theme, label)} ${theme.fg("accent", asString(args.agent_id) ?? "agent")} ${theme.fg("dim", args.recursive === false ? "· target only" : "· recursive")}`,
|
|
215
|
+
0,
|
|
216
|
+
0,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const COORDINATOR_TOOL_CALL_RENDERERS: Record<CoordinatorToolName, CoordinatorToolCallRenderer> = {
|
|
221
|
+
subagent: (args, theme) =>
|
|
222
|
+
new Text(
|
|
223
|
+
`${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", asString(args.agent_id) ?? "generated")}${coordinatorToolCallPreview(theme, args.task)}`,
|
|
224
|
+
0,
|
|
225
|
+
0,
|
|
226
|
+
),
|
|
227
|
+
agent_message: (args, theme) =>
|
|
228
|
+
new Text(
|
|
229
|
+
`${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", asString(args.agent_id) ?? "parent")}${coordinatorToolCallPreview(theme, args.message)}`,
|
|
230
|
+
0,
|
|
231
|
+
0,
|
|
232
|
+
),
|
|
233
|
+
subagent_wait: (args, theme) =>
|
|
234
|
+
new Text(
|
|
235
|
+
`${coordinatorToolCallTitle(theme, "Wait")} ${theme.fg("accent", asString(args.agent_id) ?? "agent")}`,
|
|
236
|
+
0,
|
|
237
|
+
0,
|
|
238
|
+
),
|
|
239
|
+
subagent_status: (args, theme) =>
|
|
240
|
+
new Text(
|
|
241
|
+
`${coordinatorToolCallTitle(theme, "Status")} ${theme.fg("accent", asString(args.agent_id) ?? "children")}`,
|
|
242
|
+
0,
|
|
243
|
+
0,
|
|
244
|
+
),
|
|
245
|
+
subagent_cancel: (args, theme) => renderManagementToolCall("Cancel", args, theme),
|
|
246
|
+
subagent_delete: (args, theme) => renderManagementToolCall("Delete", args, theme),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
function renderSpawnResult(
|
|
250
|
+
details: Record<string, unknown>,
|
|
251
|
+
options: ToolRenderResultOptions,
|
|
252
|
+
theme: Theme,
|
|
253
|
+
args: Record<string, unknown>,
|
|
254
|
+
): Component {
|
|
255
|
+
const agentId = asString(details.agent_id) ?? "subagent";
|
|
256
|
+
const status = asString(details.status) ?? "running";
|
|
257
|
+
const agent = asRecord(details.agent);
|
|
258
|
+
const launchContract = asRecord(agent?.launch_contract);
|
|
259
|
+
if (!options.expanded) {
|
|
260
|
+
return new Text(
|
|
261
|
+
`${renderSubagentSummary(theme, status, agentId)}${collapsedExpansionHint(theme)}`,
|
|
262
|
+
0,
|
|
263
|
+
0,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const container = new Container();
|
|
267
|
+
container.addChild(new Text(renderSubagentSummary(theme, status, agentId), 0, 0));
|
|
268
|
+
container.addChild(renderLabelValue(theme, "Turn", asString(details.turn_id) ?? "unknown"));
|
|
269
|
+
appendSection(container, theme, "Task", asString(args.task) ?? "(task unavailable)");
|
|
270
|
+
const resolvedModel = asString(launchContract?.model) ?? asString(args.model);
|
|
271
|
+
const resolvedThinking =
|
|
272
|
+
asString(launchContract?.thinking_level) ?? asString(args.thinking_level);
|
|
273
|
+
const launch = [
|
|
274
|
+
`delegation ${asString(launchContract?.delegation) ?? asString(args.delegation) ?? "none"}`,
|
|
275
|
+
`session context ${asString(launchContract?.session_context) ?? asString(args.session_context) ?? "inherit"}`,
|
|
276
|
+
`project context ${asString(launchContract?.project_context) ?? asString(args.project_context) ?? "inherit"}`,
|
|
277
|
+
resolvedModel ? `model ${resolvedModel}` : undefined,
|
|
278
|
+
resolvedThinking ? `thinking ${resolvedThinking}` : undefined,
|
|
279
|
+
].filter(Boolean);
|
|
280
|
+
appendSection(container, theme, "Launch", launch.join(" · "));
|
|
281
|
+
appendSection(
|
|
282
|
+
container,
|
|
283
|
+
theme,
|
|
284
|
+
"Resolved tools",
|
|
285
|
+
asStringArray(launchContract?.ordinary_tools ?? agent?.tools).join(", ") || "none",
|
|
286
|
+
);
|
|
287
|
+
return container;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function renderMessageResult(
|
|
291
|
+
details: Record<string, unknown>,
|
|
292
|
+
options: ToolRenderResultOptions,
|
|
293
|
+
theme: Theme,
|
|
294
|
+
args: Record<string, unknown>,
|
|
295
|
+
): Component {
|
|
296
|
+
const agentId = asString(details.agent_id) ?? asString(args.agent_id) ?? "parent";
|
|
297
|
+
const historicalBehavior = asString(details.behavior) ?? asString(args.behavior);
|
|
298
|
+
const metrics = historicalBehavior ? [historicalBehavior] : [];
|
|
299
|
+
const delivered = details.delivered === true;
|
|
300
|
+
const summary = delivered
|
|
301
|
+
? renderSubagentSummary(theme, "delivered", agentId, metrics)
|
|
302
|
+
: renderSubagentSummary(theme, "failed", agentId, metrics);
|
|
303
|
+
if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
|
|
304
|
+
const container = new Container();
|
|
305
|
+
container.addChild(new Text(summary, 0, 0));
|
|
306
|
+
appendSection(container, theme, "Message", asString(args.message) ?? "(message unavailable)");
|
|
307
|
+
appendSection(container, theme, "Recipient", agentId);
|
|
308
|
+
const error = asString(details.error);
|
|
309
|
+
if (error) appendSection(container, theme, "Error", error);
|
|
310
|
+
return container;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderWaitResult(
|
|
314
|
+
details: Record<string, unknown>,
|
|
315
|
+
options: ToolRenderResultOptions,
|
|
316
|
+
theme: Theme,
|
|
317
|
+
args: Record<string, unknown>,
|
|
318
|
+
): Component {
|
|
319
|
+
const agentId = asString(details.agent_id) ?? asString(args.agent_id) ?? "agent";
|
|
320
|
+
const status = options.isPartial ? "waiting" : (asString(details.status) ?? "completed");
|
|
321
|
+
const duration = formatSubagentDuration(asNumber(details.elapsed_ms));
|
|
322
|
+
const usage = asRecord(details.usage) as Usage | undefined;
|
|
323
|
+
const tokens = formatSubagentTokenCount(usage?.totalTokens);
|
|
324
|
+
const metrics = [duration, tokens ? `${tokens} tokens` : undefined].filter(
|
|
325
|
+
(metric): metric is string => Boolean(metric),
|
|
326
|
+
);
|
|
327
|
+
const summary = renderSubagentSummary(theme, status, agentId, metrics);
|
|
328
|
+
if (options.isPartial || !options.expanded) {
|
|
329
|
+
return new Text(`${summary}${options.isPartial ? "" : collapsedExpansionHint(theme)}`, 0, 0);
|
|
330
|
+
}
|
|
331
|
+
const container = new Container();
|
|
332
|
+
container.addChild(new Text(summary, 0, 0));
|
|
333
|
+
container.addChild(renderLabelValue(theme, "Turn", asString(details.turn_id) ?? "unknown"));
|
|
334
|
+
const output = asString(details.output) ?? "";
|
|
335
|
+
if (status === "completed") {
|
|
336
|
+
appendSection(
|
|
337
|
+
container,
|
|
338
|
+
theme,
|
|
339
|
+
"Output",
|
|
340
|
+
output.length > 0 ? new Markdown(output, 0, 0, getMarkdownTheme()) : "(no output)",
|
|
341
|
+
);
|
|
342
|
+
} else {
|
|
343
|
+
appendSection(
|
|
344
|
+
container,
|
|
345
|
+
theme,
|
|
346
|
+
"Error",
|
|
347
|
+
(asString(details.error) ?? output) || "(no error detail)",
|
|
348
|
+
);
|
|
349
|
+
appendSection(container, theme, "Diagnostics", JSON.stringify(details, null, 2));
|
|
350
|
+
}
|
|
351
|
+
const usageText = formatSubagentUsage(usage);
|
|
352
|
+
if (usageText) appendSection(container, theme, "Usage", usageText);
|
|
353
|
+
return container;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function countDirectStatusAgents(agents: unknown[]): { children: number; running: number } {
|
|
357
|
+
let children = 0;
|
|
358
|
+
let running = 0;
|
|
359
|
+
for (const item of agents) {
|
|
360
|
+
const agent = asRecord(item);
|
|
361
|
+
if (!agent) continue;
|
|
362
|
+
children++;
|
|
363
|
+
if (agent.state === "running") running++;
|
|
364
|
+
}
|
|
365
|
+
return { children, running };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function renderDirectStatusRows(agents: unknown[], theme: Theme): string[] {
|
|
369
|
+
const rows: string[] = [];
|
|
370
|
+
for (const item of agents) {
|
|
371
|
+
const agent = asRecord(item);
|
|
372
|
+
if (!agent) continue;
|
|
373
|
+
const availability = asString(agent.availability) ?? "available";
|
|
374
|
+
const latestTurn = asRecord(agent.latest_turn);
|
|
375
|
+
const status =
|
|
376
|
+
availability === "unavailable"
|
|
377
|
+
? "unavailable"
|
|
378
|
+
: asString(agent.state) === "running"
|
|
379
|
+
? "running"
|
|
380
|
+
: (asString(latestTurn?.status) ?? "idle");
|
|
381
|
+
const duration = formatSubagentDuration(asNumber(agent.elapsed_ms));
|
|
382
|
+
const childCount = asNumber(agent.child_count) ?? 0;
|
|
383
|
+
const metrics = [duration, childCount > 0 ? `${childCount} children` : undefined].filter(
|
|
384
|
+
(metric): metric is string => Boolean(metric),
|
|
385
|
+
);
|
|
386
|
+
rows.push(renderSubagentSummary(theme, status, asString(agent.agent_id) ?? "unknown", metrics));
|
|
387
|
+
}
|
|
388
|
+
return rows;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function renderStatusResult(
|
|
392
|
+
details: Record<string, unknown>,
|
|
393
|
+
options: ToolRenderResultOptions,
|
|
394
|
+
theme: Theme,
|
|
395
|
+
): Component {
|
|
396
|
+
const agents = Array.isArray(details.agents) ? details.agents : undefined;
|
|
397
|
+
if (agents) {
|
|
398
|
+
const counts = countDirectStatusAgents(agents);
|
|
399
|
+
const summary = [
|
|
400
|
+
theme.fg("muted", `${counts.children} children`),
|
|
401
|
+
theme.fg(counts.running > 0 ? "accent" : "dim", `${counts.running} running`),
|
|
402
|
+
].join(renderSubagentSeparator(theme));
|
|
403
|
+
if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
|
|
404
|
+
return new Text(
|
|
405
|
+
`${summary}\n${renderDirectStatusRows(agents, theme).join("\n") || theme.fg("dim", "(no agents)")}`,
|
|
406
|
+
0,
|
|
407
|
+
0,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const agent = asRecord(details.agent);
|
|
411
|
+
if (!agent) {
|
|
412
|
+
return new Text(
|
|
413
|
+
[theme.fg("muted", "0 children"), theme.fg("dim", "0 running")].join(
|
|
414
|
+
renderSubagentSeparator(theme),
|
|
415
|
+
),
|
|
416
|
+
0,
|
|
417
|
+
0,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const availability = asString(agent.availability) ?? "available";
|
|
421
|
+
const latestTurn = asRecord(agent.latest_turn);
|
|
422
|
+
const status =
|
|
423
|
+
availability === "unavailable"
|
|
424
|
+
? "unavailable"
|
|
425
|
+
: asString(agent.state) === "running"
|
|
426
|
+
? "running"
|
|
427
|
+
: (asString(latestTurn?.status) ?? "idle");
|
|
428
|
+
const id = asString(agent.agent_id) ?? "agent";
|
|
429
|
+
const childCount = asNumber(agent.child_count) ?? 0;
|
|
430
|
+
const duration = formatSubagentDuration(asNumber(agent.elapsed_ms));
|
|
431
|
+
const summary = renderSubagentSummary(
|
|
432
|
+
theme,
|
|
433
|
+
status,
|
|
434
|
+
id,
|
|
435
|
+
[duration, `${childCount} children`].filter((metric): metric is string => Boolean(metric)),
|
|
436
|
+
);
|
|
437
|
+
if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
|
|
438
|
+
const container = new Container();
|
|
439
|
+
container.addChild(new Text(summary, 0, 0));
|
|
440
|
+
for (const [label, value] of [
|
|
441
|
+
["Parent", agent.parent_id],
|
|
442
|
+
["Availability", availability],
|
|
443
|
+
["Turn", agent.active_turn_id ?? asRecord(agent.latest_turn)?.turn_id],
|
|
444
|
+
["Duration", duration],
|
|
445
|
+
["Model", agent.model],
|
|
446
|
+
["Thinking", agent.thinking_level],
|
|
447
|
+
["Session", agent.session_file],
|
|
448
|
+
["Spawn entry", agent.spawn_entry_id],
|
|
449
|
+
] as const) {
|
|
450
|
+
if (value !== undefined) container.addChild(renderLabelValue(theme, label, value));
|
|
451
|
+
}
|
|
452
|
+
if (asString(agent.task)) appendSection(container, theme, "Task", String(agent.task));
|
|
453
|
+
const launchContract = asRecord(agent.launch_contract);
|
|
454
|
+
if (launchContract) {
|
|
455
|
+
const launchValues = [
|
|
456
|
+
`session context ${asString(launchContract.session_context) ?? "inherit"}`,
|
|
457
|
+
`project context ${asString(launchContract.project_context) ?? "inherit"}`,
|
|
458
|
+
`model ${asString(launchContract.model) ?? asString(agent.model) ?? "unknown"}`,
|
|
459
|
+
`thinking ${asString(launchContract.thinking_level) ?? asString(agent.thinking_level) ?? "unknown"}`,
|
|
460
|
+
`delegation ${asString(launchContract.delegation) ?? "none"}`,
|
|
461
|
+
];
|
|
462
|
+
appendSection(container, theme, "Launch contract", launchValues.join(" · "));
|
|
463
|
+
}
|
|
464
|
+
appendSection(container, theme, "Tools", asStringArray(agent.tools).join(", ") || "none");
|
|
465
|
+
appendSection(
|
|
466
|
+
container,
|
|
467
|
+
theme,
|
|
468
|
+
"Capability ceiling",
|
|
469
|
+
asStringArray(agent.capability_ceiling).join(", ") || "none",
|
|
470
|
+
);
|
|
471
|
+
const missing = asStringArray(agent.missing_dependencies);
|
|
472
|
+
if (missing.length > 0)
|
|
473
|
+
appendSection(container, theme, "Missing dependencies", missing.join("\n"));
|
|
474
|
+
if (asString(agent.unavailable_reason)) {
|
|
475
|
+
appendSection(container, theme, "Unavailable reason", String(agent.unavailable_reason));
|
|
476
|
+
}
|
|
477
|
+
const recentMessages = Array.isArray(agent.recent_messages)
|
|
478
|
+
? agent.recent_messages
|
|
479
|
+
.map(asRecord)
|
|
480
|
+
.filter((item): item is Record<string, unknown> => Boolean(item))
|
|
481
|
+
: [];
|
|
482
|
+
if (recentMessages.length > 0) {
|
|
483
|
+
appendSection(
|
|
484
|
+
container,
|
|
485
|
+
theme,
|
|
486
|
+
"Recent messages",
|
|
487
|
+
recentMessages
|
|
488
|
+
.map(
|
|
489
|
+
(message) =>
|
|
490
|
+
`${asString(message.source_agent_id) ?? "unknown"}: ${asString(message.content) ?? ""}`,
|
|
491
|
+
)
|
|
492
|
+
.join("\n"),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
const latestResult = asRecord(agent.latest_result);
|
|
496
|
+
if (latestResult) {
|
|
497
|
+
const output = asString(latestResult.output) ?? "";
|
|
498
|
+
appendSection(
|
|
499
|
+
container,
|
|
500
|
+
theme,
|
|
501
|
+
"Latest result",
|
|
502
|
+
asString(latestResult.status) === "completed" && output
|
|
503
|
+
? new Markdown(output, 0, 0, getMarkdownTheme())
|
|
504
|
+
: output || JSON.stringify(latestResult, null, 2),
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const usageText = formatSubagentUsage(asRecord(agent.usage) as Usage | undefined);
|
|
508
|
+
if (usageText) appendSection(container, theme, "Usage", usageText);
|
|
509
|
+
return container;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function renderCancelResult(
|
|
513
|
+
details: Record<string, unknown>,
|
|
514
|
+
options: ToolRenderResultOptions,
|
|
515
|
+
theme: Theme,
|
|
516
|
+
): Component {
|
|
517
|
+
const id = asString(details.agent_id) ?? "agent";
|
|
518
|
+
const turns = asStringArray(details.cancelled_turn_ids);
|
|
519
|
+
const summary =
|
|
520
|
+
turns.length > 0
|
|
521
|
+
? renderSubagentSummary(theme, "cancelled", id, [`${turns.length} turns cancelled`])
|
|
522
|
+
: renderSubagentSummary(theme, "completed", id, ["no active turns"]);
|
|
523
|
+
if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
|
|
524
|
+
const container = new Container();
|
|
525
|
+
container.addChild(new Text(summary, 0, 0));
|
|
526
|
+
container.addChild(renderLabelValue(theme, "Requested target", id));
|
|
527
|
+
container.addChild(
|
|
528
|
+
renderLabelValue(theme, "Mode", details.recursive === false ? "target only" : "recursive"),
|
|
529
|
+
);
|
|
530
|
+
appendSection(
|
|
531
|
+
container,
|
|
532
|
+
theme,
|
|
533
|
+
"Affected agents",
|
|
534
|
+
asStringArray(details.affected_agent_ids).join("\n") || "(none)",
|
|
535
|
+
);
|
|
536
|
+
appendSection(container, theme, "Cancelled turns", turns.join("\n") || "(none)");
|
|
537
|
+
return container;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function renderDeleteResult(
|
|
541
|
+
details: Record<string, unknown>,
|
|
542
|
+
options: ToolRenderResultOptions,
|
|
543
|
+
theme: Theme,
|
|
544
|
+
): Component {
|
|
545
|
+
const id = asString(details.agent_id) ?? "agent";
|
|
546
|
+
const deleted = asStringArray(details.deleted_agent_ids);
|
|
547
|
+
const tombstoned = asStringArray(details.tombstoned_agent_ids);
|
|
548
|
+
const failures = Array.isArray(details.failures) ? details.failures : [];
|
|
549
|
+
const status = failures.length > 0 ? "failed" : "completed";
|
|
550
|
+
const summary = renderSubagentSummary(
|
|
551
|
+
theme,
|
|
552
|
+
status,
|
|
553
|
+
id,
|
|
554
|
+
[
|
|
555
|
+
`${deleted.length} agents deleted`,
|
|
556
|
+
`${tombstoned.length} tombstoned`,
|
|
557
|
+
failures.length > 0 ? `${failures.length} failed` : undefined,
|
|
558
|
+
].filter((metric): metric is string => Boolean(metric)),
|
|
559
|
+
);
|
|
560
|
+
if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
|
|
561
|
+
const container = new Container();
|
|
562
|
+
container.addChild(new Text(summary, 0, 0));
|
|
563
|
+
container.addChild(renderLabelValue(theme, "Requested target", id));
|
|
564
|
+
container.addChild(
|
|
565
|
+
renderLabelValue(theme, "Mode", details.recursive === false ? "target only" : "recursive"),
|
|
566
|
+
);
|
|
567
|
+
appendSection(container, theme, "Deleted agents", deleted.join("\n") || "(none)");
|
|
568
|
+
appendSection(container, theme, "Tombstones", tombstoned.join("\n") || "(none)");
|
|
569
|
+
appendSection(
|
|
570
|
+
container,
|
|
571
|
+
theme,
|
|
572
|
+
"Trashed sessions",
|
|
573
|
+
asStringArray(details.trashed_session_files).join("\n") || "(none)",
|
|
574
|
+
);
|
|
575
|
+
if (failures.length > 0) {
|
|
576
|
+
appendSection(
|
|
577
|
+
container,
|
|
578
|
+
theme,
|
|
579
|
+
"Failures",
|
|
580
|
+
theme.fg("error", JSON.stringify(failures, null, 2)),
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
return container;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
type CoordinatorToolResultRenderer = (
|
|
587
|
+
details: Record<string, unknown>,
|
|
588
|
+
options: ToolRenderResultOptions,
|
|
589
|
+
theme: Theme,
|
|
590
|
+
args: Record<string, unknown>,
|
|
591
|
+
) => Component;
|
|
592
|
+
|
|
593
|
+
const COORDINATOR_TOOL_RESULT_RENDERERS: Record<
|
|
594
|
+
CoordinatorToolName,
|
|
595
|
+
CoordinatorToolResultRenderer
|
|
596
|
+
> = {
|
|
597
|
+
subagent: renderSpawnResult,
|
|
598
|
+
agent_message: renderMessageResult,
|
|
599
|
+
subagent_wait: renderWaitResult,
|
|
600
|
+
subagent_status: (details, options, theme) => renderStatusResult(details, options, theme),
|
|
601
|
+
subagent_cancel: (details, options, theme) => renderCancelResult(details, options, theme),
|
|
602
|
+
subagent_delete: (details, options, theme) => renderDeleteResult(details, options, theme),
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const COORDINATOR_DETAIL_VALIDATORS: Record<
|
|
606
|
+
CoordinatorToolName,
|
|
607
|
+
(details: Record<string, unknown>) => boolean
|
|
608
|
+
> = {
|
|
609
|
+
subagent: (details) =>
|
|
610
|
+
asString(details.agent_id) !== undefined &&
|
|
611
|
+
asString(details.turn_id) !== undefined &&
|
|
612
|
+
asString(details.status) !== undefined,
|
|
613
|
+
agent_message: (details) =>
|
|
614
|
+
asString(details.agent_id) !== undefined && typeof details.delivered === "boolean",
|
|
615
|
+
subagent_wait: (details) =>
|
|
616
|
+
asString(details.agent_id) !== undefined && asString(details.status) !== undefined,
|
|
617
|
+
subagent_status: (details) =>
|
|
618
|
+
Array.isArray(details.agents) || asRecord(details.agent) !== undefined,
|
|
619
|
+
subagent_cancel: (details) =>
|
|
620
|
+
asString(details.agent_id) !== undefined &&
|
|
621
|
+
Array.isArray(details.affected_agent_ids) &&
|
|
622
|
+
Array.isArray(details.cancelled_turn_ids),
|
|
623
|
+
subagent_delete: (details) =>
|
|
624
|
+
asString(details.agent_id) !== undefined &&
|
|
625
|
+
Array.isArray(details.deleted_agent_ids) &&
|
|
626
|
+
Array.isArray(details.tombstoned_agent_ids) &&
|
|
627
|
+
Array.isArray(details.failures),
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
/** Render one of the six coordinator tool calls with a shared native Pi grammar. */
|
|
631
|
+
export function renderCoordinatorToolCall(
|
|
632
|
+
toolName: CoordinatorToolName,
|
|
633
|
+
args: Record<string, unknown>,
|
|
634
|
+
theme: Theme,
|
|
635
|
+
): Component {
|
|
636
|
+
return COORDINATOR_TOOL_CALL_RENDERERS[toolName](args, theme);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Render one coordinator tool result in native collapsed, expanded, or partial mode. */
|
|
640
|
+
export function renderCoordinatorToolResult(
|
|
641
|
+
toolName: CoordinatorToolName,
|
|
642
|
+
result: AgentToolResult<unknown>,
|
|
643
|
+
options: ToolRenderResultOptions,
|
|
644
|
+
theme: Theme,
|
|
645
|
+
args: Record<string, unknown>,
|
|
646
|
+
isError = false,
|
|
647
|
+
): Component {
|
|
648
|
+
const details = asRecord(result.details);
|
|
649
|
+
if (!details || !COORDINATOR_DETAIL_VALIDATORS[toolName](details)) {
|
|
650
|
+
return renderFallbackToolResult(result, theme, isError);
|
|
651
|
+
}
|
|
652
|
+
return COORDINATOR_TOOL_RESULT_RENDERERS[toolName](details, options, theme, args);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Render explicit agent messages with compact source/destination metadata. */
|
|
656
|
+
export function renderMinimalSubagentsMessage(
|
|
657
|
+
message: RenderableCoordinatorMessage,
|
|
658
|
+
options: CoordinatorMessageRenderOptions,
|
|
659
|
+
theme: Theme,
|
|
660
|
+
): Component {
|
|
661
|
+
return renderCoordinatorMessage("Agent message", "→", message, options, theme);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Render automatic successful agent results with expandable Markdown output. */
|
|
665
|
+
export function renderMinimalSubagentsResult(
|
|
666
|
+
message: RenderableCoordinatorMessage,
|
|
667
|
+
options: CoordinatorMessageRenderOptions,
|
|
668
|
+
theme: Theme,
|
|
669
|
+
): Component {
|
|
670
|
+
return renderCoordinatorMessage("Agent result", "✓", message, options, theme);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function renderCoordinatorMessage(
|
|
674
|
+
label: string,
|
|
675
|
+
symbol: string,
|
|
676
|
+
message: RenderableCoordinatorMessage,
|
|
677
|
+
options: CoordinatorMessageRenderOptions,
|
|
678
|
+
theme: Theme,
|
|
679
|
+
): Component {
|
|
680
|
+
const details = asRecord(message.details);
|
|
681
|
+
const content = coordinatorMessageText(message.content);
|
|
682
|
+
const source = asString(details?.source_agent_id) ?? "unknown";
|
|
683
|
+
const destination = asString(details?.destination_agent_id) ?? "recipient";
|
|
684
|
+
const status = asString(details?.status);
|
|
685
|
+
const route = `${theme.fg("accent", theme.bold(source))} ${theme.fg("dim", "→")} ${theme.fg("accent", theme.bold(destination))}`;
|
|
686
|
+
const heading = [
|
|
687
|
+
`${theme.fg(symbol === "✓" ? "success" : "accent", symbol)} ${theme.bold(label)}`,
|
|
688
|
+
route,
|
|
689
|
+
status ? renderSubagentStatusLabel(theme, status) : undefined,
|
|
690
|
+
]
|
|
691
|
+
.filter((part): part is string => Boolean(part))
|
|
692
|
+
.join(renderSubagentSeparator(theme));
|
|
693
|
+
const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
|
|
694
|
+
if (!options.expanded) {
|
|
695
|
+
box.addChild(
|
|
696
|
+
new Text(
|
|
697
|
+
`${heading}\n${theme.fg("muted", formatSubagentPreview(content, currentSubagentPreviewWidth(24)))}`,
|
|
698
|
+
0,
|
|
699
|
+
0,
|
|
700
|
+
),
|
|
701
|
+
);
|
|
702
|
+
return box;
|
|
703
|
+
}
|
|
704
|
+
const container = new Container();
|
|
705
|
+
container.addChild(new Text(heading, 0, 0));
|
|
706
|
+
if (asString(details?.source_turn_id)) {
|
|
707
|
+
container.addChild(renderLabelValue(theme, "Source turn", details?.source_turn_id));
|
|
708
|
+
}
|
|
709
|
+
const duration = formatSubagentDuration(asNumber(details?.elapsed_ms));
|
|
710
|
+
if (duration) container.addChild(renderLabelValue(theme, "Duration", duration));
|
|
711
|
+
container.addChild(new Spacer(1));
|
|
712
|
+
container.addChild(new Markdown(content, 0, 0, getMarkdownTheme()));
|
|
713
|
+
const usageText = formatSubagentUsage(asRecord(details?.usage) as Usage | undefined);
|
|
714
|
+
if (usageText) appendSection(container, theme, "Usage", usageText);
|
|
715
|
+
box.addChild(container);
|
|
716
|
+
return box;
|
|
717
|
+
}
|