@ian-pascoe/pi-minimal-subagents 0.12.0 → 0.13.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 +58 -4
- package/package.json +1 -1
- package/src/minimal-subagents-capabilities.ts +12 -1
- package/src/minimal-subagents-context.ts +180 -4
- package/src/minimal-subagents-coordinator.ts +115 -24
- package/src/minimal-subagents-extension.ts +3 -0
- package/src/minimal-subagents-registry.ts +3 -1
- package/src/minimal-subagents-render-contract.ts +30 -2
- package/src/minimal-subagents-rendering.ts +318 -52
- package/src/minimal-subagents-status-panel.ts +26 -203
- package/src/minimal-subagents-tool-schemas.ts +12 -3
- package/src/minimal-subagents-tools.ts +104 -33
- package/src/minimal-subagents-transcript.ts +296 -0
- package/src/minimal-subagents-types.ts +22 -5
- package/src/minimal-subagents-ui.ts +1 -1
- package/src/minimal-subagents-usage.ts +13 -2
|
@@ -90,6 +90,7 @@ const SpawnCallArgumentsSchema = Type.Object({
|
|
|
90
90
|
model: Type.Optional(Type.String()),
|
|
91
91
|
thinking_level: Type.Optional(Type.String()),
|
|
92
92
|
delegation: Type.Optional(Type.String()),
|
|
93
|
+
tools: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
|
|
93
94
|
});
|
|
94
95
|
const MessageCallArgumentsSchema = Type.Object({
|
|
95
96
|
agent_id: Type.Optional(Type.String()),
|
|
@@ -113,6 +114,7 @@ const CurrentMessageDetailsSchema = Type.Object({
|
|
|
113
114
|
agent_id: Type.String(),
|
|
114
115
|
message_id: Type.Optional(Type.String()),
|
|
115
116
|
disposition: Type.String(),
|
|
117
|
+
turn_id: Type.Optional(Type.String()),
|
|
116
118
|
behavior: Type.Optional(Type.String()),
|
|
117
119
|
error: Type.Optional(Type.String()),
|
|
118
120
|
});
|
|
@@ -209,7 +211,7 @@ const CoordinatorAgentSummaryOutputSchema = Type.Object({
|
|
|
209
211
|
latest_activity_at: Type.Optional(Type.String()),
|
|
210
212
|
task: Type.Optional(Type.String()),
|
|
211
213
|
child_count: Type.Number(),
|
|
212
|
-
children: Type.Array(Type.Unknown()),
|
|
214
|
+
children: Type.Optional(Type.Array(Type.Unknown())),
|
|
213
215
|
model: Type.String(),
|
|
214
216
|
thinking_level: Type.String(),
|
|
215
217
|
});
|
|
@@ -266,7 +268,11 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
266
268
|
agent_id: Type.String(),
|
|
267
269
|
turn_id: Type.String(),
|
|
268
270
|
status: Type.Literal("running"),
|
|
269
|
-
|
|
271
|
+
model: Type.String(),
|
|
272
|
+
thinking_level: Type.String(),
|
|
273
|
+
tools: Type.Array(Type.String()),
|
|
274
|
+
delegation: Type.Union([Type.Literal("none"), Type.Literal("fanout")]),
|
|
275
|
+
warnings: Type.Optional(Type.Array(Type.String())),
|
|
270
276
|
}),
|
|
271
277
|
agent_message: Type.Object({
|
|
272
278
|
agent_id: Type.String(),
|
|
@@ -274,8 +280,10 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
274
280
|
disposition: Type.Union([
|
|
275
281
|
Type.Literal("delivered-via-wait"),
|
|
276
282
|
Type.Literal("queued"),
|
|
283
|
+
Type.Literal("started-turn"),
|
|
277
284
|
Type.Literal("failed"),
|
|
278
285
|
]),
|
|
286
|
+
turn_id: Type.Optional(Type.String()),
|
|
279
287
|
error: Type.Optional(Type.String()),
|
|
280
288
|
}),
|
|
281
289
|
subagent_wait: Type.Union([
|
|
@@ -319,6 +327,25 @@ const WaitRenderDetailsSchema = Type.Union([
|
|
|
319
327
|
WaitTimeoutDetailsSchema,
|
|
320
328
|
]);
|
|
321
329
|
|
|
330
|
+
/** Partial `subagent_wait` updates: elapsed wait time and, while the child runs, its progress. */
|
|
331
|
+
const WaitProgressRenderDetailsSchema = Type.Object({
|
|
332
|
+
agent_id: Type.String(),
|
|
333
|
+
status: Type.Literal("waiting"),
|
|
334
|
+
elapsed_ms: Type.Number(),
|
|
335
|
+
turn_id: Type.Optional(Type.String()),
|
|
336
|
+
tool_calls: Type.Optional(Type.Number()),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
export type WaitProgressRenderDetails = Static<typeof WaitProgressRenderDetailsSchema>;
|
|
340
|
+
|
|
341
|
+
/** Parse one partial `subagent_wait` progress update. */
|
|
342
|
+
export function parseWaitProgressDetails(
|
|
343
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Partial tool details are unparsed until the progress schema checks them below.
|
|
344
|
+
details: unknown,
|
|
345
|
+
): WaitProgressRenderDetails | undefined {
|
|
346
|
+
return Value.Check(WaitProgressRenderDetailsSchema, details) ? details : undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
322
349
|
export type SpawnCallArguments = Static<typeof SpawnCallArgumentsSchema>;
|
|
323
350
|
export type MessageCallArguments = Static<typeof MessageCallArgumentsSchema>;
|
|
324
351
|
export type WaitCallArguments = Static<typeof WaitCallArgumentsSchema>;
|
|
@@ -331,6 +358,7 @@ export type StatusRenderDetails = Static<typeof StatusDetailsSchema>;
|
|
|
331
358
|
export type CancelRenderDetails = Static<typeof CancelDetailsSchema>;
|
|
332
359
|
export type DeleteRenderDetails = Static<typeof DeleteDetailsSchema>;
|
|
333
360
|
export type RenderStatusAgent = Static<typeof RenderStatusAgentSchema>;
|
|
361
|
+
export type RenderRecentActivity = Static<typeof RenderRecentActivitySchema>;
|
|
334
362
|
|
|
335
363
|
/** Raw tool arguments supplied by Pi's tool-rendering interface. */
|
|
336
364
|
export type CoordinatorToolCallInput = Parameters<NonNullable<ToolDefinition["renderCall"]>>[0];
|
|
@@ -19,10 +19,13 @@ import {
|
|
|
19
19
|
visibleWidth,
|
|
20
20
|
type Component,
|
|
21
21
|
} from "@earendil-works/pi-tui";
|
|
22
|
+
import { Type } from "typebox";
|
|
23
|
+
import { Value } from "typebox/value";
|
|
22
24
|
import {
|
|
23
25
|
parseCoordinatorMessageDetails,
|
|
24
26
|
parseCoordinatorToolCall,
|
|
25
27
|
parseCoordinatorToolResult,
|
|
28
|
+
parseWaitProgressDetails,
|
|
26
29
|
type CancelRenderDetails,
|
|
27
30
|
type CoordinatorMessageRenderDetails,
|
|
28
31
|
type CoordinatorToolCallInput,
|
|
@@ -31,11 +34,13 @@ import {
|
|
|
31
34
|
type ManagementCallArguments,
|
|
32
35
|
type MessageCallArguments,
|
|
33
36
|
type MessageRenderDetails,
|
|
37
|
+
type RenderRecentActivity,
|
|
34
38
|
type RenderStatusAgent,
|
|
35
39
|
type SpawnCallArguments,
|
|
36
40
|
type SpawnRenderDetails,
|
|
37
41
|
type StatusRenderDetails,
|
|
38
42
|
type WaitCallArguments,
|
|
43
|
+
type WaitProgressRenderDetails,
|
|
39
44
|
type WaitRenderDetails,
|
|
40
45
|
} from "./minimal-subagents-render-contract.js";
|
|
41
46
|
import { stripCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
|
|
@@ -63,6 +68,7 @@ type SubagentPresentationStatus =
|
|
|
63
68
|
| "delivered"
|
|
64
69
|
| "delivered-via-wait"
|
|
65
70
|
| "queued"
|
|
71
|
+
| "started-turn"
|
|
66
72
|
| "message"
|
|
67
73
|
| "timed out";
|
|
68
74
|
|
|
@@ -80,6 +86,7 @@ const SUBAGENT_STATUS_PRESENTATION = {
|
|
|
80
86
|
delivered: { symbol: "→", color: "accent" },
|
|
81
87
|
"delivered-via-wait": { symbol: "→", color: "accent" },
|
|
82
88
|
queued: { symbol: "↗", color: "accent" },
|
|
89
|
+
"started-turn": { symbol: "◉", color: "accent" },
|
|
83
90
|
message: { symbol: "→", color: "accent" },
|
|
84
91
|
"timed out": { symbol: "!", color: "warning" },
|
|
85
92
|
} satisfies { readonly [Status in SubagentPresentationStatus]: SubagentStatusPresentation };
|
|
@@ -106,7 +113,7 @@ export function orderActiveAgentSubtrees(agents: readonly AgentSummary[]): Agent
|
|
|
106
113
|
): { agent: AgentSummary; active: boolean }[] =>
|
|
107
114
|
siblings
|
|
108
115
|
.map((agent) => {
|
|
109
|
-
const children = orderSiblings(agent.children);
|
|
116
|
+
const children = orderSiblings(agent.children ?? []);
|
|
110
117
|
return {
|
|
111
118
|
agent: { ...agent, children: children.map((child) => child.agent) },
|
|
112
119
|
active: agent.state === "running" || children.some((child) => child.active),
|
|
@@ -215,6 +222,169 @@ function collapsedExpansionHint(theme: MinimalSubagentsRenderTheme): string {
|
|
|
215
222
|
return theme.fg("dim", ` · ${keyHint("app.tools.expand", "to expand")}`);
|
|
216
223
|
}
|
|
217
224
|
|
|
225
|
+
/** Collapsed previews match Pi's built-in `read` output height. */
|
|
226
|
+
const COLLAPSED_PREVIEW_LINES = 10;
|
|
227
|
+
|
|
228
|
+
function isBlankRenderedLine(line: string): boolean {
|
|
229
|
+
return visibleWidth(line.trim()) === 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function withoutTrailingBlankLines(lines: string[]): string[] {
|
|
233
|
+
const end = lines.findLastIndex((line) => !isBlankRenderedLine(line));
|
|
234
|
+
return lines.slice(0, end + 1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Shows the first rendered lines of `content`, noting how many lines expanding would reveal. */
|
|
238
|
+
class CollapsedPreview implements Component {
|
|
239
|
+
constructor(
|
|
240
|
+
private readonly content: Component,
|
|
241
|
+
private readonly theme: MinimalSubagentsRenderTheme,
|
|
242
|
+
) {}
|
|
243
|
+
|
|
244
|
+
render(width: number): string[] {
|
|
245
|
+
const lines = withoutTrailingBlankLines(this.content.render(width));
|
|
246
|
+
if (lines.length <= COLLAPSED_PREVIEW_LINES) return lines;
|
|
247
|
+
// A cut at a paragraph break would leave a blank line above the hint.
|
|
248
|
+
const shown = withoutTrailingBlankLines(lines.slice(0, COLLAPSED_PREVIEW_LINES));
|
|
249
|
+
const hidden = lines.length - shown.length;
|
|
250
|
+
return [
|
|
251
|
+
...shown,
|
|
252
|
+
// The result's first line already carries the expansion hint.
|
|
253
|
+
this.theme.fg("muted", `... (${hidden} more lines)`),
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
invalidate(): void {
|
|
258
|
+
this.content.invalidate();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function collapsedMarkdownPreview(content: string, theme: MinimalSubagentsRenderTheme): Component {
|
|
263
|
+
return new CollapsedPreview(new Markdown(content, 0, 0, getMarkdownTheme()), theme);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function collapsedTextPreview(content: string, theme: MinimalSubagentsRenderTheme): Component {
|
|
267
|
+
return new CollapsedPreview(new Text(content, 0, 0), theme);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const ToolArgumentsSchema = Type.Record(Type.String(), Type.Unknown());
|
|
271
|
+
const ScalarArgumentSchema = Type.Union([Type.String(), Type.Number(), Type.Boolean()]);
|
|
272
|
+
|
|
273
|
+
/** Summarize tool-call arguments by their values, e.g. a path or command rather than its JSON. */
|
|
274
|
+
function toolArgumentSummary(content: string): string {
|
|
275
|
+
let parsed: unknown;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(content);
|
|
278
|
+
} catch {
|
|
279
|
+
// Recent Activity keeps the tail of long arguments, which is no longer valid JSON.
|
|
280
|
+
return content;
|
|
281
|
+
}
|
|
282
|
+
if (!Value.Check(ToolArgumentsSchema, parsed)) return content;
|
|
283
|
+
return Object.values(parsed)
|
|
284
|
+
.filter((value) => value !== null && value !== undefined)
|
|
285
|
+
.map((value) =>
|
|
286
|
+
Value.Check(ScalarArgumentSchema, value) ? String(value) : JSON.stringify(value),
|
|
287
|
+
)
|
|
288
|
+
.join(" ");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function lastNonEmptyLine(content: string): string {
|
|
292
|
+
return (
|
|
293
|
+
content
|
|
294
|
+
.split("\n")
|
|
295
|
+
.map((line) => line.trim())
|
|
296
|
+
.findLast((line) => line.length > 0) ?? ""
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** One activity entry as a single width-bounded line, truncating plain text before styling it. */
|
|
301
|
+
function formatActivityLine(
|
|
302
|
+
activity: RenderRecentActivity,
|
|
303
|
+
theme: MinimalSubagentsRenderTheme,
|
|
304
|
+
width: number,
|
|
305
|
+
): string {
|
|
306
|
+
const fit = (prefix: string, text: string) =>
|
|
307
|
+
formatSubagentPreview(text, Math.max(1, width - visibleWidth(prefix)));
|
|
308
|
+
if (activity.label.startsWith("tool call ")) {
|
|
309
|
+
const name = activity.label.slice("tool call ".length);
|
|
310
|
+
const prefix = `\u2192 ${name} `;
|
|
311
|
+
return `${theme.fg("accent", "\u2192")} ${theme.fg("toolTitle", name)} ${theme.fg("dim", fit(prefix, toolArgumentSummary(activity.content)))}`;
|
|
312
|
+
}
|
|
313
|
+
if (activity.label.startsWith("tool result ")) {
|
|
314
|
+
const failed = activity.label.endsWith(" (error)");
|
|
315
|
+
const name = activity.label.slice("tool result ".length).replace(/ \(error\)$/, "");
|
|
316
|
+
const prefix = `${failed ? "\u2717" : "\u2190"} ${name}: `;
|
|
317
|
+
return theme.fg(failed ? "error" : "dim", `${prefix}${fit(prefix, activity.content)}`);
|
|
318
|
+
}
|
|
319
|
+
if (activity.label === "reasoning") {
|
|
320
|
+
const prefix = "thinking: ";
|
|
321
|
+
return theme.fg("dim", `${prefix}${fit(prefix, lastNonEmptyLine(activity.content))}`);
|
|
322
|
+
}
|
|
323
|
+
if (activity.label === "assistant message") {
|
|
324
|
+
return theme.fg("muted", fit("", lastNonEmptyLine(activity.content)));
|
|
325
|
+
}
|
|
326
|
+
const prefix = `${activity.label}: `;
|
|
327
|
+
return theme.fg("dim", `${prefix}${fit(prefix, activity.content)}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Columns the activity rail takes before each item's own lines. */
|
|
331
|
+
export const ACTIVITY_RAIL_WIDTH = 3;
|
|
332
|
+
|
|
333
|
+
/** Hang rendered items off a tree rail: `├─` opens each item, `│` continues it, `└─` opens the last. */
|
|
334
|
+
export function drawActivityRail(
|
|
335
|
+
items: readonly (readonly string[])[],
|
|
336
|
+
theme: MinimalSubagentsRenderTheme,
|
|
337
|
+
): string[] {
|
|
338
|
+
return items.flatMap((lines, index) => {
|
|
339
|
+
const last = index === items.length - 1;
|
|
340
|
+
const [first = "", ...rest] = lines;
|
|
341
|
+
return [
|
|
342
|
+
`${theme.fg("dim", last ? "└─ " : "├─ ")}${first}`,
|
|
343
|
+
...rest.map((line) => `${theme.fg("dim", last ? " " : "│ ")}${line}`),
|
|
344
|
+
];
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Width-aware rendering of recorded activity, one rail item per entry. */
|
|
349
|
+
class ActivityLines implements Component {
|
|
350
|
+
constructor(
|
|
351
|
+
private readonly activity: readonly RenderRecentActivity[],
|
|
352
|
+
private readonly theme: MinimalSubagentsRenderTheme,
|
|
353
|
+
) {}
|
|
354
|
+
|
|
355
|
+
render(width: number): string[] {
|
|
356
|
+
const itemWidth = Math.max(1, width - ACTIVITY_RAIL_WIDTH);
|
|
357
|
+
return drawActivityRail(
|
|
358
|
+
this.activity.map((entry) => [formatActivityLine(entry, this.theme, itemWidth)]),
|
|
359
|
+
this.theme,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
invalidate(): void {}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const COLLAPSED_ACTIVITY_LINES = 3;
|
|
367
|
+
|
|
368
|
+
/** Latest progress-relevant activity: model output and tool calls, plus only failed tool results. */
|
|
369
|
+
function latestProgressActivity(activity: readonly RenderRecentActivity[]): RenderRecentActivity[] {
|
|
370
|
+
return activity
|
|
371
|
+
.filter(
|
|
372
|
+
(entry) =>
|
|
373
|
+
entry.label === "assistant message" ||
|
|
374
|
+
entry.label === "reasoning" ||
|
|
375
|
+
entry.label.startsWith("tool call ") ||
|
|
376
|
+
(entry.label.startsWith("tool result ") && entry.label.endsWith(" (error)")),
|
|
377
|
+
)
|
|
378
|
+
.slice(-COLLAPSED_ACTIVITY_LINES);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Format a turn's cost for summary rows; sub-cent costs keep enough precision to be non-zero. */
|
|
382
|
+
function formatSubagentCost(usage: Usage | undefined): string | undefined {
|
|
383
|
+
const total = usage?.cost.total;
|
|
384
|
+
if (total === undefined || !(total > 0)) return undefined;
|
|
385
|
+
return `$${total.toFixed(total < 0.01 ? 4 : 2)}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
218
388
|
/** Format milliseconds for compact subagent rows without losing sub-second durations. */
|
|
219
389
|
export function formatSubagentDuration(elapsedMs: number | undefined): string | undefined {
|
|
220
390
|
if (elapsedMs === undefined || !Number.isFinite(elapsedMs) || elapsedMs < 0) return undefined;
|
|
@@ -245,10 +415,6 @@ export function formatSubagentPreview(content: string | undefined, maxWidth = 72
|
|
|
245
415
|
return `${sliceByColumn(singleLine, 0, boundedWidth - 1, true).trimEnd()}…`;
|
|
246
416
|
}
|
|
247
417
|
|
|
248
|
-
function currentSubagentPreviewWidth(reservedWidth: number): number {
|
|
249
|
-
return Math.max(12, Math.min(72, (process.stdout.columns || 100) - reservedWidth));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
418
|
/** Format complete Pi usage metrics for expanded subagent output. */
|
|
253
419
|
export function formatSubagentUsage(usage: Usage | undefined): string | undefined {
|
|
254
420
|
if (usage === undefined) return undefined;
|
|
@@ -267,13 +433,40 @@ function coordinatorToolCallTitle(theme: MinimalSubagentsRenderTheme, label: str
|
|
|
267
433
|
return theme.fg("toolTitle", theme.bold(label));
|
|
268
434
|
}
|
|
269
435
|
|
|
270
|
-
|
|
436
|
+
/** Launch settings the caller chose explicitly; defaults are omitted to keep the header short. */
|
|
437
|
+
function spawnCallLaunchSummary(args: SpawnCallArguments): string[] {
|
|
438
|
+
const profile = args.model
|
|
439
|
+
? `${args.model}${args.thinking_level ? `:${args.thinking_level}` : ""}`
|
|
440
|
+
: args.thinking_level
|
|
441
|
+
? `thinking ${args.thinking_level}`
|
|
442
|
+
: undefined;
|
|
443
|
+
const tools =
|
|
444
|
+
args.tools === undefined
|
|
445
|
+
? undefined
|
|
446
|
+
: `tools ${Array.isArray(args.tools) ? args.tools.join(", ") || "none" : args.tools}`;
|
|
447
|
+
return [
|
|
448
|
+
profile,
|
|
449
|
+
tools,
|
|
450
|
+
args.session_context && args.session_context !== "omit"
|
|
451
|
+
? `context ${args.session_context}`
|
|
452
|
+
: undefined,
|
|
453
|
+
args.project_context === "omit" ? "no project context" : undefined,
|
|
454
|
+
args.delegation && args.delegation !== "none" ? args.delegation : undefined,
|
|
455
|
+
].filter((part): part is string => part !== undefined);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** A call header followed by its long text argument, previewed when collapsed. */
|
|
459
|
+
function renderCallWithText(
|
|
460
|
+
header: string,
|
|
461
|
+
text: string | undefined,
|
|
271
462
|
theme: MinimalSubagentsRenderTheme,
|
|
272
|
-
|
|
273
|
-
):
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
463
|
+
expanded: boolean,
|
|
464
|
+
): Component {
|
|
465
|
+
if (!text) return new Text(header, 0, 0);
|
|
466
|
+
const container = new Container();
|
|
467
|
+
container.addChild(new Text(header, 0, 0));
|
|
468
|
+
container.addChild(expanded ? new Text(text, 0, 0) : collapsedTextPreview(text, theme));
|
|
469
|
+
return container;
|
|
277
470
|
}
|
|
278
471
|
|
|
279
472
|
function renderManagementToolCall(
|
|
@@ -308,12 +501,11 @@ function renderSpawnResult(
|
|
|
308
501
|
const container = new Container();
|
|
309
502
|
container.addChild(new Text(renderSubagentSummary(theme, status, agentId), 0, 0));
|
|
310
503
|
container.addChild(renderLabelValue(theme, "Turn", details.turn_id));
|
|
311
|
-
appendTextSection(container, theme, "Task", args.task ?? "(task unavailable)");
|
|
312
504
|
const resolvedModel = launchContract?.model ?? args.model;
|
|
313
505
|
const resolvedThinking = launchContract?.thinking_level ?? args.thinking_level;
|
|
314
506
|
const launch = [
|
|
315
507
|
`delegation ${launchContract?.delegation ?? args.delegation ?? "none"}`,
|
|
316
|
-
`session context ${launchContract?.session_context ?? args.session_context ?? "
|
|
508
|
+
`session context ${launchContract?.session_context ?? args.session_context ?? "omit"}`,
|
|
317
509
|
`project context ${launchContract?.project_context ?? args.project_context ?? "inherit"}`,
|
|
318
510
|
resolvedModel ? `model ${resolvedModel}` : undefined,
|
|
319
511
|
resolvedThinking ? `thinking ${resolvedThinking}` : undefined,
|
|
@@ -350,13 +542,65 @@ function renderMessageResult(
|
|
|
350
542
|
if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
|
|
351
543
|
const container = new Container();
|
|
352
544
|
container.addChild(new Text(summary, 0, 0));
|
|
353
|
-
appendTextSection(container, theme, "Message", args.message ?? "(message unavailable)");
|
|
354
545
|
appendTextSection(container, theme, "Recipient", agentId);
|
|
355
546
|
appendTextSection(container, theme, "Disposition", disposition);
|
|
547
|
+
if ("turn_id" in details && details.turn_id) {
|
|
548
|
+
appendTextSection(container, theme, "Started turn", details.turn_id);
|
|
549
|
+
}
|
|
356
550
|
if (details.error) appendTextSection(container, theme, "Error", details.error);
|
|
357
551
|
return container;
|
|
358
552
|
}
|
|
359
553
|
|
|
554
|
+
/** Renders a running child's turn for a waiting parent; returns nothing when the turn is not live. */
|
|
555
|
+
export type LiveTurnRenderer = (
|
|
556
|
+
agentId: string,
|
|
557
|
+
turnId: string | undefined,
|
|
558
|
+
expanded: boolean,
|
|
559
|
+
) => Component | undefined;
|
|
560
|
+
|
|
561
|
+
function renderWaitProgress(
|
|
562
|
+
details: WaitProgressRenderDetails,
|
|
563
|
+
options: ToolRenderResultOptions,
|
|
564
|
+
theme: MinimalSubagentsRenderTheme,
|
|
565
|
+
renderLiveTurn: LiveTurnRenderer | undefined,
|
|
566
|
+
): Component {
|
|
567
|
+
const toolCalls = details.tool_calls ?? 0;
|
|
568
|
+
const metrics = [
|
|
569
|
+
formatSubagentDuration(details.elapsed_ms),
|
|
570
|
+
toolCalls > 0 ? `${toolCalls} tool ${toolCalls === 1 ? "call" : "calls"}` : undefined,
|
|
571
|
+
].filter((metric): metric is string => metric !== undefined);
|
|
572
|
+
const summary = renderSubagentSummary(theme, "waiting", details.agent_id, metrics);
|
|
573
|
+
const liveTurn = renderLiveTurn?.(details.agent_id, details.turn_id, options.expanded);
|
|
574
|
+
if (!liveTurn) return new Text(summary, 0, 0);
|
|
575
|
+
const container = new Container();
|
|
576
|
+
container.addChild(
|
|
577
|
+
new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
|
|
578
|
+
);
|
|
579
|
+
container.addChild(liveTurn);
|
|
580
|
+
return container;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** The collapsed body of a settled wait: what the child said, or why it stopped. */
|
|
584
|
+
function collapsedWaitBody(
|
|
585
|
+
details: WaitRenderDetails,
|
|
586
|
+
status: string,
|
|
587
|
+
theme: MinimalSubagentsRenderTheme,
|
|
588
|
+
): Component | undefined {
|
|
589
|
+
if (details.event === "message") return collapsedTextPreview(details.message, theme);
|
|
590
|
+
if (details.event === "timeout") {
|
|
591
|
+
const activity = latestProgressActivity(details.agent.recent_activity ?? []);
|
|
592
|
+
return activity.length > 0 ? new ActivityLines(activity, theme) : undefined;
|
|
593
|
+
}
|
|
594
|
+
const output = details.output ?? "";
|
|
595
|
+
if (status === "completed") {
|
|
596
|
+
return output ? collapsedMarkdownPreview(output, theme) : undefined;
|
|
597
|
+
}
|
|
598
|
+
return collapsedTextPreview(
|
|
599
|
+
theme.fg("error", details.error ?? (output || "(no error detail)")),
|
|
600
|
+
theme,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
360
604
|
function renderWaitResult(
|
|
361
605
|
details: WaitRenderDetails,
|
|
362
606
|
options: ToolRenderResultOptions,
|
|
@@ -374,9 +618,8 @@ function renderWaitResult(
|
|
|
374
618
|
const duration = formatSubagentDuration(
|
|
375
619
|
details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
|
|
376
620
|
);
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
);
|
|
621
|
+
const usage = details.event === "timeout" ? undefined : details.usage;
|
|
622
|
+
const tokens = formatSubagentTokenCount(usage?.totalTokens);
|
|
380
623
|
const drainedMessageCount =
|
|
381
624
|
details.event === "message" || details.event === "timeout"
|
|
382
625
|
? 0
|
|
@@ -384,11 +627,19 @@ function renderWaitResult(
|
|
|
384
627
|
const metrics = [
|
|
385
628
|
duration,
|
|
386
629
|
tokens ? `${tokens} tokens` : undefined,
|
|
630
|
+
formatSubagentCost(usage),
|
|
387
631
|
drainedMessageCount > 0 ? `${drainedMessageCount} messages` : undefined,
|
|
388
632
|
].filter((metric): metric is string => metric !== undefined);
|
|
389
633
|
const summary = renderSubagentSummary(theme, status, agentId, metrics);
|
|
390
|
-
if (options.isPartial
|
|
391
|
-
|
|
634
|
+
if (options.isPartial) return new Text(summary, 0, 0);
|
|
635
|
+
if (!options.expanded) {
|
|
636
|
+
const body = collapsedWaitBody(details, status, theme);
|
|
637
|
+
const heading = `${summary}${collapsedExpansionHint(theme)}`;
|
|
638
|
+
if (!body) return new Text(heading, 0, 0);
|
|
639
|
+
const container = new Container();
|
|
640
|
+
container.addChild(new Text(heading, 0, 0));
|
|
641
|
+
container.addChild(body);
|
|
642
|
+
return container;
|
|
392
643
|
}
|
|
393
644
|
const container = new Container();
|
|
394
645
|
container.addChild(new Text(summary, 0, 0));
|
|
@@ -460,9 +711,10 @@ function renderDirectStatusRows(
|
|
|
460
711
|
return agents.map((agent) => {
|
|
461
712
|
const duration = formatSubagentDuration(agent.elapsed_ms);
|
|
462
713
|
const childCount = agent.child_count ?? 0;
|
|
463
|
-
const metrics = [
|
|
464
|
-
|
|
465
|
-
|
|
714
|
+
const metrics = [
|
|
715
|
+
duration,
|
|
716
|
+
childCount > 0 ? `${childCount} ${childCount === 1 ? "child" : "children"}` : undefined,
|
|
717
|
+
].filter((metric): metric is string => metric !== undefined);
|
|
466
718
|
return renderSubagentSummary(
|
|
467
719
|
theme,
|
|
468
720
|
statusAgentPresentation(agent),
|
|
@@ -484,11 +736,19 @@ function renderDetailedStatusAgent(
|
|
|
484
736
|
const id = agent.agent_id ?? "agent";
|
|
485
737
|
const childCount = agent.child_count ?? 0;
|
|
486
738
|
const duration = formatSubagentDuration(agent.elapsed_ms);
|
|
487
|
-
const metrics = [duration, `${childCount} children`].filter(
|
|
739
|
+
const metrics = [duration, `${childCount} ${childCount === 1 ? "child" : "children"}`].filter(
|
|
488
740
|
(metric): metric is string => metric !== undefined,
|
|
489
741
|
);
|
|
490
742
|
const summary = renderSubagentSummary(theme, status, id, metrics);
|
|
491
|
-
if (!options.expanded)
|
|
743
|
+
if (!options.expanded) {
|
|
744
|
+
const activity = latestProgressActivity(agent.recent_activity ?? []);
|
|
745
|
+
const heading = `${summary}${collapsedExpansionHint(theme)}`;
|
|
746
|
+
if (activity.length === 0) return new Text(heading, 0, 0);
|
|
747
|
+
const collapsed = new Container();
|
|
748
|
+
collapsed.addChild(new Text(heading, 0, 0));
|
|
749
|
+
collapsed.addChild(new ActivityLines(activity, theme));
|
|
750
|
+
return collapsed;
|
|
751
|
+
}
|
|
492
752
|
const container = new Container();
|
|
493
753
|
container.addChild(new Text(summary, 0, 0));
|
|
494
754
|
const labels = [
|
|
@@ -590,12 +850,14 @@ function renderStatusResult(
|
|
|
590
850
|
theme.fg("muted", `${counts.children} children`),
|
|
591
851
|
theme.fg(counts.running > 0 ? "accent" : "dim", `${counts.running} running`),
|
|
592
852
|
].join(renderSubagentSeparator(theme));
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
0,
|
|
853
|
+
const rows =
|
|
854
|
+
renderDirectStatusRows(details.agents, theme).join("\n") || theme.fg("dim", "(no agents)");
|
|
855
|
+
const container = new Container();
|
|
856
|
+
container.addChild(
|
|
857
|
+
new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
|
|
598
858
|
);
|
|
859
|
+
container.addChild(options.expanded ? new Text(rows, 0, 0) : collapsedTextPreview(rows, theme));
|
|
860
|
+
return container;
|
|
599
861
|
}
|
|
600
862
|
return renderDetailedStatusAgent(details.agent, options, theme);
|
|
601
863
|
}
|
|
@@ -609,7 +871,8 @@ function renderCancelResult(
|
|
|
609
871
|
const summary =
|
|
610
872
|
turns.length > 0
|
|
611
873
|
? renderSubagentSummary(theme, "cancelled", details.agent_id, [
|
|
612
|
-
`${turns.length} turns cancelled`,
|
|
874
|
+
`${turns.length} ${turns.length === 1 ? "turn" : "turns"} cancelled`,
|
|
875
|
+
details.affected_agent_ids.join(", "),
|
|
613
876
|
])
|
|
614
877
|
: renderSubagentSummary(theme, "completed", details.agent_id, ["no active turns"]);
|
|
615
878
|
if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
|
|
@@ -635,9 +898,10 @@ function renderDeleteResult(
|
|
|
635
898
|
theme: MinimalSubagentsRenderTheme,
|
|
636
899
|
): Component {
|
|
637
900
|
const status = details.failures.length > 0 ? "failed" : "completed";
|
|
901
|
+
const deletedCount = details.deleted_agent_ids.length;
|
|
638
902
|
const metrics = [
|
|
639
|
-
`${
|
|
640
|
-
|
|
903
|
+
`${deletedCount} ${deletedCount === 1 ? "agent" : "agents"} deleted`,
|
|
904
|
+
deletedCount > 0 ? details.deleted_agent_ids.join(", ") : undefined,
|
|
641
905
|
details.failures.length > 0 ? `${details.failures.length} failed` : undefined,
|
|
642
906
|
].filter((metric): metric is string => metric !== undefined);
|
|
643
907
|
const summary = renderSubagentSummary(theme, status, details.agent_id, metrics);
|
|
@@ -676,21 +940,24 @@ export function renderCoordinatorToolCall(
|
|
|
676
940
|
toolName: CoordinatorToolName,
|
|
677
941
|
args: CoordinatorToolCallInput,
|
|
678
942
|
theme: MinimalSubagentsRenderTheme,
|
|
943
|
+
expanded = false,
|
|
679
944
|
): Component {
|
|
680
945
|
const parsed = parseCoordinatorToolCall(toolName, args);
|
|
681
946
|
if (parsed === undefined) return new Text(coordinatorToolCallTitle(theme, toolName), 0, 0);
|
|
682
947
|
switch (parsed.toolName) {
|
|
683
|
-
case "subagent":
|
|
684
|
-
|
|
685
|
-
`${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", parsed.args.agent_id ?? "generated")}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
);
|
|
948
|
+
case "subagent": {
|
|
949
|
+
const header = [
|
|
950
|
+
`${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", parsed.args.agent_id ?? "generated")}`,
|
|
951
|
+
...spawnCallLaunchSummary(parsed.args).map((part) => theme.fg("dim", part)),
|
|
952
|
+
].join(theme.fg("dim", " · "));
|
|
953
|
+
return renderCallWithText(header, parsed.args.task, theme, expanded);
|
|
954
|
+
}
|
|
689
955
|
case "agent_message":
|
|
690
|
-
return
|
|
691
|
-
`${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", parsed.args.agent_id ?? "parent")}
|
|
692
|
-
|
|
693
|
-
|
|
956
|
+
return renderCallWithText(
|
|
957
|
+
`${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", parsed.args.agent_id ?? "parent")}`,
|
|
958
|
+
parsed.args.message,
|
|
959
|
+
theme,
|
|
960
|
+
expanded,
|
|
694
961
|
);
|
|
695
962
|
case "subagent_wait":
|
|
696
963
|
return new Text(
|
|
@@ -719,7 +986,12 @@ export function renderCoordinatorToolResult(
|
|
|
719
986
|
theme: MinimalSubagentsRenderTheme,
|
|
720
987
|
args: CoordinatorToolCallInput,
|
|
721
988
|
isError = false,
|
|
989
|
+
renderLiveTurn?: LiveTurnRenderer,
|
|
722
990
|
): Component {
|
|
991
|
+
if (toolName === "subagent_wait" && options.isPartial) {
|
|
992
|
+
const progress = parseWaitProgressDetails(result.details);
|
|
993
|
+
if (progress) return renderWaitProgress(progress, options, theme, renderLiveTurn);
|
|
994
|
+
}
|
|
723
995
|
const parsedResult = parseCoordinatorToolResult(toolName, result.details);
|
|
724
996
|
if (parsedResult === undefined) return renderFallbackToolResult(result, theme, isError);
|
|
725
997
|
const parsedCall = parseCoordinatorToolCall(toolName, args);
|
|
@@ -795,30 +1067,24 @@ function renderCoordinatorMessage(
|
|
|
795
1067
|
const destination = details?.destination_agent_id ?? "recipient";
|
|
796
1068
|
const sourceTurn = messageSourceTurn(details);
|
|
797
1069
|
const route = `${theme.fg("accent", theme.bold(source))} ${theme.fg("dim", "→")} ${theme.fg("accent", theme.bold(destination))}`;
|
|
1070
|
+
const metrics = [formatSubagentDuration(details?.elapsed_ms), formatSubagentCost(details?.usage)];
|
|
798
1071
|
const heading = [
|
|
799
1072
|
`${theme.fg(symbol === "✓" ? "success" : "accent", symbol)} ${theme.bold(label)}`,
|
|
800
1073
|
route,
|
|
801
|
-
sourceTurn ? theme.fg("dim", `turn ${sourceTurn}`) : undefined,
|
|
802
1074
|
details?.status ? renderSubagentStatusLabel(theme, details.status) : undefined,
|
|
1075
|
+
...metrics.map((metric) => (metric ? theme.fg("muted", metric) : undefined)),
|
|
803
1076
|
]
|
|
804
1077
|
.filter((part): part is string => part !== undefined)
|
|
805
1078
|
.join(renderSubagentSeparator(theme));
|
|
806
1079
|
const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
|
|
807
1080
|
if (!options.expanded) {
|
|
808
|
-
box.addChild(
|
|
809
|
-
|
|
810
|
-
`${heading}\n${theme.fg("muted", formatSubagentPreview(content, currentSubagentPreviewWidth(24)))}`,
|
|
811
|
-
0,
|
|
812
|
-
0,
|
|
813
|
-
),
|
|
814
|
-
);
|
|
1081
|
+
box.addChild(new Text(`${heading}${collapsedExpansionHint(theme)}`, 0, 0));
|
|
1082
|
+
box.addChild(collapsedMarkdownPreview(content, theme));
|
|
815
1083
|
return box;
|
|
816
1084
|
}
|
|
817
1085
|
const container = new Container();
|
|
818
1086
|
container.addChild(new Text(heading, 0, 0));
|
|
819
1087
|
if (sourceTurn) container.addChild(renderLabelValue(theme, "Source turn", sourceTurn));
|
|
820
|
-
const duration = formatSubagentDuration(details?.elapsed_ms);
|
|
821
|
-
if (duration) container.addChild(renderLabelValue(theme, "Duration", duration));
|
|
822
1088
|
container.addChild(new Spacer(1));
|
|
823
1089
|
container.addChild(new Markdown(content, 0, 0, getMarkdownTheme()));
|
|
824
1090
|
const usageText = formatSubagentUsage(details?.usage);
|