Adding support for pi-local
This commit is contained in:
182
packages/adapters/pi-local/src/server/parse.ts
Normal file
182
packages/adapters/pi-local/src/server/parse.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { asNumber, asString, parseJson, parseObject } from "@paperclipai/adapter-utils/server-utils";
|
||||
|
||||
interface ParsedPiOutput {
|
||||
sessionId: string | null;
|
||||
messages: string[];
|
||||
errors: string[];
|
||||
usage: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cachedInputTokens: number;
|
||||
costUsd: number;
|
||||
};
|
||||
finalMessage: string | null;
|
||||
toolCalls: Array<{ toolName: string; args: unknown; result: string | null; isError: boolean }>;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function extractTextContent(content: string | Array<{ type: string; text?: string }>): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content
|
||||
.filter((c) => c.type === "text" && c.text)
|
||||
.map((c) => c.text!)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function parsePiJsonl(stdout: string): ParsedPiOutput {
|
||||
const result: ParsedPiOutput = {
|
||||
sessionId: null,
|
||||
messages: [],
|
||||
errors: [],
|
||||
usage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cachedInputTokens: 0,
|
||||
costUsd: 0,
|
||||
},
|
||||
finalMessage: null,
|
||||
toolCalls: [],
|
||||
};
|
||||
|
||||
let currentToolCall: { toolName: string; args: unknown } | null = null;
|
||||
|
||||
for (const rawLine of stdout.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
|
||||
const event = parseJson(line);
|
||||
if (!event) continue;
|
||||
|
||||
const eventType = asString(event.type, "");
|
||||
|
||||
// Agent lifecycle
|
||||
if (eventType === "agent_start") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === "agent_end") {
|
||||
const messages = event.messages as Array<Record<string, unknown>> | undefined;
|
||||
if (messages && messages.length > 0) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (lastMessage?.role === "assistant") {
|
||||
const content = lastMessage.content as string | Array<{ type: string; text?: string }>;
|
||||
result.finalMessage = extractTextContent(content);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Turn lifecycle
|
||||
if (eventType === "turn_start") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === "turn_end") {
|
||||
const message = asRecord(event.message);
|
||||
if (message) {
|
||||
const content = message.content as string | Array<{ type: string; text?: string }>;
|
||||
const text = extractTextContent(content);
|
||||
if (text) {
|
||||
result.finalMessage = text;
|
||||
result.messages.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
// Tool results are in toolResults array
|
||||
const toolResults = event.toolResults as Array<Record<string, unknown>> | undefined;
|
||||
if (toolResults) {
|
||||
for (const tr of toolResults) {
|
||||
const toolCallId = asString(tr.toolCallId, "");
|
||||
const content = tr.content;
|
||||
const isError = tr.isError === true;
|
||||
|
||||
// Find matching tool call
|
||||
const existingCall = result.toolCalls.find((tc) => tc.toolName === toolCallId);
|
||||
if (existingCall) {
|
||||
existingCall.result = typeof content === "string" ? content : JSON.stringify(content);
|
||||
existingCall.isError = isError;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Message updates (streaming)
|
||||
if (eventType === "message_update") {
|
||||
const assistantEvent = asRecord(event.assistantMessageEvent);
|
||||
if (assistantEvent) {
|
||||
const msgType = asString(assistantEvent.type, "");
|
||||
if (msgType === "text_delta") {
|
||||
const delta = asString(assistantEvent.delta, "");
|
||||
if (delta) {
|
||||
// Append to last message or create new
|
||||
if (result.messages.length === 0) {
|
||||
result.messages.push(delta);
|
||||
} else {
|
||||
result.messages[result.messages.length - 1] += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tool execution
|
||||
if (eventType === "tool_execution_start") {
|
||||
const toolName = asString(event.toolName, "");
|
||||
const args = event.args;
|
||||
currentToolCall = { toolName, args };
|
||||
result.toolCalls.push({
|
||||
toolName,
|
||||
args,
|
||||
result: null,
|
||||
isError: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === "tool_execution_end") {
|
||||
const toolCallId = asString(event.toolCallId, "");
|
||||
const toolName = asString(event.toolName, "");
|
||||
const toolResult = event.result;
|
||||
const isError = event.isError === true;
|
||||
|
||||
// Find the tool call
|
||||
const existingCall = result.toolCalls.find((tc) => tc.toolName === toolName);
|
||||
if (existingCall) {
|
||||
existingCall.result = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult);
|
||||
existingCall.isError = isError;
|
||||
}
|
||||
currentToolCall = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Usage tracking if available in the event
|
||||
if (eventType === "usage" || event.usage) {
|
||||
const usage = asRecord(event.usage);
|
||||
if (usage) {
|
||||
result.usage.inputTokens += asNumber(usage.inputTokens, 0);
|
||||
result.usage.outputTokens += asNumber(usage.outputTokens, 0);
|
||||
result.usage.cachedInputTokens += asNumber(usage.cachedInputTokens, 0);
|
||||
result.usage.costUsd += asNumber(usage.costUsd, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isPiUnknownSessionError(stdout: string, stderr: string): boolean {
|
||||
const haystack = `${stdout}\n${stderr}`
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
return /unknown\s+session|session\s+not\s+found|session\s+.*\s+not\s+found|no\s+session/i.test(haystack);
|
||||
}
|
||||
Reference in New Issue
Block a user