@probelabs/probe 0.6.0-rc166 → 0.6.0-rc168

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.
@@ -0,0 +1,556 @@
1
+ /**
2
+ * Enhanced Claude Code Engine with session management and better streaming
3
+ */
4
+
5
+ import { spawn } from 'child_process';
6
+ import { randomBytes } from 'crypto';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import { EventEmitter } from 'events';
11
+ import { BuiltInMCPServer } from '../mcp/built-in-server.js';
12
+ import { Session } from '../shared/Session.js';
13
+
14
+ /**
15
+ * Enhanced Claude Code Engine
16
+ */
17
+ export async function createEnhancedClaudeCLIEngine(options = {}) {
18
+ const { agent, systemPrompt, customPrompt, debug, sessionId, allowedTools } = options;
19
+
20
+ // Create or reuse session
21
+ const session = new Session(
22
+ sessionId || randomBytes(8).toString('hex'),
23
+ debug
24
+ );
25
+
26
+ // Start built-in MCP server with ephemeral port
27
+ let mcpServer = null;
28
+ let mcpConfigPath = null;
29
+
30
+ if (agent) {
31
+ mcpServer = new BuiltInMCPServer(agent, {
32
+ port: 0, // Ephemeral port
33
+ host: '127.0.0.1',
34
+ debug: debug
35
+ });
36
+
37
+ const { host, port } = await mcpServer.start();
38
+
39
+ if (debug) {
40
+ console.log('[DEBUG] Built-in MCP server started');
41
+ console.log('[DEBUG] MCP URL:', `http://${host}:${port}/mcp`);
42
+ }
43
+
44
+ // Create MCP config for Claude Code to use
45
+ // Note: Claude Code currently requires spawning a process, not HTTP transport
46
+ // Keep built-in server running but also provide process-based config for CLI
47
+ mcpConfigPath = path.join(os.tmpdir(), `probe-mcp-${session.id}.json`);
48
+ const mcpConfig = {
49
+ mcpServers: {
50
+ probe: {
51
+ command: 'node',
52
+ args: [path.join(process.cwd(), 'mcp-probe-server.js')],
53
+ env: {
54
+ PROBE_WORKSPACE: process.cwd(),
55
+ DEBUG: debug ? 'true' : 'false'
56
+ }
57
+ }
58
+ }
59
+ };
60
+
61
+ await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
62
+ }
63
+
64
+ if (debug) {
65
+ console.log('[DEBUG] Enhanced Claude Code Engine');
66
+ console.log('[DEBUG] Session:', session.id);
67
+ console.log('[DEBUG] MCP Config:', mcpConfigPath);
68
+ }
69
+
70
+ // Combine prompts
71
+ const fullSystemPrompt = combinePrompts(systemPrompt, customPrompt, agent);
72
+
73
+ return {
74
+ sessionId: session.id,
75
+ session,
76
+
77
+ /**
78
+ * Query Claude with advanced streaming
79
+ */
80
+ async *query(prompt, opts = {}) {
81
+ const emitter = new EventEmitter();
82
+ let buffer = '';
83
+ let processEnded = false;
84
+ let currentToolCall = null;
85
+ let isSchemaMode = false;
86
+
87
+ // Check if this is a schema reminder or validation request
88
+ // In these cases, we treat Claude Code as a black box - just get the response
89
+ if (opts.schema || prompt.includes('JSON schema') || prompt.includes('mermaid diagram')) {
90
+ isSchemaMode = true;
91
+ if (debug) {
92
+ console.log('[DEBUG] Schema/validation mode - treating as black box');
93
+ }
94
+ }
95
+
96
+ // For schema mode, append the schema requirement to the prompt
97
+ let finalPrompt = prompt;
98
+ if (opts.schema && isSchemaMode) {
99
+ finalPrompt = `${prompt}\n\nPlease provide your response in the following JSON format:\n${opts.schema}`;
100
+ }
101
+
102
+ // Build command arguments
103
+ const args = buildClaudeArgs({
104
+ systemPrompt: fullSystemPrompt,
105
+ mcpConfigPath,
106
+ session,
107
+ debug,
108
+ prompt: finalPrompt, // Use finalPrompt which may include schema
109
+ allowedTools: allowedTools || opts.allowedTools // Support tool filtering
110
+ });
111
+
112
+ if (debug) {
113
+ console.log('[DEBUG] Executing: claude', args.join(' '));
114
+ }
115
+
116
+ // CRITICAL: Claude Code requires echo pipe to work when spawned from Node.js
117
+ // Without it, the process hangs indefinitely waiting for stdin
118
+ // This has been tested extensively - see CLAUDE_CLI_ECHO_REQUIREMENT.md
119
+ // DO NOT REMOVE THE echo "" | PREFIX
120
+ // SECURITY: Shell argument escaping using POSIX single-quote method
121
+ // Single quotes in POSIX shells protect ALL metacharacters (;|&$`><*?) except single quote itself
122
+ // The pattern 'text'\''more' correctly handles embedded quotes by:
123
+ // 1. Closing the current quote with '
124
+ // 2. Adding an escaped quote \'
125
+ // 3. Opening a new quote with '
126
+ // This is the standard POSIX-compliant method and is completely safe against injection
127
+ const shellCmd = `echo "" | claude ${args.map(arg => {
128
+ // Validate arg is a string (paranoid check)
129
+ if (typeof arg !== 'string') {
130
+ throw new TypeError(`Invalid argument type: expected string, got ${typeof arg}`);
131
+ }
132
+ // Escape single quotes using POSIX method: ' -> '\''
133
+ const escaped = arg.replace(/'/g, "'\\''");
134
+ // Wrap in single quotes for complete shell metacharacter protection
135
+ return `'${escaped}'`;
136
+ }).join(' ')}`;
137
+
138
+ if (debug) {
139
+ console.log('[DEBUG] Shell command length:', shellCmd.length);
140
+ // Don't log full command if too long (e.g. system prompt)
141
+ if (shellCmd.length < 500) {
142
+ console.log('[DEBUG] Shell command:', shellCmd);
143
+ } else {
144
+ console.log('[DEBUG] Shell command (truncated):', shellCmd.substring(0, 200) + '...');
145
+ }
146
+ }
147
+
148
+ // Initialize tool collector for batch emission
149
+ const toolCollector = [];
150
+
151
+ // Spawn using shell wrapper with echo pipe
152
+ const proc = spawn('sh', ['-c', shellCmd], {
153
+ env: { ...process.env, FORCE_COLOR: '0' },
154
+ stdio: ['ignore', 'pipe', 'pipe'] // Ignore stdin since echo handles it
155
+ });
156
+
157
+ // Handle stdout
158
+ proc.stdout.on('data', (data) => {
159
+ buffer += data.toString();
160
+ processJsonBuffer(buffer, emitter, session, debug, toolCollector);
161
+
162
+ // Keep only incomplete line in buffer
163
+ const lines = buffer.split('\n');
164
+ buffer = lines[lines.length - 1] || '';
165
+ });
166
+
167
+ // Handle stderr
168
+ proc.stderr.on('data', (data) => {
169
+ const stderr = data.toString();
170
+ if (debug) {
171
+ console.error('[STDERR]', stderr);
172
+ }
173
+
174
+ // Check for important errors
175
+ if (stderr.includes('command not found')) {
176
+ emitter.emit('error', new Error('Claude Code not found. Please install it first.'));
177
+ }
178
+ });
179
+
180
+ // Handle process end
181
+ proc.on('close', (code) => {
182
+ processEnded = true;
183
+ if (code !== 0 && debug) {
184
+ console.log(`[DEBUG] Process exited with code ${code}`);
185
+ }
186
+
187
+ // Process any remaining buffer
188
+ if (buffer.trim()) {
189
+ processJsonBuffer(buffer, emitter, session, debug, toolCollector);
190
+ }
191
+
192
+ // Emit collected tool events as batch
193
+ if (toolCollector.length > 0) {
194
+ emitter.emit('toolBatch', {
195
+ tools: toolCollector,
196
+ timestamp: new Date().toISOString()
197
+ });
198
+
199
+ if (debug) {
200
+ console.log(`[DEBUG] Emitting batch of ${toolCollector.length} tool events`);
201
+ }
202
+ }
203
+
204
+ emitter.emit('end');
205
+ });
206
+
207
+ proc.on('error', (error) => {
208
+ emitter.emit('error', error);
209
+ });
210
+
211
+ // Stream generator
212
+ const messageQueue = [];
213
+ let resolver = null;
214
+
215
+ emitter.on('message', (msg) => {
216
+ messageQueue.push(msg);
217
+ if (resolver) {
218
+ resolver();
219
+ resolver = null;
220
+ }
221
+ });
222
+
223
+ emitter.on('toolBatch', (batch) => {
224
+ messageQueue.push({ type: 'toolBatch', ...batch });
225
+ if (resolver) {
226
+ resolver();
227
+ resolver = null;
228
+ }
229
+ });
230
+
231
+ emitter.on('end', () => {
232
+ processEnded = true;
233
+ if (resolver) {
234
+ resolver();
235
+ resolver = null;
236
+ }
237
+ });
238
+
239
+ emitter.on('error', (error) => {
240
+ messageQueue.push({ type: 'error', error });
241
+ if (resolver) {
242
+ resolver();
243
+ resolver = null;
244
+ }
245
+ });
246
+
247
+ // Process messages
248
+ while (!processEnded || messageQueue.length > 0) {
249
+ if (messageQueue.length > 0) {
250
+ const msg = messageQueue.shift();
251
+
252
+ if (msg.type === 'text') {
253
+ yield { type: 'text', content: msg.content };
254
+ } else if (msg.type === 'tool_use') {
255
+ // Start tool execution
256
+ currentToolCall = msg;
257
+ yield {
258
+ type: 'text',
259
+ content: `\n🔧 Using ${msg.name}: ${JSON.stringify(msg.input)}\n`
260
+ };
261
+
262
+ // Execute tool
263
+ const result = await executeProbleTool(agent, msg.name, msg.input);
264
+ yield { type: 'text', content: `${result}\n` };
265
+ } else if (msg.type === 'toolBatch') {
266
+ // Pass through the tool batch for ProbeAgent to emit
267
+ yield { type: 'toolBatch', tools: msg.tools, timestamp: msg.timestamp };
268
+ } else if (msg.type === 'session_update') {
269
+ // Session was updated with conversation ID
270
+ if (debug) {
271
+ console.log('[DEBUG] Session updated:', msg.conversationId);
272
+ }
273
+ } else if (msg.type === 'error') {
274
+ yield { type: 'error', error: msg.error };
275
+ break;
276
+ }
277
+ } else if (!processEnded) {
278
+ // Wait for more messages
279
+ await new Promise(resolve => {
280
+ resolver = resolve;
281
+ });
282
+ }
283
+ }
284
+
285
+ // Increment message count
286
+ session.incrementMessageCount();
287
+
288
+ // Return session info for potential resume
289
+ yield {
290
+ type: 'metadata',
291
+ data: {
292
+ sessionId: session.id,
293
+ conversationId: session.conversationId,
294
+ messageCount: session.messageCount
295
+ }
296
+ };
297
+ },
298
+
299
+ /**
300
+ * Get session info
301
+ */
302
+ getSession() {
303
+ return session.getInfo();
304
+ },
305
+
306
+ /**
307
+ * Clean up - MUST be called to stop MCP server and clean resources
308
+ */
309
+ async close() {
310
+ try {
311
+ // Stop built-in MCP server
312
+ if (mcpServer) {
313
+ await mcpServer.stop();
314
+ if (debug) {
315
+ console.log('[DEBUG] Built-in MCP server stopped');
316
+ }
317
+ }
318
+
319
+ // Remove temporary MCP config file
320
+ if (mcpConfigPath) {
321
+ await fs.unlink(mcpConfigPath).catch(() => {});
322
+ if (debug) {
323
+ console.log('[DEBUG] MCP config file removed');
324
+ }
325
+ }
326
+
327
+ if (debug) {
328
+ console.log('[DEBUG] Engine closed, session:', session.id);
329
+ }
330
+ } catch (error) {
331
+ if (debug) {
332
+ console.error('[DEBUG] Error during cleanup:', error.message);
333
+ }
334
+ }
335
+ }
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Process JSON buffer and emit messages
341
+ */
342
+ function processJsonBuffer(buffer, emitter, session, debug, toolCollector = null) {
343
+ const lines = buffer.split('\n');
344
+
345
+ for (const line of lines) {
346
+ if (!line.trim()) continue;
347
+
348
+ try {
349
+ const parsed = JSON.parse(line);
350
+
351
+ // Claude Code might return an array of messages
352
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
353
+
354
+ for (const msg of messages) {
355
+
356
+ switch (msg.type) {
357
+ case 'result':
358
+ // Claude Code returns a complete result object
359
+ if (msg.result) {
360
+ emitter.emit('message', { type: 'text', content: msg.result });
361
+ }
362
+ if (msg.session_id) {
363
+ session.setConversationId(msg.session_id);
364
+ emitter.emit('message', { type: 'session_update', conversationId: msg.session_id });
365
+ }
366
+ break;
367
+
368
+ case 'conversation':
369
+ session.setConversationId(msg.id);
370
+ emitter.emit('message', { type: 'session_update', conversationId: msg.id });
371
+ break;
372
+
373
+ case 'text':
374
+ if (msg.text) {
375
+ emitter.emit('message', { type: 'text', content: msg.text });
376
+ }
377
+ break;
378
+
379
+ case 'assistant':
380
+ // Claude Code emits assistant messages when using internal agents/tools
381
+ if (msg.message && msg.message.content) {
382
+ // Extract text from the content array
383
+ for (const content of msg.message.content) {
384
+ if (content.type === 'text' && content.text) {
385
+ emitter.emit('message', { type: 'text', content: content.text });
386
+ } else if (content.type === 'tool_use') {
387
+ // Collect tool call for batch emission
388
+ if (toolCollector) {
389
+ toolCollector.push({
390
+ timestamp: new Date().toISOString(),
391
+ name: content.name,
392
+ args: content.input || {},
393
+ id: content.id,
394
+ status: 'started'
395
+ });
396
+ }
397
+ // Internal tool use - already handled by Claude Code
398
+ if (debug) {
399
+ console.log('[DEBUG] Assistant internal tool use:', content.name);
400
+ }
401
+ }
402
+ }
403
+ }
404
+ break;
405
+
406
+ case 'tool_use':
407
+ // Collect tool call for batch emission
408
+ if (toolCollector) {
409
+ toolCollector.push({
410
+ timestamp: new Date().toISOString(),
411
+ name: msg.name,
412
+ args: msg.input || {},
413
+ id: msg.id,
414
+ status: 'started'
415
+ });
416
+ }
417
+ emitter.emit('message', {
418
+ type: 'tool_use',
419
+ id: msg.id,
420
+ name: msg.name,
421
+ input: msg.input
422
+ });
423
+ break;
424
+
425
+ case 'tool_result':
426
+ // Mark tool as completed in collector
427
+ if (toolCollector && msg.tool_use_id) {
428
+ // Find the matching tool call and update its status
429
+ const toolCall = toolCollector.find(t => t.id === msg.tool_use_id);
430
+ if (toolCall) {
431
+ toolCall.status = 'completed';
432
+ toolCall.resultPreview = msg.content ?
433
+ (typeof msg.content === 'string' ?
434
+ msg.content.substring(0, 200) :
435
+ JSON.stringify(msg.content).substring(0, 200)) + '...' :
436
+ 'No Result';
437
+ }
438
+ }
439
+ // Tool results are handled internally
440
+ if (debug) {
441
+ console.log('[DEBUG] Tool result:', msg);
442
+ }
443
+ break;
444
+
445
+ case 'error':
446
+ emitter.emit('error', new Error(msg.message || 'Unknown error'));
447
+ break;
448
+
449
+ default:
450
+ if (debug) {
451
+ console.log('[DEBUG] Unknown message type:', msg.type);
452
+ console.log('[DEBUG] Full message:', JSON.stringify(msg).substring(0, 200));
453
+ }
454
+ }
455
+ } // Close inner for loop for messages array
456
+ } catch (e) {
457
+ // Not valid JSON, might be partial
458
+ if (debug && line.trim()) {
459
+ console.log('[DEBUG] Non-JSON output:', line);
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Build claude command arguments
467
+ */
468
+ function buildClaudeArgs({ systemPrompt, mcpConfigPath, session, debug, prompt, allowedTools }) {
469
+ const args = [
470
+ '-p', // Short form of --print
471
+ prompt, // The prompt text goes right after -p
472
+ '--output-format', 'json'
473
+ ];
474
+
475
+ // Add session resume if available
476
+ const resumeArgs = session.getResumeArgs();
477
+ if (resumeArgs.length > 0) {
478
+ args.push(...resumeArgs);
479
+ }
480
+
481
+ // Add system prompt
482
+ if (systemPrompt) {
483
+ args.push('--system-prompt', systemPrompt);
484
+ }
485
+
486
+ // Add MCP config
487
+ args.push('--mcp-config', mcpConfigPath);
488
+
489
+ // Add allowed tools filter if specified
490
+ // If no filter specified, allow all probe tools
491
+ if (allowedTools && Array.isArray(allowedTools) && allowedTools.length > 0) {
492
+ // Convert tool names to MCP format: mcp__probe__<toolname>
493
+ const mcpTools = allowedTools.map(tool =>
494
+ tool.startsWith('mcp__') ? tool : `mcp__probe__${tool}`
495
+ ).join(',');
496
+ args.push('--allowedTools', mcpTools);
497
+ } else {
498
+ // Default: allow all probe tools
499
+ args.push('--allowedTools', 'mcp__probe__*');
500
+ }
501
+
502
+ // Add debug flag
503
+ if (debug) {
504
+ args.push('--verbose');
505
+ }
506
+
507
+ return args;
508
+ }
509
+
510
+ /**
511
+ * Execute Probe tool through agent
512
+ */
513
+ async function executeProbleTool(agent, toolName, params) {
514
+ if (!agent || !agent.toolImplementations) {
515
+ return 'Tool execution not available';
516
+ }
517
+
518
+ // Remove MCP prefix: mcp__probe__<toolname> -> <toolname>
519
+ const name = toolName.replace(/^mcp__probe__/, '');
520
+ const tool = agent.toolImplementations[name];
521
+
522
+ if (!tool) {
523
+ return `Unknown tool: ${name}`;
524
+ }
525
+
526
+ try {
527
+ const result = await tool.execute(params);
528
+ return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
529
+ } catch (error) {
530
+ return `Tool error: ${error.message}`;
531
+ }
532
+ }
533
+
534
+ // Old createEnhancedMCPConfig function removed - now using built-in MCP server
535
+
536
+ /**
537
+ * Combine prompts intelligently
538
+ */
539
+ function combinePrompts(systemPrompt, customPrompt, agent) {
540
+ // For Claude Code, the systemPrompt already contains all necessary instructions
541
+ // from getClaudeNativeSystemPrompt(), so we don't need to add a base prompt
542
+
543
+ // If only customPrompt is provided (no systemPrompt), use it as the main prompt
544
+ if (!systemPrompt && customPrompt) {
545
+ return customPrompt;
546
+ }
547
+
548
+ // If systemPrompt is provided, it's already complete from getClaudeNativeSystemPrompt
549
+ // Just add customPrompt if available
550
+ if (systemPrompt && customPrompt) {
551
+ return systemPrompt + '\n\n## Additional Instructions\n' + customPrompt;
552
+ }
553
+
554
+ // Return systemPrompt as-is if no customPrompt
555
+ return systemPrompt || '';
556
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Enhanced Vercel AI SDK Engine with proper tool and prompt support
3
+ */
4
+
5
+ import { streamText } from 'ai';
6
+
7
+ /**
8
+ * Create an enhanced Vercel AI SDK engine with full tool support
9
+ * @param {Object} agent - The ProbeAgent instance
10
+ * @returns {Object} Engine interface
11
+ */
12
+ export function createEnhancedVercelEngine(agent) {
13
+ return {
14
+ /**
15
+ * Query the model using existing Vercel AI SDK implementation
16
+ * @param {string} prompt - The prompt to send
17
+ * @param {Object} options - Additional options
18
+ * @returns {AsyncIterable} Response stream
19
+ */
20
+ async *query(prompt, options = {}) {
21
+ // Get the system message with tools embedded (existing behavior)
22
+ const systemMessage = await agent.getSystemMessage();
23
+
24
+ // Build messages array with system prompt
25
+ const messages = [
26
+ { role: 'system', content: systemMessage },
27
+ ...agent.history,
28
+ { role: 'user', content: prompt }
29
+ ];
30
+
31
+ // Use existing streamText with retry and fallback
32
+ const result = await agent.streamTextWithRetryAndFallback({
33
+ model: agent.provider(agent.model),
34
+ messages,
35
+ maxTokens: options.maxTokens || agent.maxResponseTokens,
36
+ temperature: options.temperature || 0.3,
37
+ // Note: Vercel AI SDK doesn't use structured tools for XML format
38
+ // The tools are embedded in the system prompt
39
+ experimental_telemetry: options.telemetry
40
+ });
41
+
42
+ // Stream the response
43
+ let fullContent = '';
44
+ for await (const chunk of result.textStream) {
45
+ fullContent += chunk;
46
+ yield { type: 'text', content: chunk };
47
+ }
48
+
49
+ // Parse XML tool calls from the response if any
50
+ // This maintains compatibility with existing XML tool format
51
+ const toolCalls = agent.parseXmlToolCalls ? agent.parseXmlToolCalls(fullContent) : null;
52
+ if (toolCalls && toolCalls.length > 0) {
53
+ yield { type: 'tool_calls', toolCalls };
54
+ }
55
+
56
+ // Handle finish reason
57
+ if (result.finishReason) {
58
+ yield { type: 'finish', reason: result.finishReason };
59
+ }
60
+ },
61
+
62
+ /**
63
+ * Get available tools for this engine
64
+ */
65
+ getTools() {
66
+ return agent.toolImplementations || {};
67
+ },
68
+
69
+ /**
70
+ * Get system prompt for this engine
71
+ */
72
+ async getSystemPrompt() {
73
+ return agent.getSystemMessage();
74
+ },
75
+
76
+ /**
77
+ * Optional cleanup
78
+ */
79
+ async close() {
80
+ // Nothing to cleanup for Vercel AI
81
+ }
82
+ };
83
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Vercel AI SDK Engine - wraps existing ProbeAgent logic
3
+ * This maintains full backward compatibility
4
+ */
5
+
6
+ import { streamText } from 'ai';
7
+
8
+ /**
9
+ * Create a Vercel AI SDK engine
10
+ * @param {Object} agent - The ProbeAgent instance
11
+ * @returns {Object} Engine interface
12
+ */
13
+ export function createVercelEngine(agent) {
14
+ return {
15
+ /**
16
+ * Query the model using existing Vercel AI SDK implementation
17
+ * @param {string} prompt - The prompt to send
18
+ * @param {Object} options - Additional options
19
+ * @returns {AsyncIterable} Response stream
20
+ */
21
+ async *query(prompt, options = {}) {
22
+ // Build messages array
23
+ const messages = [
24
+ ...agent.history,
25
+ { role: 'user', content: prompt }
26
+ ];
27
+
28
+ // Use existing streamText with retry and fallback
29
+ const result = await agent.streamTextWithRetryAndFallback({
30
+ model: agent.provider(agent.model),
31
+ messages,
32
+ maxTokens: options.maxTokens || agent.maxResponseTokens,
33
+ temperature: options.temperature,
34
+ tools: options.tools,
35
+ toolChoice: options.toolChoice,
36
+ experimental_telemetry: options.telemetry
37
+ });
38
+
39
+ // Stream the response
40
+ for await (const chunk of result.textStream) {
41
+ yield { type: 'text', content: chunk };
42
+ }
43
+
44
+ // Handle tool calls if any
45
+ if (result.toolCalls && result.toolCalls.length > 0) {
46
+ yield { type: 'tool_calls', toolCalls: result.toolCalls };
47
+ }
48
+
49
+ // Handle finish reason
50
+ if (result.finishReason) {
51
+ yield { type: 'finish', reason: result.finishReason };
52
+ }
53
+ },
54
+
55
+ /**
56
+ * Optional cleanup
57
+ */
58
+ async close() {
59
+ // Nothing to cleanup for Vercel AI
60
+ }
61
+ };
62
+ }