@fosterg4/pi-subagent 1.0.1 → 1.0.3
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/agents/planner.md +3 -0
- package/agents/reviewer.md +2 -1
- package/agents/scout.md +3 -0
- package/agents/worker.md +2 -1
- package/index.ts +86 -417
- package/package.json +1 -1
- package/utils.ts +37 -0
package/agents/planner.md
CHANGED
|
@@ -47,6 +47,9 @@ outputSchema:
|
|
|
47
47
|
|
|
48
48
|
You are a planning specialist. You receive context (from a scout) and requirements, then produce a clear implementation plan.
|
|
49
49
|
|
|
50
|
+
## STRICT OUTPUT RULE
|
|
51
|
+
Your ENTIRE response must be ONLY a valid JSON object matching the outputSchema below. Do NOT include any conversational text, greetings, explanations, markdown code fences, or any wrapping. No ```json blocks. No "Here is the plan:" prefix. Nothing but the raw JSON object.
|
|
52
|
+
|
|
50
53
|
You must NOT make any changes. Only read, analyze, and plan.
|
|
51
54
|
|
|
52
55
|
Return your plan as a JSON object matching the outputSchema.
|
package/agents/reviewer.md
CHANGED
|
@@ -59,7 +59,8 @@ Strategy:
|
|
|
59
59
|
2. Read the modified files
|
|
60
60
|
3. Check for bugs, security issues, code smells
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
## STRICT OUTPUT RULE
|
|
63
|
+
Your ENTIRE response must be ONLY a valid JSON object matching the outputSchema below. Do NOT include any conversational text, greetings, explanations, markdown code fences, or any wrapping. No ```json blocks. Nothing but the raw JSON object.
|
|
63
64
|
|
|
64
65
|
## Output format (JSON)
|
|
65
66
|
|
package/agents/scout.md
CHANGED
|
@@ -36,6 +36,9 @@ outputSchema:
|
|
|
36
36
|
|
|
37
37
|
You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
|
|
38
38
|
|
|
39
|
+
## STRICT OUTPUT RULE
|
|
40
|
+
Your ENTIRE response must be ONLY a valid JSON object matching the outputSchema below. Do NOT include any conversational text, greetings, explanations, markdown code fences, or any wrapping. No ```json blocks. No "Here are my findings:" prefix. Nothing but the raw JSON object.
|
|
41
|
+
|
|
39
42
|
Your output will be passed to an agent who has NOT seen the files you explored.
|
|
40
43
|
|
|
41
44
|
Thoroughness (infer from task, default medium):
|
package/agents/worker.md
CHANGED
|
@@ -32,7 +32,8 @@ You are a worker agent with full capabilities. You operate in an isolated contex
|
|
|
32
32
|
|
|
33
33
|
Work autonomously to complete the assigned task. Use all available tools as needed.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
## STRICT OUTPUT RULE
|
|
36
|
+
Your ENTIRE response must be ONLY a valid JSON object matching the outputSchema below. Do NOT include any conversational text, greetings, explanations, markdown code fences, or any wrapping. No ```json blocks. Nothing but the raw JSON object.
|
|
36
37
|
|
|
37
38
|
## Output format (JSON)
|
|
38
39
|
|
package/index.ts
CHANGED
|
@@ -35,116 +35,13 @@ import {
|
|
|
35
35
|
formatAgentList,
|
|
36
36
|
} from "./agents.ts";
|
|
37
37
|
import { type ValidationResult, validateSchema } from "./validate.ts";
|
|
38
|
+
import { fmt, usageLine, sumUsage } from "./utils.ts";
|
|
38
39
|
|
|
39
40
|
const MAX_PARALLEL_TASKS = 8;
|
|
40
41
|
const MAX_CONCURRENCY = 4;
|
|
41
|
-
const COLLAPSED_ITEM_COUNT = 10;
|
|
42
42
|
const PER_TASK_OUTPUT_CAP = 50 * 1024;
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
// Formatting utilities
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
function formatTokens(count: number): string {
|
|
49
|
-
if (count < 1000) return count.toString();
|
|
50
|
-
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
51
|
-
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
52
|
-
return `${(count / 1000000).toFixed(1)}M`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function formatUsageStats(
|
|
56
|
-
usage: {
|
|
57
|
-
input: number;
|
|
58
|
-
output: number;
|
|
59
|
-
cacheRead: number;
|
|
60
|
-
cacheWrite: number;
|
|
61
|
-
cost: number;
|
|
62
|
-
contextTokens?: number;
|
|
63
|
-
turns?: number;
|
|
64
|
-
},
|
|
65
|
-
model?: string,
|
|
66
|
-
): string {
|
|
67
|
-
const parts: string[] = [];
|
|
68
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
69
|
-
if (usage.input) parts.push(`\u2191${formatTokens(usage.input)}`);
|
|
70
|
-
if (usage.output) parts.push(`\u2193${formatTokens(usage.output)}`);
|
|
71
|
-
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
72
|
-
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
73
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
74
|
-
if (usage.contextTokens && usage.contextTokens > 0) {
|
|
75
|
-
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
76
|
-
}
|
|
77
|
-
if (model) parts.push(model);
|
|
78
|
-
return parts.join(" ");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function formatToolCall(
|
|
82
|
-
toolName: string,
|
|
83
|
-
args: Record<string, unknown>,
|
|
84
|
-
themeFg: (color: string, text: string) => string,
|
|
85
|
-
): string {
|
|
86
|
-
const shortenPath = (p: string) => {
|
|
87
|
-
const home = os.homedir();
|
|
88
|
-
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
switch (toolName) {
|
|
92
|
-
case "bash": {
|
|
93
|
-
const command = (args.command as string) || "...";
|
|
94
|
-
const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
|
|
95
|
-
return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
|
|
96
|
-
}
|
|
97
|
-
case "read": {
|
|
98
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
99
|
-
const filePath = shortenPath(rawPath);
|
|
100
|
-
const offset = args.offset as number | undefined;
|
|
101
|
-
const limit = args.limit as number | undefined;
|
|
102
|
-
let text = themeFg("accent", filePath);
|
|
103
|
-
if (offset !== undefined || limit !== undefined) {
|
|
104
|
-
const startLine = offset ?? 1;
|
|
105
|
-
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
106
|
-
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
107
|
-
}
|
|
108
|
-
return themeFg("muted", "read ") + text;
|
|
109
|
-
}
|
|
110
|
-
case "write": {
|
|
111
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
112
|
-
const filePath = shortenPath(rawPath);
|
|
113
|
-
const content = (args.content || "") as string;
|
|
114
|
-
const lines = content.split("\n").length;
|
|
115
|
-
let text = themeFg("muted", "write ") + themeFg("accent", filePath);
|
|
116
|
-
if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
|
|
117
|
-
return text;
|
|
118
|
-
}
|
|
119
|
-
case "edit": {
|
|
120
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
121
|
-
return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
|
|
122
|
-
}
|
|
123
|
-
case "ls":
|
|
124
|
-
return themeFg("muted", "ls ") + themeFg("accent", shortenPath((args.path || ".") as string));
|
|
125
|
-
case "find":
|
|
126
|
-
return (
|
|
127
|
-
themeFg("muted", "find ") +
|
|
128
|
-
themeFg("accent", (args.pattern || "*") as string) +
|
|
129
|
-
themeFg("dim", ` in ${shortenPath((args.path || ".") as string)}`)
|
|
130
|
-
);
|
|
131
|
-
case "grep":
|
|
132
|
-
return (
|
|
133
|
-
themeFg("muted", "grep ") +
|
|
134
|
-
themeFg("accent", `/${(args.pattern || "") as string}/`) +
|
|
135
|
-
themeFg("dim", ` in ${shortenPath((args.path || ".") as string)}`)
|
|
136
|
-
);
|
|
137
|
-
default:
|
|
138
|
-
return (
|
|
139
|
-
themeFg("accent", toolName) +
|
|
140
|
-
themeFg("dim", ` ${JSON.stringify(args).slice(0, 50)}...`)
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
// Types
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
44
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
148
45
|
|
|
149
46
|
interface UsageStats {
|
|
150
47
|
input: number;
|
|
@@ -225,25 +122,6 @@ function truncateParallelOutput(output: string): string {
|
|
|
225
122
|
return `${truncated}\n\n[Output truncated: ${byteLength - Buffer.byteLength(truncated, "utf8")} bytes omitted. Full output preserved in tool details.]`;
|
|
226
123
|
}
|
|
227
124
|
|
|
228
|
-
type DisplayItem =
|
|
229
|
-
| { type: "text"; text: string }
|
|
230
|
-
| { type: "toolCall"; name: string; args: Record<string, unknown> };
|
|
231
|
-
|
|
232
|
-
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
233
|
-
const items: DisplayItem[] = [];
|
|
234
|
-
for (const msg of messages) {
|
|
235
|
-
if (msg.role === "assistant") {
|
|
236
|
-
for (const part of msg.content) {
|
|
237
|
-
if (part.type === "text")
|
|
238
|
-
items.push({ type: "text", text: part.text });
|
|
239
|
-
else if (part.type === "toolCall")
|
|
240
|
-
items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return items;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
125
|
// ---------------------------------------------------------------------------
|
|
248
126
|
// Concurrency
|
|
249
127
|
// ---------------------------------------------------------------------------
|
|
@@ -1065,363 +943,154 @@ export default function (pi: ExtensionAPI) {
|
|
|
1065
943
|
|
|
1066
944
|
const mdTheme = getMarkdownTheme();
|
|
1067
945
|
|
|
1068
|
-
const
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
} else {
|
|
1081
|
-
text += `${theme.fg("muted", "\u2192 ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
return text.trimEnd();
|
|
946
|
+
const getFinalOutputText = (messages: Message[]): string => {
|
|
947
|
+
const text = getFinalOutput(messages);
|
|
948
|
+
// Try to extract clean text from JSON output (remove wrapper text)
|
|
949
|
+
const extracted = extractStructuredOutput(messages);
|
|
950
|
+
if (extracted) return JSON.stringify(extracted, null, 2);
|
|
951
|
+
return text;
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const getStatusText = (r: SingleResult): string => {
|
|
955
|
+
if (r.exitCode === 0) return theme.fg("success", "\u2713");
|
|
956
|
+
if (r.exitCode === -1) return theme.fg("warning", "\u23F3");
|
|
957
|
+
return theme.fg("error", "\u2717");
|
|
1085
958
|
};
|
|
1086
959
|
|
|
1087
|
-
|
|
960
|
+
// --- Single mode ---
|
|
1088
961
|
if (details.mode === "single" && details.results.length === 1) {
|
|
1089
962
|
const r = details.results[0];
|
|
1090
|
-
const
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
const displayItems = getDisplayItems(r.messages);
|
|
963
|
+
const status = getStatusText(r);
|
|
964
|
+
const finalOutput = getFinalOutputText(r.messages);
|
|
965
|
+
const usage = usageLine(r.usage);
|
|
966
|
+
const usg = usage ? theme.fg("dim", usage) : "";
|
|
1095
967
|
|
|
1096
968
|
if (expanded) {
|
|
1097
969
|
const container = new Container();
|
|
1098
|
-
|
|
1099
|
-
if (isError && r.stopReason)
|
|
1100
|
-
header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
970
|
+
const header = `${status} ${theme.fg("accent", r.agent)}${theme.fg("muted", r.model ? ` \u00B7 ${r.model}` : "")}`;
|
|
1101
971
|
container.addChild(new Text(header, 0, 0));
|
|
1102
|
-
if (
|
|
1103
|
-
container.addChild(
|
|
1104
|
-
new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0),
|
|
1105
|
-
);
|
|
1106
|
-
container.addChild(new Spacer(1));
|
|
1107
|
-
container.addChild(new Text(theme.fg("muted", "\u2500\u2500\u2500 Task \u2500\u2500\u2500"), 0, 0));
|
|
1108
|
-
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
1109
|
-
|
|
1110
|
-
// Show tool calls
|
|
1111
|
-
const toolCalls = displayItems.filter(
|
|
1112
|
-
(i) => i.type === "toolCall",
|
|
1113
|
-
);
|
|
1114
|
-
if (toolCalls.length > 0) {
|
|
1115
|
-
container.addChild(new Spacer(1));
|
|
1116
|
-
container.addChild(
|
|
1117
|
-
new Text(
|
|
1118
|
-
theme.fg("muted", "\u2500\u2500\u2500 Tool Calls \u2500\u2500\u2500"),
|
|
1119
|
-
0,
|
|
1120
|
-
0,
|
|
1121
|
-
),
|
|
1122
|
-
);
|
|
1123
|
-
for (const item of toolCalls) {
|
|
1124
|
-
container.addChild(
|
|
1125
|
-
new Text(
|
|
1126
|
-
theme.fg("muted", "\u2192 ") +
|
|
1127
|
-
formatToolCall(
|
|
1128
|
-
item.name,
|
|
1129
|
-
item.args,
|
|
1130
|
-
theme.fg.bind(theme),
|
|
1131
|
-
),
|
|
1132
|
-
0,
|
|
1133
|
-
0,
|
|
1134
|
-
),
|
|
1135
|
-
);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// Show structured output if available
|
|
1140
|
-
if (r.structuredOutput && Object.keys(r.structuredOutput).length > 0) {
|
|
1141
|
-
container.addChild(new Spacer(1));
|
|
1142
|
-
container.addChild(
|
|
1143
|
-
new Text(
|
|
1144
|
-
theme.fg("muted", "\u2500\u2500\u2500 Structured Output \u2500\u2500\u2500"),
|
|
1145
|
-
0,
|
|
1146
|
-
0,
|
|
1147
|
-
),
|
|
1148
|
-
);
|
|
1149
|
-
container.addChild(
|
|
1150
|
-
new Text(
|
|
1151
|
-
theme.fg("toolOutput", JSON.stringify(r.structuredOutput, null, 2)),
|
|
1152
|
-
0,
|
|
1153
|
-
0,
|
|
1154
|
-
),
|
|
1155
|
-
);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// Show final output
|
|
1159
|
-
const finalOutput = getFinalOutput(r.messages);
|
|
972
|
+
if (r.stderr) container.addChild(new Text(theme.fg("error", r.stderr), 0, 0));
|
|
1160
973
|
if (finalOutput) {
|
|
1161
974
|
container.addChild(new Spacer(1));
|
|
1162
|
-
container.addChild(
|
|
1163
|
-
new Text(theme.fg("muted", "\u2500\u2500\u2500 Output \u2500\u2500\u2500"), 0, 0),
|
|
1164
|
-
);
|
|
1165
975
|
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1166
976
|
}
|
|
1167
|
-
|
|
1168
|
-
const usageStr = formatUsageStats(r.usage, r.model);
|
|
1169
|
-
if (usageStr) {
|
|
977
|
+
if (usg) {
|
|
1170
978
|
container.addChild(new Spacer(1));
|
|
1171
|
-
container.addChild(new Text(
|
|
979
|
+
container.addChild(new Text(usg, 0, 0));
|
|
1172
980
|
}
|
|
1173
981
|
return container;
|
|
1174
982
|
}
|
|
1175
983
|
|
|
1176
|
-
let text = `${
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
984
|
+
let text = `${status} ${theme.fg("accent", r.agent)}`;
|
|
985
|
+
if (finalOutput) {
|
|
986
|
+
const preview = finalOutput.split("\n").slice(0, 5).join("\n");
|
|
987
|
+
const truncated = finalOutput.split("\n").length > 5;
|
|
988
|
+
text += `\n${theme.fg("toolOutput", preview)}`;
|
|
989
|
+
if (truncated) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
990
|
+
} else {
|
|
1181
991
|
text += `\n${theme.fg("muted", "(no output)")}`;
|
|
1182
|
-
else {
|
|
1183
|
-
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
|
|
1184
|
-
if (displayItems.length > COLLAPSED_ITEM_COUNT)
|
|
1185
|
-
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1186
992
|
}
|
|
1187
|
-
|
|
1188
|
-
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
993
|
+
if (usg) text += `\n${usg}`;
|
|
1189
994
|
return new Text(text, 0, 0);
|
|
1190
995
|
}
|
|
1191
996
|
|
|
1192
|
-
const aggregateUsage = (results: SingleResult[]) => {
|
|
1193
|
-
const total = {
|
|
1194
|
-
input: 0,
|
|
1195
|
-
output: 0,
|
|
1196
|
-
cacheRead: 0,
|
|
1197
|
-
cacheWrite: 0,
|
|
1198
|
-
cost: 0,
|
|
1199
|
-
turns: 0,
|
|
1200
|
-
};
|
|
1201
|
-
for (const r of results) {
|
|
1202
|
-
total.input += r.usage.input;
|
|
1203
|
-
total.output += r.usage.output;
|
|
1204
|
-
total.cacheRead += r.usage.cacheRead;
|
|
1205
|
-
total.cacheWrite += r.usage.cacheWrite;
|
|
1206
|
-
total.cost += r.usage.cost;
|
|
1207
|
-
total.turns += r.usage.turns;
|
|
1208
|
-
}
|
|
1209
|
-
return total;
|
|
1210
|
-
};
|
|
1211
|
-
|
|
1212
997
|
// --- Chain mode ---
|
|
1213
998
|
if (details.mode === "chain") {
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
).
|
|
1217
|
-
const icon =
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
999
|
+
const lastResult = details.results[details.results.length - 1];
|
|
1000
|
+
const finalOutput = lastResult ? getFinalOutputText(lastResult.messages) : "";
|
|
1001
|
+
const allOk = details.results.every((r) => r.exitCode === 0);
|
|
1002
|
+
const icon = allOk ? theme.fg("success", "\u2713") : theme.fg("error", "\u2717");
|
|
1003
|
+
const steps = details.results.map((r) => r.agent).join(" \u2192 ");
|
|
1004
|
+
const total = sumUsage(details.results);
|
|
1005
|
+
const totalUsg = usageLine(total);
|
|
1221
1006
|
|
|
1222
1007
|
if (expanded) {
|
|
1223
1008
|
const container = new Container();
|
|
1224
1009
|
container.addChild(
|
|
1225
|
-
new Text(
|
|
1226
|
-
icon +
|
|
1227
|
-
" " +
|
|
1228
|
-
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
1229
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`),
|
|
1230
|
-
0,
|
|
1231
|
-
0,
|
|
1232
|
-
),
|
|
1010
|
+
new Text(`${icon} ${theme.fg("accent", steps)}`, 0, 0),
|
|
1233
1011
|
);
|
|
1234
|
-
|
|
1235
1012
|
for (const r of details.results) {
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
const finalOutput = getFinalOutput(r.messages);
|
|
1242
|
-
|
|
1243
|
-
container.addChild(new Spacer(1));
|
|
1244
|
-
container.addChild(
|
|
1245
|
-
new Text(
|
|
1246
|
-
`${theme.fg("muted", `\u2500\u2500\u2500 Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
|
|
1247
|
-
0,
|
|
1248
|
-
0,
|
|
1249
|
-
),
|
|
1250
|
-
);
|
|
1251
|
-
container.addChild(
|
|
1252
|
-
new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0),
|
|
1253
|
-
);
|
|
1254
|
-
|
|
1255
|
-
for (const item of displayItems) {
|
|
1256
|
-
if (item.type === "toolCall") {
|
|
1257
|
-
container.addChild(
|
|
1258
|
-
new Text(
|
|
1259
|
-
theme.fg("muted", "\u2192 ") +
|
|
1260
|
-
formatToolCall(
|
|
1261
|
-
item.name,
|
|
1262
|
-
item.args,
|
|
1263
|
-
theme.fg.bind(theme),
|
|
1264
|
-
),
|
|
1265
|
-
0,
|
|
1266
|
-
0,
|
|
1267
|
-
),
|
|
1268
|
-
);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (finalOutput) {
|
|
1013
|
+
const out = getFinalOutputText(r.messages);
|
|
1014
|
+
const stepUsage = usageLine(r.usage);
|
|
1015
|
+
const stepUsg = stepUsage ? theme.fg("dim", stepUsage) : "";
|
|
1016
|
+
const label = `${getStatusText(r)} ${theme.fg("accent", r.agent)}${r.model ? theme.fg("muted", ` \u00B7 ${r.model}`) : ""}${stepUsg ? " " + stepUsg : ""}`;
|
|
1017
|
+
if (out) {
|
|
1273
1018
|
container.addChild(new Spacer(1));
|
|
1274
|
-
container.addChild(
|
|
1275
|
-
|
|
1276
|
-
);
|
|
1019
|
+
container.addChild(new Text(label, 0, 0));
|
|
1020
|
+
container.addChild(new Markdown(out.trim(), 0, 0, mdTheme));
|
|
1277
1021
|
}
|
|
1278
|
-
|
|
1279
|
-
const stepUsage = formatUsageStats(r.usage, r.model);
|
|
1280
|
-
if (stepUsage)
|
|
1281
|
-
container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
|
|
1282
1022
|
}
|
|
1283
|
-
|
|
1284
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
1285
|
-
if (usageStr) {
|
|
1023
|
+
if (totalUsg && details.results.length > 1) {
|
|
1286
1024
|
container.addChild(new Spacer(1));
|
|
1287
|
-
container.addChild(
|
|
1288
|
-
new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0),
|
|
1289
|
-
);
|
|
1025
|
+
container.addChild(new Text(totalUsg, 0, 0));
|
|
1290
1026
|
}
|
|
1291
1027
|
return container;
|
|
1292
1028
|
}
|
|
1293
1029
|
|
|
1294
|
-
let text =
|
|
1295
|
-
|
|
1296
|
-
" "
|
|
1297
|
-
|
|
1298
|
-
theme.fg("
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
? theme.fg("success", "\u2713")
|
|
1303
|
-
: theme.fg("error", "\u2717");
|
|
1304
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1305
|
-
text += `\n\n${theme.fg("muted", `\u2500\u2500\u2500 Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
1306
|
-
if (displayItems.length === 0)
|
|
1307
|
-
text += `\n${theme.fg("muted", "(no output)")}`;
|
|
1308
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1030
|
+
let text = `${icon} ${theme.fg("accent", steps)}`;
|
|
1031
|
+
if (finalOutput) {
|
|
1032
|
+
const preview = finalOutput.split("\n").slice(0, 5).join("\n");
|
|
1033
|
+
const truncated = finalOutput.split("\n").length > 5;
|
|
1034
|
+
text += `\n${theme.fg("toolOutput", preview)}`;
|
|
1035
|
+
if (truncated) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1036
|
+
} else {
|
|
1037
|
+
text += `\n${theme.fg("muted", "(no output)")}`;
|
|
1309
1038
|
}
|
|
1310
|
-
|
|
1311
|
-
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
1312
|
-
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1039
|
+
if (totalUsg) text += `\n${totalUsg}`;
|
|
1313
1040
|
return new Text(text, 0, 0);
|
|
1314
1041
|
}
|
|
1315
1042
|
|
|
1316
1043
|
// --- Parallel mode ---
|
|
1317
1044
|
if (details.mode === "parallel") {
|
|
1318
|
-
const running = details.results.filter(
|
|
1319
|
-
|
|
1320
|
-
).
|
|
1321
|
-
const
|
|
1322
|
-
(r) => r.exitCode !== -1 && !isFailedResult(r),
|
|
1323
|
-
).length;
|
|
1324
|
-
const failCount = details.results.filter(
|
|
1325
|
-
(r) => r.exitCode !== -1 && isFailedResult(r),
|
|
1326
|
-
).length;
|
|
1327
|
-
const isRunning = running > 0;
|
|
1328
|
-
const icon = isRunning
|
|
1045
|
+
const running = details.results.filter((r) => r.exitCode === -1).length;
|
|
1046
|
+
const done = details.results.filter((r) => r.exitCode !== -1).length;
|
|
1047
|
+
const allOk = details.results.every((r) => r.exitCode === 0);
|
|
1048
|
+
const icon = running > 0
|
|
1329
1049
|
? theme.fg("warning", "\u23F3")
|
|
1330
|
-
:
|
|
1331
|
-
? theme.fg("
|
|
1332
|
-
: theme.fg("
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
: `${successCount}/${details.results.length} tasks`;
|
|
1336
|
-
|
|
1337
|
-
if (expanded && !isRunning) {
|
|
1050
|
+
: allOk
|
|
1051
|
+
? theme.fg("success", "\u2713")
|
|
1052
|
+
: theme.fg("error", "\u2717");
|
|
1053
|
+
|
|
1054
|
+
if (expanded && running === 0) {
|
|
1338
1055
|
const container = new Container();
|
|
1056
|
+
const total = sumUsage(details.results);
|
|
1057
|
+
const totalUsg = usageLine(total);
|
|
1339
1058
|
container.addChild(
|
|
1340
1059
|
new Text(
|
|
1341
|
-
`${icon} ${theme.fg("
|
|
1060
|
+
`${icon} ${theme.fg("accent", `${done} tasks`)}`,
|
|
1342
1061
|
0,
|
|
1343
1062
|
0,
|
|
1344
1063
|
),
|
|
1345
1064
|
);
|
|
1346
|
-
|
|
1347
1065
|
for (const r of details.results) {
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
const finalOutput = getFinalOutput(r.messages);
|
|
1353
|
-
|
|
1354
|
-
container.addChild(new Spacer(1));
|
|
1355
|
-
container.addChild(
|
|
1356
|
-
new Text(
|
|
1357
|
-
`${theme.fg("muted", "\u2500\u2500\u2500 ") + theme.fg("accent", r.agent)} ${rIcon}`,
|
|
1358
|
-
0,
|
|
1359
|
-
0,
|
|
1360
|
-
),
|
|
1361
|
-
);
|
|
1362
|
-
container.addChild(
|
|
1363
|
-
new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0),
|
|
1364
|
-
);
|
|
1365
|
-
|
|
1366
|
-
for (const item of displayItems) {
|
|
1367
|
-
if (item.type === "toolCall") {
|
|
1368
|
-
container.addChild(
|
|
1369
|
-
new Text(
|
|
1370
|
-
theme.fg("muted", "\u2192 ") +
|
|
1371
|
-
formatToolCall(
|
|
1372
|
-
item.name,
|
|
1373
|
-
item.args,
|
|
1374
|
-
theme.fg.bind(theme),
|
|
1375
|
-
),
|
|
1376
|
-
0,
|
|
1377
|
-
0,
|
|
1378
|
-
),
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
if (finalOutput) {
|
|
1066
|
+
const out = getFinalOutputText(r.messages);
|
|
1067
|
+
const stepUsage = usageLine(r.usage);
|
|
1068
|
+
const stepUsg = stepUsage ? theme.fg("dim", stepUsage) : "";
|
|
1069
|
+
if (out) {
|
|
1384
1070
|
container.addChild(new Spacer(1));
|
|
1385
1071
|
container.addChild(
|
|
1386
|
-
new
|
|
1072
|
+
new Text(
|
|
1073
|
+
`${getStatusText(r)} ${theme.fg("accent", r.agent)}${stepUsg ? " " + stepUsg : ""}`,
|
|
1074
|
+
0,
|
|
1075
|
+
0,
|
|
1076
|
+
),
|
|
1387
1077
|
);
|
|
1078
|
+
container.addChild(new Markdown(out.trim(), 0, 0, mdTheme));
|
|
1388
1079
|
}
|
|
1389
|
-
|
|
1390
|
-
const taskUsage = formatUsageStats(r.usage, r.model);
|
|
1391
|
-
if (taskUsage)
|
|
1392
|
-
container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
1393
1080
|
}
|
|
1394
|
-
|
|
1395
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
1396
|
-
if (usageStr) {
|
|
1081
|
+
if (totalUsg && details.results.length > 1) {
|
|
1397
1082
|
container.addChild(new Spacer(1));
|
|
1398
|
-
container.addChild(
|
|
1399
|
-
new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0),
|
|
1400
|
-
);
|
|
1083
|
+
container.addChild(new Text(totalUsg, 0, 0));
|
|
1401
1084
|
}
|
|
1402
1085
|
return container;
|
|
1403
1086
|
}
|
|
1404
1087
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
: isFailedResult(r)
|
|
1411
|
-
? theme.fg("error", "\u2717")
|
|
1412
|
-
: theme.fg("success", "\u2713");
|
|
1413
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1414
|
-
text += `\n\n${theme.fg("muted", "\u2500\u2500\u2500 ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
1415
|
-
if (displayItems.length === 0)
|
|
1416
|
-
text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
1417
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1418
|
-
}
|
|
1419
|
-
if (!isRunning) {
|
|
1420
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
1421
|
-
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
1088
|
+
const usg = usageLine(sumUsage(details.results));
|
|
1089
|
+
let text = `${icon} ${theme.fg("accent", `${done}/${details.results.length} tasks`)}`;
|
|
1090
|
+
if (running === 0) {
|
|
1091
|
+
if (usg) text += `\n${usg}`;
|
|
1092
|
+
if (!expanded) text += theme.fg("muted", " (Ctrl+O to expand)");
|
|
1422
1093
|
}
|
|
1423
|
-
if (!expanded)
|
|
1424
|
-
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1425
1094
|
return new Text(text, 0, 0);
|
|
1426
1095
|
}
|
|
1427
1096
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fosterg4/pi-subagent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Delegate tasks to specialized subagents with isolated context windows, structured JSON handoff, contract schemas, and live TUI streaming",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/utils.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting helpers — testable without pi runtime deps
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { UsageStats } from "./index.ts";
|
|
6
|
+
|
|
7
|
+
export function fmt(n: number): string {
|
|
8
|
+
if (n < 1000) return n.toString();
|
|
9
|
+
if (n < 10000) return (n / 1000).toFixed(1) + "k";
|
|
10
|
+
if (n < 1000000) return Math.round(n / 1000) + "k";
|
|
11
|
+
return (n / 1000000).toFixed(1) + "M";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function usageLine(u: UsageStats): string {
|
|
15
|
+
const parts: string[] = [];
|
|
16
|
+
if (u.input) parts.push("\u2191" + fmt(u.input));
|
|
17
|
+
if (u.output) parts.push("\u2193" + fmt(u.output));
|
|
18
|
+
if (u.cacheRead) {
|
|
19
|
+
parts.push("R" + fmt(u.cacheRead));
|
|
20
|
+
const total = u.input + u.cacheRead;
|
|
21
|
+
if (total > 0) parts.push("CH" + ((u.cacheRead / total) * 100).toFixed(1) + "%");
|
|
22
|
+
}
|
|
23
|
+
if (u.cost) parts.push("$" + u.cost.toFixed(4));
|
|
24
|
+
return parts.join(" ");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sumUsage(results: ReadonlyArray<{ usage: UsageStats }>): UsageStats {
|
|
28
|
+
const u: UsageStats = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
29
|
+
for (const r of results) {
|
|
30
|
+
u.input += r.usage.input;
|
|
31
|
+
u.output += r.usage.output;
|
|
32
|
+
u.cacheRead += r.usage.cacheRead;
|
|
33
|
+
u.cacheWrite += r.usage.cacheWrite;
|
|
34
|
+
u.cost += r.usage.cost;
|
|
35
|
+
}
|
|
36
|
+
return u;
|
|
37
|
+
}
|