import type { AttachedFileMeta, ContentBlock, RawMessage, RuntimeToolEvidence, ToolStatus, } from '@/types/chat'; const EMPTY_ASSISTANT_RESPONSE_MESSAGE = '模型没有返回任何内容。请重试,或切换到支持图片输入的模型。'; function toMs(value: unknown): number | undefined { if (typeof value === 'number' && Number.isFinite(value)) { return value < 1e12 ? value * 1000 : value; } if (typeof value === 'string' && value.trim()) { const numeric = Number(value); if (Number.isFinite(numeric)) { return numeric < 1e12 ? numeric * 1000 : numeric; } const date = new Date(value); if (!Number.isNaN(date.getTime())) { return date.getTime(); } } return undefined; } function getRecord(input: unknown): Record | null { return input && typeof input === 'object' && !Array.isArray(input) ? input as Record : null; } function getErrorMessage(input: unknown): string | undefined { if (typeof input === 'string') { const trimmed = input.trim(); return trimmed || undefined; } const record = getRecord(input); if (!record) return undefined; const data = getRecord(record.data); const nestedError = getRecord(record.error); const metadata = getRecord(record.metadata); const candidates = [ data?.message, data?.error, nestedError?.message, record.message, record.errorMessage, record.error_message, record.detail, metadata?.message, ]; for (const candidate of candidates) { const message = getErrorMessage(candidate); if (message) return message; } if (typeof record.data === 'string') { const rawData = record.data.trim(); if (!rawData) return undefined; try { const parsed = JSON.parse(rawData) as unknown; return getErrorMessage(parsed) ?? rawData; } catch { return rawData; } } return undefined; } function normalizeRole(input: unknown): RawMessage['role'] { switch (input) { case 'user': case 'assistant': case 'system': case 'toolresult': return input; default: return 'assistant'; } } function basenameFromPath(path: string): string { const parts = path.split(/[\\/]/).filter(Boolean); return parts.at(-1) ?? path; } function normalizeTimestamp(record: Record): number | undefined { const time = getRecord(record.time); return toMs(time?.created) ?? toMs(record.createdAt) ?? toMs(record.updatedAt) ?? toMs(record.timestamp); } function hasValue(value: unknown): boolean { if (value === true) return true; if (typeof value === 'number' && Number.isFinite(value)) return true; if (typeof value === 'string' && value.trim()) return true; return false; } function hasCompletedAssistantSignal( info: Record, input: Record, parts: unknown[], ): boolean { const time = getRecord(info.time); if ( hasValue(time?.completed) || hasValue(info.completed) || hasValue(info.completedAt) || hasValue(input.completed) || hasValue(input.completedAt) ) { return true; } return parts.some((part) => getRecord(part)?.type === 'step-finish'); } function normalizeToolStatus(input: unknown): ToolStatus['status'] { switch (input) { case 'completed': return 'completed'; case 'error': case 'errored': return 'error'; default: return 'running'; } } function sanitizeToolEvidenceText(value: unknown, limit = 600): string | undefined { const redact = (text: string) => text .replace(/(authorization|bearer|token|secret|password|api[_-]?key)(\s*[:=]\s*|\s+)[^\s,;]+/gi, '$1=[REDACTED]') .replace(/\s+/g, ' ') .trim(); const visit = (candidate: unknown, depth = 0): string => { if (depth > 3 || candidate === null || candidate === undefined) return ''; if (typeof candidate === 'string') return candidate; if (typeof candidate === 'number' || typeof candidate === 'boolean') return String(candidate); if (Array.isArray(candidate)) { return candidate.map((item) => visit(item, depth + 1)).filter(Boolean).join(' '); } const record = getRecord(candidate); if (!record) return ''; return Object.entries(record) .filter(([key]) => !/(authorization|bearer|token|secret|password|api[_-]?key|cookie)/i.test(key)) .map(([key, item]) => `${key}: ${visit(item, depth + 1)}`) .filter(Boolean) .join('; '); }; const text = redact(visit(value)); return text ? text.slice(0, limit) : undefined; } function getToolExitCode(state: Record | null): number | undefined { const metadata = getRecord(state?.metadata); const candidates = [ state?.exitCode, state?.exit_code, state?.exit, metadata?.exitCode, metadata?.exit_code, metadata?.exit, ]; for (const candidate of candidates) { const numeric = typeof candidate === 'number' ? candidate : typeof candidate === 'string' && candidate.trim() ? Number(candidate) : Number.NaN; if (Number.isInteger(numeric)) return numeric; } return undefined; } function normalizeToolEvidence(part: Record): RuntimeToolEvidence | null { if (part.type !== 'tool' || typeof part.tool !== 'string' || !part.tool.trim()) return null; const state = getRecord(part.state); const time = getRecord(state?.time); const startedAt = toMs(time?.start); const endedAt = toMs(time?.end); return { id: typeof part.id === 'string' ? part.id : undefined, toolCallId: typeof part.callID === 'string' ? part.callID : undefined, name: part.tool, status: normalizeToolStatus(state?.status), inputSummary: sanitizeToolEvidenceText(state?.input), outputExcerpt: sanitizeToolEvidenceText(state?.output ?? state?.error), exitCode: getToolExitCode(state), startedAt, endedAt, }; } function mergeToolEvidence( current: RuntimeToolEvidence[] | undefined, next: RuntimeToolEvidence | null, ): RuntimeToolEvidence[] | undefined { if (!next) return current; const combined = [...(current ?? [])]; const key = next.toolCallId ?? next.id ?? `${next.name}:${next.startedAt ?? ''}`; const index = combined.findIndex((item) => ( (item.toolCallId ?? item.id ?? `${item.name}:${item.startedAt ?? ''}`) === key )); if (index >= 0) { combined[index] = { ...combined[index], ...next }; } else { combined.push(next); } return combined; } function normalizeLegacyBlocks(content: unknown): ContentBlock[] { if (typeof content === 'string') { return [{ type: 'text', text: content }]; } if (!Array.isArray(content)) return []; const blocks: ContentBlock[] = []; for (const block of content) { const record = getRecord(block); if (!record) continue; const type = record.type; if (type === 'text' && typeof record.text === 'string') { blocks.push({ type: 'text', text: record.text, id: typeof record.id === 'string' ? record.id : undefined }); continue; } if (type === 'thinking' && typeof record.thinking === 'string') { blocks.push({ type: 'thinking', thinking: record.thinking, id: typeof record.id === 'string' ? record.id : undefined }); continue; } if (type === 'reasoning') { const reasoning = typeof record.text === 'string' ? record.text : typeof record.reasoning === 'string' ? record.reasoning : typeof record.reasoning_content === 'string' ? record.reasoning_content : typeof record.reasoning_text === 'string' ? record.reasoning_text : undefined; if (reasoning) { blocks.push({ type: 'thinking', thinking: reasoning, id: typeof record.id === 'string' ? record.id : undefined }); } continue; } if ((type === 'tool_use' || type === 'toolCall') && typeof record.name === 'string') { blocks.push({ type: 'tool_use', id: typeof record.id === 'string' ? record.id : undefined, name: record.name, input: record.input ?? record.arguments ?? {}, }); continue; } if ((type === 'image' || type === 'file') && typeof record.url === 'string' && typeof record.mimeType === 'string') { blocks.push({ type: 'image', url: record.url, mimeType: record.mimeType, alt: typeof record.alt === 'string' ? record.alt : undefined, id: typeof record.id === 'string' ? record.id : undefined, }); } } return blocks; } function normalizeLegacyMessage(input: Record): RawMessage { const contentBlocks = normalizeLegacyBlocks(input.content); const topLevelReasoning = typeof input.reasoning === 'string' ? input.reasoning : typeof input.reasoning_content === 'string' ? input.reasoning_content : typeof input.reasoning_text === 'string' ? input.reasoning_text : undefined; if (topLevelReasoning?.trim()) { contentBlocks.unshift({ type: 'thinking', thinking: topLevelReasoning }); } if (contentBlocks.length > 0) { return { id: typeof input.id === 'string' ? input.id : undefined, role: normalizeRole(input.role), timestamp: normalizeTimestamp(input), content: contentBlocks, }; } if (typeof input.content === 'string') { return { id: typeof input.id === 'string' ? input.id : undefined, role: normalizeRole(input.role), timestamp: normalizeTimestamp(input), content: [{ type: 'text', text: input.content }], }; } const fallbackText = typeof input.text === 'string' ? input.text : typeof input.message === 'string' ? input.message : JSON.stringify(input); return { id: typeof input.id === 'string' ? input.id : undefined, role: normalizeRole(input.role), timestamp: normalizeTimestamp(input), content: [{ type: 'text', text: fallbackText }], }; } function normalizeFilePart(part: Record): { blocks: ContentBlock[]; attachments: AttachedFileMeta[] } { const url = typeof part.url === 'string' ? part.url : undefined; const mimeType = typeof part.mime === 'string' && part.mime.trim() ? part.mime : 'application/octet-stream'; const source = getRecord(part.source); const filePath = source?.type === 'file' && typeof source.path === 'string' ? source.path : undefined; const fileName = typeof part.filename === 'string' && part.filename.trim() ? part.filename : filePath ? basenameFromPath(filePath) : url ? basenameFromPath(url) : 'file'; const attachments: AttachedFileMeta[] = [{ fileName, mimeType, fileSize: 0, preview: null, filePath, source: 'message-ref', }]; if (mimeType.trim().toLowerCase().startsWith('image/') && url) { return { blocks: [{ type: 'image', id: typeof part.id === 'string' ? part.id : undefined, url, mimeType, alt: fileName, }], attachments, }; } return { blocks: [], attachments }; } function normalizePart(part: unknown): { blocks: ContentBlock[]; attachments: AttachedFileMeta[] } { const record = getRecord(part); if (!record || typeof record.type !== 'string') { return { blocks: [], attachments: [] }; } if (record.type === 'text' && typeof record.text === 'string') { return { blocks: [{ type: 'text', id: typeof record.id === 'string' ? record.id : undefined, text: record.text, ...(record.synthetic === true ? { synthetic: true as const } : {}), }], attachments: [], }; } if (record.type === 'reasoning' && typeof record.text === 'string') { return { blocks: [{ type: 'thinking', id: typeof record.id === 'string' ? record.id : undefined, thinking: record.text, }], attachments: [], }; } if (record.type === 'tool' && typeof record.tool === 'string') { const state = getRecord(record.state); return { blocks: [{ type: 'tool_use', id: typeof record.callID === 'string' ? record.callID : typeof record.id === 'string' ? record.id : undefined, name: record.tool, input: state?.input ?? {}, }], attachments: [], }; } if (record.type === 'file') { return normalizeFilePart(record); } return { blocks: [], attachments: [] }; } const COMMAND_HISTORY_PART_ID = /^prt[A-Za-z0-9_-]+$/u; const COMMAND_HISTORY_NAME = /^[^\s/][^\s]*$/u; const COMMAND_HISTORY_CONTROL = /\p{Cc}/u; interface CommandHistorySelection { invocationBlock?: ContentBlock; parts: unknown[]; } function isSafeCommandHistoryContextPart(input: unknown): boolean { const record = getRecord(input); return record?.type === 'text' || record?.type === 'file'; } function selectCommandHistoryParts(parts: unknown[]): CommandHistorySelection { const markers = parts .map(getRecord) .filter((part) => part?.type === 'command-invocation'); if (markers.length === 0) return { parts }; const marker = markers.length === 1 ? markers[0] : null; const command = typeof marker?.command === 'string' ? marker.command : ''; const args = typeof marker?.arguments === 'string' ? marker.arguments : ''; const ids = Array.isArray(marker?.contextPartIDs) ? marker.contextPartIDs : null; const valid = Boolean( marker && typeof marker.id === 'string' && COMMAND_HISTORY_PART_ID.test(marker.id) && command.length > 0 && command.length <= 256 && COMMAND_HISTORY_NAME.test(command) && !COMMAND_HISTORY_CONTROL.test(command) && typeof marker.arguments === 'string' && args.length <= 8_192 && ids && ids.length <= 64 && ids.every((id) => typeof id === 'string' && COMMAND_HISTORY_PART_ID.test(id)), ); if (!valid) { return { invocationBlock: { type: 'text', text: '[Project command metadata unavailable]' }, parts: [], }; } const allowed = new Set(ids as string[]); return { invocationBlock: { id: marker!.id as string, type: 'text', text: `/${command}${args ? ` ${args}` : ''}`, }, parts: parts.filter((part) => { const record = getRecord(part); return typeof record?.id === 'string' && allowed.has(record.id) && isSafeCommandHistoryContextPart(record); }), }; } const RUNTIME_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u; function boundedRuntimeIdentifier(value: unknown, maxLength: number): string | undefined { if (typeof value !== 'string') return undefined; const candidate = value.trim(); return candidate.length > 0 && candidate.length <= maxLength && RUNTIME_IDENTIFIER_PATTERN.test(candidate) ? candidate : undefined; } function getRuntimeMessageContext( role: RawMessage['role'], info: Record, ): RawMessage['_runtimeContext'] { if (role !== 'user') return undefined; const model = getRecord(info.model); const agent = boundedRuntimeIdentifier(info.agent, 64); const providerID = boundedRuntimeIdentifier(model?.providerID, 128); const modelID = boundedRuntimeIdentifier(model?.modelID, 128); const variant = boundedRuntimeIdentifier(model?.variant, 64); const context = { ...(agent ? { agent } : {}), ...(providerID && modelID ? { model: `${providerID}/${modelID}` } : {}), ...(variant ? { variant } : {}), }; return Object.keys(context).length > 0 ? context : undefined; } function mergeAttachments( current: AttachedFileMeta[] | undefined, next: AttachedFileMeta[], ): AttachedFileMeta[] | undefined { if (next.length === 0) return current; const combined = [...(current ?? [])]; const seen = new Set(combined.map((file) => `${file.filePath ?? file.fileName}:${file.mimeType}`)); for (const file of next) { const key = `${file.filePath ?? file.fileName}:${file.mimeType}`; if (seen.has(key)) continue; seen.add(key); combined.push(file); } return combined; } function appendAttachments( current: AttachedFileMeta[] | undefined, next: AttachedFileMeta[], ): AttachedFileMeta[] | undefined { if (next.length === 0) return current; return [...(current ?? []), ...next]; } function isCommandInvocationMarker(input: unknown): boolean { return getRecord(input)?.type === 'command-invocation'; } function hasDirectCommandInvocationMarker(input: unknown): boolean { if (isCommandInvocationMarker(input)) return true; if (Array.isArray(input)) return input.some(isCommandInvocationMarker); const record = getRecord(input); return record ? Object.values(record).some(isCommandInvocationMarker) : false; } function hasLegacyCommandInvocationMarker(input: Record): boolean { if (isCommandInvocationMarker(input)) return true; if (Array.isArray(input.content)) { return input.content.some(isCommandInvocationMarker); } return isCommandInvocationMarker(input.content); } function hasOwn(record: Record, key: string): boolean { return Object.prototype.hasOwnProperty.call(record, key); } function commandHistoryUnavailableMessage(input: Record): RawMessage { const info = getRecord(input.info); return { id: typeof info?.id === 'string' ? info.id : typeof input.id === 'string' ? input.id : undefined, role: normalizeRole(info?.role ?? input.role), timestamp: normalizeTimestamp(info ?? input), content: [{ type: 'text', text: '[Project command metadata unavailable]', }], }; } function getStreamingDelta(record: Record | null, part: Record): string { const rawDelta = record?.delta ?? part.delta; if (typeof rawDelta === 'string') return rawDelta; const delta = getRecord(rawDelta); if (typeof delta?.text === 'string') return delta.text; if (typeof delta?.thinking === 'string') return delta.thinking; if (typeof delta?.reasoning === 'string') return delta.reasoning; if (typeof delta?.reasoning_content === 'string') return delta.reasoning_content; if (typeof delta?.reasoning_text === 'string') return delta.reasoning_text; if (typeof part.reasoning === 'string') return part.reasoning; if (typeof part.reasoning_content === 'string') return part.reasoning_content; if (typeof part.reasoning_text === 'string') return part.reasoning_text; return ''; } function mergeStreamingText(existingText: string, incomingText: string, delta: string): string { if (!delta) return incomingText; if (incomingText && incomingText.startsWith(existingText) && incomingText.length > existingText.length) return incomingText; return `${existingText}${delta}`; } function applyStreamingDeltaToBlock( candidate: ContentBlock, existingBlock: ContentBlock | undefined, delta: string, ): ContentBlock { if (!delta) return candidate; if (candidate.type === 'text') { const existingText = existingBlock?.type === 'text' && typeof existingBlock.text === 'string' ? existingBlock.text : ''; return { ...candidate, text: mergeStreamingText(existingText, candidate.text ?? '', delta), }; } if (candidate.type === 'thinking') { const existingThinking = existingBlock?.type === 'thinking' && typeof existingBlock.thinking === 'string' ? existingBlock.thinking : ''; return { ...candidate, thinking: mergeStreamingText(existingThinking, candidate.thinking ?? '', delta), }; } return candidate; } function normalizeStructuredMessage(input: Record): RawMessage | null { const info = getRecord(input.info); const parts = Array.isArray(input.parts) ? input.parts : null; if (!info || !parts) return null; const errorMessage = getErrorMessage(info.error ?? input.error ?? input.errorMessage ?? input.error_message); const role = normalizeRole(info.role); const commandHistory: CommandHistorySelection = role === 'user' ? selectCommandHistoryParts(parts) : { parts }; const blocks: ContentBlock[] = []; let attachedFiles: AttachedFileMeta[] | undefined; let toolEvidence: RuntimeToolEvidence[] | undefined; if (commandHistory.invocationBlock) blocks.push(commandHistory.invocationBlock); for (const part of commandHistory.parts) { const record = getRecord(part); const normalized = normalizePart(part); blocks.push(...normalized.blocks); attachedFiles = appendAttachments(attachedFiles, normalized.attachments); toolEvidence = mergeToolEvidence(toolEvidence, record ? normalizeToolEvidence(record) : null); } const finalErrorMessage = errorMessage ?? (role === 'assistant' && blocks.length === 0 && hasCompletedAssistantSignal(info, input, parts) ? EMPTY_ASSISTANT_RESPONSE_MESSAGE : undefined); const runtimeContext = getRuntimeMessageContext(role, info); return { id: typeof info.id === 'string' ? info.id : undefined, role, timestamp: normalizeTimestamp(info), content: blocks.length > 0 || !finalErrorMessage ? blocks : [{ type: 'text', text: finalErrorMessage }], ...(attachedFiles?.length ? { _attachedFiles: attachedFiles } : {}), ...(toolEvidence?.length ? { _toolEvidence: toolEvidence } : {}), ...(runtimeContext ? { _runtimeContext: runtimeContext } : {}), ...(finalErrorMessage ? { isError: true, errorMessage: finalErrorMessage } : {}), }; } export function normalizeOpencodeSessionMessages(input: unknown): RawMessage[] { if (!Array.isArray(input)) return []; return input.flatMap((entry) => { const record = getRecord(entry); if (!record) return []; const structuredInfo = getRecord(record.info); const structuredParts = Array.isArray(record.parts) ? record.parts : null; const structuredLike = hasOwn(record, 'info') || hasOwn(record, 'parts'); if (structuredLike && (!structuredInfo || !structuredParts)) { return hasDirectCommandInvocationMarker(record.parts) || hasLegacyCommandInvocationMarker(record) ? [commandHistoryUnavailableMessage(record)] : [normalizeLegacyMessage(record)]; } if (structuredInfo && structuredParts) { const containsCommandMarker = structuredParts.some( isCommandInvocationMarker, ); if ( containsCommandMarker && normalizeRole(structuredInfo.role) !== 'user' ) { return [commandHistoryUnavailableMessage(record)]; } const structured = normalizeStructuredMessage(record); return structured ? [structured] : [commandHistoryUnavailableMessage(record)]; } if (hasLegacyCommandInvocationMarker(record)) { return [commandHistoryUnavailableMessage(record)]; } return [normalizeLegacyMessage(record)]; }); } export function createOptimisticUserMessage( text: string, timestamp = Date.now(), deliveryStatus?: RawMessage['_deliveryStatus'], attachedFiles?: AttachedFileMeta[], ): RawMessage { return { id: `local-user-${timestamp}`, role: 'user', timestamp, content: [{ type: 'text', text }], ...(attachedFiles?.length ? { _attachedFiles: attachedFiles } : {}), ...(deliveryStatus ? { _deliveryStatus: deliveryStatus } : {}), }; } export function createStreamingMessageFromEvent(input: unknown): RawMessage | null { const record = getRecord(input); const info = getRecord(record?.info ?? input); if (!info) return null; return { id: typeof info.id === 'string' ? info.id : undefined, role: normalizeRole(info.role), timestamp: normalizeTimestamp(info), content: [], }; } export function getOpencodeEventSessionId(input: unknown): string | null { const record = getRecord(input); if (!record) return null; if (typeof record.sessionID === 'string' && record.sessionID.trim()) return record.sessionID; if (typeof record.sessionId === 'string' && record.sessionId.trim()) return record.sessionId; return getOpencodeEventSessionId(record.part) ?? getOpencodeEventSessionId(record.info) ?? getOpencodeEventSessionId(record.message) ?? null; } export function applyStreamingPartToMessage(message: RawMessage | null, input: unknown): RawMessage | null { const record = getRecord(input); const part = getRecord(record?.part ?? input); if (!part) return message; const current = message ?? { role: 'assistant', content: [], }; const nextContent = Array.isArray(current.content) ? [...current.content] : []; const normalized = normalizePart(part); const toolEvidence = normalizeToolEvidence(part); const rawCandidate = normalized.blocks.at(0); const delta = getStreamingDelta(record, part); const messageId = typeof record?.messageID === 'string' ? record.messageID : typeof record?.messageId === 'string' ? record.messageId : typeof part.messageID === 'string' ? part.messageID : undefined; const blockId = rawCandidate?.id ?? (typeof part.id === 'string' ? part.id : undefined); const index = blockId ? nextContent.findIndex((block) => ('id' in block ? block.id : undefined) === blockId) : -1; const existingBlock = index >= 0 ? nextContent[index] : undefined; const candidate = rawCandidate ? applyStreamingDeltaToBlock(rawCandidate, existingBlock, delta) : delta ? { type: 'text', id: blockId, text: delta } satisfies ContentBlock : undefined; if (candidate) { const nextBlock = blockId ? { ...candidate, id: blockId } : candidate; if (index >= 0) { nextContent[index] = nextBlock; } else { nextContent.push(nextBlock); } } return { ...current, ...(current.id || !messageId ? {} : { id: messageId }), content: nextContent, _attachedFiles: mergeAttachments(current._attachedFiles, normalized.attachments), _toolEvidence: mergeToolEvidence(current._toolEvidence, toolEvidence), }; } export function applyStreamingPartDeltaToMessage(message: RawMessage | null, input: unknown): RawMessage | null { const record = getRecord(input); if (!record) return message; const part = getRecord(record.part) ?? record; const delta = getStreamingDelta(record, part); if (!delta) return message; const messageID = typeof record.messageID === 'string' ? record.messageID : typeof record.messageId === 'string' ? record.messageId : undefined; const partID = typeof record.partID === 'string' ? record.partID : typeof record.partId === 'string' ? record.partId : typeof part?.id === 'string' ? part.id : undefined; const field = typeof record.field === 'string' ? record.field : typeof part?.field === 'string' ? part.field : undefined; const isThinkingDelta = part?.type === 'reasoning' || part?.type === 'thinking' || field === 'reasoning' || field === 'thinking' || field === 'reasoning_content' || field === 'reasoning_text'; if (message?.id && messageID && message.id !== messageID) return message; const current = message ?? { id: messageID, role: 'assistant', content: [], } satisfies RawMessage; const nextContent = Array.isArray(current.content) ? [...current.content] : []; const index = partID ? nextContent.findIndex((block) => ('id' in block ? block.id : undefined) === partID) : -1; const existingBlock = index >= 0 ? nextContent[index] : undefined; if (existingBlock?.type === 'thinking' || isThinkingDelta) { const existingThinking = existingBlock?.type === 'thinking' ? existingBlock.thinking ?? '' : existingBlock?.type === 'text' ? existingBlock.text ?? '' : ''; const nextBlock = { type: 'thinking', id: partID, thinking: `${existingThinking}${delta}`, } satisfies ContentBlock; if (index >= 0) { nextContent[index] = nextBlock; } else { nextContent.push(nextBlock); } } else if (existingBlock?.type === 'text') { nextContent[index] = { ...existingBlock, text: `${existingBlock.text ?? ''}${delta}`, }; } else { nextContent.push({ type: 'text', id: partID, text: delta, }); } return { ...current, content: nextContent, }; } export function removeStreamingPartFromMessage( message: RawMessage | null, input: unknown, ): RawMessage | null { if (!message || !input || typeof input !== 'object') return message; const record = input as Record; const messageID = typeof record.messageID === 'string' ? record.messageID : typeof record.messageId === 'string' ? record.messageId : undefined; if (message.id && messageID && message.id !== messageID) return message; const part = record.part && typeof record.part === 'object' && !Array.isArray(record.part) ? record.part as Record : null; const partID = typeof record.partID === 'string' ? record.partID : typeof record.partId === 'string' ? record.partId : typeof part?.id === 'string' ? part.id : undefined; if (!partID || !Array.isArray(message.content)) return message; const content = message.content.filter((block) => ( !('id' in block) || block.id !== partID )); const toolEvidence = message._toolEvidence?.filter((evidence) => ( evidence.id !== partID && evidence.toolCallId !== partID )); return { ...message, content, ...(toolEvidence?.length ? { _toolEvidence: toolEvidence } : { _toolEvidence: undefined }), }; } export function getStreamingToolStatus(input: unknown): ToolStatus | null { const record = getRecord(input); const part = getRecord(record?.part ?? input); if (!part || part.type !== 'tool' || typeof part.tool !== 'string') return null; const state = getRecord(part.state); const time = getRecord(state?.time); const startedAt = toMs(time?.start); const endedAt = toMs(time?.end); return { id: typeof part.id === 'string' ? part.id : undefined, toolCallId: typeof part.callID === 'string' ? part.callID : undefined, name: part.tool, status: normalizeToolStatus(state?.status), durationMs: startedAt !== undefined && endedAt !== undefined ? Math.max(0, endedAt - startedAt) : undefined, summary: typeof state?.title === 'string' && state.title.trim() ? state.title : typeof state?.error === 'string' && state.error.trim() ? state.error : undefined, updatedAt: endedAt ?? startedAt ?? Date.now(), }; }