Files
paperclip/packages/adapters/pi-local/src/server/parse.test.ts
Richard Anaya a6b5f12daf feat(pi-local): fix bugs, add RPC mode, improve cost tracking and output handling
Major improvements to the Pi local adapter:

Bug Fixes (Greptile-identified):
- Fix string interpolation in models.ts error message (was showing literal ${detail})
- Fix tool matching in parse.ts to use toolCallId instead of toolName for correct
  multi-call handling and result assignment
- Fix dead code in execute.ts by tracking instructionsReadFailed flag

Feature Improvements:
- Switch from print mode (-p) to RPC mode (--mode rpc) to prevent agent from
  exiting prematurely and ensure proper lifecycle completion
- Add stdin command sending via JSON-RPC format for prompt delivery
- Add line buffering in execute.ts to handle partial JSON chunks correctly
- Filter RPC protocol messages (response, extension_ui_request, etc.) from transcript

Cost Tracking:
- Extract cost and usage data from turn_end assistant messages
- Support both Pi format (input/output/cacheRead/cost.total) and generic format
- Add tests for cost extraction and accumulation across multiple turns

All tests pass (12/12), typecheck clean, server builds successfully.
2026-03-07 07:23:44 -08:00

223 lines
6.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { parsePiJsonl, isPiUnknownSessionError } from "./parse.js";
describe("parsePiJsonl", () => {
it("parses agent lifecycle and messages", () => {
const stdout = [
JSON.stringify({ type: "agent_start" }),
JSON.stringify({
type: "turn_end",
message: {
role: "assistant",
content: [{ type: "text", text: "Hello from Pi" }],
},
}),
JSON.stringify({ type: "agent_end", messages: [] }),
].join("\n");
const parsed = parsePiJsonl(stdout);
expect(parsed.messages).toContain("Hello from Pi");
expect(parsed.finalMessage).toBe("Hello from Pi");
});
it("parses streaming text deltas", () => {
const stdout = [
JSON.stringify({
type: "message_update",
assistantMessageEvent: { type: "text_delta", delta: "Hello " },
}),
JSON.stringify({
type: "message_update",
assistantMessageEvent: { type: "text_delta", delta: "World" },
}),
JSON.stringify({
type: "turn_end",
message: {
role: "assistant",
content: "Hello World",
},
}),
].join("\n");
const parsed = parsePiJsonl(stdout);
expect(parsed.messages).toContain("Hello World");
});
it("parses tool execution", () => {
const stdout = [
JSON.stringify({
type: "tool_execution_start",
toolCallId: "tool_1",
toolName: "read",
args: { path: "/tmp/test.txt" },
}),
JSON.stringify({
type: "tool_execution_end",
toolCallId: "tool_1",
toolName: "read",
result: "file contents",
isError: false,
}),
JSON.stringify({
type: "turn_end",
message: { role: "assistant", content: "Done" },
toolResults: [
{
toolCallId: "tool_1",
content: "file contents",
isError: false,
},
],
}),
].join("\n");
const parsed = parsePiJsonl(stdout);
expect(parsed.toolCalls).toHaveLength(1);
expect(parsed.toolCalls[0].toolName).toBe("read");
expect(parsed.toolCalls[0].result).toBe("file contents");
expect(parsed.toolCalls[0].isError).toBe(false);
});
it("handles errors in tool execution", () => {
const stdout = [
JSON.stringify({
type: "tool_execution_start",
toolCallId: "tool_1",
toolName: "read",
args: { path: "/missing.txt" },
}),
JSON.stringify({
type: "tool_execution_end",
toolCallId: "tool_1",
toolName: "read",
result: "File not found",
isError: true,
}),
].join("\n");
const parsed = parsePiJsonl(stdout);
expect(parsed.toolCalls).toHaveLength(1);
expect(parsed.toolCalls[0].isError).toBe(true);
expect(parsed.toolCalls[0].result).toBe("File not found");
});
it("extracts usage and cost from turn_end events", () => {
const stdout = [
JSON.stringify({
type: "turn_end",
message: {
role: "assistant",
content: "Response with usage",
usage: {
input: 100,
output: 50,
cacheRead: 20,
totalTokens: 170,
cost: {
input: 0.001,
output: 0.0015,
cacheRead: 0.0001,
cacheWrite: 0,
total: 0.0026,
},
},
},
toolResults: [],
}),
].join("\n");
const parsed = parsePiJsonl(stdout);
expect(parsed.usage.inputTokens).toBe(100);
expect(parsed.usage.outputTokens).toBe(50);
expect(parsed.usage.cachedInputTokens).toBe(20);
expect(parsed.usage.costUsd).toBeCloseTo(0.0026, 4);
});
it("accumulates usage from multiple turns", () => {
const stdout = [
JSON.stringify({
type: "turn_end",
message: {
role: "assistant",
content: "First response",
usage: {
input: 50,
output: 25,
cacheRead: 0,
cost: { total: 0.001 },
},
},
}),
JSON.stringify({
type: "turn_end",
message: {
role: "assistant",
content: "Second response",
usage: {
input: 30,
output: 20,
cacheRead: 10,
cost: { total: 0.0015 },
},
},
}),
].join("\n");
const parsed = parsePiJsonl(stdout);
expect(parsed.usage.inputTokens).toBe(80);
expect(parsed.usage.outputTokens).toBe(45);
expect(parsed.usage.cachedInputTokens).toBe(10);
expect(parsed.usage.costUsd).toBeCloseTo(0.0025, 4);
});
it("handles standalone usage events with Pi format", () => {
const stdout = [
JSON.stringify({
type: "usage",
usage: {
input: 200,
output: 100,
cacheRead: 50,
cost: { total: 0.005 },
},
}),
].join("\n");
const parsed = parsePiJsonl(stdout);
expect(parsed.usage.inputTokens).toBe(200);
expect(parsed.usage.outputTokens).toBe(100);
expect(parsed.usage.cachedInputTokens).toBe(50);
expect(parsed.usage.costUsd).toBe(0.005);
});
it("handles standalone usage events with generic format", () => {
const stdout = [
JSON.stringify({
type: "usage",
usage: {
inputTokens: 150,
outputTokens: 75,
cachedInputTokens: 25,
costUsd: 0.003,
},
}),
].join("\n");
const parsed = parsePiJsonl(stdout);
expect(parsed.usage.inputTokens).toBe(150);
expect(parsed.usage.outputTokens).toBe(75);
expect(parsed.usage.cachedInputTokens).toBe(25);
expect(parsed.usage.costUsd).toBe(0.003);
});
});
describe("isPiUnknownSessionError", () => {
it("detects unknown session errors", () => {
expect(isPiUnknownSessionError("session not found: s_123", "")).toBe(true);
expect(isPiUnknownSessionError("", "unknown session id")).toBe(true);
expect(isPiUnknownSessionError("", "no session available")).toBe(true);
expect(isPiUnknownSessionError("all good", "")).toBe(false);
expect(isPiUnknownSessionError("working fine", "no errors")).toBe(false);
});
});