diff options
Diffstat (limited to 'packages/core/src/agent/agent.ts')
| -rw-r--r-- | packages/core/src/agent/agent.ts | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/packages/core/src/agent/agent.ts b/packages/core/src/agent/agent.ts new file mode 100644 index 0000000..a20a4ce --- /dev/null +++ b/packages/core/src/agent/agent.ts @@ -0,0 +1,142 @@ +import type { CoreMessage } from "ai"; +import { streamText } from "ai"; +import { createProvider } from "../llm/provider.js"; +import { createToolRegistry } from "../tools/registry.js"; +import type { + AgentConfig, + AgentEvent, + AgentStatus, + ChatMessage, + ToolCall, + ToolResult, +} from "../types/index.js"; + +function toCoreMessages(messages: ChatMessage[]): CoreMessage[] { + const result: CoreMessage[] = []; + for (const msg of messages) { + if (msg.role === "user") { + result.push({ role: "user", content: msg.content }); + } else if (msg.role === "assistant") { + result.push({ role: "assistant", content: msg.content }); + } + } + return result; +} + +function formatError(err: unknown, config: AgentConfig): string { + const context = `[model=${config.model}, baseURL=${config.baseURL}]`; + + if (err instanceof Error) { + const cause = err.cause ? ` | cause: ${JSON.stringify(err.cause)}` : ""; + // AI SDK errors often have statusCode, responseBody, or url properties + const extras: string[] = []; + const errRecord = err as unknown as Record<string, unknown>; + if ("statusCode" in errRecord) extras.push(`status=${errRecord.statusCode}`); + if ("url" in errRecord) extras.push(`url=${errRecord.url}`); + if ("responseBody" in errRecord) extras.push(`body=${JSON.stringify(errRecord.responseBody)}`); + if ("responseHeaders" in errRecord) + extras.push(`headers=${JSON.stringify(errRecord.responseHeaders)}`); + + const detail = extras.length > 0 ? ` (${extras.join(", ")})` : ""; + return `${err.message}${detail}${cause} ${context}`; + } + + return `${String(err)} ${context}`; +} + +export class Agent { + status: AgentStatus = "idle"; + messages: ChatMessage[] = []; + + private config: AgentConfig; + + constructor(config: AgentConfig) { + this.config = config; + } + + async *run(userMessage: string): AsyncGenerator<AgentEvent> { + this.status = "running"; + yield { type: "status", status: "running" }; + + this.messages.push({ role: "user", content: userMessage }); + + const registry = createToolRegistry(this.config.tools); + const providerFactory = createProvider({ + apiKey: this.config.apiKey, + baseURL: this.config.baseURL, + }); + + try { + const result = streamText({ + model: providerFactory(this.config.model), + system: this.config.systemPrompt, + messages: toCoreMessages(this.messages), + tools: registry.getAISDKTools(), + maxSteps: 10, + }); + + let fullText = ""; + const toolCalls: ToolCall[] = []; + const toolResults: ToolResult[] = []; + + for await (const event of result.fullStream) { + if (event.type === "text-delta") { + fullText += event.textDelta; + yield { type: "text-delta", delta: event.textDelta }; + } else if (event.type === "tool-call") { + const toolCall: ToolCall = { + id: event.toolCallId, + name: event.toolName, + arguments: event.args as Record<string, unknown>, + }; + toolCalls.push(toolCall); + yield { type: "tool-call", toolCall }; + } else if (event.type === "error") { + const errorMsg = formatError(event.error, this.config); + yield { type: "error", error: errorMsg }; + this.status = "error"; + yield { type: "status", status: "error" }; + return; + } + } + + // Tool results are available from completed steps after streaming. + // The generic TOOLS type resolves to never[] at compile time, so + // we cast through unknown to access the runtime shape. + const steps = await result.steps; + for (const step of steps) { + const stepToolResults = step.toolResults as unknown as Array<{ + toolCallId: string; + result: unknown; + }>; + for (const tr of stepToolResults) { + const toolResult: ToolResult = { + toolCallId: tr.toolCallId, + result: typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result), + isError: false, + }; + toolResults.push(toolResult); + yield { type: "tool-result", toolResult }; + } + } + + const assistantMessage: ChatMessage = { + role: "assistant", + content: fullText, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + toolResults: toolResults.length > 0 ? toolResults : undefined, + }; + this.messages.push(assistantMessage); + yield { type: "done", message: assistantMessage }; + } catch (err) { + const errorMsg = formatError(err, this.config); + yield { type: "error", error: errorMsg }; + this.status = "error"; + yield { type: "status", status: "error" }; + return; + } + + this.status = "idle"; + yield { type: "status", status: "idle" }; + } +} |
