import { createLlmClient, type ChatMessage, type LlmClient } from "@/lib/llm"; import { buildVisionContentParts, textFromMessageContent } from "@/lib/llm/content"; import { AGENT_SYSTEM_PROMPT, AGENT_TOOLS } from "../tools"; import type { ClientChatMessage } from "../messages"; import { describeToolActivity } from "../tool-labels"; import { createApiToolExecutor, type ToolExecutor } from "../tool-executor"; import { resolveAssistantImageDataUrls } from "./resolve-images"; import { thinkingLabel, type AgentProgressEvent } from "./progress"; const MAX_TOOL_ROUNDS = 8; export type AgentToolCallSummary = { name: string; status: number; }; export type AgentChatResult = { message: ClientChatMessage; toolCalls: AgentToolCallSummary[]; }; export type AgentProgressHandler = (event: AgentProgressEvent) => void; async function toLlmUserMessage(message: ClientChatMessage): Promise { if (message.role === "assistant") { return { role: "assistant", content: message.content }; } const imageUrls = message.attachments?.filter((attachment) => attachment.type === "image").map((a) => a.url) ?? []; if (imageUrls.length === 0) { return { role: "user", content: message.content }; } const dataUrls = await resolveAssistantImageDataUrls(imageUrls); return { role: "user", content: buildVisionContentParts(message.content, dataUrls), }; } export async function runAgentChat(options: { messages: ClientChatMessage[]; request: Request; systemPrompt?: string; model?: string; llm?: LlmClient; executeTool?: ToolExecutor; onProgress?: AgentProgressHandler; }): Promise { const llm = options.llm ?? createLlmClient({ model: options.model }); const executeTool = options.executeTool ?? createApiToolExecutor(options.request); const onProgress = options.onProgress; const systemPrompt = options.systemPrompt ?? AGENT_SYSTEM_PROMPT; const userMessages = await Promise.all( options.messages.map((message) => toLlmUserMessage(message)), ); const transcript: ChatMessage[] = [{ role: "system", content: systemPrompt }, ...userMessages]; const toolCalls: AgentToolCallSummary[] = []; for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) { onProgress?.({ type: "thinking", label: thinkingLabel(round), round }); const completion = await llm.chatCompletion({ messages: transcript, tools: AGENT_TOOLS, }); const assistantMessage = completion.message; transcript.push(assistantMessage); if (!assistantMessage.tool_calls?.length) { onProgress?.({ type: "responding", label: "Writing a reply…" }); return { message: { role: "assistant", content: textFromMessageContent(assistantMessage.content).trim() || "I couldn't generate a response.", }, toolCalls, }; } for (const toolCall of assistantMessage.tool_calls) { const label = describeToolActivity(toolCall.function.name, toolCall.function.arguments); onProgress?.({ type: "tool", name: toolCall.function.name, label }); let result: string; let status: number; try { result = await executeTool(toolCall.function.name, toolCall.function.arguments); const parsed = JSON.parse(result) as { status?: number }; status = typeof parsed.status === "number" ? parsed.status : 200; } catch (err) { status = 500; result = JSON.stringify({ status: 500, body: { error: err instanceof Error ? err.message : "Tool execution failed" }, }); } toolCalls.push({ name: toolCall.function.name, status }); transcript.push({ role: "tool", tool_call_id: toolCall.id, name: toolCall.function.name, content: result, }); } } onProgress?.({ type: "responding", label: "Wrapping up…" }); return { message: { role: "assistant", content: "I hit the tool-call limit for this request. Please try a simpler question.", }, toolCalls, }; }