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 | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; return value as Record; } 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> | 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> | 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); }