import { readFileSync } from "node:fs"; export interface HerdrOutputMetadata { responseText?: string; finalEntryFound: boolean; toolUses: number; transcriptEntries: string[]; warnings: string[]; firstMalformedError?: unknown; } const KNOWN_HERDR_ENTRY_TYPES = new Set([ "assistant", "final", "message", "system", "toolResult", "user", ]); const TRANSCRIPT_ENTRY_TEXT_LIMIT = 500; function truncateText(text: string): string { if (text.length <= TRANSCRIPT_ENTRY_TEXT_LIMIT) { return text; } return `${text.slice(0, TRANSCRIPT_ENTRY_TEXT_LIMIT)}... [truncated]`; } function getTextPart(part: unknown): string { return part && typeof part === "object" && "text" in part ? String((part as { text: unknown }).text) : ""; } function getAssistantText(content: unknown): string | undefined { if (typeof content === "string") { return content.trim() || undefined; } if (!Array.isArray(content)) { return undefined; } return content.map(getTextPart).join("").trim() || undefined; } function stringifyValue(value: unknown): string | undefined { if (value == null) { return undefined; } if (typeof value === "string") { return value.trim() || undefined; } try { return JSON.stringify(value); } catch { return String(value); } } function formatTranscriptEntry( lineNumber: number, entry: { type?: string; message?: { role?: string; content?: unknown }; } & Record ): string { const message = entry.message && typeof entry.message === "object" ? (entry.message as Record) : undefined; const role = message?.role ? String(message.role) : undefined; const toolName = stringifyValue(message?.toolName ?? entry.toolName); const content = getAssistantText(message?.content ?? entry.content) ?? stringifyValue(message?.content ?? entry.content ?? entry.message) ?? ""; const label = [entry.type, toolName, role ? `(${role})` : undefined] .filter(Boolean) .join(" "); const suffix = content ? `: ${truncateText(content)}` : ""; return `[${lineNumber}] ${label}${suffix}`; } export function parseHerdrOutputFile(path: string): HerdrOutputMetadata { const fileContent = readFileSync(path, "utf-8"); const rawLines = fileContent.split("\n"); const hasTrailingPartialLine = fileContent.length > 0 && !fileContent.endsWith("\n"); const warnings: string[] = []; const transcriptEntries: string[] = []; let responseText: string | undefined; let finalEntryFound = false; let toolUses = 0; let firstMalformedError: unknown; for (let index = 0; index < rawLines.length; index++) { const text = rawLines[index]?.trim(); if (!text) { continue; } const isTrailingPartialLine = hasTrailingPartialLine && index === rawLines.length - 1; let entry: { type?: string; message?: { role?: string; content?: unknown }; }; try { entry = JSON.parse(text) as typeof entry; } catch (error) { if (isTrailingPartialLine) { continue; } firstMalformedError ??= error; warnings.push( `Skipping malformed Herdr JSONL entry ${index + 1} in ${path}.` ); continue; } if (!(entry.type && KNOWN_HERDR_ENTRY_TYPES.has(entry.type))) { warnings.push( `Skipping unknown Herdr JSONL entry ${index + 1} in ${path}.` ); continue; } transcriptEntries.push(formatTranscriptEntry(index + 1, entry)); if (entry.type === "toolResult") { toolUses++; } if (entry.type === "final") { finalEntryFound = true; responseText = getAssistantText(entry.message?.content) ?? ""; } } return { finalEntryFound, firstMalformedError, responseText, toolUses, transcriptEntries, warnings, }; }