@toolplex/ai-engine 0.1.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.
Files changed (88) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +292 -0
  3. package/dist/adapters/index.d.ts +9 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +9 -0
  6. package/dist/adapters/index.js.map +1 -0
  7. package/dist/adapters/types.d.ts +137 -0
  8. package/dist/adapters/types.d.ts.map +1 -0
  9. package/dist/adapters/types.js +14 -0
  10. package/dist/adapters/types.js.map +1 -0
  11. package/dist/core/ChatEngine.d.ts +47 -0
  12. package/dist/core/ChatEngine.d.ts.map +1 -0
  13. package/dist/core/ChatEngine.js +355 -0
  14. package/dist/core/ChatEngine.js.map +1 -0
  15. package/dist/core/ToolBuilder.d.ts +25 -0
  16. package/dist/core/ToolBuilder.d.ts.map +1 -0
  17. package/dist/core/ToolBuilder.js +215 -0
  18. package/dist/core/ToolBuilder.js.map +1 -0
  19. package/dist/core/index.d.ts +6 -0
  20. package/dist/core/index.d.ts.map +1 -0
  21. package/dist/core/index.js +6 -0
  22. package/dist/core/index.js.map +1 -0
  23. package/dist/index.d.ts +41 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +49 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mcp/DefaultStdioTransportFactory.d.ts +27 -0
  28. package/dist/mcp/DefaultStdioTransportFactory.d.ts.map +1 -0
  29. package/dist/mcp/DefaultStdioTransportFactory.js +60 -0
  30. package/dist/mcp/DefaultStdioTransportFactory.js.map +1 -0
  31. package/dist/mcp/MCPClient.d.ts +60 -0
  32. package/dist/mcp/MCPClient.d.ts.map +1 -0
  33. package/dist/mcp/MCPClient.js +164 -0
  34. package/dist/mcp/MCPClient.js.map +1 -0
  35. package/dist/mcp/index.d.ts +10 -0
  36. package/dist/mcp/index.d.ts.map +1 -0
  37. package/dist/mcp/index.js +11 -0
  38. package/dist/mcp/index.js.map +1 -0
  39. package/dist/mcp/paths.d.ts +16 -0
  40. package/dist/mcp/paths.d.ts.map +1 -0
  41. package/dist/mcp/paths.js +58 -0
  42. package/dist/mcp/paths.js.map +1 -0
  43. package/dist/mcp/types.d.ts +85 -0
  44. package/dist/mcp/types.d.ts.map +1 -0
  45. package/dist/mcp/types.js +7 -0
  46. package/dist/mcp/types.js.map +1 -0
  47. package/dist/providers/index.d.ts +40 -0
  48. package/dist/providers/index.d.ts.map +1 -0
  49. package/dist/providers/index.js +148 -0
  50. package/dist/providers/index.js.map +1 -0
  51. package/dist/providers/toolplex.d.ts +43 -0
  52. package/dist/providers/toolplex.d.ts.map +1 -0
  53. package/dist/providers/toolplex.js +168 -0
  54. package/dist/providers/toolplex.js.map +1 -0
  55. package/dist/types/index.d.ts +218 -0
  56. package/dist/types/index.d.ts.map +1 -0
  57. package/dist/types/index.js +8 -0
  58. package/dist/types/index.js.map +1 -0
  59. package/dist/utils/index.d.ts +8 -0
  60. package/dist/utils/index.d.ts.map +1 -0
  61. package/dist/utils/index.js +8 -0
  62. package/dist/utils/index.js.map +1 -0
  63. package/dist/utils/models.d.ts +30 -0
  64. package/dist/utils/models.d.ts.map +1 -0
  65. package/dist/utils/models.js +52 -0
  66. package/dist/utils/models.js.map +1 -0
  67. package/dist/utils/schema.d.ts +74 -0
  68. package/dist/utils/schema.d.ts.map +1 -0
  69. package/dist/utils/schema.js +253 -0
  70. package/dist/utils/schema.js.map +1 -0
  71. package/package.json +70 -0
  72. package/src/adapters/index.ts +9 -0
  73. package/src/adapters/types.ts +241 -0
  74. package/src/core/ChatEngine.ts +464 -0
  75. package/src/core/ToolBuilder.ts +323 -0
  76. package/src/core/index.ts +6 -0
  77. package/src/index.ts +86 -0
  78. package/src/mcp/DefaultStdioTransportFactory.ts +71 -0
  79. package/src/mcp/MCPClient.ts +209 -0
  80. package/src/mcp/index.ts +24 -0
  81. package/src/mcp/paths.ts +91 -0
  82. package/src/mcp/types.ts +93 -0
  83. package/src/providers/index.ts +177 -0
  84. package/src/providers/toolplex.ts +217 -0
  85. package/src/types/index.ts +290 -0
  86. package/src/utils/index.ts +8 -0
  87. package/src/utils/models.ts +59 -0
  88. package/src/utils/schema.ts +307 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * @toolplex/ai-engine - Adapter Interface
