import { asNumber, asString, parseJson, parseObject } from "@paperclipai/adapter-utils/server-utils"; function collectMessageText(message: unknown): string[] { if (typeof message === "string") { const trimmed = message.trim(); return trimmed ? [trimmed] : []; } const record = parseObject(message); const direct = asString(record.text, "").trim(); const lines: string[] = direct ? [direct] : []; const content = Array.isArray(record.content) ? record.content : []; for (const partRaw of content) { const part = parseObject(partRaw); const type = asString(part.type, "").trim(); if (type === "output_text" || type === "text" || type === "content") { const text = asString(part.text, "").trim() || asString(part.content, "").trim(); if (text) lines.push(text); } } return lines; } function readSessionId(event: Record): string | null { return ( asString(event.session_id, "").trim() || asString(event.sessionId, "").trim() || asString(event.sessionID, "").trim() || asString(event.checkpoint_id, "").trim() || asString(event.thread_id, "").trim() || null ); } function asErrorText(value: unknown): string { if (typeof value === "string") return value; const rec = parseObject(value); const message = asString(rec.message, "") || asString(rec.error, "") || asString(rec.code, "") || asString(rec.detail, ""); if (message) return message; try { return JSON.stringify(rec); } catch { return ""; } } function accumulateUsage( target: { inputTokens: number; cachedInputTokens: number; outputTokens: number }, usageRaw: unknown, ) { const usage = parseObject(usageRaw); const usageMetadata = parseObject(usage.usageMetadata); const source = Object.keys(usageMetadata).length > 0 ? usageMetadata : usage; target.inputTokens += asNumber( source.input_tokens, asNumber(source.inputTokens, asNumber(source.promptTokenCount, 0)), ); target.cachedInputTokens += asNumber( source.cached_input_tokens, asNumber(source.cachedInputTokens, asNumber(source.cachedContentTokenCount, 0)), ); target.outputTokens += asNumber( source.output_tokens, asNumber(source.outputTokens, asNumber(source.candidatesTokenCount, 0)), ); } export function parseGeminiJsonl(stdout: string) { let sessionId: string | null = null; const messages: string[] = []; let errorMessage: string | null = null; let costUsd: number | null = null; let resultEvent: Record | null = null; let question: { prompt: string; choices: Array<{ key: string; label: string; description?: string }> } | null = null; const usage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, }; for (const rawLine of stdout.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; const event = parseJson(line); if (!event) continue; const foundSessionId = readSessionId(event); if (foundSessionId) sessionId = foundSessionId; const type = asString(event.type, "").trim(); if (type === "assistant") { messages.push(...collectMessageText(event.message)); const messageObj = parseObject(event.message); const content = Array.isArray(messageObj.content) ? messageObj.content : []; for (const partRaw of content) { const part = parseObject(partRaw); if (asString(part.type, "").trim() === "question") { question = { prompt: asString(part.prompt, "").trim(), choices: (Array.isArray(part.choices) ? part.choices : []).map((choiceRaw) => { const choice = parseObject(choiceRaw); return { key: asString(choice.key, "").trim(), label: asString(choice.label, "").trim(), description: asString(choice.description, "").trim() || undefined, }; }), }; break; // only one question per message } } continue; } if (type === "result") { resultEvent = event; accumulateUsage(usage, event.usage ?? event.usageMetadata); const resultText = asString(event.result, "").trim() || asString(event.text, "").trim() || asString(event.response, "").trim(); if (resultText && messages.length === 0) messages.push(resultText); costUsd = asNumber(event.total_cost_usd, asNumber(event.cost_usd, asNumber(event.cost, costUsd ?? 0))) || costUsd; const isError = event.is_error === true || asString(event.subtype, "").toLowerCase() === "error"; if (isError) { const text = asErrorText(event.error ?? event.message ?? event.result).trim(); if (text) errorMessage = text; } continue; } if (type === "error") { const text = asErrorText(event.error ?? event.message ?? event.detail).trim(); if (text) errorMessage = text; continue; } if (type === "system") { const subtype = asString(event.subtype, "").trim().toLowerCase(); if (subtype === "error") { const text = asErrorText(event.error ?? event.message ?? event.detail).trim(); if (text) errorMessage = text; } continue; } if (type === "text") { const part = parseObject(event.part); const text = asString(part.text, "").trim(); if (text) messages.push(text); continue; } if (type === "step_finish" || event.usage || event.usageMetadata) { accumulateUsage(usage, event.usage ?? event.usageMetadata); costUsd = asNumber(event.total_cost_usd, asNumber(event.cost_usd, asNumber(event.cost, costUsd ?? 0))) || costUsd; continue; } } return { sessionId, summary: messages.join("\n\n").trim(), usage, costUsd, errorMessage, resultEvent, question, }; } export function isGeminiUnknownSessionError(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+.*\s+not\s+found|resume\s+.*\s+not\s+found|checkpoint\s+.*\s+not\s+found|cannot\s+resume|failed\s+to\s+resume/i.test( haystack, ); } function extractGeminiErrorMessages(parsed: Record): string[] { const messages: string[] = []; const errorMsg = asString(parsed.error, "").trim(); if (errorMsg) messages.push(errorMsg); const raw = Array.isArray(parsed.errors) ? parsed.errors : []; for (const entry of raw) { if (typeof entry === "string") { const msg = entry.trim(); if (msg) messages.push(msg); continue; } if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue; const obj = entry as Record; const msg = asString(obj.message, "") || asString(obj.error, "") || asString(obj.code, ""); if (msg) { messages.push(msg); continue; } try { messages.push(JSON.stringify(obj)); } catch { // skip non-serializable entry } } return messages; } export function describeGeminiFailure(parsed: Record): string | null { const status = asString(parsed.status, ""); const errors = extractGeminiErrorMessages(parsed); const detail = errors[0] ?? ""; const parts = ["Gemini run failed"]; if (status) parts.push(`status=${status}`); if (detail) parts.push(detail); return parts.length > 1 ? parts.join(": ") : null; } const GEMINI_AUTH_REQUIRED_RE = /(?:not\s+authenticated|please\s+authenticate|api[_ ]?key\s+(?:required|missing|invalid)|authentication\s+required|unauthorized|invalid\s+credentials|not\s+logged\s+in|login\s+required|run\s+`?gemini\s+auth(?:\s+login)?`?\s+first)/i; export function detectGeminiAuthRequired(input: { parsed: Record | null; stdout: string; stderr: string; }): { requiresAuth: boolean } { const errors = extractGeminiErrorMessages(input.parsed ?? {}); const messages = [...errors, input.stdout, input.stderr] .join("\n") .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); const requiresAuth = messages.some((line) => GEMINI_AUTH_REQUIRED_RE.test(line)); return { requiresAuth }; } export function isGeminiTurnLimitResult( parsed: Record | null | undefined, exitCode?: number | null, ): boolean { if (exitCode === 53) return true; if (!parsed) return false; const status = asString(parsed.status, "").trim().toLowerCase(); if (status === "turn_limit" || status === "max_turns") return true; const error = asString(parsed.error, "").trim(); return /turn\s*limit|max(?:imum)?\s+turns?/i.test(error); }