offgrid-ai 0.8.14 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,274 @@
1
+ // ── Semantic stream renderer for Pi benchmark output ─────────────────────────
2
+
3
+ import { relative, basename } from "node:path";
4
+ import { pc } from "../ui.mjs";
5
+
6
+ export const BENCH_COLORS = {
7
+ thinking: pc.magenta,
8
+ text: pc.green,
9
+ tool: pc.yellow,
10
+ success: pc.green,
11
+ warning: pc.yellow,
12
+ toolOutput: pc.dim,
13
+ error: pc.red,
14
+ info: pc.cyan,
15
+ dim: pc.dim,
16
+ };
17
+
18
+ export function formatToolCall(toolCall) {
19
+ const path = toolCall.arguments?.path || toolCall.arguments?.file_path || toolCall.arguments?.filename || "";
20
+ const summary = path ? ` → ${path}` : "";
21
+ return `[toolCall] ${toolCall.name}${summary}`;
22
+ }
23
+
24
+ export function formatTokens(n) {
25
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
26
+ if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
27
+ return String(Math.round(n));
28
+ }
29
+
30
+ export function estimatedTokensFromBytes(bytes) {
31
+ return Math.max(1, Math.ceil(bytes / 4));
32
+ }
33
+
34
+ export function clearStatusLine() {
35
+ if (process.stdout.isTTY) {
36
+ process.stdout.write("\r\x1b[K");
37
+ }
38
+ }
39
+
40
+ export function printStatusLine(text) {
41
+ if (process.stdout.isTTY) {
42
+ process.stdout.write(`\r\x1b[K${text}`);
43
+ }
44
+ }
45
+
46
+ export function printFinalLine(text) {
47
+ clearStatusLine();
48
+ console.log(text);
49
+ }
50
+
51
+ export function renderStreamEvent(parsed, state, opts = {}) {
52
+ const verbose = Boolean(opts.verbose);
53
+ const type = parsed.type;
54
+
55
+ switch (type) {
56
+ case "session":
57
+ printFinalLine(BENCH_COLORS.info("Pi benchmark started"));
58
+ if (parsed.id) printFinalLine(BENCH_COLORS.dim(` Session ${parsed.id}`));
59
+ break;
60
+ case "agent_start":
61
+ break;
62
+ case "turn_start": {
63
+ state.turn += 1;
64
+ state.turnHadToolError = false;
65
+ resetStatus(state, "thinking");
66
+ printFinalLine("");
67
+ printFinalLine(BENCH_COLORS.info(`Turn ${state.turn}`));
68
+ break;
69
+ }
70
+ case "message_start": {
71
+ const msg = parsed.message;
72
+ if (!state.modelPrinted && msg?.role === "assistant" && msg.provider && msg.model) {
73
+ state.modelPrinted = true;
74
+ printFinalLine(BENCH_COLORS.dim(` Model ${msg.provider}/${msg.model}`));
75
+ }
76
+ break;
77
+ }
78
+ case "message_update": {
79
+ const evt = parsed.assistantMessageEvent;
80
+ if (!evt) return;
81
+ const subtype = String(evt.type ?? "").replace(/_/gu, "");
82
+ if (subtype === "thinkingstart") {
83
+ resetStatus(state, "thinking");
84
+ } else if (subtype === "thinkingdelta") {
85
+ if (verbose) process.stdout.write(BENCH_COLORS.thinking(evt.delta || ""));
86
+ updateStatusFromDelta(state, evt.delta, "thinking");
87
+ } else if (subtype === "textstart") {
88
+ resetStatus(state, "text");
89
+ } else if (subtype === "textdelta") {
90
+ if (verbose) process.stdout.write(BENCH_COLORS.text(evt.delta || ""));
91
+ updateStatusFromDelta(state, evt.delta, "text");
92
+ } else if (subtype === "toolcallstart") {
93
+ resetStatus(state, "tool");
94
+ } else if (subtype === "toolcalldelta") {
95
+ if (verbose) process.stdout.write(BENCH_COLORS.tool(evt.delta || ""));
96
+ updateStatusFromDelta(state, evt.delta, "tool");
97
+ }
98
+ break;
99
+ }
100
+ case "message_end":
101
+ break;
102
+ case "tool_execution_start": {
103
+ state.activeTool = {
104
+ name: parsed.toolName,
105
+ args: parsed.args ?? {},
106
+ outputText: "",
107
+ };
108
+ resetStatus(state, "exec", parsed.toolName);
109
+ printFinalLine(BENCH_COLORS.tool(formatToolStart(parsed.toolName, parsed.args ?? {}, state)));
110
+ break;
111
+ }
112
+ case "tool_execution_update": {
113
+ const text = toolResultText(parsed.partialResult ?? parsed.result ?? parsed);
114
+ if (text) {
115
+ if (verbose) process.stdout.write(BENCH_COLORS.toolOutput(text));
116
+ if (state.activeTool) state.activeTool.outputText = text;
117
+ updateStatusFromDelta(state, text, "exec");
118
+ }
119
+ break;
120
+ }
121
+ case "tool_execution_end": {
122
+ const lines = formatToolEnd(parsed, state);
123
+ if (parsed.isError) state.turnHadToolError = true;
124
+ for (const line of lines) printFinalLine(line);
125
+ state.activeTool = null;
126
+ resetStatus(state, "idle");
127
+ break;
128
+ }
129
+ case "toolResult": {
130
+ if (parsed.isError) state.turnHadToolError = true;
131
+ const status = parsed.isError ? BENCH_COLORS.error("✗") : BENCH_COLORS.success("✓");
132
+ printFinalLine(`${status} ${parsed.toolName ?? "tool"}`);
133
+ break;
134
+ }
135
+ case "turn_end": {
136
+ const usage = parsed.message?.usage;
137
+ const tokenPart = usage ? ` · ${formatTokens(usage.output ?? usage.totalTokens ?? 0)} tokens` : "";
138
+ const marker = state.turnHadToolError ? BENCH_COLORS.warning("⚠") : BENCH_COLORS.success("✓");
139
+ const suffix = state.turnHadToolError ? " · tool issue" : "";
140
+ printFinalLine(`${marker} turn ${state.turn}${tokenPart}${suffix}`);
141
+ break;
142
+ }
143
+ case "agent_end":
144
+ clearStatusLine();
145
+ printFinalLine(BENCH_COLORS.info("Pi benchmark finished"));
146
+ break;
147
+ default:
148
+ break;
149
+ }
150
+ }
151
+
152
+ export function resetStatus(state, mode, toolName = null) {
153
+ state.status.mode = mode;
154
+ state.status.toolName = toolName;
155
+ state.status.bytes = 0;
156
+ state.status.tokens = 0;
157
+ }
158
+
159
+ export function updateStatusFromDelta(state, delta, mode = state.status.mode) {
160
+ if (!delta) return;
161
+ state.status.mode = mode;
162
+ state.status.bytes += Buffer.byteLength(delta, "utf8");
163
+ state.status.tokens = estimatedTokensFromBytes(state.status.bytes);
164
+ const label = state.status.toolName ? ` · ${state.status.toolName}` : "";
165
+ const modeLabel = {
166
+ thinking: "thinking…",
167
+ text: "drafting response…",
168
+ tool: "preparing tool…",
169
+ exec: "running tool…",
170
+ }[state.status.mode] ?? "working…";
171
+ const bytes = formatBytes(state.status.bytes);
172
+ const tokens = formatTokens(state.status.tokens);
173
+ printStatusLine(BENCH_COLORS.dim(`Turn ${state.turn} ${modeLabel}${label} · ${bytes} (~${tokens} tokens)`));
174
+ }
175
+
176
+ export function formatToolStart(toolName, args, state) {
177
+ if (toolName === "read") return `→ read ${displayPath(args.path, state)}`;
178
+ if (toolName === "write") {
179
+ const size = args.content ? ` · ${formatBytes(Buffer.byteLength(String(args.content), "utf8"))}` : "";
180
+ return `→ write ${displayPath(args.path, state)}${size}`;
181
+ }
182
+ if (toolName === "edit") {
183
+ const count = Array.isArray(args.edits) ? args.edits.length : 0;
184
+ const suffix = count > 0 ? ` · ${count} replacement${count === 1 ? "" : "s"}` : "";
185
+ return `→ edit ${displayPath(args.path, state)}${suffix}`;
186
+ }
187
+ if (toolName === "bash") return `→ run ${truncateOneLine(args.command ?? "")}`;
188
+ return `→ ${toolName}${compactArgs(args)}`;
189
+ }
190
+
191
+ export function formatToolEnd(parsed, state) {
192
+ const toolName = parsed.toolName ?? state.activeTool?.name ?? "tool";
193
+ const args = parsed.args ?? state.activeTool?.args ?? {};
194
+ const text = toolResultText(parsed.result) || state.activeTool?.outputText || "";
195
+ const marker = parsed.isError ? BENCH_COLORS.error("✗") : BENCH_COLORS.success("✓");
196
+
197
+ if (parsed.isError) {
198
+ return [`${marker} ${toolName} failed · ${firstUsefulLine(text)}`];
199
+ }
200
+
201
+ if (toolName === "write") return [`${marker} wrote ${displayPath(args.path, state)}${parsedWriteSize(text)}`];
202
+ if (toolName === "read") return [`${marker} read ${displayPath(args.path, state)}${text ? ` · ${formatBytes(Buffer.byteLength(text, "utf8"))}` : ""}`];
203
+ if (toolName === "edit") return [`${marker} edited ${displayPath(args.path, state)}`];
204
+ if (toolName === "bash") return formatBashResult(marker, text);
205
+
206
+ const summary = firstUsefulLine(text);
207
+ return [`${marker} ${toolName}${summary ? ` · ${summary}` : ""}`];
208
+ }
209
+
210
+ export function formatBashResult(marker, text) {
211
+ const lines = meaningfulLines(text).slice(0, 2);
212
+ if (lines.length === 0) return [`${marker} command completed`];
213
+ return [`${marker} ${lines[0]}`, ...lines.slice(1).map((line) => BENCH_COLORS.dim(` ${line}`))];
214
+ }
215
+
216
+ export function parsedWriteSize(text) {
217
+ const match = String(text).match(/Successfully wrote\s+([0-9,]+)\s+bytes/iu);
218
+ if (!match) return "";
219
+ const bytes = Number(match[1].replace(/,/gu, ""));
220
+ return Number.isFinite(bytes) ? ` · ${formatBytes(bytes)}` : "";
221
+ }
222
+
223
+ export function toolResultText(result) {
224
+ const content = result?.content;
225
+ if (typeof content === "string") return content;
226
+ if (!Array.isArray(content)) return "";
227
+ return content
228
+ .map((item) => typeof item?.text === "string" ? item.text : "")
229
+ .filter(Boolean)
230
+ .join("\n");
231
+ }
232
+
233
+ export function firstUsefulLine(text) {
234
+ return meaningfulLines(text)[0] ?? "no details";
235
+ }
236
+
237
+ export function meaningfulLines(text) {
238
+ const lines = String(text ?? "")
239
+ .split(/\r?\n/u)
240
+ .map((line) => line.trim())
241
+ .filter(Boolean)
242
+ .filter((line) => !/^\^+$/u.test(line));
243
+ const errorLine = lines.find((line) => /(?:error|exception|failed|not found|command exited with code|validation failed)/iu.test(line));
244
+ if (errorLine) return [errorLine, ...lines.filter((line) => line !== errorLine)];
245
+ return lines;
246
+ }
247
+
248
+ export function displayPath(value, state) {
249
+ if (!value) return "unknown";
250
+ const path = String(value);
251
+ const rel = state.cwd ? relative(state.cwd, path) : path;
252
+ if (rel && !rel.startsWith("..") && rel !== ".") return rel;
253
+ return basename(path) || path;
254
+ }
255
+
256
+ export function compactArgs(args) {
257
+ const entries = Object.entries(args ?? {}).filter(([, value]) => value !== undefined && value !== null && value !== "");
258
+ if (entries.length === 0) return "";
259
+ return ` · ${truncateOneLine(entries.map(([key, value]) => `${key}=${String(value)}`).join(" "))}`;
260
+ }
261
+
262
+ export function truncateOneLine(value, max = Math.max(60, Math.min(process.stdout.columns ?? 100, 140) - 12)) {
263
+ const text = String(value ?? "").replace(/\s+/gu, " ").trim();
264
+ return text.length > max ? `${text.slice(0, Math.max(1, max - 1))}…` : text;
265
+ }
266
+
267
+ export function formatBytes(bytes) {
268
+ if (!Number.isFinite(bytes)) return "unknown";
269
+ const units = ["B", "KB", "MB", "GB", "TB"];
270
+ let size = bytes;
271
+ let unit = 0;
272
+ while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1; }
273
+ return `${size.toFixed(unit === 0 ? 0 : 2)} ${units[unit]}`;
274
+ }