@bdsqqq/pi 0.0.1
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/dist/chunk-DsIazq42.js +26 -0
- package/dist/command-palette-B97yKqne.js +406 -0
- package/dist/core/agents-md.d.ts +22 -0
- package/dist/core/agents-md.js +174 -0
- package/dist/core/box-chrome.d.ts +56 -0
- package/dist/core/box-chrome.js +207 -0
- package/dist/core/box-format.d.ts +82 -0
- package/dist/core/box-format.js +381 -0
- package/dist/core/config.d.ts +14 -0
- package/dist/core/config.js +201 -0
- package/dist/core/file-tracker.d.ts +65 -0
- package/dist/core/file-tracker.js +439 -0
- package/dist/core/github-api.d.ts +45 -0
- package/dist/core/github-api.js +101 -0
- package/dist/core/html-to-md.d.ts +5 -0
- package/dist/core/html-to-md.js +289 -0
- package/dist/core/interpolate.d.ts +44 -0
- package/dist/core/interpolate.js +475 -0
- package/dist/core/mutex.d.ts +8 -0
- package/dist/core/mutex.js +142 -0
- package/dist/core/output-buffer.d.ts +80 -0
- package/dist/core/output-buffer.js +517 -0
- package/dist/core/permissions.d.ts +19 -0
- package/dist/core/permissions.js +140 -0
- package/dist/core/pi-spawn.d.ts +62 -0
- package/dist/core/pi-spawn.js +244 -0
- package/dist/core/prompt-patch.d.ts +11 -0
- package/dist/core/prompt-patch.js +16 -0
- package/dist/core/show-renderer.d.ts +10 -0
- package/dist/core/show-renderer.js +39 -0
- package/dist/core/show.d.ts +29 -0
- package/dist/core/show.js +288 -0
- package/dist/core/sub-agent-render.d.ts +56 -0
- package/dist/core/sub-agent-render.js +579 -0
- package/dist/core/tool-cost.d.ts +17 -0
- package/dist/core/tool-cost.js +58 -0
- package/dist/core/tui.d.ts +9 -0
- package/dist/core/tui.js +33 -0
- package/dist/editor-BljciPdx.js +537 -0
- package/dist/extensions/bash.d.ts +12 -0
- package/dist/extensions/bash.js +261 -0
- package/dist/extensions/code-review.d.ts +14 -0
- package/dist/extensions/code-review.js +175 -0
- package/dist/extensions/command-palette.d.ts +6 -0
- package/dist/extensions/command-palette.js +2 -0
- package/dist/extensions/create-file.d.ts +7 -0
- package/dist/extensions/create-file.js +88 -0
- package/dist/extensions/edit-file.d.ts +7 -0
- package/dist/extensions/edit-file.js +395 -0
- package/dist/extensions/editor.d.ts +6 -0
- package/dist/extensions/editor.js +2 -0
- package/dist/extensions/finder.d.ts +13 -0
- package/dist/extensions/finder.js +108 -0
- package/dist/extensions/format-file.d.ts +11 -0
- package/dist/extensions/format-file.js +145 -0
- package/dist/extensions/github.d.ts +13 -0
- package/dist/extensions/github.js +527 -0
- package/dist/extensions/glob.d.ts +10 -0
- package/dist/extensions/glob.js +163 -0
- package/dist/extensions/grep.d.ts +13 -0
- package/dist/extensions/grep.js +356 -0
- package/dist/extensions/handoff.d.ts +6 -0
- package/dist/extensions/handoff.js +277 -0
- package/dist/extensions/librarian.d.ts +13 -0
- package/dist/extensions/librarian.js +113 -0
- package/dist/extensions/look-at.d.ts +13 -0
- package/dist/extensions/look-at.js +121 -0
- package/dist/extensions/ls.d.ts +8 -0
- package/dist/extensions/ls.js +87 -0
- package/dist/extensions/mermaid.d.ts +20 -0
- package/dist/extensions/mermaid.js +2 -0
- package/dist/extensions/oracle.d.ts +13 -0
- package/dist/extensions/oracle.js +143 -0
- package/dist/extensions/read-session.d.ts +13 -0
- package/dist/extensions/read-session.js +265 -0
- package/dist/extensions/read-web-page.d.ts +11 -0
- package/dist/extensions/read-web-page.js +234 -0
- package/dist/extensions/read.d.ts +23 -0
- package/dist/extensions/read.js +323 -0
- package/dist/extensions/search-sessions.d.ts +29 -0
- package/dist/extensions/search-sessions.js +426 -0
- package/dist/extensions/session-name.d.ts +6 -0
- package/dist/extensions/session-name.js +54 -0
- package/dist/extensions/skill.d.ts +7 -0
- package/dist/extensions/skill.js +232 -0
- package/dist/extensions/system-prompt.d.ts +6 -0
- package/dist/extensions/system-prompt.js +31 -0
- package/dist/extensions/task.d.ts +11 -0
- package/dist/extensions/task.js +114 -0
- package/dist/extensions/tool-harness.d.ts +6 -0
- package/dist/extensions/tool-harness.js +37 -0
- package/dist/extensions/undo-edit.d.ts +7 -0
- package/dist/extensions/undo-edit.js +127 -0
- package/dist/extensions/web-search.d.ts +12 -0
- package/dist/extensions/web-search.js +254 -0
- package/dist/extensions.d.ts +29 -0
- package/dist/extensions.js +29 -0
- package/dist/mermaid-C7xCVBrw.js +95296 -0
- package/package.json +67 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { UsageStats } from "./pi-spawn.js";
|
|
2
|
+
import { ToolCostDetails } from "./tool-cost.js";
|
|
3
|
+
import { Container } from "@mariozechner/pi-tui";
|
|
4
|
+
import { Message } from "@mariozechner/pi-ai";
|
|
5
|
+
|
|
6
|
+
//#region packages/core/sub-agent-render/index.d.ts
|
|
7
|
+
type DisplayItem = {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "toolCall";
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
args: Record<string, any>;
|
|
15
|
+
isError?: boolean;
|
|
16
|
+
};
|
|
17
|
+
interface SingleResult {
|
|
18
|
+
agent: string;
|
|
19
|
+
task: string;
|
|
20
|
+
exitCode: number;
|
|
21
|
+
messages: Message[];
|
|
22
|
+
usage: UsageStats;
|
|
23
|
+
model?: string;
|
|
24
|
+
stopReason?: string;
|
|
25
|
+
errorMessage?: string;
|
|
26
|
+
}
|
|
27
|
+
declare function getFinalOutput(messages: Message[]): string;
|
|
28
|
+
declare function getDisplayItems(messages: Message[]): DisplayItem[];
|
|
29
|
+
/**
|
|
30
|
+
* build the standard tool result for a piSpawn-based sub-agent.
|
|
31
|
+
* all sub-agent tools should use this instead of constructing
|
|
32
|
+
* return values manually — cost tagging is automatic.
|
|
33
|
+
*/
|
|
34
|
+
declare function subAgentResult(text: string, details: SingleResult, isError?: boolean): {
|
|
35
|
+
content: {
|
|
36
|
+
type: "text";
|
|
37
|
+
text: string;
|
|
38
|
+
}[];
|
|
39
|
+
details: SingleResult & ToolCostDetails;
|
|
40
|
+
isError?: boolean;
|
|
41
|
+
};
|
|
42
|
+
declare function formatUsageStats(usage: {
|
|
43
|
+
input: number;
|
|
44
|
+
output: number;
|
|
45
|
+
cacheRead: number;
|
|
46
|
+
cacheWrite: number;
|
|
47
|
+
cost: number;
|
|
48
|
+
contextTokens?: number;
|
|
49
|
+
turns?: number;
|
|
50
|
+
}, model?: string): string;
|
|
51
|
+
declare function renderAgentTree(r: SingleResult, container: Container, showExpanded: boolean, theme: any, labelOrOpts?: string | {
|
|
52
|
+
label?: string;
|
|
53
|
+
header?: "full" | "statusOnly";
|
|
54
|
+
}): void;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { DisplayItem, SingleResult, formatUsageStats, getDisplayItems, getFinalOutput, renderAgentTree, subAgentResult };
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { Markdown, Text, TruncatedText } from "@mariozechner/pi-tui";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
//#region packages/core/sub-agent-render/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* rendering utilities for sub-agent tool results.
|
|
7
|
+
*
|
|
8
|
+
* extracts DisplayItem, message parsing, and TUI tree rendering
|
|
9
|
+
* from the generic subagent extension. dedicated tools (finder,
|
|
10
|
+
* oracle, Task) use these for consistent renderResult display.
|
|
11
|
+
*
|
|
12
|
+
* reimplemented here because tools/ can't import from sub-agents/
|
|
13
|
+
* (separate nix store paths).
|
|
14
|
+
*/
|
|
15
|
+
function getFinalOutput(messages) {
|
|
16
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
17
|
+
const msg = messages[i];
|
|
18
|
+
if (msg && msg.role === "assistant") {
|
|
19
|
+
for (const part of msg.content) if (typeof part === "object" && part !== null && "type" in part && part.type === "text") return part.text;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
function getDisplayItems(messages) {
|
|
25
|
+
const errorMap = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const msg of messages) if (msg.role === "toolResult") errorMap.set(msg.toolCallId, msg.isError);
|
|
27
|
+
const items = [];
|
|
28
|
+
for (const msg of messages) if (msg.role === "assistant") {
|
|
29
|
+
for (const part of msg.content) if (part.type === "text") items.push({
|
|
30
|
+
type: "text",
|
|
31
|
+
text: part.text
|
|
32
|
+
});
|
|
33
|
+
else if (part.type === "toolCall") items.push({
|
|
34
|
+
type: "toolCall",
|
|
35
|
+
id: part.id,
|
|
36
|
+
name: part.name,
|
|
37
|
+
args: part.arguments,
|
|
38
|
+
isError: errorMap.get(part.id)
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return items;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* build the standard tool result for a piSpawn-based sub-agent.
|
|
45
|
+
* all sub-agent tools should use this instead of constructing
|
|
46
|
+
* return values manually — cost tagging is automatic.
|
|
47
|
+
*/
|
|
48
|
+
function subAgentResult(text, details, isError = false) {
|
|
49
|
+
return {
|
|
50
|
+
content: [{
|
|
51
|
+
type: "text",
|
|
52
|
+
text
|
|
53
|
+
}],
|
|
54
|
+
details: {
|
|
55
|
+
...details,
|
|
56
|
+
cost: details.usage.cost
|
|
57
|
+
},
|
|
58
|
+
...isError && { isError: true }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function formatTokens(count) {
|
|
62
|
+
if (count < 1e3) return count.toString();
|
|
63
|
+
if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
|
|
64
|
+
if (count < 1e6) return `${Math.round(count / 1e3)}k`;
|
|
65
|
+
return `${(count / 1e6).toFixed(1)}M`;
|
|
66
|
+
}
|
|
67
|
+
function formatUsageStats(usage, model) {
|
|
68
|
+
const parts = [];
|
|
69
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
70
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
71
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
72
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
73
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
74
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
75
|
+
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
76
|
+
if (model) parts.push(model);
|
|
77
|
+
return parts.join(" ");
|
|
78
|
+
}
|
|
79
|
+
function shortenPath(p) {
|
|
80
|
+
const home = os.homedir();
|
|
81
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
82
|
+
}
|
|
83
|
+
function toolLabel(name) {
|
|
84
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
85
|
+
}
|
|
86
|
+
function toolArgSummary(toolName, args) {
|
|
87
|
+
switch (toolName) {
|
|
88
|
+
case "bash": {
|
|
89
|
+
const command = args.cmd ?? args.command ?? "...";
|
|
90
|
+
return command.split("\n")[0] ?? command;
|
|
91
|
+
}
|
|
92
|
+
case "read": {
|
|
93
|
+
let text = shortenPath(args.file_path || args.path || "...");
|
|
94
|
+
const readRange = args.read_range;
|
|
95
|
+
const offset = args.offset;
|
|
96
|
+
const limit = args.limit;
|
|
97
|
+
if (readRange) text += `:${readRange[0]}-${readRange[1]}`;
|
|
98
|
+
else if (offset !== void 0 || limit !== void 0) {
|
|
99
|
+
const startLine = offset ?? 1;
|
|
100
|
+
const endLine = limit !== void 0 ? startLine + limit - 1 : "";
|
|
101
|
+
text += `:${startLine}${endLine ? `-${endLine}` : ""}`;
|
|
102
|
+
}
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
105
|
+
case "write": {
|
|
106
|
+
const rawPath = args.file_path || args.path || "...";
|
|
107
|
+
const lines = (args.content || "").split("\n").length;
|
|
108
|
+
let text = shortenPath(rawPath);
|
|
109
|
+
if (lines > 1) text += ` (${lines} lines)`;
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
112
|
+
case "edit": return shortenPath(args.file_path || args.path || "...");
|
|
113
|
+
case "ls": return shortenPath(args.path || ".");
|
|
114
|
+
case "find": return `${args.filePattern || args.pattern || "*"} in ${shortenPath(args.path || ".")}`;
|
|
115
|
+
case "grep": return `/${args.pattern || ""}/ in ${shortenPath(args.path || ".")}`;
|
|
116
|
+
default: return JSON.stringify(args);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function renderToolLine(item, fg) {
|
|
120
|
+
return `${item.isError === true ? fg("error", "✕") : item.isError === false ? fg("success", "✓") : fg("muted", "⋯")} ${fg("accent", toolLabel(item.name))} ${fg("dim", toolArgSummary(item.name, item.args))}`;
|
|
121
|
+
}
|
|
122
|
+
const COLLAPSED_ITEM_COUNT = 10;
|
|
123
|
+
function renderAgentTree(r, container, showExpanded, theme, labelOrOpts) {
|
|
124
|
+
const fg = theme.fg.bind(theme);
|
|
125
|
+
const opts = typeof labelOrOpts === "string" ? {
|
|
126
|
+
label: labelOrOpts,
|
|
127
|
+
header: "full"
|
|
128
|
+
} : {
|
|
129
|
+
label: labelOrOpts?.label,
|
|
130
|
+
header: labelOrOpts?.header ?? "full"
|
|
131
|
+
};
|
|
132
|
+
const MID = fg("muted", "├── ");
|
|
133
|
+
const END = fg("muted", "╰── ");
|
|
134
|
+
const CONT = fg("muted", "│ ");
|
|
135
|
+
const mdTheme = getMarkdownTheme();
|
|
136
|
+
const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
|
137
|
+
const icon = r.exitCode === -1 ? fg("warning", "⋯") : isError ? fg("error", "✕") : fg("success", "✓");
|
|
138
|
+
if (opts.header === "statusOnly") {
|
|
139
|
+
let header = icon;
|
|
140
|
+
if (isError && r.stopReason) header += ` ${fg("error", `[${r.stopReason}]`)}`;
|
|
141
|
+
container.addChild(new Text(header, 0, 0));
|
|
142
|
+
} else {
|
|
143
|
+
let header = `${icon} ${fg("toolTitle", theme.bold(opts.label ?? r.agent))}`;
|
|
144
|
+
if (isError && r.stopReason) header += ` ${fg("error", `[${r.stopReason}]`)}`;
|
|
145
|
+
container.addChild(new Text(header, 0, 0));
|
|
146
|
+
}
|
|
147
|
+
if (isError && r.errorMessage) container.addChild(new Text(MID + fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
148
|
+
const toolCalls = getDisplayItems(r.messages).filter((d) => d.type === "toolCall");
|
|
149
|
+
const finalOutput = getFinalOutput(r.messages);
|
|
150
|
+
const children = [];
|
|
151
|
+
if (showExpanded) children.push({
|
|
152
|
+
kind: "text",
|
|
153
|
+
content: r.task
|
|
154
|
+
});
|
|
155
|
+
const visibleTools = showExpanded ? toolCalls : toolCalls.slice(-COLLAPSED_ITEM_COUNT);
|
|
156
|
+
const skippedTools = showExpanded ? 0 : toolCalls.length - visibleTools.length;
|
|
157
|
+
if (skippedTools > 0) children.push({
|
|
158
|
+
kind: "text",
|
|
159
|
+
content: `... ${skippedTools} earlier calls`
|
|
160
|
+
});
|
|
161
|
+
for (const tc of visibleTools) children.push({
|
|
162
|
+
kind: "tool",
|
|
163
|
+
item: tc
|
|
164
|
+
});
|
|
165
|
+
if (finalOutput) children.push({
|
|
166
|
+
kind: "summary",
|
|
167
|
+
output: finalOutput.trim()
|
|
168
|
+
});
|
|
169
|
+
if (children.length === 0) container.addChild(new Text(END + fg("muted", "(no output)"), 0, 0));
|
|
170
|
+
else for (let i = 0; i < children.length; i++) {
|
|
171
|
+
const child = children[i];
|
|
172
|
+
const isLast = i === children.length - 1;
|
|
173
|
+
const connector = isLast ? END : MID;
|
|
174
|
+
if (child.kind === "text") container.addChild(new Text(connector + fg("dim", child.content), 0, 0));
|
|
175
|
+
else if (child.kind === "tool") container.addChild(new TruncatedText(connector + renderToolLine(child.item, fg), 0, 0));
|
|
176
|
+
else if (child.kind === "summary") {
|
|
177
|
+
container.addChild(new Text(connector + fg("muted", "Summary:"), 0, 0));
|
|
178
|
+
const indent = isLast ? " " : CONT;
|
|
179
|
+
container.addChild(new Text(indent, 0, 0));
|
|
180
|
+
container.addChild(new Markdown(child.output, 0, 0, mdTheme));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!showExpanded && toolCalls.length > COLLAPSED_ITEM_COUNT) container.addChild(new Text(fg("muted", "(Ctrl+O to expand)"), 0, 0));
|
|
184
|
+
const usageStr = formatUsageStats(r.usage, r.model);
|
|
185
|
+
if (usageStr) container.addChild(new Text(fg("dim", usageStr), 0, 0));
|
|
186
|
+
}
|
|
187
|
+
if (import.meta.vitest) {
|
|
188
|
+
const { describe, it, expect } = import.meta.vitest;
|
|
189
|
+
describe("formatUsageStats", () => {
|
|
190
|
+
it("formats all fields when present", () => {
|
|
191
|
+
const result = formatUsageStats({
|
|
192
|
+
input: 1500,
|
|
193
|
+
output: 500,
|
|
194
|
+
cacheRead: 2e3,
|
|
195
|
+
cacheWrite: 1e3,
|
|
196
|
+
cost: .0023,
|
|
197
|
+
contextTokens: 5e3,
|
|
198
|
+
turns: 2
|
|
199
|
+
}, "gpt-4");
|
|
200
|
+
expect(result).toContain("2 turns");
|
|
201
|
+
expect(result).toContain("↑1.5k");
|
|
202
|
+
expect(result).toContain("↓500");
|
|
203
|
+
expect(result).toContain("R2.0k");
|
|
204
|
+
expect(result).toContain("W1.0k");
|
|
205
|
+
expect(result).toContain("$0.0023");
|
|
206
|
+
expect(result).toContain("ctx:5.0k");
|
|
207
|
+
expect(result).toContain("gpt-4");
|
|
208
|
+
});
|
|
209
|
+
it("omits zero/undefined fields", () => {
|
|
210
|
+
const result = formatUsageStats({
|
|
211
|
+
input: 100,
|
|
212
|
+
output: 50,
|
|
213
|
+
cacheRead: 0,
|
|
214
|
+
cacheWrite: 0,
|
|
215
|
+
cost: 0
|
|
216
|
+
});
|
|
217
|
+
expect(result).toContain("↑100");
|
|
218
|
+
expect(result).toContain("↓50");
|
|
219
|
+
expect(result).not.toContain("turn");
|
|
220
|
+
expect(result).not.toContain("R");
|
|
221
|
+
expect(result).not.toContain("W");
|
|
222
|
+
expect(result).not.toContain("$");
|
|
223
|
+
expect(result).not.toContain("ctx");
|
|
224
|
+
});
|
|
225
|
+
it("formats large token counts", () => {
|
|
226
|
+
expect(formatUsageStats({
|
|
227
|
+
input: 15e5,
|
|
228
|
+
output: 0,
|
|
229
|
+
cacheRead: 0,
|
|
230
|
+
cacheWrite: 0,
|
|
231
|
+
cost: 0
|
|
232
|
+
})).toContain("↑1.5M");
|
|
233
|
+
expect(formatUsageStats({
|
|
234
|
+
input: 15e3,
|
|
235
|
+
output: 0,
|
|
236
|
+
cacheRead: 0,
|
|
237
|
+
cacheWrite: 0,
|
|
238
|
+
cost: 0
|
|
239
|
+
})).toContain("↑15k");
|
|
240
|
+
expect(formatUsageStats({
|
|
241
|
+
input: 1500,
|
|
242
|
+
output: 0,
|
|
243
|
+
cacheRead: 0,
|
|
244
|
+
cacheWrite: 0,
|
|
245
|
+
cost: 0
|
|
246
|
+
})).toContain("↑1.5k");
|
|
247
|
+
expect(formatUsageStats({
|
|
248
|
+
input: 500,
|
|
249
|
+
output: 0,
|
|
250
|
+
cacheRead: 0,
|
|
251
|
+
cacheWrite: 0,
|
|
252
|
+
cost: 0
|
|
253
|
+
})).toContain("↑500");
|
|
254
|
+
});
|
|
255
|
+
it("handles single turn", () => {
|
|
256
|
+
const result = formatUsageStats({
|
|
257
|
+
input: 100,
|
|
258
|
+
output: 50,
|
|
259
|
+
cacheRead: 0,
|
|
260
|
+
cacheWrite: 0,
|
|
261
|
+
cost: 0,
|
|
262
|
+
turns: 1
|
|
263
|
+
});
|
|
264
|
+
expect(result).toContain("1 turn");
|
|
265
|
+
expect(result).not.toContain("1 turns");
|
|
266
|
+
});
|
|
267
|
+
it("handles plural turns", () => {
|
|
268
|
+
expect(formatUsageStats({
|
|
269
|
+
input: 100,
|
|
270
|
+
output: 50,
|
|
271
|
+
cacheRead: 0,
|
|
272
|
+
cacheWrite: 0,
|
|
273
|
+
cost: 0,
|
|
274
|
+
turns: 3
|
|
275
|
+
})).toContain("3 turns");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
describe("getFinalOutput", () => {
|
|
279
|
+
it("returns text from last assistant message", () => {
|
|
280
|
+
expect(getFinalOutput([
|
|
281
|
+
{
|
|
282
|
+
role: "user",
|
|
283
|
+
content: [{
|
|
284
|
+
type: "text",
|
|
285
|
+
text: "hello"
|
|
286
|
+
}],
|
|
287
|
+
timestamp: 0
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
role: "assistant",
|
|
291
|
+
content: [{
|
|
292
|
+
type: "text",
|
|
293
|
+
text: "first response"
|
|
294
|
+
}],
|
|
295
|
+
api: "anthropic-messages",
|
|
296
|
+
provider: "anthropic",
|
|
297
|
+
model: "claude",
|
|
298
|
+
usage: {
|
|
299
|
+
totalTokens: 0,
|
|
300
|
+
cost: {
|
|
301
|
+
input: 0,
|
|
302
|
+
output: 0,
|
|
303
|
+
cacheRead: 0,
|
|
304
|
+
cacheWrite: 0,
|
|
305
|
+
total: 0
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
stopReason: "stop",
|
|
309
|
+
timestamp: 0
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
role: "user",
|
|
313
|
+
content: [{
|
|
314
|
+
type: "text",
|
|
315
|
+
text: "more"
|
|
316
|
+
}],
|
|
317
|
+
timestamp: 0
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
role: "assistant",
|
|
321
|
+
content: [{
|
|
322
|
+
type: "text",
|
|
323
|
+
text: "final response"
|
|
324
|
+
}],
|
|
325
|
+
api: "anthropic-messages",
|
|
326
|
+
provider: "anthropic",
|
|
327
|
+
model: "claude",
|
|
328
|
+
usage: {
|
|
329
|
+
totalTokens: 0,
|
|
330
|
+
cost: {
|
|
331
|
+
input: 0,
|
|
332
|
+
output: 0,
|
|
333
|
+
cacheRead: 0,
|
|
334
|
+
cacheWrite: 0,
|
|
335
|
+
total: 0
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
stopReason: "stop",
|
|
339
|
+
timestamp: 0
|
|
340
|
+
}
|
|
341
|
+
])).toBe("final response");
|
|
342
|
+
});
|
|
343
|
+
it("returns empty string when no assistant messages", () => {
|
|
344
|
+
expect(getFinalOutput([{
|
|
345
|
+
role: "user",
|
|
346
|
+
content: [{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: "hello"
|
|
349
|
+
}],
|
|
350
|
+
timestamp: 0
|
|
351
|
+
}])).toBe("");
|
|
352
|
+
});
|
|
353
|
+
it("skips tool calls, returns only text", () => {
|
|
354
|
+
expect(getFinalOutput([{
|
|
355
|
+
role: "assistant",
|
|
356
|
+
content: [{
|
|
357
|
+
type: "toolCall",
|
|
358
|
+
id: "tc1",
|
|
359
|
+
name: "bash",
|
|
360
|
+
arguments: { cmd: "ls" }
|
|
361
|
+
}, {
|
|
362
|
+
type: "text",
|
|
363
|
+
text: "here's the output"
|
|
364
|
+
}],
|
|
365
|
+
api: "anthropic-messages",
|
|
366
|
+
provider: "anthropic",
|
|
367
|
+
model: "claude",
|
|
368
|
+
usage: {
|
|
369
|
+
totalTokens: 0,
|
|
370
|
+
cost: {
|
|
371
|
+
input: 0,
|
|
372
|
+
output: 0,
|
|
373
|
+
cacheRead: 0,
|
|
374
|
+
cacheWrite: 0,
|
|
375
|
+
total: 0
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
stopReason: "stop",
|
|
379
|
+
timestamp: 0
|
|
380
|
+
}])).toBe("here's the output");
|
|
381
|
+
});
|
|
382
|
+
it("handles empty message array", () => {
|
|
383
|
+
expect(getFinalOutput([])).toBe("");
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
describe("getDisplayItems", () => {
|
|
387
|
+
it("extracts text and tool calls from messages", () => {
|
|
388
|
+
const items = getDisplayItems([
|
|
389
|
+
{
|
|
390
|
+
role: "user",
|
|
391
|
+
content: [{
|
|
392
|
+
type: "text",
|
|
393
|
+
text: "q"
|
|
394
|
+
}]
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
role: "assistant",
|
|
398
|
+
content: [{
|
|
399
|
+
type: "text",
|
|
400
|
+
text: "response"
|
|
401
|
+
}, {
|
|
402
|
+
type: "toolCall",
|
|
403
|
+
id: "tc1",
|
|
404
|
+
name: "read",
|
|
405
|
+
arguments: { path: "/file" }
|
|
406
|
+
}]
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
role: "toolResult",
|
|
410
|
+
toolCallId: "tc1",
|
|
411
|
+
content: [{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: "file content"
|
|
414
|
+
}]
|
|
415
|
+
}
|
|
416
|
+
]);
|
|
417
|
+
expect(items).toHaveLength(2);
|
|
418
|
+
expect(items[0]).toEqual({
|
|
419
|
+
type: "text",
|
|
420
|
+
text: "response"
|
|
421
|
+
});
|
|
422
|
+
expect(items[1]).toEqual({
|
|
423
|
+
type: "toolCall",
|
|
424
|
+
id: "tc1",
|
|
425
|
+
name: "read",
|
|
426
|
+
args: { path: "/file" },
|
|
427
|
+
isError: void 0
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
it("marks tool calls as error when toolResult has isError", () => {
|
|
431
|
+
const items = getDisplayItems([{
|
|
432
|
+
role: "assistant",
|
|
433
|
+
content: [{
|
|
434
|
+
type: "toolCall",
|
|
435
|
+
id: "tc1",
|
|
436
|
+
name: "bash",
|
|
437
|
+
arguments: { cmd: "false" }
|
|
438
|
+
}]
|
|
439
|
+
}, {
|
|
440
|
+
role: "toolResult",
|
|
441
|
+
toolCallId: "tc1",
|
|
442
|
+
content: [{
|
|
443
|
+
type: "text",
|
|
444
|
+
text: "error"
|
|
445
|
+
}],
|
|
446
|
+
isError: true
|
|
447
|
+
}]);
|
|
448
|
+
expect(items).toHaveLength(1);
|
|
449
|
+
expect(items[0]).toMatchObject({
|
|
450
|
+
type: "toolCall",
|
|
451
|
+
id: "tc1",
|
|
452
|
+
isError: true
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
it("marks tool calls as success when isError is false", () => {
|
|
456
|
+
expect(getDisplayItems([{
|
|
457
|
+
role: "assistant",
|
|
458
|
+
content: [{
|
|
459
|
+
type: "toolCall",
|
|
460
|
+
id: "tc1",
|
|
461
|
+
name: "bash",
|
|
462
|
+
arguments: { cmd: "true" }
|
|
463
|
+
}]
|
|
464
|
+
}, {
|
|
465
|
+
role: "toolResult",
|
|
466
|
+
toolCallId: "tc1",
|
|
467
|
+
content: [{
|
|
468
|
+
type: "text",
|
|
469
|
+
text: "done"
|
|
470
|
+
}],
|
|
471
|
+
isError: false
|
|
472
|
+
}])[0]).toMatchObject({
|
|
473
|
+
type: "toolCall",
|
|
474
|
+
id: "tc1",
|
|
475
|
+
isError: false
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
it("handles multiple assistant messages", () => {
|
|
479
|
+
const items = getDisplayItems([{
|
|
480
|
+
role: "assistant",
|
|
481
|
+
content: [{
|
|
482
|
+
type: "text",
|
|
483
|
+
text: "first"
|
|
484
|
+
}],
|
|
485
|
+
api: "anthropic-messages",
|
|
486
|
+
provider: "anthropic",
|
|
487
|
+
model: "claude",
|
|
488
|
+
usage: {
|
|
489
|
+
totalTokens: 0,
|
|
490
|
+
cost: {
|
|
491
|
+
input: 0,
|
|
492
|
+
output: 0,
|
|
493
|
+
cacheRead: 0,
|
|
494
|
+
cacheWrite: 0,
|
|
495
|
+
total: 0
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
stopReason: "stop",
|
|
499
|
+
timestamp: 0
|
|
500
|
+
}, {
|
|
501
|
+
role: "assistant",
|
|
502
|
+
content: [{
|
|
503
|
+
type: "text",
|
|
504
|
+
text: "second"
|
|
505
|
+
}],
|
|
506
|
+
api: "anthropic-messages",
|
|
507
|
+
provider: "anthropic",
|
|
508
|
+
model: "claude",
|
|
509
|
+
usage: {
|
|
510
|
+
totalTokens: 0,
|
|
511
|
+
cost: {
|
|
512
|
+
input: 0,
|
|
513
|
+
output: 0,
|
|
514
|
+
cacheRead: 0,
|
|
515
|
+
cacheWrite: 0,
|
|
516
|
+
total: 0
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
stopReason: "stop",
|
|
520
|
+
timestamp: 0
|
|
521
|
+
}]);
|
|
522
|
+
expect(items).toHaveLength(2);
|
|
523
|
+
expect(items[0]).toEqual({
|
|
524
|
+
type: "text",
|
|
525
|
+
text: "first"
|
|
526
|
+
});
|
|
527
|
+
expect(items[1]).toEqual({
|
|
528
|
+
type: "text",
|
|
529
|
+
text: "second"
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
describe("subAgentResult", () => {
|
|
534
|
+
it("builds result with cost from usage", () => {
|
|
535
|
+
const result = subAgentResult("found it", {
|
|
536
|
+
agent: "finder",
|
|
537
|
+
task: "search for x",
|
|
538
|
+
exitCode: 0,
|
|
539
|
+
messages: [],
|
|
540
|
+
usage: {
|
|
541
|
+
turns: 1,
|
|
542
|
+
input: 100,
|
|
543
|
+
output: 50,
|
|
544
|
+
cacheRead: 0,
|
|
545
|
+
cacheWrite: 0,
|
|
546
|
+
cost: .002,
|
|
547
|
+
contextTokens: 0
|
|
548
|
+
},
|
|
549
|
+
model: "gemini-flash"
|
|
550
|
+
});
|
|
551
|
+
expect(result.content).toEqual([{
|
|
552
|
+
type: "text",
|
|
553
|
+
text: "found it"
|
|
554
|
+
}]);
|
|
555
|
+
expect(result.details.cost).toBe(.002);
|
|
556
|
+
expect(result.details.model).toBe("gemini-flash");
|
|
557
|
+
expect(result.isError).toBeUndefined();
|
|
558
|
+
});
|
|
559
|
+
it("sets isError when passed true", () => {
|
|
560
|
+
expect(subAgentResult("failed", {
|
|
561
|
+
agent: "oracle",
|
|
562
|
+
task: "advise",
|
|
563
|
+
exitCode: 1,
|
|
564
|
+
messages: [],
|
|
565
|
+
usage: {
|
|
566
|
+
turns: 0,
|
|
567
|
+
input: 0,
|
|
568
|
+
output: 0,
|
|
569
|
+
cacheRead: 0,
|
|
570
|
+
cacheWrite: 0,
|
|
571
|
+
cost: 0,
|
|
572
|
+
contextTokens: 0
|
|
573
|
+
}
|
|
574
|
+
}, true).isError).toBe(true);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
//#endregion
|
|
579
|
+
export { formatUsageStats, getDisplayItems, getFinalOutput, renderAgentTree, subAgentResult };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region packages/core/tool-cost/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* shared cost contract for tool result details.
|
|
4
|
+
*
|
|
5
|
+
* any tool that incurs costs includes `cost: number` in its
|
|
6
|
+
* details object. editor/index.ts uses the type guard to narrow
|
|
7
|
+
* details at the session read boundary — no casting needed.
|
|
8
|
+
*
|
|
9
|
+
* sub-agent tools satisfy this via SingleResult & ToolCostDetails.
|
|
10
|
+
* direct-cost tools (web_search) return { cost } directly.
|
|
11
|
+
*/
|
|
12
|
+
interface ToolCostDetails {
|
|
13
|
+
cost: number;
|
|
14
|
+
}
|
|
15
|
+
declare function hasToolCost(v: unknown): v is ToolCostDetails;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { ToolCostDetails, hasToolCost };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//#region packages/core/tool-cost/index.ts
|
|
2
|
+
function hasToolCost(v) {
|
|
3
|
+
return v != null && typeof v === "object" && "cost" in v && typeof v.cost === "number";
|
|
4
|
+
}
|
|
5
|
+
if (import.meta.vitest) {
|
|
6
|
+
const { describe, it, expect } = import.meta.vitest;
|
|
7
|
+
describe("hasToolCost", () => {
|
|
8
|
+
it("returns true for valid ToolCostDetails", () => {
|
|
9
|
+
expect(hasToolCost({ cost: .0023 })).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
it("returns true for object with cost number", () => {
|
|
12
|
+
expect(hasToolCost({ cost: 0 })).toBe(true);
|
|
13
|
+
expect(hasToolCost({ cost: 1.5 })).toBe(true);
|
|
14
|
+
expect(hasToolCost({ cost: -.001 })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it("returns true for object with additional properties", () => {
|
|
17
|
+
expect(hasToolCost({
|
|
18
|
+
cost: .5,
|
|
19
|
+
turns: 3,
|
|
20
|
+
model: "gpt-4"
|
|
21
|
+
})).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it("returns false for null", () => {
|
|
24
|
+
expect(hasToolCost(null)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it("returns false for undefined", () => {
|
|
27
|
+
expect(hasToolCost(void 0)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
it("returns false for non-objects", () => {
|
|
30
|
+
expect(hasToolCost("string")).toBe(false);
|
|
31
|
+
expect(hasToolCost(123)).toBe(false);
|
|
32
|
+
expect(hasToolCost(true)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
it("returns false for object without cost", () => {
|
|
35
|
+
expect(hasToolCost({})).toBe(false);
|
|
36
|
+
expect(hasToolCost({ turns: 3 })).toBe(false);
|
|
37
|
+
expect(hasToolCost({ model: "gpt-4" })).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
it("returns false for object with non-number cost", () => {
|
|
40
|
+
expect(hasToolCost({ cost: "0.002" })).toBe(false);
|
|
41
|
+
expect(hasToolCost({ cost: null })).toBe(false);
|
|
42
|
+
expect(hasToolCost({ cost: void 0 })).toBe(false);
|
|
43
|
+
expect(hasToolCost({ cost: true })).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
it("narrowing works correctly", () => {
|
|
46
|
+
const unknown = {
|
|
47
|
+
cost: .5,
|
|
48
|
+
turns: 2
|
|
49
|
+
};
|
|
50
|
+
if (hasToolCost(unknown)) {
|
|
51
|
+
const cost = unknown.cost;
|
|
52
|
+
expect(cost).toBe(.5);
|
|
53
|
+
} else expect(true).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { hasToolCost };
|