1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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" };
}
}
|