@agentxjs/mono-driver 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +244 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +450 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
- package/src/MonoDriver.ts +444 -0
- package/src/converters.ts +175 -0
- package/src/index.ts +53 -0
- package/src/types.ts +84 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MonoDriver - Unified Cross-Platform Driver
|
|
3
|
+
*
|
|
4
|
+
* Implements the Driver interface using Vercel AI SDK.
|
|
5
|
+
* Supports multiple LLM providers (Anthropic, OpenAI, Google).
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* UserMessage
|
|
9
|
+
* │
|
|
10
|
+
* ▼
|
|
11
|
+
* ┌─────────────────┐
|
|
12
|
+
* │ MonoDriver │
|
|
13
|
+
* │ │
|
|
14
|
+
* │ receive() │──► AsyncIterable<DriverStreamEvent>
|
|
15
|
+
* │ │ │
|
|
16
|
+
* │ ▼ │
|
|
17
|
+
* │ Vercel AI SDK │
|
|
18
|
+
* └─────────────────┘
|
|
19
|
+
* │
|
|
20
|
+
* ▼
|
|
21
|
+
* LLM Provider
|
|
22
|
+
* (Anthropic/OpenAI/...)
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { streamText, stepCountIs } from "ai";
|
|
27
|
+
import type { ToolSet } from "ai";
|
|
28
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
29
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
30
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
31
|
+
import { createXai } from "@ai-sdk/xai";
|
|
32
|
+
import { createDeepSeek } from "@ai-sdk/deepseek";
|
|
33
|
+
import { createMistral } from "@ai-sdk/mistral";
|
|
34
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
35
|
+
import { createMCPClient } from "@ai-sdk/mcp";
|
|
36
|
+
import type { MCPClient } from "@ai-sdk/mcp";
|
|
37
|
+
import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
|
|
38
|
+
import type { Driver, DriverState, DriverStreamEvent } from "@agentxjs/core/driver";
|
|
39
|
+
import type { UserMessage } from "@agentxjs/core/agent";
|
|
40
|
+
import type { Session } from "@agentxjs/core/session";
|
|
41
|
+
import { createLogger } from "commonxjs/logger";
|
|
42
|
+
import type { MonoDriverConfig, MonoProvider, OpenAICompatibleConfig } from "./types";
|
|
43
|
+
import { toVercelMessages, toStopReason, createEvent, toVercelTools } from "./converters";
|
|
44
|
+
|
|
45
|
+
const logger = createLogger("mono-driver/MonoDriver");
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* MonoDriver - Driver implementation using Vercel AI SDK
|
|
49
|
+
*/
|
|
50
|
+
export class MonoDriver implements Driver {
|
|
51
|
+
readonly name = "MonoDriver";
|
|
52
|
+
|
|
53
|
+
private _sessionId: string | null = null;
|
|
54
|
+
private _state: DriverState = "idle";
|
|
55
|
+
private abortController: AbortController | null = null;
|
|
56
|
+
|
|
57
|
+
private readonly config: MonoDriverConfig;
|
|
58
|
+
private readonly session?: Session;
|
|
59
|
+
private readonly provider: MonoProvider;
|
|
60
|
+
private readonly maxSteps: number;
|
|
61
|
+
private readonly compatibleConfig?: OpenAICompatibleConfig;
|
|
62
|
+
|
|
63
|
+
/** MCP clients created during initialize() */
|
|
64
|
+
private mcpClients: MCPClient[] = [];
|
|
65
|
+
/** Tools discovered from MCP servers */
|
|
66
|
+
private mcpTools: ToolSet = {};
|
|
67
|
+
|
|
68
|
+
constructor(config: MonoDriverConfig) {
|
|
69
|
+
this.config = config;
|
|
70
|
+
this.session = config.session;
|
|
71
|
+
this.provider = config.options?.provider ?? "anthropic";
|
|
72
|
+
this.maxSteps = config.options?.maxSteps ?? 10;
|
|
73
|
+
this.compatibleConfig = config.options?.compatibleConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Driver Interface Properties
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
get sessionId(): string | null {
|
|
81
|
+
return this._sessionId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get state(): DriverState {
|
|
85
|
+
return this._state;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Lifecycle Methods
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
async initialize(): Promise<void> {
|
|
93
|
+
if (this._state !== "idle") {
|
|
94
|
+
throw new Error(`Cannot initialize: Driver is in "${this._state}" state`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logger.info("Initializing MonoDriver", {
|
|
98
|
+
agentId: this.config.agentId,
|
|
99
|
+
provider: this.provider,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Generate a session ID for tracking
|
|
103
|
+
this._sessionId = `mono_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
104
|
+
|
|
105
|
+
// Notify session ID captured
|
|
106
|
+
this.config.onSessionIdCaptured?.(this._sessionId);
|
|
107
|
+
|
|
108
|
+
// Initialize MCP servers
|
|
109
|
+
if (this.config.mcpServers) {
|
|
110
|
+
for (const [name, serverConfig] of Object.entries(this.config.mcpServers)) {
|
|
111
|
+
let client: MCPClient;
|
|
112
|
+
|
|
113
|
+
if ("command" in serverConfig) {
|
|
114
|
+
// Stdio transport — local subprocess
|
|
115
|
+
const transport = new Experimental_StdioMCPTransport({
|
|
116
|
+
command: serverConfig.command,
|
|
117
|
+
args: serverConfig.args,
|
|
118
|
+
env: serverConfig.env,
|
|
119
|
+
});
|
|
120
|
+
client = await createMCPClient({ transport });
|
|
121
|
+
} else {
|
|
122
|
+
// HTTP Streamable transport — remote server
|
|
123
|
+
client = await createMCPClient({
|
|
124
|
+
transport: {
|
|
125
|
+
type: serverConfig.type,
|
|
126
|
+
url: serverConfig.url,
|
|
127
|
+
headers: serverConfig.headers,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.mcpClients.push(client);
|
|
133
|
+
const tools = await client.tools();
|
|
134
|
+
Object.assign(this.mcpTools, tools);
|
|
135
|
+
logger.info("MCP server connected", {
|
|
136
|
+
name,
|
|
137
|
+
toolCount: Object.keys(tools).length,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
logger.info("MonoDriver initialized", { sessionId: this._sessionId });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async dispose(): Promise<void> {
|
|
146
|
+
if (this._state === "disposed") {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
logger.info("Disposing MonoDriver", { agentId: this.config.agentId });
|
|
151
|
+
|
|
152
|
+
// Abort any ongoing request
|
|
153
|
+
this.abortController?.abort();
|
|
154
|
+
this.abortController = null;
|
|
155
|
+
|
|
156
|
+
// Close MCP clients
|
|
157
|
+
for (const client of this.mcpClients) {
|
|
158
|
+
await client.close();
|
|
159
|
+
}
|
|
160
|
+
this.mcpClients = [];
|
|
161
|
+
this.mcpTools = {};
|
|
162
|
+
|
|
163
|
+
this._state = "disposed";
|
|
164
|
+
logger.info("MonoDriver disposed");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Core Methods
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
async *receive(message: UserMessage): AsyncIterable<DriverStreamEvent> {
|
|
172
|
+
if (this._state === "disposed") {
|
|
173
|
+
throw new Error("Cannot receive: Driver is disposed");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (this._state === "active") {
|
|
177
|
+
throw new Error("Cannot receive: Driver is already processing a message");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this._state = "active";
|
|
181
|
+
this.abortController = new AbortController();
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// Get history from Session
|
|
185
|
+
const history = this.session ? await this.session.getMessages() : [];
|
|
186
|
+
|
|
187
|
+
// Convert to Vercel AI SDK format
|
|
188
|
+
const messages = toVercelMessages(history);
|
|
189
|
+
|
|
190
|
+
// Add current user message
|
|
191
|
+
messages.push({
|
|
192
|
+
role: "user",
|
|
193
|
+
content:
|
|
194
|
+
typeof message.content === "string"
|
|
195
|
+
? message.content
|
|
196
|
+
: message.content.map((part) => {
|
|
197
|
+
if ("text" in part) return { type: "text" as const, text: part.text };
|
|
198
|
+
return { type: "text" as const, text: String(part) };
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
logger.debug("Sending message to LLM", {
|
|
203
|
+
provider: this.provider,
|
|
204
|
+
messageCount: messages.length,
|
|
205
|
+
agentId: this.config.agentId,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Call Vercel AI SDK (v6)
|
|
209
|
+
const result = streamText({
|
|
210
|
+
model: this.getModel(),
|
|
211
|
+
system: this.config.systemPrompt,
|
|
212
|
+
messages,
|
|
213
|
+
tools: this.mergeTools(),
|
|
214
|
+
stopWhen: stepCountIs(this.maxSteps),
|
|
215
|
+
abortSignal: this.abortController.signal,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Track state for event conversion
|
|
219
|
+
let messageStartEmitted = false;
|
|
220
|
+
// Track tool calls in current step for correct message ordering.
|
|
221
|
+
// AI SDK emits: tool-call → tool-result → finish-step
|
|
222
|
+
// Engine needs: AssistantMessage(with tool-calls) BEFORE ToolResultMessage
|
|
223
|
+
// So we inject message_stop before the first tool-result in each step.
|
|
224
|
+
let hasToolCallsInStep = false;
|
|
225
|
+
|
|
226
|
+
// Process fullStream (AI SDK v6 event types)
|
|
227
|
+
for await (const part of result.fullStream) {
|
|
228
|
+
if (this.abortController?.signal.aborted) {
|
|
229
|
+
yield createEvent("interrupted", { reason: "user" });
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
switch (part.type) {
|
|
234
|
+
case "start":
|
|
235
|
+
case "start-step":
|
|
236
|
+
if (!messageStartEmitted) {
|
|
237
|
+
const messageId = `msg_${Date.now()}`;
|
|
238
|
+
const model = this.config.model ?? this.getDefaultModel();
|
|
239
|
+
yield createEvent("message_start", { messageId, model });
|
|
240
|
+
messageStartEmitted = true;
|
|
241
|
+
}
|
|
242
|
+
hasToolCallsInStep = false;
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case "text-delta":
|
|
246
|
+
yield createEvent("text_delta", { text: part.text });
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case "tool-input-start":
|
|
250
|
+
yield createEvent("tool_use_start", {
|
|
251
|
+
toolCallId: part.id,
|
|
252
|
+
toolName: part.toolName,
|
|
253
|
+
});
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case "tool-input-delta":
|
|
257
|
+
yield createEvent("input_json_delta", {
|
|
258
|
+
partialJson: part.delta,
|
|
259
|
+
});
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case "tool-call":
|
|
263
|
+
hasToolCallsInStep = true;
|
|
264
|
+
yield createEvent("tool_use_stop", {
|
|
265
|
+
toolCallId: part.toolCallId,
|
|
266
|
+
toolName: part.toolName,
|
|
267
|
+
input: part.input as Record<string, unknown>,
|
|
268
|
+
});
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
case "tool-result":
|
|
272
|
+
// Flush AssistantMessage before first tool-result in this step.
|
|
273
|
+
// Ensures correct ordering: Assistant(tool-calls) → ToolResult
|
|
274
|
+
if (hasToolCallsInStep) {
|
|
275
|
+
yield createEvent("message_stop", {
|
|
276
|
+
stopReason: toStopReason("tool-calls"),
|
|
277
|
+
});
|
|
278
|
+
hasToolCallsInStep = false;
|
|
279
|
+
}
|
|
280
|
+
yield createEvent("tool_result", {
|
|
281
|
+
toolCallId: part.toolCallId,
|
|
282
|
+
result: part.output,
|
|
283
|
+
isError: false,
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
286
|
+
|
|
287
|
+
case "tool-error":
|
|
288
|
+
if (hasToolCallsInStep) {
|
|
289
|
+
yield createEvent("message_stop", {
|
|
290
|
+
stopReason: toStopReason("tool-calls"),
|
|
291
|
+
});
|
|
292
|
+
hasToolCallsInStep = false;
|
|
293
|
+
}
|
|
294
|
+
yield createEvent("tool_result", {
|
|
295
|
+
toolCallId: part.toolCallId,
|
|
296
|
+
result: part.error,
|
|
297
|
+
isError: true,
|
|
298
|
+
});
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case "finish-step":
|
|
302
|
+
// Emit usage data for this step
|
|
303
|
+
if (part.usage) {
|
|
304
|
+
yield createEvent("message_delta", {
|
|
305
|
+
usage: {
|
|
306
|
+
inputTokens: part.usage.inputTokens ?? 0,
|
|
307
|
+
outputTokens: part.usage.outputTokens ?? 0,
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Reset for next step so start-step emits a new message_start
|
|
312
|
+
messageStartEmitted = false;
|
|
313
|
+
break;
|
|
314
|
+
|
|
315
|
+
case "finish":
|
|
316
|
+
yield createEvent("message_stop", {
|
|
317
|
+
stopReason: toStopReason(part.finishReason),
|
|
318
|
+
});
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case "error":
|
|
322
|
+
yield createEvent("error", {
|
|
323
|
+
message: String(part.error),
|
|
324
|
+
errorCode: "stream_error",
|
|
325
|
+
});
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
331
|
+
yield createEvent("interrupted", { reason: "user" });
|
|
332
|
+
} else {
|
|
333
|
+
yield createEvent("error", {
|
|
334
|
+
message: error instanceof Error ? error.message : String(error),
|
|
335
|
+
errorCode: "runtime_error",
|
|
336
|
+
});
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
this._state = "idle";
|
|
341
|
+
this.abortController = null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interrupt(): void {
|
|
346
|
+
if (this._state !== "active") {
|
|
347
|
+
logger.debug("Interrupt called but no active operation");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
logger.debug("Interrupting MonoDriver");
|
|
352
|
+
this.abortController?.abort();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ============================================================================
|
|
356
|
+
// Private Methods
|
|
357
|
+
// ============================================================================
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Merge MCP tools and config tools into a single ToolSet.
|
|
361
|
+
* Config tools (bash etc.) take precedence over MCP tools with the same name.
|
|
362
|
+
*/
|
|
363
|
+
private mergeTools(): ToolSet | undefined {
|
|
364
|
+
const configTools = this.config.tools?.length ? toVercelTools(this.config.tools) : {};
|
|
365
|
+
const merged = { ...this.mcpTools, ...configTools };
|
|
366
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private getModel() {
|
|
370
|
+
const modelId = this.config.model ?? this.getDefaultModel();
|
|
371
|
+
const { apiKey } = this.config;
|
|
372
|
+
const baseURL = this.getBaseURL();
|
|
373
|
+
|
|
374
|
+
switch (this.provider) {
|
|
375
|
+
case "anthropic":
|
|
376
|
+
return createAnthropic({ apiKey, baseURL })(modelId);
|
|
377
|
+
case "openai":
|
|
378
|
+
return createOpenAI({ apiKey, baseURL })(modelId);
|
|
379
|
+
case "google":
|
|
380
|
+
return createGoogleGenerativeAI({ apiKey, baseURL })(modelId);
|
|
381
|
+
case "xai":
|
|
382
|
+
return createXai({ apiKey, baseURL })(modelId);
|
|
383
|
+
case "deepseek":
|
|
384
|
+
return createDeepSeek({ apiKey, baseURL })(modelId);
|
|
385
|
+
case "mistral":
|
|
386
|
+
return createMistral({ apiKey, baseURL })(modelId);
|
|
387
|
+
case "openai-compatible": {
|
|
388
|
+
if (!this.compatibleConfig) {
|
|
389
|
+
throw new Error("openai-compatible provider requires compatibleConfig in options");
|
|
390
|
+
}
|
|
391
|
+
const provider = createOpenAICompatible({
|
|
392
|
+
name: this.compatibleConfig.name,
|
|
393
|
+
baseURL: this.compatibleConfig.baseURL,
|
|
394
|
+
apiKey: this.compatibleConfig.apiKey ?? apiKey,
|
|
395
|
+
});
|
|
396
|
+
return provider.chatModel(modelId);
|
|
397
|
+
}
|
|
398
|
+
default:
|
|
399
|
+
return createAnthropic({ apiKey, baseURL })(modelId);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get the base URL for the provider SDK.
|
|
405
|
+
*
|
|
406
|
+
* Provider SDKs expect baseURL to include the version path (e.g. /v1).
|
|
407
|
+
* DriverConfig.baseUrl is the API root without version path.
|
|
408
|
+
* This method bridges the gap.
|
|
409
|
+
*/
|
|
410
|
+
private getBaseURL(): string | undefined {
|
|
411
|
+
if (!this.config.baseUrl) return undefined;
|
|
412
|
+
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
413
|
+
if (base.endsWith("/v1")) return base;
|
|
414
|
+
return `${base}/v1`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private getDefaultModel(): string {
|
|
418
|
+
switch (this.provider) {
|
|
419
|
+
case "anthropic":
|
|
420
|
+
return "claude-sonnet-4-20250514";
|
|
421
|
+
case "openai":
|
|
422
|
+
return "gpt-4o";
|
|
423
|
+
case "google":
|
|
424
|
+
return "gemini-2.0-flash";
|
|
425
|
+
case "xai":
|
|
426
|
+
return "grok-3";
|
|
427
|
+
case "deepseek":
|
|
428
|
+
return "deepseek-chat";
|
|
429
|
+
case "mistral":
|
|
430
|
+
return "mistral-large-latest";
|
|
431
|
+
case "openai-compatible":
|
|
432
|
+
return "default";
|
|
433
|
+
default:
|
|
434
|
+
return "claude-sonnet-4-20250514";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Create a MonoDriver instance
|
|
441
|
+
*/
|
|
442
|
+
export function createMonoDriver(config: MonoDriverConfig): Driver {
|
|
443
|
+
return new MonoDriver(config);
|
|
444
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message and Event Converters
|
|
3
|
+
*
|
|
4
|
+
* Converts between AgentX types and Vercel AI SDK v6 types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { tool, jsonSchema } from "ai";
|
|
8
|
+
import type { ModelMessage, ToolSet } from "ai";
|
|
9
|
+
import type { Message, ToolResultMessage } from "@agentxjs/core/agent";
|
|
10
|
+
import type { DriverStreamEvent, StopReason, ToolDefinition } from "@agentxjs/core/driver";
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Message Converters (AgentX → Vercel AI SDK v6)
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert AgentX Message to Vercel ModelMessage
|
|
18
|
+
*/
|
|
19
|
+
export function toVercelMessage(message: Message): ModelMessage | null {
|
|
20
|
+
switch (message.subtype) {
|
|
21
|
+
case "user":
|
|
22
|
+
return {
|
|
23
|
+
role: "user",
|
|
24
|
+
content:
|
|
25
|
+
typeof message.content === "string" ? message.content : extractText(message.content),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
case "assistant": {
|
|
29
|
+
// Assistant message may contain text and tool calls in content
|
|
30
|
+
const content = "content" in message ? message.content : "";
|
|
31
|
+
if (typeof content === "string") {
|
|
32
|
+
return { role: "assistant", content };
|
|
33
|
+
}
|
|
34
|
+
// Extract tool calls from content parts
|
|
35
|
+
const toolCalls = content.filter(
|
|
36
|
+
(p): p is { type: "tool-call"; id: string; name: string; input: Record<string, unknown> } =>
|
|
37
|
+
p.type === "tool-call"
|
|
38
|
+
);
|
|
39
|
+
if (toolCalls.length > 0) {
|
|
40
|
+
// Vercel AI SDK format: assistant with tool-call content
|
|
41
|
+
return {
|
|
42
|
+
role: "assistant",
|
|
43
|
+
content: [
|
|
44
|
+
...content
|
|
45
|
+
.filter((p) => p.type === "text")
|
|
46
|
+
.map((p) => ({ type: "text" as const, text: (p as { text: string }).text })),
|
|
47
|
+
...toolCalls.map((tc) => ({
|
|
48
|
+
type: "tool-call" as const,
|
|
49
|
+
toolCallId: tc.id,
|
|
50
|
+
toolName: tc.name,
|
|
51
|
+
input: tc.input,
|
|
52
|
+
})),
|
|
53
|
+
],
|
|
54
|
+
} as unknown as ModelMessage;
|
|
55
|
+
}
|
|
56
|
+
return { role: "assistant", content: extractText(content) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case "tool-result": {
|
|
60
|
+
const msg = message as ToolResultMessage;
|
|
61
|
+
return {
|
|
62
|
+
role: "tool",
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "tool-result" as const,
|
|
66
|
+
toolCallId: msg.toolCallId,
|
|
67
|
+
toolName: msg.toolResult.name,
|
|
68
|
+
output: msg.toolResult.output,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
} as unknown as ModelMessage;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convert array of AgentX Messages to Vercel ModelMessages
|
|
81
|
+
*/
|
|
82
|
+
export function toVercelMessages(messages: Message[]): ModelMessage[] {
|
|
83
|
+
const result: ModelMessage[] = [];
|
|
84
|
+
for (const message of messages) {
|
|
85
|
+
const converted = toVercelMessage(message);
|
|
86
|
+
if (converted) {
|
|
87
|
+
result.push(converted);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract text from any content shape
|
|
95
|
+
*/
|
|
96
|
+
function extractText(content: unknown): string {
|
|
97
|
+
if (typeof content === "string") return content;
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(content)) {
|
|
100
|
+
return content
|
|
101
|
+
.filter(
|
|
102
|
+
(part) =>
|
|
103
|
+
part !== null &&
|
|
104
|
+
typeof part === "object" &&
|
|
105
|
+
"text" in part &&
|
|
106
|
+
typeof part.text === "string"
|
|
107
|
+
)
|
|
108
|
+
.map((part) => part.text)
|
|
109
|
+
.join("");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return String(content ?? "");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Event Converters (Vercel AI SDK → AgentX)
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Map Vercel AI SDK v6 finish reason to AgentX StopReason
|
|
121
|
+
*/
|
|
122
|
+
export function toStopReason(finishReason: string | null | undefined): StopReason {
|
|
123
|
+
switch (finishReason) {
|
|
124
|
+
case "stop":
|
|
125
|
+
return "end_turn";
|
|
126
|
+
case "length":
|
|
127
|
+
return "max_tokens";
|
|
128
|
+
case "tool-calls":
|
|
129
|
+
return "tool_use";
|
|
130
|
+
case "content-filter":
|
|
131
|
+
return "content_filter";
|
|
132
|
+
case "error":
|
|
133
|
+
return "error";
|
|
134
|
+
default:
|
|
135
|
+
return "other";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create a DriverStreamEvent with timestamp
|
|
141
|
+
*/
|
|
142
|
+
export function createEvent<T extends DriverStreamEvent["type"]>(
|
|
143
|
+
type: T,
|
|
144
|
+
data: Extract<DriverStreamEvent, { type: T }>["data"]
|
|
145
|
+
): DriverStreamEvent {
|
|
146
|
+
return {
|
|
147
|
+
type,
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
data,
|
|
150
|
+
} as DriverStreamEvent;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Tool Converters (AgentX → Vercel AI SDK v6)
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Convert AgentX ToolDefinitions to Vercel AI SDK tool format
|
|
159
|
+
*
|
|
160
|
+
* Uses jsonSchema() instead of Zod to avoid adding Zod dependency to core.
|
|
161
|
+
* Type casts are needed to bridge our ToolDefinition.parameters (simplified
|
|
162
|
+
* JSON Schema) to the AI SDK's strict JSONSchema7 type.
|
|
163
|
+
*/
|
|
164
|
+
export function toVercelTools(tools: ToolDefinition[]): ToolSet {
|
|
165
|
+
const result: ToolSet = {};
|
|
166
|
+
for (const t of tools) {
|
|
167
|
+
result[t.name] = tool({
|
|
168
|
+
description: t.description,
|
|
169
|
+
|
|
170
|
+
inputSchema: jsonSchema(t.parameters as any),
|
|
171
|
+
execute: async (input) => t.execute(input as Record<string, unknown>),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentxjs/mono-driver
|
|
3
|
+
*
|
|
4
|
+
* Unified cross-platform Driver using Vercel AI SDK.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Multi-provider: Anthropic, OpenAI, Google (and more)
|
|
8
|
+
* - Cross-platform: Node.js, Bun, Cloudflare Workers, Edge Runtime
|
|
9
|
+
* - Lightweight: Direct HTTP API calls, no subprocess
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { createMonoDriver } from "@agentxjs/mono-driver";
|
|
14
|
+
*
|
|
15
|
+
* const driver = createMonoDriver({
|
|
16
|
+
* apiKey: process.env.ANTHROPIC_API_KEY!,
|
|
17
|
+
* agentId: "my-agent",
|
|
18
|
+
* systemPrompt: "You are helpful",
|
|
19
|
+
* options: {
|
|
20
|
+
* provider: "anthropic",
|
|
21
|
+
* maxSteps: 10,
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await driver.initialize();
|
|
26
|
+
*
|
|
27
|
+
* for await (const event of driver.receive({ content: "Hello" })) {
|
|
28
|
+
* if (event.type === "text_delta") {
|
|
29
|
+
* process.stdout.write(event.data.text);
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* await driver.dispose();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Main exports
|
|
38
|
+
export { MonoDriver, createMonoDriver } from "./MonoDriver";
|
|
39
|
+
|
|
40
|
+
// Types
|
|
41
|
+
export type {
|
|
42
|
+
MonoDriverConfig,
|
|
43
|
+
MonoDriverOptions,
|
|
44
|
+
MonoProvider,
|
|
45
|
+
MonoBuiltinProvider,
|
|
46
|
+
OpenAICompatibleConfig,
|
|
47
|
+
} from "./types";
|
|
48
|
+
|
|
49
|
+
// Converters (for advanced usage)
|
|
50
|
+
export { toVercelMessage, toVercelMessages, toStopReason, createEvent } from "./converters";
|
|
51
|
+
|
|
52
|
+
// Re-export Vercel AI SDK utilities for advanced usage
|
|
53
|
+
export { stepCountIs } from "ai";
|