oira666_pi-subagent 0.3.11 → 0.3.13
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 +7 -2
- package/agents.ts +21 -0
- package/index.ts +46 -8
- package/package.json +1 -1
- package/render.ts +114 -25
- package/runner.ts +5 -11
- package/tree.ts +163 -76
- package/types.ts +11 -7
package/README.md
CHANGED
|
@@ -130,7 +130,9 @@ The Markdown body becomes the agent's system prompt (appended to Pi's default, n
|
|
|
130
130
|
|
|
131
131
|
## Delegation Guards
|
|
132
132
|
|
|
133
|
-
Depth and cycle guards prevent runaway recursive delegation. Layer availability is evaluated for the child being launched: depth 1 is the first layer, and `PI_SUBAGENT_MAX_DEPTH` is the last layer. The bundled `team-lead` agent sets `last-layer: disabled` so it cannot consume the final delegation layer.
|
|
133
|
+
Depth and cycle guards prevent runaway recursive delegation. Layer availability is evaluated for the child being launched: depth 1 is the first layer, and `PI_SUBAGENT_MAX_DEPTH` is the last layer. The bundled `team-lead` agent sets `last-layer: disabled` so it cannot consume the final delegation layer. When cycle prevention is enabled, agents already in the current delegation stack are omitted from the child model's available-agent list. The runner still checks every task as a safety boundary: in a mixed parallel call, cyclic tasks fail while legal siblings still run.
|
|
134
|
+
|
|
135
|
+
A nested delegation failure is returned to its calling agent as a recoverable tool error. If that agent subsequently retries, completes the work itself, and produces a successful final answer, the earlier tool error does not incorrectly turn the completed agent—and all of its ancestors—into failures.
|
|
134
136
|
|
|
135
137
|
| Config | Default | Description |
|
|
136
138
|
| ------------------------------ | ------- | ------------------------------------------------ |
|
|
@@ -175,7 +177,10 @@ reported immediately as failures.
|
|
|
175
177
|
## Timestamps & Status Footer
|
|
176
178
|
|
|
177
179
|
Subagent tool calls and live activity lines render a dim `hh:mm:ss` timestamp
|
|
178
|
-
(call start, per-subagent run start, and each live log entry).
|
|
180
|
+
(call start, per-subagent run start, and each live log entry). The collapsed tool
|
|
181
|
+
row keeps the normal compact progress text. Press `Ctrl+O` to expand it into the
|
|
182
|
+
subagent tree immediately; every running node shows its latest six activity
|
|
183
|
+
lines (thinking, tool starts/completions, and completed turns).
|
|
179
184
|
|
|
180
185
|
In the interactive TUI the extension publishes the combined `total` usage line
|
|
181
186
|
(parent + all subagents, recursively) via Pi's normal `ctx.ui.setStatus()`
|
package/agents.ts
CHANGED
|
@@ -200,6 +200,27 @@ export function isAgentEnabledAtLayer(
|
|
|
200
200
|
return true;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Return only agents that may be advertised for the next delegation.
|
|
205
|
+
*
|
|
206
|
+
* The runner still enforces cycle prevention as a final safety boundary, but
|
|
207
|
+
* forbidden agents must not be presented to the model as available choices.
|
|
208
|
+
*/
|
|
209
|
+
export function filterAdvertisedAgents(
|
|
210
|
+
agents: AgentConfig[],
|
|
211
|
+
targetDepth: number,
|
|
212
|
+
maxDepth: number,
|
|
213
|
+
delegationStack: string[],
|
|
214
|
+
preventCycles: boolean,
|
|
215
|
+
): AgentConfig[] {
|
|
216
|
+
const blocked = preventCycles ? new Set(delegationStack) : null;
|
|
217
|
+
return agents.filter(
|
|
218
|
+
(agent) =>
|
|
219
|
+
isAgentEnabledAtLayer(agent, targetDepth, maxDepth) &&
|
|
220
|
+
!blocked?.has(agent.name),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
203
224
|
/**
|
|
204
225
|
* Discover all available agents according to the requested scope.
|
|
205
226
|
*
|
package/index.ts
CHANGED
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
lazyStream,
|
|
19
19
|
} from "@mariozechner/pi-ai";
|
|
20
20
|
import { Type } from "@sinclair/typebox";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
type AgentConfig,
|
|
23
|
+
discoverAgents,
|
|
24
|
+
filterAdvertisedAgents,
|
|
25
|
+
isAgentEnabledAtLayer,
|
|
26
|
+
} from "./agents.js";
|
|
22
27
|
import { loadPiSubagentsConfig } from "./config.js";
|
|
23
28
|
import {
|
|
24
29
|
allocateSubagentNames,
|
|
@@ -430,6 +435,22 @@ function filterAgentsForCurrentLayer(
|
|
|
430
435
|
return agents.filter((agent) => isAgentEnabledAtLayer(agent, targetDepth, maxDepth));
|
|
431
436
|
}
|
|
432
437
|
|
|
438
|
+
function filterAgentsForPrompt(
|
|
439
|
+
agents: AgentConfig[],
|
|
440
|
+
currentDepth: number,
|
|
441
|
+
maxDepth: number,
|
|
442
|
+
delegationStack: string[],
|
|
443
|
+
preventCycles: boolean,
|
|
444
|
+
): AgentConfig[] {
|
|
445
|
+
return filterAdvertisedAgents(
|
|
446
|
+
agents,
|
|
447
|
+
currentDepth + 1,
|
|
448
|
+
maxDepth,
|
|
449
|
+
delegationStack,
|
|
450
|
+
preventCycles,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
433
454
|
function formatAgentNames(agents: AgentConfig[]): string {
|
|
434
455
|
return agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
435
456
|
}
|
|
@@ -1424,7 +1445,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1424
1445
|
if (!canDelegate) return;
|
|
1425
1446
|
|
|
1426
1447
|
const discovery = discoverAgents(ctx.cwd, "both");
|
|
1427
|
-
discoveredAgents =
|
|
1448
|
+
discoveredAgents = filterAgentsForPrompt(
|
|
1449
|
+
discovery.agents,
|
|
1450
|
+
currentDepth,
|
|
1451
|
+
maxDepth,
|
|
1452
|
+
ancestorAgentStack,
|
|
1453
|
+
preventCycles,
|
|
1454
|
+
);
|
|
1428
1455
|
currentSessionId = ctx.sessionManager.getSessionId?.() ?? "ephemeral";
|
|
1429
1456
|
currentSubagentSessionRoot = getDefaultSubagentSessionRoot(ctx);
|
|
1430
1457
|
if (resumableSubagentsDisabled()) {
|
|
@@ -1720,11 +1747,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1720
1747
|
pi.on("before_agent_start", async (event) => {
|
|
1721
1748
|
try {
|
|
1722
1749
|
if (!canDelegate) return;
|
|
1723
|
-
if (discoveredAgents.length === 0) return;
|
|
1724
1750
|
|
|
1725
|
-
const agentList = discoveredAgents
|
|
1726
|
-
|
|
1727
|
-
|
|
1751
|
+
const agentList = discoveredAgents.length > 0
|
|
1752
|
+
? discoveredAgents
|
|
1753
|
+
.map((a) => `- **${a.name}**: ${a.description}`)
|
|
1754
|
+
.join("\n")
|
|
1755
|
+
: "_No agents are available for the next delegation layer. Do not call the subagents tool._";
|
|
1728
1756
|
const subagentsGuidance = configuredToolPrompts[SUBAGENT_TOOL_NAME] ?? `### How to call the subagents tool
|
|
1729
1757
|
|
|
1730
1758
|
Each subagent runs in an **isolated process**.
|
|
@@ -1746,8 +1774,18 @@ calls one after another. Do NOT put dependent tasks in the same array.
|
|
|
1746
1774
|
{ "tasks": [{ "agent": "agent-a", "task": "..." }, { "agent": "agent-b", "task": "..." }] }
|
|
1747
1775
|
\`\`\`
|
|
1748
1776
|
|
|
1749
|
-
- Max depth: current depth ${currentDepth}, max depth ${maxDepth}
|
|
1750
1777
|
- Max subagents per tool call: ${maxParallelTasks}`;
|
|
1778
|
+
const delegationStackText = ancestorAgentStack.length > 0
|
|
1779
|
+
? ancestorAgentStack.join(" -> ")
|
|
1780
|
+
: "(root)";
|
|
1781
|
+
const delegationGuardGuidance = `### Delegation guards
|
|
1782
|
+
|
|
1783
|
+
- Current depth: ${currentDepth}; max depth: ${maxDepth}
|
|
1784
|
+
- Cycle prevention: ${preventCycles ? "enabled" : "disabled"}
|
|
1785
|
+
- Current delegation stack: ${delegationStackText}
|
|
1786
|
+
${preventCycles
|
|
1787
|
+
? "- Agents already in this stack are intentionally omitted from the available list. Do not request omitted agent names."
|
|
1788
|
+
: "- Cyclic delegation is allowed by configuration."}`;
|
|
1751
1789
|
const resumeGuidance = resumableSubagentsDisabled()
|
|
1752
1790
|
? ""
|
|
1753
1791
|
: configuredToolPrompts[RESUME_SUBAGENTS_TOOL_NAME] ?? `### Resumable subagents
|
|
@@ -1772,7 +1810,7 @@ keeping their full previous context:
|
|
|
1772
1810
|
|
|
1773
1811
|
The following subagents are available via the \`subagents\` tool:
|
|
1774
1812
|
|
|
1775
|
-
${agentList}\n\n${subagentsGuidance}${resumeGuidance ? `\n\n${resumeGuidance}` : ""}`,
|
|
1813
|
+
${agentList}\n\n${subagentsGuidance}\n\n${delegationGuardGuidance}${resumeGuidance ? `\n\n${resumeGuidance}` : ""}`,
|
|
1776
1814
|
};
|
|
1777
1815
|
} catch (err) {
|
|
1778
1816
|
console.error("[pi-subagent] Error in before_agent_start:", err);
|
package/package.json
CHANGED
package/render.ts
CHANGED
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
MAX_LIVE_LOG_ENTRIES,
|
|
12
|
+
isResultError,
|
|
13
|
+
isSubagentDetails,
|
|
14
|
+
type SingleResult,
|
|
15
|
+
type SubagentDetails,
|
|
16
|
+
} from "./types.js";
|
|
11
17
|
import {
|
|
12
18
|
type ThemeFg,
|
|
13
19
|
buildTopLevelNodes,
|
|
14
20
|
countNodes,
|
|
15
21
|
formatClockTime,
|
|
22
|
+
formatLiveLogEntry,
|
|
16
23
|
hasNestedChildren,
|
|
17
24
|
renderTreeLines,
|
|
18
25
|
setBroadcastNumberingActive,
|
|
@@ -122,37 +129,119 @@ export function renderResumeCall(
|
|
|
122
129
|
// renderResult — shown after the tool completes / streams updates
|
|
123
130
|
// ---------------------------------------------------------------------------
|
|
124
131
|
|
|
132
|
+
function getResultText(
|
|
133
|
+
result: { content?: Array<{ type: string; text?: string }> },
|
|
134
|
+
): string {
|
|
135
|
+
const first = Array.isArray(result.content) ? result.content[0] : undefined;
|
|
136
|
+
return first?.type === "text" && typeof first.text === "string" && first.text
|
|
137
|
+
? first.text
|
|
138
|
+
: "(no output)";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function compactText(text: string, maxLength = 240): string {
|
|
142
|
+
const firstLine = text.replace(/\r\n?/g, "\n").split("\n").find((line) => line.trim()) ?? "(no output)";
|
|
143
|
+
return truncate(firstLine, maxLength);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isRenderableResult(value: unknown): value is SingleResult {
|
|
147
|
+
return value !== null &&
|
|
148
|
+
typeof value === "object" &&
|
|
149
|
+
!Array.isArray(value) &&
|
|
150
|
+
typeof (value as Partial<SingleResult>).agent === "string" &&
|
|
151
|
+
typeof (value as Partial<SingleResult>).exitCode === "number";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getCollapsedResultText(details: SubagentDetails, fallbackText: string): string {
|
|
155
|
+
const results = details.results.filter(isRenderableResult);
|
|
156
|
+
if (results.length === 0) return compactText(fallbackText);
|
|
157
|
+
|
|
158
|
+
const running = results.filter((item) => item.exitCode === -1).length;
|
|
159
|
+
const finished = results.length - running;
|
|
160
|
+
const succeeded = results.filter((item) => item.exitCode === 0 && !isResultError(item)).length;
|
|
161
|
+
const failed = finished - succeeded;
|
|
162
|
+
|
|
163
|
+
if (details.mode === "parallel") {
|
|
164
|
+
return running > 0
|
|
165
|
+
? `Parallel: ${finished}/${results.length} done, ${running} running...`
|
|
166
|
+
: `Parallel: ${succeeded}/${results.length} succeeded${failed > 0 ? `, ${failed} failed` : ""}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const item = results[0];
|
|
170
|
+
if (item.exitCode === -1) return `Agent ${item.agent}: running...`;
|
|
171
|
+
if (isResultError(item)) {
|
|
172
|
+
const reason = [item.errorMessage, item.stderr, item.stopReason]
|
|
173
|
+
.find((value): value is string => typeof value === "string" && value.length > 0);
|
|
174
|
+
return `Agent ${item.agent}: failed${reason ? ` — ${compactText(reason, 180)}` : ""}`;
|
|
175
|
+
}
|
|
176
|
+
return `Agent ${item.agent}: completed`;
|
|
177
|
+
}
|
|
178
|
+
|
|
125
179
|
export function renderResult(
|
|
126
180
|
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
127
|
-
|
|
181
|
+
expanded: boolean,
|
|
128
182
|
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
129
183
|
): Container | Text {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
const fallbackText = getResultText(result);
|
|
185
|
+
|
|
186
|
+
// Keep the normal row compact. Ctrl+O switches to the live tree below.
|
|
187
|
+
if (!expanded) {
|
|
188
|
+
const text = isSubagentDetails(result.details)
|
|
189
|
+
? getCollapsedResultText(result.details, fallbackText)
|
|
190
|
+
: compactText(fallbackText);
|
|
191
|
+
return new Text(text, 0, 0);
|
|
134
192
|
}
|
|
135
193
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
? theme.fg("warning", "⏳")
|
|
141
|
-
: counts.error > 0
|
|
142
|
-
? theme.fg("error", "❌")
|
|
143
|
-
: theme.fg("success", "✅");
|
|
194
|
+
if (!isSubagentDetails(result.details) || result.details.results.length === 0) {
|
|
195
|
+
return new Text(fallbackText, 0, 0);
|
|
196
|
+
}
|
|
197
|
+
const details: SubagentDetails = result.details;
|
|
144
198
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
199
|
+
try {
|
|
200
|
+
const nodes = buildTopLevelNodes(details);
|
|
201
|
+
const counts = countNodes(nodes);
|
|
202
|
+
const showOutputPreview = !hasNestedChildren(nodes);
|
|
203
|
+
const icon = counts.running > 0
|
|
204
|
+
? theme.fg("warning", "⏳")
|
|
205
|
+
: counts.error > 0
|
|
206
|
+
? theme.fg("error", "❌")
|
|
207
|
+
: theme.fg("success", "✅");
|
|
153
208
|
|
|
154
|
-
|
|
155
|
-
|
|
209
|
+
const container = new Container();
|
|
210
|
+
container.addChild(
|
|
211
|
+
new Text(
|
|
212
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("subagent tree "))}${theme.fg("dim", topLevelSummary(details, counts))}`,
|
|
213
|
+
0,
|
|
214
|
+
0,
|
|
215
|
+
),
|
|
216
|
+
);
|
|
156
217
|
|
|
157
|
-
|
|
218
|
+
container.addChild(new Spacer(1));
|
|
219
|
+
container.addChild(new Text(renderTreeLines(nodes, theme, showOutputPreview).join("\n"), 0, 0));
|
|
220
|
+
return container;
|
|
221
|
+
} catch {
|
|
222
|
+
// Pi falls back to raw result.content when a custom renderer throws, which
|
|
223
|
+
// made Ctrl+O appear broken until the offending rolling log entry expired.
|
|
224
|
+
// Keep an expanded tree visible even for malformed third-party/RPC data.
|
|
225
|
+
const lines: string[] = [];
|
|
226
|
+
for (const item of details.results) {
|
|
227
|
+
const agent = typeof item?.agent === "string" ? item.agent : "unknown agent";
|
|
228
|
+
const icon = item?.exitCode === -1
|
|
229
|
+
? theme.fg("warning", "⏳")
|
|
230
|
+
: item?.exitCode === 0
|
|
231
|
+
? theme.fg("success", "✅")
|
|
232
|
+
: theme.fg("error", "❌");
|
|
233
|
+
lines.push(`${icon} ${theme.fg("accent", agent)}`);
|
|
234
|
+
const liveLog = Array.isArray(item?.liveLog)
|
|
235
|
+
? item.liveLog.slice(-MAX_LIVE_LOG_ENTRIES)
|
|
236
|
+
: [];
|
|
237
|
+
for (const entry of liveLog) {
|
|
238
|
+
lines.push(` ${formatLiveLogEntry(entry, theme)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return new Text(
|
|
242
|
+
`${theme.fg("toolTitle", theme.bold("subagent tree"))}\n${lines.join("\n") || fallbackText}`,
|
|
243
|
+
0,
|
|
244
|
+
0,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
158
247
|
}
|
package/runner.ts
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
emptyUsage,
|
|
22
22
|
extractToolCalls,
|
|
23
23
|
getFinalOutput,
|
|
24
|
-
getNestedSubagentErrorSummary,
|
|
25
24
|
isResultError,
|
|
26
25
|
isResultSuccess,
|
|
27
26
|
isSubagentDetails,
|
|
@@ -1239,16 +1238,11 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
1239
1238
|
if (!result.stderr.trim()) result.stderr = result.errorMessage;
|
|
1240
1239
|
}
|
|
1241
1240
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
result.errorMessage = nestedErrorSummary;
|
|
1248
|
-
if (!result.stderr.trim()) result.stderr = nestedErrorSummary;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1241
|
+
// A failed nested delegation is a recoverable tool error, just like a
|
|
1242
|
+
// failed bash/read call. Pi returns that error to the calling model, which
|
|
1243
|
+
// may retry, choose another approach, or finish the task itself. Do not
|
|
1244
|
+
// overwrite a later successful terminal answer with an earlier nested
|
|
1245
|
+
// tool failure.
|
|
1252
1246
|
return result;
|
|
1253
1247
|
} catch (err) {
|
|
1254
1248
|
const msg = err instanceof Error ? err.message : String(err);
|
package/tree.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type LiveLogEntry,
|
|
14
14
|
type NestedSubagentResult,
|
|
15
15
|
type SingleResult,
|
|
16
|
+
MAX_LIVE_LOG_ENTRIES,
|
|
16
17
|
type SubagentDetails,
|
|
17
18
|
type UsageStats,
|
|
18
19
|
aggregateUsage,
|
|
@@ -65,17 +66,32 @@ interface PendingSubagentCall {
|
|
|
65
66
|
// Formatting helpers
|
|
66
67
|
// ---------------------------------------------------------------------------
|
|
67
68
|
|
|
69
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
70
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
71
|
+
? value as Record<string, unknown>
|
|
72
|
+
: {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function finiteNumber(value: unknown): number {
|
|
76
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stringValue(value: unknown, fallback = ""): string {
|
|
80
|
+
return typeof value === "string" ? value : fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
export function formatClockTime(epochMs: number): string {
|
|
69
|
-
const d = new Date(epochMs);
|
|
84
|
+
const d = new Date(Number.isFinite(epochMs) ? epochMs : 0);
|
|
70
85
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
71
86
|
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
export function formatTokens(count: number): string {
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
if (
|
|
78
|
-
return `${(
|
|
90
|
+
const safeCount = finiteNumber(count);
|
|
91
|
+
if (safeCount < 1000) return safeCount.toString();
|
|
92
|
+
if (safeCount < 10000) return `${(safeCount / 1000).toFixed(1)}k`;
|
|
93
|
+
if (safeCount < 1000000) return `${Math.round(safeCount / 1000)}k`;
|
|
94
|
+
return `${(safeCount / 1000000).toFixed(1)}M`;
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
export function formatCombinedUsageStatusLine(
|
|
@@ -95,36 +111,41 @@ export function formatCombinedUsageStatusLine(
|
|
|
95
111
|
return `WITH SUBS: (${subagentCount}) Σ $${cost.toFixed(4)} • ↑${formatTokens(input)} ↓${formatTokens(output)}${cache} T${formatTokens(total)} • ${turns} turn${turns === 1 ? "" : "s"}`;
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
export function formatUsage(usage: Partial<UsageStats
|
|
114
|
+
export function formatUsage(usage: Partial<UsageStats> | unknown, model?: unknown): string {
|
|
115
|
+
const safeUsage = asRecord(usage);
|
|
116
|
+
const input = finiteNumber(safeUsage.input);
|
|
117
|
+
const output = finiteNumber(safeUsage.output);
|
|
118
|
+
const cacheRead = finiteNumber(safeUsage.cacheRead);
|
|
119
|
+
const cacheWrite = finiteNumber(safeUsage.cacheWrite);
|
|
120
|
+
const cost = finiteNumber(safeUsage.cost);
|
|
121
|
+
const contextTokens = finiteNumber(safeUsage.contextTokens);
|
|
122
|
+
const turns = finiteNumber(safeUsage.turns);
|
|
99
123
|
const parts: string[] = [];
|
|
100
|
-
const totalTokens =
|
|
101
|
-
|
|
102
|
-
(usage.output || 0) +
|
|
103
|
-
(usage.cacheRead || 0) +
|
|
104
|
-
(usage.cacheWrite || 0);
|
|
105
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
124
|
+
const totalTokens = input + output + cacheRead + cacheWrite;
|
|
125
|
+
if (turns) parts.push(`${turns} turn${turns > 1 ? "s" : ""}`);
|
|
106
126
|
if (totalTokens > 0) parts.push(`tok:${formatTokens(totalTokens)}`);
|
|
107
|
-
if (
|
|
108
|
-
if (
|
|
109
|
-
if (
|
|
110
|
-
if (
|
|
111
|
-
if (
|
|
112
|
-
if (
|
|
113
|
-
if (model) parts.push(model);
|
|
127
|
+
if (input) parts.push(`in:${formatTokens(input)}`);
|
|
128
|
+
if (output) parts.push(`out:${formatTokens(output)}`);
|
|
129
|
+
if (cacheRead) parts.push(`cacheR:${formatTokens(cacheRead)}`);
|
|
130
|
+
if (cacheWrite) parts.push(`cacheW:${formatTokens(cacheWrite)}`);
|
|
131
|
+
if (cost) parts.push(`$${cost.toFixed(4)}`);
|
|
132
|
+
if (contextTokens > 0) parts.push(`ctx:${formatTokens(contextTokens)}`);
|
|
133
|
+
if (typeof model === "string" && model) parts.push(model);
|
|
114
134
|
return parts.join(" • ");
|
|
115
135
|
}
|
|
116
136
|
|
|
117
|
-
export function truncate(text:
|
|
118
|
-
|
|
137
|
+
export function truncate(text: unknown, maxLen: number): string {
|
|
138
|
+
const safeText = stringValue(text);
|
|
139
|
+
return safeText.length > maxLen ? `${safeText.slice(0, maxLen)}...` : safeText;
|
|
119
140
|
}
|
|
120
141
|
|
|
121
|
-
function splitOutputLines(text:
|
|
122
|
-
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
142
|
+
function splitOutputLines(text: unknown): string[] {
|
|
143
|
+
const lines = stringValue(text).replace(/\r\n?/g, "\n").split("\n");
|
|
123
144
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
124
145
|
return lines;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
|
-
function lastNonEmptyLines(text:
|
|
148
|
+
function lastNonEmptyLines(text: unknown, limit: number): string[] {
|
|
128
149
|
return splitOutputLines(text)
|
|
129
150
|
.map((line) => line.trimEnd())
|
|
130
151
|
.filter((line) => line.trim().length > 0)
|
|
@@ -173,11 +194,12 @@ export function hasNestedChildren(nodes: TreeNode[]): boolean {
|
|
|
173
194
|
return nodes.some((node) => node.children.length > 0 || hasNestedChildren(node.children));
|
|
174
195
|
}
|
|
175
196
|
|
|
176
|
-
function extractPendingSubagentCalls(messages: SingleResult["messages"]): PendingSubagentCall[] {
|
|
197
|
+
function extractPendingSubagentCalls(messages: SingleResult["messages"] | unknown): PendingSubagentCall[] {
|
|
198
|
+
const history = Array.isArray(messages) ? messages : [];
|
|
177
199
|
const calls: PendingSubagentCall[] = [];
|
|
178
|
-
for (let messageIndex = 0; messageIndex <
|
|
179
|
-
const message =
|
|
180
|
-
if (message
|
|
200
|
+
for (let messageIndex = 0; messageIndex < history.length; messageIndex++) {
|
|
201
|
+
const message = history[messageIndex] as any;
|
|
202
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
181
203
|
for (let partIndex = 0; partIndex < message.content.length; partIndex++) {
|
|
182
204
|
const part = message.content[partIndex] as any;
|
|
183
205
|
if (part?.type !== "toolCall" || !isSubagentToolName(part?.name)) continue;
|
|
@@ -235,11 +257,16 @@ function subagentCallSignature(call: PendingSubagentCall): string {
|
|
|
235
257
|
|
|
236
258
|
function nestedResultIsHealthy(nested: NestedSubagentResult | undefined): boolean {
|
|
237
259
|
if (!nested || nested.isError) return false;
|
|
238
|
-
return nested.details.results.every(
|
|
260
|
+
return nested.details.results.every(
|
|
261
|
+
(result) => result !== null && typeof result === "object" && !Array.isArray(result) && !isResultError(result),
|
|
262
|
+
);
|
|
239
263
|
}
|
|
240
264
|
|
|
241
265
|
function buildLiveDetailsSignature(details: SubagentDetails): string {
|
|
242
|
-
return JSON.stringify(details.results.map((result) =>
|
|
266
|
+
return JSON.stringify(details.results.map((result) => {
|
|
267
|
+
const value = asRecord(result);
|
|
268
|
+
return { agent: stringValue(value.agent), task: stringValue(value.task) };
|
|
269
|
+
}));
|
|
243
270
|
}
|
|
244
271
|
|
|
245
272
|
function findLiveNestedDetailsForCall(
|
|
@@ -276,7 +303,9 @@ function findLiveNestedDetailsForCall(
|
|
|
276
303
|
const agentSignature = JSON.stringify(call.tasks.map((task) => task.agent));
|
|
277
304
|
for (const [key, details] of Object.entries(live)) {
|
|
278
305
|
if (usedLiveKeys.has(key) || !isSubagentDetails(details)) continue;
|
|
279
|
-
const liveAgentSignature = JSON.stringify(
|
|
306
|
+
const liveAgentSignature = JSON.stringify(
|
|
307
|
+
details.results.map((nestedResult) => stringValue(asRecord(nestedResult).agent)),
|
|
308
|
+
);
|
|
280
309
|
if (liveAgentSignature !== agentSignature) continue;
|
|
281
310
|
usedLiveKeys.add(key);
|
|
282
311
|
return details;
|
|
@@ -339,38 +368,53 @@ function buildNestedChildren(result: SingleResult): TreeNode[] {
|
|
|
339
368
|
return nodes;
|
|
340
369
|
}
|
|
341
370
|
|
|
342
|
-
function formatToolArgPreview(toolName:
|
|
343
|
-
const
|
|
371
|
+
function formatToolArgPreview(toolName: unknown, rawArgs: unknown): string {
|
|
372
|
+
const args = asRecord(rawArgs);
|
|
373
|
+
const shorten = (value: unknown) => {
|
|
374
|
+
const p = stringValue(value);
|
|
344
375
|
const home = os.homedir();
|
|
345
376
|
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
346
377
|
};
|
|
347
|
-
const truncateTo = (
|
|
348
|
-
|
|
378
|
+
const truncateTo = (value: unknown, n: number) => {
|
|
379
|
+
const text = stringValue(value);
|
|
380
|
+
return text.length > n ? text.slice(0, n) + "\u2026" : text;
|
|
381
|
+
};
|
|
349
382
|
|
|
350
383
|
switch (toolName) {
|
|
351
|
-
case "bash":
|
|
352
|
-
|
|
353
|
-
return truncateTo(cmd.replace(/\s+/g, " "), 52);
|
|
354
|
-
}
|
|
384
|
+
case "bash":
|
|
385
|
+
return truncateTo(stringValue(args.command).replace(/\s+/g, " "), 52);
|
|
355
386
|
case "read":
|
|
356
387
|
case "write":
|
|
357
388
|
case "edit":
|
|
358
|
-
return shorten(truncateTo(
|
|
359
|
-
case "grep":
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
389
|
+
return shorten(truncateTo(args.path ?? args.file_path, 52));
|
|
390
|
+
case "grep": {
|
|
391
|
+
const pattern = stringValue(args.pattern);
|
|
392
|
+
const target = stringValue(args.path);
|
|
393
|
+
return truncateTo(`/${pattern}/`, 30) + (target ? ` in ${shorten(target)}` : "");
|
|
394
|
+
}
|
|
395
|
+
case "find": {
|
|
396
|
+
const pattern = stringValue(args.pattern, "*");
|
|
397
|
+
const target = stringValue(args.path);
|
|
398
|
+
return truncateTo(pattern, 30) + (target ? ` in ${shorten(target)}` : "");
|
|
399
|
+
}
|
|
365
400
|
case "subagent":
|
|
366
401
|
case "subagents": {
|
|
367
|
-
const tasks = (args.tasks
|
|
368
|
-
return tasks
|
|
402
|
+
const tasks = Array.isArray(args.tasks) ? args.tasks : [];
|
|
403
|
+
return tasks
|
|
404
|
+
.map((task) => stringValue(asRecord(task).agent))
|
|
405
|
+
.filter(Boolean)
|
|
406
|
+
.join(", ");
|
|
369
407
|
}
|
|
370
408
|
case "resume_subagents": {
|
|
371
|
-
const raw =
|
|
372
|
-
const resumes = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
373
|
-
return resumes
|
|
409
|
+
const raw = args.resumes;
|
|
410
|
+
const resumes = Array.isArray(raw) ? raw : raw && typeof raw === "object" ? [raw] : [];
|
|
411
|
+
return resumes
|
|
412
|
+
.map((resume) => {
|
|
413
|
+
const item = asRecord(resume);
|
|
414
|
+
return stringValue(item.subagent) || stringValue(item.name);
|
|
415
|
+
})
|
|
416
|
+
.filter(Boolean)
|
|
417
|
+
.join(", ");
|
|
374
418
|
}
|
|
375
419
|
default:
|
|
376
420
|
return "";
|
|
@@ -381,12 +425,14 @@ export function formatLiveLogEntry(
|
|
|
381
425
|
entry: LiveLogEntry,
|
|
382
426
|
theme: { fg: ThemeFg },
|
|
383
427
|
): string {
|
|
384
|
-
const
|
|
385
|
-
|
|
428
|
+
const value = asRecord(entry);
|
|
429
|
+
const at = typeof value.at === "number" && Number.isFinite(value.at) ? value.at : undefined;
|
|
430
|
+
const stamp = at !== undefined ? theme.fg("dim", formatClockTime(at)) + " " : "";
|
|
431
|
+
return stamp + formatLiveLogEntryBody(value, theme);
|
|
386
432
|
}
|
|
387
433
|
|
|
388
434
|
function formatLiveLogEntryBody(
|
|
389
|
-
entry:
|
|
435
|
+
entry: Record<string, unknown>,
|
|
390
436
|
theme: { fg: ThemeFg },
|
|
391
437
|
): string {
|
|
392
438
|
switch (entry.kind) {
|
|
@@ -394,24 +440,28 @@ function formatLiveLogEntryBody(
|
|
|
394
440
|
return theme.fg("muted", "\u27f3") + " " + theme.fg("dim", "thinking\u2026");
|
|
395
441
|
|
|
396
442
|
case "turn_end": {
|
|
397
|
-
const
|
|
443
|
+
const inputTokens = finiteNumber(entry.inputTokens);
|
|
444
|
+
const outputTokens = finiteNumber(entry.outputTokens);
|
|
445
|
+
const tokens = inputTokens || outputTokens
|
|
398
446
|
? " " + theme.fg("dim",
|
|
399
|
-
`\u2191${formatTokens(
|
|
447
|
+
`\u2191${formatTokens(inputTokens)} \u2193${formatTokens(outputTokens)}`)
|
|
400
448
|
: "";
|
|
449
|
+
const turn = finiteNumber(entry.turn);
|
|
401
450
|
return (
|
|
402
451
|
theme.fg("success", "\u2713") +
|
|
403
452
|
" " +
|
|
404
|
-
theme.fg("muted", `turn ${
|
|
453
|
+
theme.fg("muted", `turn ${turn}`) +
|
|
405
454
|
tokens
|
|
406
455
|
);
|
|
407
456
|
}
|
|
408
457
|
|
|
409
458
|
case "tool_start": {
|
|
410
|
-
const
|
|
459
|
+
const toolName = stringValue(entry.toolName, "unknown tool");
|
|
460
|
+
const argPreview = formatToolArgPreview(toolName, entry.args);
|
|
411
461
|
return (
|
|
412
462
|
theme.fg("muted", "\u2192") +
|
|
413
463
|
" " +
|
|
414
|
-
theme.fg("accent",
|
|
464
|
+
theme.fg("accent", toolName) +
|
|
415
465
|
(argPreview ? " " + theme.fg("dim", argPreview) : "")
|
|
416
466
|
);
|
|
417
467
|
}
|
|
@@ -420,8 +470,11 @@ function formatLiveLogEntryBody(
|
|
|
420
470
|
return (
|
|
421
471
|
theme.fg("success", "\u2713") +
|
|
422
472
|
" " +
|
|
423
|
-
theme.fg("accent", entry.toolName)
|
|
473
|
+
theme.fg("accent", stringValue(entry.toolName, "unknown tool"))
|
|
424
474
|
);
|
|
475
|
+
|
|
476
|
+
default:
|
|
477
|
+
return theme.fg("muted", "activity");
|
|
425
478
|
}
|
|
426
479
|
}
|
|
427
480
|
|
|
@@ -482,33 +535,64 @@ function loadMessagesFromSession(sessionDir: string | undefined): SingleResult["
|
|
|
482
535
|
}
|
|
483
536
|
|
|
484
537
|
function hydrateResultFromSession(result: SingleResult): SingleResult {
|
|
485
|
-
if (result.messages
|
|
486
|
-
const messages = loadMessagesFromSession(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
538
|
+
if (Array.isArray(result.messages) && result.messages.length > 0) return result;
|
|
539
|
+
const messages = loadMessagesFromSession(
|
|
540
|
+
typeof result.sessionDir === "string" ? result.sessionDir : undefined,
|
|
541
|
+
);
|
|
542
|
+
if (messages.length > 0) return { ...result, messages };
|
|
543
|
+
return Array.isArray(result.messages) ? result : { ...result, messages: [] };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function buildResultNode(rawResult: SingleResult): TreeNode {
|
|
547
|
+
let result: SingleResult;
|
|
548
|
+
if (
|
|
549
|
+
rawResult !== null &&
|
|
550
|
+
typeof rawResult === "object" &&
|
|
551
|
+
!Array.isArray(rawResult) &&
|
|
552
|
+
typeof rawResult.agent === "string" &&
|
|
553
|
+
typeof rawResult.exitCode === "number"
|
|
554
|
+
) {
|
|
555
|
+
result = hydrateResultFromSession(rawResult);
|
|
556
|
+
} else {
|
|
557
|
+
result = {
|
|
558
|
+
agent: "unknown agent",
|
|
559
|
+
agentSource: "unknown",
|
|
560
|
+
task: "",
|
|
561
|
+
exitCode: 1,
|
|
562
|
+
messages: [],
|
|
563
|
+
stderr: "Malformed subagent result.",
|
|
564
|
+
usage: {} as UsageStats,
|
|
565
|
+
toolCalls: {},
|
|
566
|
+
completedTurns: 0,
|
|
567
|
+
turnInProgress: false,
|
|
568
|
+
liveLog: [],
|
|
569
|
+
};
|
|
570
|
+
}
|
|
492
571
|
const status = statusFromResult(result);
|
|
493
|
-
const usage = formatUsage(result.usage
|
|
572
|
+
const usage = formatUsage(result.usage, result.model);
|
|
494
573
|
const metaParts: string[] = [];
|
|
495
|
-
if (result.name) metaParts.push(result.name);
|
|
496
|
-
metaParts.push(result.agentSource);
|
|
574
|
+
if (typeof result.name === "string" && result.name) metaParts.push(result.name);
|
|
575
|
+
if (typeof result.agentSource === "string" && result.agentSource) metaParts.push(result.agentSource);
|
|
497
576
|
if (usage) metaParts.push(usage);
|
|
498
577
|
if (status === "error") {
|
|
499
578
|
const errorText = result.errorMessage || result.stderr || result.stopReason;
|
|
500
|
-
if (
|
|
579
|
+
if (typeof errorText === "string" && errorText) {
|
|
580
|
+
metaParts.push(truncate(errorText.replace(/\s+/g, " "), 120));
|
|
581
|
+
}
|
|
501
582
|
}
|
|
502
583
|
|
|
503
584
|
const children = buildNestedChildren(result);
|
|
504
585
|
const isRunning = status === "running";
|
|
586
|
+
const liveLog = Array.isArray(result.liveLog) ? result.liveLog : [];
|
|
505
587
|
return {
|
|
506
|
-
label: result.agent,
|
|
588
|
+
label: stringValue(result.agent, "unknown agent"),
|
|
507
589
|
status,
|
|
508
590
|
meta: metaParts.join(" • "),
|
|
509
|
-
task: result.task,
|
|
510
|
-
startedAt: result.startedAt
|
|
511
|
-
|
|
591
|
+
task: typeof result.task === "string" ? result.task : undefined,
|
|
592
|
+
startedAt: typeof result.startedAt === "number" && Number.isFinite(result.startedAt)
|
|
593
|
+
? result.startedAt
|
|
594
|
+
: undefined,
|
|
595
|
+
liveActivity: isRunning && liveLog.length > 0 ? liveLog.slice(-MAX_LIVE_LOG_ENTRIES) : undefined,
|
|
512
596
|
outputPreview: !isRunning && children.length === 0 ? buildLeafPreview(result) : undefined,
|
|
513
597
|
children,
|
|
514
598
|
};
|
|
@@ -567,8 +651,11 @@ export function renderTreeLines(
|
|
|
567
651
|
export function topLevelSummary(details: SubagentDetails, counts: TreeCounts): string {
|
|
568
652
|
// aggregatedUsage includes own agents + all their nested descendants;
|
|
569
653
|
// fall back to summing only direct results for old serialised data lacking the field.
|
|
654
|
+
const safeResults = details.results.filter(
|
|
655
|
+
(result): result is SingleResult => result !== null && typeof result === "object" && !Array.isArray(result),
|
|
656
|
+
);
|
|
570
657
|
const totalUsage = formatUsage(
|
|
571
|
-
usageSummaryToUsageStats(details.usageSummary) ?? details.aggregatedUsage ?? aggregateUsage(
|
|
658
|
+
usageSummaryToUsageStats(details.usageSummary) ?? details.aggregatedUsage ?? aggregateUsage(safeResults),
|
|
572
659
|
);
|
|
573
660
|
const parts = [
|
|
574
661
|
`${counts.running} running`,
|
package/types.ts
CHANGED
|
@@ -204,7 +204,7 @@ function asMessageArray(messages: unknown): Message[] {
|
|
|
204
204
|
export function extractToolCalls(messages: unknown): ToolCallCounts {
|
|
205
205
|
const counts: ToolCallCounts = {};
|
|
206
206
|
for (const msg of asMessageArray(messages)) {
|
|
207
|
-
if (msg
|
|
207
|
+
if ((msg as any)?.role !== "assistant") continue;
|
|
208
208
|
for (const part of (Array.isArray((msg as any).content) ? (msg as any).content : [])) {
|
|
209
209
|
if ((part as any)?.type !== "toolCall") continue;
|
|
210
210
|
const name: string = typeof (part as any).name === "string" ? (part as any).name : "unknown";
|
|
@@ -479,10 +479,10 @@ export function getFinalOutput(messages: unknown, fallback?: string): string {
|
|
|
479
479
|
const history = asMessageArray(messages);
|
|
480
480
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
481
481
|
const msg = history[i];
|
|
482
|
-
if (msg
|
|
482
|
+
if ((msg as any)?.role === "assistant" && Array.isArray((msg as any).content)) {
|
|
483
483
|
for (let j = (msg as any).content.length - 1; j >= 0; j--) {
|
|
484
484
|
const part = (msg as any).content[j];
|
|
485
|
-
if (part?.type === "text") return part.text;
|
|
485
|
+
if (part?.type === "text" && typeof part.text === "string") return part.text;
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
488
|
}
|
|
@@ -493,12 +493,16 @@ export function getFinalOutput(messages: unknown, fallback?: string): string {
|
|
|
493
493
|
export function getDisplayItems(messages: unknown): DisplayItem[] {
|
|
494
494
|
const items: DisplayItem[] = [];
|
|
495
495
|
for (const msg of asMessageArray(messages)) {
|
|
496
|
-
if (msg
|
|
496
|
+
if ((msg as any)?.role === "assistant" && Array.isArray((msg as any).content)) {
|
|
497
497
|
for (const part of (msg as any).content) {
|
|
498
|
-
if (part?.type === "text") {
|
|
498
|
+
if (part?.type === "text" && typeof part.text === "string") {
|
|
499
499
|
items.push({ type: "text", text: part.text });
|
|
500
|
-
} else if (part?.type === "toolCall") {
|
|
501
|
-
items.push({
|
|
500
|
+
} else if (part?.type === "toolCall" && typeof part.name === "string") {
|
|
501
|
+
items.push({
|
|
502
|
+
type: "toolCall",
|
|
503
|
+
name: part.name,
|
|
504
|
+
args: part.arguments && typeof part.arguments === "object" ? part.arguments : {},
|
|
505
|
+
});
|
|
502
506
|
}
|
|
503
507
|
}
|
|
504
508
|
}
|