3
+ *
4
+ * The EngineAdapter interface defines how the AI engine interacts with
5
+ * the host platform. This abstraction allows the same engine core to
6
+ * run in Electron, server-side (cloud), or CLI environments.
7
+ *
8
+ * Implementations:
9
+ * - ElectronAdapter: Desktop app with IPC, webContents, confirmations
10
+ * - HTTPAdapter: Cloud/server with HTTP streaming, no confirmations
11
+ * - CLIAdapter: CLI with terminal I/O
12
+ */
13
+
14
+ import type {
15
+ ProviderCredentials,
16
+ ConfirmationRequest,
17
+ ConfirmationResult,
18
+ MCPResult,
19
+ MCPSessionInfo,
20
+ MCPTool,
21
+ MCPToolResult,
22
+ UsageData,
23
+ ChatSession,
24
+ ChatMessage,
25
+ } from "../types/index.js";
26
+
27
+ // ============================================================================
28
+ // Event Emitter Interface
29
+ // ============================================================================
30
+
31
+ /**
32
+ * Interface for emitting engine events to the platform
33
+ */
34
+ export interface EngineEventEmitter {
35
+ /** Emit a text chunk during streaming */
36
+ emitChunk(streamId: string, chunk: string): void;
37
+
38
+ /** Emit stream completion */
39
+ emitComplete(streamId: string, fullText: string, usage?: UsageData): void;
40
+
41
+ /** Emit stream error */
42
+ emitError(streamId: string, error: string): void;
43
+
44
+ /** Emit tool input start (for streaming tool arguments) */
45
+ emitToolInputStart(
46
+ streamId: string,
47
+ toolCallId: string,
48
+ toolName: string,
49
+ ): void;
50
+
51
+ /** Emit tool input delta (streaming argument chunks) */
52
+ emitToolInputDelta(
53
+ streamId: string,
54
+ toolCallId: string,
55
+ argsDelta: string,
56
+ ): void;
57
+
58
+ /** Emit tool execution result */
59
+ emitToolResult(
60
+ streamId: string,
61
+ toolCallId: string,
62
+ result: MCPToolResult,
63
+ toolName: string,
64
+ args: any,
65
+ ): void;
66
+ }
67
+
68
+ // ============================================================================
69
+ // Confirmation Handler Interface
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Interface for handling user confirmations
74
+ * Desktop: Shows modal dialogs via IPC
75
+ * Cloud: Auto-approves or uses policy-based decisions
76
+ * CLI: Prompts in terminal
77
+ */
78
+ export interface ConfirmationHandler {
79
+ /**
80
+ * Request user confirmation for a tool operation
81
+ * @param streamId - The current stream ID
82
+ * @param request - The confirmation request details
83
+ * @returns Confirmation result with allowed/denied and any edits
84
+ */
85
+ requestConfirmation(
86
+ streamId: string,
87
+ request: ConfirmationRequest,
88
+ ): Promise<ConfirmationResult>;
89
+
90
+ /**
91
+ * Whether this handler supports interactive confirmations
92
+ * Cloud handlers may return false to auto-approve based on policy
93
+ */
94
+ isInteractive(): boolean;
95
+ }
96
+
97
+ // ============================================================================
98
+ // MCP Transport Interface
99
+ // ============================================================================
100
+
101
+ /**
102
+ * Interface for MCP transport operations
103
+ * Abstracts away the transport mechanism (stdio, HTTP, etc.)
104
+ */
105
+ export interface MCPTransportAdapter {
106
+ /** Create/connect MCP transport for a session */
107
+ createTransport(
108
+ sessionId: string,
109
+ apiKey: string,
110
+ sessionResumeHistory?: string,
111
+ ): Promise<MCPResult>;
112
+
113
+ /** Get session info */
114
+ getSessionInfo(sessionId: string): MCPSessionInfo;
115
+
116
+ /** List available tools from MCP server */
117
+ listTools(sessionId: string): Promise<{ tools: MCPTool[] }>;
118
+
119
+ /** Call an MCP tool */
120
+ callTool(
121
+ sessionId: string,
122
+ toolName: string,
123
+ args: any,
124
+ ): Promise<MCPToolResult>;
125
+
126
+ /** Destroy/disconnect MCP transport */
127
+ destroyTransport(sessionId: string): Promise<MCPResult>;
128
+ }
129
+
130
+ // ============================================================================
131
+ // Credentials Provider Interface
132
+ // ============================================================================
133
+
134
+ /**
135
+ * Interface for accessing API credentials
136
+ * Different platforms load credentials differently
137
+ */
138
+ export interface CredentialsProvider {
139
+ /** Get provider credentials for AI SDK */
140
+ getCredentials(): Promise<ProviderCredentials>;
141
+
142
+ /** Get ToolPlex API key specifically */
143
+ getToolPlexApiKey(): Promise<string>;
144
+ }
145
+
146
+ // ============================================================================
147
+ // Persistence Interface (Optional)
148
+ // ============================================================================
149
+
150
+ /**
151
+ * Interface for persisting chat sessions and messages
152
+ * Optional - cloud may use different storage than desktop
153
+ */
154
+ export interface PersistenceAdapter {
155
+ // Session operations
156
+ createSession(metadata?: Record<string, any>): Promise<ChatSession>;
157
+ getSession(sessionId: string): Promise<ChatSession | null>;
158
+ updateSession(
159
+ sessionId: string,
160
+ updates: Partial<ChatSession>,
161
+ ): Promise<void>;
162
+ deleteSession(sessionId: string): Promise<void>;
163
+
164
+ // Message operations
165
+ saveMessage(
166
+ message: Omit<ChatMessage, "id" | "createdAt">,
167
+ ): Promise<ChatMessage>;
168
+ getMessages(sessionId: string): Promise<ChatMessage[]>;
169
+ updateMessage(
170
+ messageId: string,
171
+ updates: Partial<ChatMessage>,
172
+ ): Promise<void>;
173
+ deleteMessage(messageId: string): Promise<void>;
174
+ }
175
+
176
+ // ============================================================================
177
+ // Logger Interface
178
+ // ============================================================================
179
+
180
+ /**
181
+ * Interface for logging
182
+ * Allows different logging implementations per platform
183
+ */
184
+ export interface LoggerAdapter {
185
+ debug(message: string, meta?: Record<string, any>): void;
186
+ info(message: string, meta?: Record<string, any>): void;
187
+ warn(message: string, meta?: Record<string, any>): void;
188
+ error(message: string, meta?: Record<string, any>): void;
189
+ }
190
+
191
+ // ============================================================================
192
+ // Main Engine Adapter Interface
193
+ // ============================================================================
194
+
195
+ /**
196
+ * The main adapter interface that platforms must implement
197
+ * This brings together all the sub-interfaces needed by the engine
198
+ */
199
+ export interface EngineAdapter {
200
+ /** Event emitter for streaming events */
201
+ readonly events: EngineEventEmitter;
202
+
203
+ /** Confirmation handler for user approvals */
204
+ readonly confirmations: ConfirmationHandler;
205
+
206
+ /** MCP transport adapter */
207
+ readonly mcp: MCPTransportAdapter;
208
+
209
+ /** Credentials provider */
210
+ readonly credentials: CredentialsProvider;
211
+
212
+ /** Logger */
213
+ readonly logger: LoggerAdapter;
214
+
215
+ /** Persistence adapter (optional) */
216
+ readonly persistence?: PersistenceAdapter;
217
+
218
+ /** Client version string (for API headers) */
219
+ getClientVersion(): string;
220
+
221
+ /**
222
+ * Initialize the adapter
223
+ * Called once when engine is created
224
+ */
225
+ initialize(): Promise<void>;
226
+
227
+ /**
228
+ * Cleanup/shutdown the adapter
229
+ * Called when engine is destroyed
230
+ */
231
+ shutdown(): Promise<void>;
232
+ }
233
+
234
+ // ============================================================================
235
+ // Adapter Factory Type
236
+ // ============================================================================
237
+
238
+ /**
239
+ * Factory function type for creating adapters
240
+ */
241
+ export type AdapterFactory<TConfig = any> = (config: TConfig) => EngineAdapter;
@@ -0,0 +1,464 @@
1
+ /**
2
+ * @toolplex/ai-engine - Chat Engine
3
+ *
4
+ * Core streaming engine that orchestrates AI chat sessions.
5
+ * Uses adapters for all platform-specific I/O operations.
6
+ */
7
+
8
+ import { streamText, stepCountIs } from "ai";
9
+ import type { CoreMessage } from "ai";
10
+ import { randomUUID } from "crypto";
11
+
12
+ import type { EngineAdapter } from "../adapters/types.js";
13
+ import type {
14
+ StreamOptions,
15
+ StreamResult,
16
+ EngineConfig,
17
+ FileAttachment,
18
+ ModelConfigFlags,
19
+ } from "../types/index.js";
20
+ import { getModel, toolplexUsageMap } from "../providers/index.js";
21
+ import { buildMCPTools } from "./ToolBuilder.js";
22
+
23
+ export interface ChatEngineOptions {
24
+ adapter: EngineAdapter;
25
+ config?: EngineConfig;
26
+ }
27
+
28
+ export class ChatEngine {
29
+ private adapter: EngineAdapter;
30
+ private config: EngineConfig;
31
+ private initialized: boolean = false;
32
+
33
+ constructor(options: ChatEngineOptions) {
34
+ this.adapter = options.adapter;
35
+ this.config = {
36
+ maxSteps: 50,
37
+ debug: false,
38
+ hiddenTools: ["initialize_toolplex"],
39
+ ...options.config,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Initialize the engine
45
+ */
46
+ async initialize(): Promise<void> {
47
+ if (this.initialized) return;
48
+ await this.adapter.initialize();
49
+ this.initialized = true;
50
+ }
51
+
52
+ /**
53
+ * Shutdown the engine
54
+ */
55
+ async shutdown(): Promise<void> {
56
+ if (!this.initialized) return;
57
+ await this.adapter.shutdown();
58
+ this.initialized = false;
59
+ }
60
+
61
+ /**
62
+ * Initialize MCP for a session
63
+ */
64
+ async initializeMCP(sessionId: string): Promise<void> {
65
+ const apiKey = await this.adapter.credentials.getToolPlexApiKey();
66
+ const sessionInfo = this.adapter.mcp.getSessionInfo(sessionId);
67
+
68
+ if (!sessionInfo.exists) {
69
+ this.adapter.logger.debug("ChatEngine: Initializing MCP transport", {
70
+ sessionId,
71
+ });
72
+ const result = await this.adapter.mcp.createTransport(sessionId, apiKey);
73
+
74
+ if (!result.success) {
75
+ throw new Error(`Failed to create MCP transport: ${result.error}`);
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Initialize a session with ToolPlex context
82
+ */
83
+ async initializeSession(
84
+ sessionId: string,
85
+ modelId: string,
86
+ provider: string,
87
+ ): Promise<{ success: boolean; context?: string; error?: string }> {
88
+ try {
89
+ this.adapter.logger.debug(
90
+ "ChatEngine: Initializing session with ToolPlex",
91
+ {
92
+ sessionId,
93
+ modelId,
94
+ provider,
95
+ },
96
+ );
97
+
98
+ // Initialize MCP transport
99
+ await this.initializeMCP(sessionId);
100
+
101
+ // Extract model metadata
102
+ const modelParts = modelId.split("/");
103
+ const modelName = modelParts[modelParts.length - 1] || modelId;
104
+
105
+ const toolArgs = {
106
+ llm_context: {
107
+ model_family: provider,
108
+ model_name: modelName,
109
+ model_version: modelId,
110
+ chat_client: "toolplex",
111
+ },
112
+ };
113
+
114
+ // Call initialize_toolplex to get the context
115
+ const result = await this.adapter.mcp.callTool(
116
+ sessionId,
117
+ "initialize_toolplex",
118
+ toolArgs,
119
+ );
120
+
121
+ // Extract text content from the result
122
+ let contextText = "";
123
+ if (result && typeof result === "object" && result.content) {
124
+ for (const item of result.content) {
125
+ if (item.type === "text" && item.text) {
126
+ contextText += item.text + "\n\n";
127
+ }
128
+ }
129
+ } else if (typeof result === "string") {
130
+ contextText = result;
131
+ }
132
+
133
+ return {
134
+ success: true,
135
+ context: contextText.trim(),
136
+ };
137
+ } catch (error) {
138
+ this.adapter.logger.error("ChatEngine: Failed to initialize session", {
139
+ sessionId,
140
+ error,
141
+ });
142
+ return {
143
+ success: false,
144
+ error: error instanceof Error ? error.message : "Unknown error",
145
+ };
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Stream a chat completion
151
+ */
152
+ async stream(options: StreamOptions): Promise<StreamResult> {
153
+ const credentials = await this.adapter.credentials.getCredentials();
154
+ const streamId = options.streamId || randomUUID();
155
+
156
+ const {
157
+ sessionId,
158
+ modelId,
159
+ provider,
160
+ messages,
161
+ tools: providedTools,
162
+ temperature,
163
+ topP,
164
+ fileAttachments,
165
+ modelConfig,
166
+ } = options;
167
+
168
+ this.adapter.logger.debug("ChatEngine: Starting stream", {
169
+ sessionId,
170
+ modelId,
171
+ provider,
172
+ messageCount: messages.length,
173
+ hasTools: !!providedTools,
174
+ hasAttachments: !!fileAttachments?.length,
175
+ streamId,
176
+ });
177
+
178
+ // Create abort controller
179
+ const abortController = new AbortController();
180
+
181
+ // Get the model
182
+ const model = getModel(modelId, credentials, {
183
+ logger: this.adapter.logger,
184
+ clientVersion: this.adapter.getClientVersion(),
185
+ });
186
+
187
+ // Build MCP tools
188
+ let mcpTools: Record<string, any> = {};
189
+ const apiKey = await this.adapter.credentials.getToolPlexApiKey();
190
+ if (apiKey) {
191
+ try {
192
+ await this.initializeMCP(sessionId);
193
+ mcpTools = await buildMCPTools({
194
+ sessionId,
195
+ streamId,
196
+ modelId,
197
+ abortSignal: abortController.signal,
198
+ adapter: this.adapter,
199
+ hiddenTools: this.config.hiddenTools,
200
+ });
201
+ } catch (error) {
202
+ this.adapter.logger.error(
203
+ "ChatEngine: Failed to initialize MCP tools",
204
+ {
205
+ sessionId,
206
+ error,
207
+ },
208
+ );
209
+ // Continue without tools
210
+ }
211
+ }
212
+
213
+ // Merge tools
214
+ const allTools = { ...providedTools, ...mcpTools };
215
+
216
+ // Process messages
217
+ const processedMessages = this.processMessages(
218
+ messages,
219
+ fileAttachments,
220
+ modelConfig,
221
+ );
222
+
223
+ // Track usage data
224
+ let capturedUsage: {
225
+ prompt_tokens: number;
226
+ completion_tokens: number;
227
+ total_tokens: number;
228
+ } | null = null;
229
+
230
+ // Promise for onFinish coordination
231
+ let resolveOnFinish: (() => void) | null = null;
232
+ const onFinishPromise = new Promise<void>((resolve) => {
233
+ resolveOnFinish = resolve;
234
+ });
235
+
236
+ // Prepare stream options
237
+ const streamTextOptions: any = {
238
+ model,
239
+ messages: processedMessages,
240
+ tools: allTools,
241
+ stopWhen: stepCountIs(this.config.maxSteps!),
242
+ temperature,
243
+ topP,
244
+ abortSignal: abortController.signal,
245
+ };
246
+
247
+ // Enforce maxTokens if specified by model config
248
+ if (modelConfig?.enforceMaxTokens && modelConfig?.maxOutputTokens) {
249
+ streamTextOptions.maxTokens = modelConfig.maxOutputTokens;
250
+ }
251
+
252
+ // Start streaming
253
+ const result = streamText({
254
+ ...streamTextOptions,
255
+ headers: {
256
+ "x-session-id": sessionId,
257
+ ...(provider === "openrouter" && {
258
+ "HTTP-Referer": "https://toolplex.ai",
259
+ "X-Title": "ToolPlex AI",
260
+ }),
261
+ },
262
+ onChunk: async (event: any) => {
263
+ // Capture usage data
264
+ if (provider === "toolplex" && event.chunk) {
265
+ const chunk = event.chunk as any;
266
+
267
+ if (chunk.providerMetadata?.usage || chunk.usage) {
268
+ const usage = chunk.providerMetadata?.usage || chunk.usage;
269
+ const promptTokens = usage.prompt_tokens || usage.input_tokens || 0;
270
+ const completionTokens =
271
+ usage.completion_tokens || usage.output_tokens || 0;
272
+ const totalTokens =
273
+ usage.total_tokens || promptTokens + completionTokens;
274
+
275
+ capturedUsage = {
276
+ prompt_tokens: promptTokens,
277
+ completion_tokens: completionTokens,
278
+ total_tokens: totalTokens,
279
+ };
280
+ }
281
+ }
282
+ },
283
+ onStepFinish: async (event: any) => {
284
+ // Emit step finish event if there are tool calls
285
+ const hasToolCalls = event.toolCalls && event.toolCalls.length > 0;
286
+
287
+ if (hasToolCalls) {
288
+ this.adapter.logger.debug(
289
+ "ChatEngine: Step finished with tool calls",
290
+ {
291
+ sessionId,
292
+ textLength: event.text?.length || 0,
293
+ toolCallCount: event.toolCalls.length,
294
+ },
295
+ );
296
+ }
297
+ },
298
+ onFinish: async (completion: any) => {
299
+ this.adapter.logger.debug("ChatEngine: Stream finished", {
300
+ sessionId,
301
+ textLength: completion.text?.length,
302
+ finishReason: completion.finishReason,
303
+ usage: completion.usage,
304
+ });
305
+
306
+ // Get usage data
307
+ let usageSource = completion.usage;
308
+
309
+ if (provider === "toolplex") {
310
+ const mapUsage = toolplexUsageMap.get(sessionId);
311
+ if (mapUsage) {
312
+ usageSource = mapUsage;
313
+ toolplexUsageMap.delete(sessionId);
314
+ } else if (capturedUsage) {
315
+ usageSource = capturedUsage;
316
+ }
317
+ }
318
+
319
+ // Emit complete event
320
+ this.adapter.events.emitComplete(
321
+ streamId,
322
+ completion.text || "",
323
+ usageSource
324
+ ? {
325
+ promptTokens:
326
+ usageSource.prompt_tokens ||
327
+ usageSource.inputTokens ||
328
+ usageSource.promptTokens ||
329
+ 0,
330
+ completionTokens:
331
+ usageSource.completion_tokens ||
332
+ usageSource.outputTokens ||
333
+ usageSource.completionTokens ||
334
+ 0,
335
+ totalTokens:
336
+ usageSource.total_tokens || usageSource.totalTokens || 0,
337
+ }
338
+ : undefined,
339
+ );
340
+
341
+ if (resolveOnFinish) {
342
+ resolveOnFinish();
343
+ }
344
+ },
345
+ onError: (error) => {
346
+ this.adapter.logger.error("ChatEngine: Stream error", {
347
+ error,
348
+ sessionId,
349
+ modelId,
350
+ });
351
+
352
+ this.adapter.events.emitError(
353
+ streamId,
354
+ error instanceof Error ? error.message : String(error),
355
+ );
356
+
357
+ if (resolveOnFinish) {
358
+ resolveOnFinish();
359
+ }
360
+ },
361
+ });
362
+
363
+ return {
364
+ streamId,
365
+ textStream: result.textStream,
366
+ fullStream: result.fullStream,
367
+ onFinishPromise,
368
+ abort: async () => {
369
+ this.adapter.logger.debug("ChatEngine: Aborting stream", {
370
+ streamId,
371
+ sessionId,
372
+ });
373
+ abortController.abort();
374
+ },
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Process messages for streaming (handle attachments, filter empty blocks)
380
+ */
381
+ private processMessages(
382
+ messages: CoreMessage[],
383
+ fileAttachments?: FileAttachment[],
384
+ modelConfig?: ModelConfigFlags,
385
+ ): CoreMessage[] {
386
+ let processedMessages: CoreMessage[] = [...messages];
387
+
388
+ // Filter empty text blocks (unless model requires preserving them)
389
+ if (!modelConfig?.preserveEmptyContentBlocks) {
390
+ processedMessages = processedMessages.map((msg) => {
391
+ if (
392
+ (msg.role === "user" || msg.role === "assistant") &&
393
+ Array.isArray(msg.content)
394
+ ) {
395
+ const filteredContent = msg.content.filter((part: any) => {
396
+ if (part.type !== "text") return true;
397
+ return part.text && part.text.trim().length > 0;
398
+ });
399
+
400
+ return {
401
+ ...msg,
402
+ content: filteredContent.length > 0 ? filteredContent : "",
403
+ } as CoreMessage;
404
+ }
405
+ return msg;
406
+ });
407
+ }
408
+
409
+ // Handle file attachments
410
+ if (fileAttachments && fileAttachments.length > 0) {
411
+ const lastMessage = processedMessages[processedMessages.length - 1];
412
+ if (lastMessage && lastMessage.role === "user") {
413
+ const textContent =
414
+ typeof lastMessage.content === "string" ? lastMessage.content : "";
415
+
416
+ const parts: any[] = textContent.trim()
417
+ ? [{ type: "text", text: textContent }]
418
+ : [];
419
+
420
+ for (const attachment of fileAttachments) {
421
+ const mimeType = attachment.mimeType || (attachment as any).type;
422
+
423
+ if (!mimeType) {
424
+ parts.push({
425
+ type: "text",
426
+ text: `[Attached file: ${attachment.name} - type unknown]`,
427
+ });
428
+ continue;
429
+ }
430
+
431
+ if (mimeType.startsWith("image/")) {
432
+ parts.push({
433
+ type: "image",
434
+ image: attachment.data,
435
+ mimeType: mimeType,
436
+ });
437
+ } else if (
438
+ mimeType === "application/pdf" ||
439
+ mimeType.startsWith("text/") ||
440
+ mimeType.startsWith("application/")
441
+ ) {
442
+ parts.push({
443
+ type: "file",
444
+ data: Buffer.from(attachment.data, "base64"),
445
+ mediaType: mimeType,
446
+ });
447
+ } else {
448
+ parts.push({
449
+ type: "text",
450
+ text: `[Attached file: ${attachment.name}]`,
451
+ });
452
+ }
453
+ }
454
+
455
+ processedMessages[processedMessages.length - 1] = {
456
+ ...lastMessage,
457
+ content: parts,
458
+ };
459
+ }
460
+ }
461
+
462
+ return processedMessages;
463
+ }
464
+ }