1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5import type { AgentEvent } from "@mariozechner/pi-agent-core";
6
7const MAX_OUTPUT_LINES = 20;
8
9export interface OutputOptions {
10 verbose: boolean;
11}
12
13export function formatToolOutput(output: string): string {
14 const lines = output.split(/\r?\n/);
15 if (lines.length <= MAX_OUTPUT_LINES) {
16 return output;
17 }
18
19 const truncated = lines.slice(0, MAX_OUTPUT_LINES).join("\n");
20 return `${truncated}\n(+${lines.length - MAX_OUTPUT_LINES} more lines)`;
21}
22
23export function createEventLogger(options: OutputOptions) {
24 return (event: AgentEvent) => {
25 if (!options.verbose) return;
26
27 if (event.type === "tool_execution_start") {
28 console.error(`\n[tool] ${event.toolName}`);
29 console.error(JSON.stringify(event.args, null, 2));
30 }
31
32 if (event.type === "tool_execution_end") {
33 if (event.isError) {
34 console.error("[tool error]");
35 console.error(String(event.result));
36 return;
37 }
38
39 const text = (event.result?.content ?? [])
40 .filter((content: { type: string }) => content.type === "text")
41 .map((content: { text?: string }) => content.text ?? "")
42 .join("");
43
44 if (text) {
45 console.error(formatToolOutput(text));
46 }
47 }
48 };
49}
50
51export function printUsageSummary(
52 usage: { cost?: { total?: number }; totalTokens?: number; output?: number; input?: number } | undefined,
53 requestCount?: number,
54) {
55 if (!usage) return;
56
57 const tokens = usage.totalTokens ?? (usage.output ?? 0) + (usage.input ?? 0);
58 const rawCost = usage.cost?.total;
59 const cost = typeof rawCost === "number" && !isNaN(rawCost) && rawCost > 0 ? rawCost : undefined;
60
61 let line = `\nusage: ${tokens} tokens`;
62 if (requestCount !== undefined && requestCount > 0) {
63 line += ` across ${requestCount} ${requestCount === 1 ? "request" : "requests"}`;
64 }
65 if (cost !== undefined) {
66 line += `, cost $${cost.toFixed(4)}`;
67 }
68 console.error(line);
69}