import type { UIMessage } from "ai"; import type { Dispatch, SetStateAction } from "react"; import { parseAnswerUiBlock, type AnswerUiPart, applyPermissionResponse, agentTaskContextReferenceSchema, applyQuestionResponse, toTodoUpdate, upsertPermission, upsertProgress, upsertQuestion, type AgentChatMessage, type AgentPermissionReply, type AgentPermissionStatus, type AgentQuestionRequest, type AgentSessionStreamEvent, type AgentStreamRenderState } from "@/features/agent"; type AgentUiDataParts = { session_created: Record; agent_message: Record; task_context: Record; execution_plan: Record; progress: Record; final_answer: Record; credential_refresh_required: Record; credential_refreshed: Record; credential_refresh_failed: Record; todo_update: Record; permission_request: Record; permission_response: Record; question_request: Record; question_response: Record; stream_token: Record; tool_call: Record; frontend_action: Record; frontend_action_result: Record; answer_ui: Record; ui_envelope: Record; session_title: Record; user_input: Record; }; export type AgentUiMessage = UIMessage; export type AgentDataPart = Extract; export type PermissionOverride = { status: AgentPermissionStatus; error?: string; }; export type QuestionOverride = { status: AgentQuestionRequest["status"]; answers?: string[][]; error?: string; }; type StreamRenderStateSetter = Dispatch>; export function markStreamRenderPending( data: Record, messages: AgentUiMessage[], setStreamRenderState: StreamRenderStateSetter ) { const text = getDataString(data, "content"); if (!text) { return; } const messageId = getDataString(data, "message_id") ?? getLastAssistantUiMessageId(messages); if (!messageId) { return; } setStreamRenderState((current) => { if (current[messageId]?.done === false) { return current; } return { ...current, [messageId]: { done: false } }; }); } export function createCompletedStreamRenderState(messages: AgentUiMessage[]): AgentStreamRenderState { return messages.reduce((next, message) => { if (message.role === "assistant") { next[message.id] = { done: true }; } return next; }, {}); } export function markLastAssistantStreamDone( messages: AgentUiMessage[], setStreamRenderState: StreamRenderStateSetter ) { const messageId = getLastAssistantUiMessageId(messages); setStreamRenderState((current) => { if (!messageId) { return Object.fromEntries( Object.entries(current).map(([id, state]) => [id, { ...state, done: true }]) ); } return { ...current, [messageId]: { done: true } }; }); } export function toAgentChatMessages( messages: AgentUiMessage[], permissionOverrides: Record, questionOverrides: Record ): AgentChatMessage[] { return messages.flatMap((message) => { if (message.role !== "user" && message.role !== "assistant") { return []; } const answerParts = collectAnswerParts(message); let next: AgentChatMessage = { id: message.id, role: message.role, content: answerParts ? answerParts.map((part) => part.type === "text" ? part.text : part.block.fallback_text).join("\n\n") : collectMessageText(message), ...(answerParts ? { answerParts } : {}) }; for (const part of message.parts) { if (!isAgentDataPart(part)) { continue; } if (part.type === "data-task_context") { const parsed = agentTaskContextReferenceSchema.safeParse(part.data); if (parsed.success) next = { ...next, taskContext: parsed.data }; } else if (part.type === "data-final_answer") { const content = getDataString(part.data, "content"); if (content?.trim()) { const refs = part.data?.ui_block_ids; const uiBlockIds = Array.isArray(refs) ? [...new Set(refs.filter((id): id is string => typeof id === "string"))].slice(0, 48) : []; next = { ...next, finalAnswer: { content, uiBlockIds } }; } } else if (part.type === "data-progress") { next = { ...next, progress: upsertProgress(next.progress, part.data) }; } else if (part.type === "data-todo_update") { const todoUpdate = toTodoUpdate(part.data); if (todoUpdate) { next = { ...next, todos: todoUpdate }; } } else if (part.type === "data-permission_request") { next = { ...next, permissions: upsertPermission(next.permissions, part.data) }; } else if (part.type === "data-permission_response") { next = { ...next, permissions: applyPermissionResponse(next.permissions, part.data) }; } else if (part.type === "data-question_request") { next = { ...next, questions: upsertQuestion(next.questions, part.data) }; } else if (part.type === "data-question_response") { next = { ...next, questions: applyQuestionResponse(next.questions, part.data) }; } } if (next.finalAnswer) { const selected = new Set(next.finalAnswer.uiBlockIds); next.content = [next.finalAnswer.content, ...(next.answerParts ?? []).flatMap((part) => part.type === "answer-ui" && selected.has(part.block.block_id) ? [part.block.fallback_text] : [])].join("\n\n"); } return [applyQuestionOverrides(applyPermissionOverrides(next, permissionOverrides), questionOverrides)]; }); } export function toAgentUiMessages(messages: unknown[]): AgentUiMessage[] { return messages.flatMap((message) => { if (isAgentUiMessage(message)) { return [message]; } const legacyMessage = toAgentUiMessageFromLegacy(message); return legacyMessage ? [legacyMessage] : []; }); } export function applySessionStreamEvent(messages: AgentUiMessage[], event: AgentSessionStreamEvent): AgentUiMessage[] { if (event.type === "state") { return toAgentUiMessages(event.messages); } if (event.type === "user_input") { const id = getDataString(event.data, "id"); const text = getDataString(event.data, "text"); if (event.data.status !== "delivered" || !id || !text || messages.some((message) => message.id === id)) return messages; const input: AgentUiMessage = { id, role: "user", parts: [{ type: "text", text }] }; const tail = messages.at(-1); return tail?.role === "assistant" ? [...messages.slice(0, -1), input, tail] : [...messages, input]; } if (event.type === "token") { const token = getDataString(event.data, "content"); return token ? updateLastAssistantUiMessage(messages, (message) => appendTextToUiMessage(message, token)) : messages; } if ( event.type === "final_answer" || event.type === "progress" || event.type === "execution_plan" || event.type === "todo_update" || event.type === "permission_request" || event.type === "permission_response" || event.type === "question_request" || event.type === "question_response" || event.type === "answer_ui" || event.type === "ui_envelope" || event.type === "session_title" ) { return updateLastAssistantUiMessage(messages, (message) => upsertUiDataPart(message, event.type, event.data) ); } if (event.type === "error") { const message = getDataString(event.data, "message") ?? "Agent stream failed"; return updateLastAssistantUiMessage(messages, (item) => appendTextToUiMessage(item, item.parts.some((part) => part.type === "text") ? `\n\n错误:${message}` : `错误:${message}`) ); } return messages; } export function toPermissionStatus(reply: AgentPermissionReply): AgentPermissionStatus { if (reply === "always") { return "approved_always"; } if (reply === "once") { return "approved_once"; } return "rejected"; } export function getDataString(data: unknown, key: string) { if (typeof data !== "object" || data === null) { return undefined; } const value = (data as Record)[key]; return typeof value === "string" ? value : undefined; } export function readBodyString(body: unknown, key: string) { if (typeof body !== "object" || body === null) { return undefined; } const value = (body as Record)[key]; return typeof value === "string" ? value : undefined; } function getLastAssistantUiMessageId(messages: AgentUiMessage[]) { for (let index = messages.length - 1; index >= 0; index -= 1) { if (messages[index].role === "assistant") { return messages[index].id; } } return undefined; } function isAgentUiMessage(value: unknown): value is AgentUiMessage { if (typeof value !== "object" || value === null) { return false; } const message = value as Record; return ( typeof message.id === "string" && (message.role === "user" || message.role === "assistant") && Array.isArray(message.parts) ); } function toAgentUiMessageFromLegacy(value: unknown): AgentUiMessage | null { if (typeof value !== "object" || value === null) { return null; } const message = value as Record; if ( typeof message.id !== "string" || (message.role !== "user" && message.role !== "assistant") ) { return null; } const parts: AgentUiMessage["parts"] = []; if (typeof message.content === "string" && message.content) { parts.push({ type: "text", text: message.content }); } appendLegacyDataParts(parts, "progress", message.progress); appendLegacyDataParts(parts, "permission_request", message.permissions); appendLegacyDataParts(parts, "question_request", message.questions); appendLegacyDataParts(parts, "todo_update", message.todos); appendLegacyDataParts(parts, "execution_plan", message.executionPlans); return { id: message.id, role: message.role, parts } as AgentUiMessage; } function appendLegacyDataParts( parts: AgentUiMessage["parts"], eventType: string, value: unknown ) { if (!value) { return; } const values = Array.isArray(value) ? value : [value]; values.forEach((item, index) => { if (typeof item !== "object" || item === null) { return; } const data = normalizeLegacyDataPart(eventType, item as Record); const id = getDataString(data, "id") ?? getDataString(data, "request_id") ?? `${eventType}-${index}`; parts.push({ type: `data-${eventType}`, id, data } as AgentDataPart); }); } function normalizeLegacyDataPart(eventType: string, value: Record) { if (eventType === "progress") { return { ...value, started_at: value.started_at ?? value.startedAt, ended_at: value.ended_at ?? value.endedAt, elapsed_ms: value.elapsed_ms ?? value.elapsedMs, duration_ms: value.duration_ms ?? value.durationMs }; } if (eventType === "permission_request" || eventType === "question_request") { return { ...value, session_id: value.session_id ?? value.sessionId, request_id: value.request_id ?? value.requestId, created_at: value.created_at ?? value.createdAt }; } if (eventType === "todo_update") { return { ...value, session_id: value.session_id ?? value.sessionId, message_id: value.message_id ?? value.messageId, created_at: value.created_at ?? value.createdAt }; } return value; } function updateLastAssistantUiMessage( messages: AgentUiMessage[], updater: (message: AgentUiMessage) => AgentUiMessage ) { for (let index = messages.length - 1; index >= 0; index -= 1) { if (messages[index].role === "assistant") { const next = [...messages]; next[index] = updater(messages[index]); return next; } } return [...messages, updater(createAssistantUiMessage())]; } function createAssistantUiMessage(): AgentUiMessage { return { id: `assistant-${Date.now().toString(36)}`, role: "assistant", parts: [] } as AgentUiMessage; } function appendTextToUiMessage(message: AgentUiMessage, text: string): AgentUiMessage { let textIndex = -1; for (let index = message.parts.length - 1; index >= 0; index -= 1) { if (message.parts[index].type === "text") { textIndex = index; break; } } if (message.parts.slice(textIndex + 1).some((part) => part.type === "data-answer_ui")) textIndex = -1; if (textIndex === -1) { return { ...message, parts: [...message.parts, { type: "text", text }] }; } return { ...message, parts: message.parts.map((part, index) => index === textIndex && part.type === "text" ? { ...part, text: `${part.text}${text}` } : part ) }; } function upsertUiDataPart( message: AgentUiMessage, eventType: string, data: Record ): AgentUiMessage { const type = `data-${eventType}`; const id = getDataString(data, "id") ?? getDataString(data, "request_id") ?? getDataString(data, "envelope_id") ?? getDataString(data, "block_id"); const existingIndex = id === undefined ? -1 : message.parts.findIndex((part) => part.type === type && "id" in part && part.id === id); const part = (id ? { type, id, data } : { type, data }) as AgentDataPart; if (existingIndex === -1) { return { ...message, parts: [...message.parts, part] }; } return { ...message, parts: message.parts.map((item, index) => (index === existingIndex ? part : item)) }; } function applyPermissionOverrides( message: AgentChatMessage, permissionOverrides: Record ) { if (!message.permissions?.length) { return message; } return { ...message, permissions: message.permissions.map((permission) => { const override = permissionOverrides[permission.requestId]; return override ? { ...permission, status: override.status, error: override.error, repliedAt: override.status === "submitting" || override.status === "error" ? permission.repliedAt : Date.now() } : permission; }) }; } function applyQuestionOverrides( message: AgentChatMessage, questionOverrides: Record ) { if (!message.questions?.length) { return message; } return { ...message, questions: message.questions.map((question) => { const override = questionOverrides[question.requestId]; return override ? { ...question, status: override.status, answers: override.answers ?? question.answers, error: override.error, repliedAt: override.status === "submitting" || override.status === "error" ? question.repliedAt : Date.now() } : question; }) }; } function collectMessageText(message: AgentUiMessage) { return message.parts .flatMap((part) => (part.type === "text" ? [part.text] : [])) .join(""); } function isAgentDataPart(part: AgentUiMessage["parts"][number]): part is AgentDataPart { return typeof part.type === "string" && part.type.startsWith("data-") && "data" in part; } function collectAnswerParts(message: AgentUiMessage): AnswerUiPart[] | undefined { if (!message.parts.some((part) => part.type === "data-answer_ui" || part.type === "data-final_answer")) return undefined; const result: AnswerUiPart[] = []; for (const part of message.parts) { if (part.type === "text" && part.text) result.push({ type: "text", text: part.text }); if (part.type === "data-answer_ui") { const data = typeof part.data === "object" && part.data !== null ? part.data : {}; const { version, block_id, fallback_text, spec } = data; const block = parseAnswerUiBlock({ version, block_id, fallback_text, spec }); if (block) result.push({ type: "answer-ui", block }); else result.push({ type: "text", text: typeof fallback_text === "string" ? fallback_text.slice(0, 4000) : "这段结构化内容无法显示。" }); } } return result; }