import { formatTokenCount } from "../utils/usage-format.js"; import { formatRawAssistantErrorForUi } from "../agents/pi-embedded-helpers.js"; export function resolveFinalAssistantText(params: { finalText?: string | null; streamedText?: string | null; }) { const finalText = params.finalText ?? ""; if (finalText.trim()) return finalText; const streamedText = params.streamedText ?? ""; if (streamedText.trim()) return streamedText; return "(no output)"; } export function composeThinkingAndContent(params: { thinkingText?: string; contentText?: string; showThinking?: boolean; }) { const thinkingText = params.thinkingText?.trim() ?? ""; const contentText = params.contentText?.trim() ?? ""; const parts: string[] = []; if (params.showThinking && thinkingText) { parts.push(`[thinking]\n${thinkingText}`); } if (contentText) { parts.push(contentText); } return parts.join("\n\n").trim(); } /** * Extract ONLY thinking blocks from message content. * Model-agnostic: returns empty string if no thinking blocks exist. */ export function extractThinkingFromMessage(message: unknown): string { if (!message || typeof message !== "object") return ""; const record = message as Record; const content = record.content; if (typeof content === "string") return ""; if (!Array.isArray(content)) return ""; const parts: string[] = []; for (const block of content) { if (!block || typeof block !== "object") continue; const rec = block as Record; if (rec.type === "thinking" && typeof rec.thinking === "string") { parts.push(rec.thinking); } } return parts.join("\n").trim(); } /** * Extract ONLY text content blocks from message (excludes thinking). * Model-agnostic: works for any model with text content blocks. */ export function extractContentFromMessage(message: unknown): string { if (!message || typeof message !== "object") return ""; const record = message as Record; const content = record.content; if (typeof content === "string") return content.trim(); // Check for error BEFORE returning empty for non-array content if (!Array.isArray(content)) { const stopReason = typeof record.stopReason === "string" ? record.stopReason : ""; if (stopReason === "error") { const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : ""; return formatRawAssistantErrorForUi(errorMessage); } return ""; } const parts: string[] = []; for (const block of content) { if (!block || typeof block !== "object") continue; const rec = block as Record; if (rec.type === "text" && typeof rec.text === "string") { parts.push(rec.text); } } // If no text blocks found, check for error if (parts.length === 0) { const stopReason = typeof record.stopReason === "string" ? record.stopReason : ""; if (stopReason === "error") { const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : ""; return formatRawAssistantErrorForUi(errorMessage); } } return parts.join("\n").trim(); } function extractTextBlocks(content: unknown, opts?: { includeThinking?: boolean }): string { if (typeof content === "string") return content.trim(); if (!Array.isArray(content)) return ""; const thinkingParts: string[] = []; const textParts: string[] = []; for (const block of content) { if (!block || typeof block !== "object") continue; const record = block as Record; if (record.type === "text" && typeof record.text === "string") { textParts.push(record.text); } if ( opts?.includeThinking && record.type === "thinking" && typeof record.thinking === "string" ) { thinkingParts.push(record.thinking); } } return composeThinkingAndContent({ thinkingText: thinkingParts.join("\n").trim(), contentText: textParts.join("\n").trim(), showThinking: opts?.includeThinking ?? false, }); } export function extractTextFromMessage( message: unknown, opts?: { includeThinking?: boolean }, ): string { if (!message || typeof message !== "object") return ""; const record = message as Record; const text = extractTextBlocks(record.content, opts); if (text) return text; const stopReason = typeof record.stopReason === "string" ? record.stopReason : ""; if (stopReason !== "error") return ""; const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : ""; return formatRawAssistantErrorForUi(errorMessage); } export function formatTokens(total?: number | null, context?: number | null) { if (total == null && context == null) return "tokens ?"; const totalLabel = total == null ? "?" : formatTokenCount(total); if (context == null) return `tokens ${totalLabel}`; const pct = typeof total === "number" && context > 0 ? Math.min(999, Math.round((total / context) * 100)) : null; return `tokens ${totalLabel}/${formatTokenCount(context)}${pct !== null ? ` (${pct}%)` : ""}`; } export function formatContextUsageLine(params: { total?: number | null; context?: number | null; remaining?: number | null; percent?: number | null; }) { const totalLabel = typeof params.total === "number" ? formatTokenCount(params.total) : "?"; const ctxLabel = typeof params.context === "number" ? formatTokenCount(params.context) : "?"; const pct = typeof params.percent === "number" ? Math.min(999, Math.round(params.percent)) : null; const remainingLabel = typeof params.remaining === "number" ? `${formatTokenCount(params.remaining)} left` : null; const pctLabel = pct !== null ? `${pct}%` : null; const extra = [remainingLabel, pctLabel].filter(Boolean).join(", "); return `tokens ${totalLabel}/${ctxLabel}${extra ? ` (${extra})` : ""}`; } export function asString(value: unknown, fallback = ""): string { if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") { return String(value); } return fallback; }