@objectstack/service-ai 4.0.0 → 4.0.2

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 (40) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +20 -0
  3. package/dist/index.cjs +1245 -54
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +344 -77
  6. package/dist/index.d.ts +344 -77
  7. package/dist/index.js +1230 -51
  8. package/dist/index.js.map +1 -1
  9. package/package.json +26 -4
  10. package/src/__tests__/ai-service.test.ts +248 -27
  11. package/src/__tests__/auth-and-toolcalling.test.ts +627 -0
  12. package/src/__tests__/chatbot-features.test.ts +229 -82
  13. package/src/__tests__/metadata-tools.test.ts +964 -0
  14. package/src/__tests__/objectql-conversation-service.test.ts +34 -16
  15. package/src/__tests__/vercel-stream-encoder.test.ts +263 -0
  16. package/src/adapters/index.ts +2 -0
  17. package/src/adapters/memory-adapter.ts +17 -9
  18. package/src/adapters/vercel-adapter.ts +148 -0
  19. package/src/agent-runtime.ts +27 -3
  20. package/src/agents/index.ts +1 -0
  21. package/src/agents/metadata-assistant-agent.ts +87 -0
  22. package/src/ai-service.ts +174 -22
  23. package/src/conversation/in-memory-conversation-service.ts +2 -2
  24. package/src/conversation/objectql-conversation-service.ts +67 -18
  25. package/src/index.ts +22 -3
  26. package/src/plugin.ts +166 -9
  27. package/src/routes/agent-routes.ts +28 -3
  28. package/src/routes/ai-routes.ts +231 -14
  29. package/src/routes/index.ts +1 -1
  30. package/src/stream/index.ts +3 -0
  31. package/src/stream/vercel-stream-encoder.ts +129 -0
  32. package/src/tools/add-field.tool.ts +70 -0
  33. package/src/tools/create-object.tool.ts +66 -0
  34. package/src/tools/delete-field.tool.ts +38 -0
  35. package/src/tools/describe-metadata-object.tool.ts +32 -0
  36. package/src/tools/index.ts +12 -1
  37. package/src/tools/list-metadata-objects.tool.ts +34 -0
  38. package/src/tools/metadata-tools.ts +430 -0
  39. package/src/tools/modify-field.tool.ts +44 -0
  40. package/src/tools/tool-registry.ts +32 -9
@@ -0,0 +1,87 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import type { Agent } from '@objectstack/spec';
4
+
5
+ /**
6
+ * Built-in `metadata_assistant` agent definition.
7
+ *
8
+ * This agent powers AI-driven metadata management — users can create objects,
9
+ * add/modify/delete fields, and inspect schema definitions through natural
10
+ * language conversation.
11
+ *
12
+ * It is registered automatically by the AI service plugin alongside the
13
+ * `data_chat` agent when the metadata service is available.
14
+ *
15
+ * @example
16
+ * ```
17
+ * POST /api/v1/ai/agents/metadata_assistant/chat
18
+ * {
19
+ * "messages": [{ "role": "user", "content": "Create a contracts table with name, value, and status fields" }],
20
+ * "context": {}
21
+ * }
22
+ * ```
23
+ */
24
+ export const METADATA_ASSISTANT_AGENT: Agent = {
25
+ name: 'metadata_assistant',
26
+ label: 'Metadata Assistant',
27
+ role: 'Schema Architect',
28
+ instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
29
+
30
+ Capabilities:
31
+ - Create new data objects (tables) with fields
32
+ - Add fields (columns) to existing objects
33
+ - Modify field properties (label, type, required, default value)
34
+ - Delete fields from objects
35
+ - List all registered metadata objects and their schemas
36
+ - Describe the full schema of a specific object
37
+
38
+ Guidelines:
39
+ 1. Before creating a new object, use list_metadata_objects to check if a similar one already exists.
40
+ 2. Before modifying or deleting fields, use describe_metadata_object to understand the current schema.
41
+ 3. Always use snake_case for object names and field names (e.g. project_task, due_date).
42
+ 4. Suggest meaningful field types based on the user's description (e.g. "deadline" → date, "active" → boolean).
43
+ 5. When creating objects, propose a reasonable set of initial fields based on the entity type.
44
+ 6. Explain what changes you are about to make before executing them.
45
+ 7. After making changes, confirm the result by describing the updated schema.
46
+ 8. For destructive operations (deleting fields), always warn the user about potential data loss.
47
+ 9. Always answer in the same language the user is using.
48
+ 10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
49
+
50
+ model: {
51
+ provider: 'openai',
52
+ model: 'gpt-4',
53
+ temperature: 0.2,
54
+ maxTokens: 4096,
55
+ },
56
+
57
+ tools: [
58
+ { type: 'action', name: 'create_object', description: 'Create a new data object (table)' },
59
+ { type: 'action', name: 'add_field', description: 'Add a field to an existing object' },
60
+ { type: 'action', name: 'modify_field', description: 'Modify an existing field definition' },
61
+ { type: 'action', name: 'delete_field', description: 'Delete a field from an object' },
62
+ { type: 'query', name: 'list_metadata_objects', description: 'List all metadata objects' },
63
+ { type: 'query', name: 'describe_metadata_object', description: 'Describe an object schema' },
64
+ ],
65
+
66
+ active: true,
67
+ visibility: 'global',
68
+
69
+ guardrails: {
70
+ maxTokensPerInvocation: 8192,
71
+ maxExecutionTimeSec: 60,
72
+ blockedTopics: ['drop_database', 'raw_sql', 'system_tables'],
73
+ },
74
+
75
+ planning: {
76
+ strategy: 'react',
77
+ maxIterations: 10,
78
+ allowReplan: true,
79
+ },
80
+
81
+ memory: {
82
+ shortTerm: {
83
+ maxMessages: 30,
84
+ maxTokens: 8192,
85
+ },
86
+ },
87
+ };
package/src/ai-service.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
3
  import type {
4
- AIMessage,
4
+ ModelMessage,
5
+ ToolCallPart,
6
+ TextStreamPart,
7
+ ToolSet,
5
8
  AIRequestOptions,
6
9
  AIResult,
7
- AIStreamEvent,
8
10
  IAIService,
9
11
  IAIConversationService,
10
12
  ChatWithToolsOptions,
@@ -14,8 +16,28 @@ import type { Logger } from '@objectstack/spec/contracts';
14
16
  import { createLogger } from '@objectstack/core';
15
17
  import { MemoryLLMAdapter } from './adapters/memory-adapter.js';
16
18
  import { ToolRegistry } from './tools/tool-registry.js';
19
+ import type { ToolExecutionResult } from './tools/tool-registry.js';
17
20
  import { InMemoryConversationService } from './conversation/in-memory-conversation-service.js';
18
21
 
22
+ // ── Stream event helpers ──────────────────────────────────────────
23
+ // These helpers construct properly-typed Vercel AI SDK stream parts
24
+ // to avoid repeated `as unknown as TextStreamPart<ToolSet>` casts.
25
+
26
+ /** Create a text-delta stream part. */
27
+ function textDeltaPart(id: string, text: string): TextStreamPart<ToolSet> {
28
+ return { type: 'text-delta', id, text } as TextStreamPart<ToolSet>;
29
+ }
30
+
31
+ /** Create a finish stream part from an AIResult. */
32
+ function finishPart(result?: AIResult): TextStreamPart<ToolSet> {
33
+ return {
34
+ type: 'finish',
35
+ finishReason: 'stop',
36
+ totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
37
+ rawFinishReason: 'stop',
38
+ } as unknown as TextStreamPart<ToolSet>;
39
+ }
40
+
19
41
  /**
20
42
  * Configuration for AIService.
21
43
  */
@@ -70,7 +92,7 @@ export class AIService implements IAIService {
70
92
 
71
93
  // ── IAIService implementation ──────────────────────────────────
72
94
 
73
- async chat(messages: AIMessage[], options?: AIRequestOptions): Promise<AIResult> {
95
+ async chat(messages: ModelMessage[], options?: AIRequestOptions): Promise<AIResult> {
74
96
  this.logger.debug('[AI] chat', { messageCount: messages.length, model: options?.model });
75
97
  return this.adapter.chat(messages, options);
76
98
  }
@@ -81,16 +103,16 @@ export class AIService implements IAIService {
81
103
  }
82
104
 
83
105
  async *streamChat(
84
- messages: AIMessage[],
106
+ messages: ModelMessage[],
85
107
  options?: AIRequestOptions,
86
- ): AsyncIterable<AIStreamEvent> {
108
+ ): AsyncIterable<TextStreamPart<ToolSet>> {
87
109
  this.logger.debug('[AI] streamChat', { messageCount: messages.length, model: options?.model });
88
110
 
89
111
  if (!this.adapter.streamChat) {
90
112
  // Fallback: emit the entire response as a single text-delta + finish
91
113
  const result = await this.adapter.chat(messages, options);
92
- yield { type: 'text-delta', textDelta: result.content };
93
- yield { type: 'finish', result };
114
+ yield textDeltaPart('fallback', result.content);
115
+ yield finishPart(result);
94
116
  return;
95
117
  }
96
118
 
@@ -116,6 +138,12 @@ export class AIService implements IAIService {
116
138
  /** Default maximum iterations for the tool call loop. */
117
139
  static readonly DEFAULT_MAX_ITERATIONS = 10;
118
140
 
141
+ /** Extract the text value from a ToolExecutionResult's output. */
142
+ private static extractOutputText(tr: ToolExecutionResult): string {
143
+ return tr.output && typeof tr.output === 'object' && 'value' in tr.output
144
+ ? String(tr.output.value) : 'unknown error';
145
+ }
146
+
119
147
  /**
120
148
  * Chat with automatic tool call resolution.
121
149
  *
@@ -128,11 +156,11 @@ export class AIService implements IAIService {
128
156
  * maximum number of iterations (`maxIterations`) is reached.
129
157
  */
130
158
  async chatWithTools(
131
- messages: AIMessage[],
159
+ messages: ModelMessage[],
132
160
  options?: ChatWithToolsOptions,
133
161
  ): Promise<AIResult> {
134
- // Destructure maxIterations out so it is never forwarded to the adapter
135
- const { maxIterations: maxIter, ...restOptions } = options ?? {};
162
+ // Destructure loop-specific options so they are never forwarded to the adapter
163
+ const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
136
164
  const maxIterations = maxIter ?? AIService.DEFAULT_MAX_ITERATIONS;
137
165
  const registeredTools = this.toolRegistry.getAll();
138
166
 
@@ -152,12 +180,17 @@ export class AIService implements IAIService {
152
180
  // Working copy of the conversation
153
181
  const conversation = [...messages];
154
182
 
183
+ // Track errors across iterations for diagnostics
184
+ const toolErrors: Array<{ iteration: number; toolName: string; error: string }> = [];
185
+
155
186
  this.logger.debug('[AI] chatWithTools start', {
156
187
  messageCount: conversation.length,
157
188
  toolCount: mergedTools.length,
158
189
  maxIterations,
159
190
  });
160
191
 
192
+ let abortedByCallback = false;
193
+
161
194
  for (let iteration = 0; iteration < maxIterations; iteration++) {
162
195
  const result = await this.adapter.chat(conversation, chatOptions);
163
196
 
@@ -169,32 +202,62 @@ export class AIService implements IAIService {
169
202
 
170
203
  this.logger.debug('[AI] chatWithTools tool calls', {
171
204
  iteration,
172
- calls: result.toolCalls.map(tc => tc.name),
205
+ calls: result.toolCalls.map(tc => tc.toolName),
173
206
  });
174
207
 
175
208
  // Append the assistant's response (with tool call metadata) to the conversation
209
+ const assistantContent: Array<{ type: 'text'; text: string } | ToolCallPart> = [];
210
+ if (result.content) assistantContent.push({ type: 'text', text: result.content });
211
+ assistantContent.push(...result.toolCalls);
176
212
  conversation.push({
177
213
  role: 'assistant',
178
- content: result.content ?? '',
179
- toolCalls: result.toolCalls,
180
- });
214
+ content: assistantContent,
215
+ } as ModelMessage);
181
216
 
182
217
  // Execute all tool calls in parallel
183
- const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
218
+ const toolResults: ToolExecutionResult[] = await this.toolRegistry.executeAll(result.toolCalls);
184
219
 
185
- // Append each tool result as a `role: 'tool'` message
220
+ // Process results: track errors and honour onToolError callback
186
221
  for (const tr of toolResults) {
222
+ if (tr.isError) {
223
+ // Match tool call by toolCallId for robust attribution
224
+ const matchedCall = result.toolCalls!.find(tc => tc.toolCallId === tr.toolCallId);
225
+ const toolName = matchedCall?.toolName ?? 'unknown';
226
+ const errorText = AIService.extractOutputText(tr);
227
+ const errorEntry = { iteration, toolName, error: errorText };
228
+ toolErrors.push(errorEntry);
229
+ this.logger.warn('[AI] chatWithTools tool error', errorEntry);
230
+
231
+ if (onToolError && matchedCall) {
232
+ const action = onToolError(matchedCall, errorText);
233
+ if (action === 'abort') {
234
+ abortedByCallback = true;
235
+ }
236
+ }
237
+ }
238
+
239
+ // Append each tool result as a `role: 'tool'` message
187
240
  conversation.push({
188
241
  role: 'tool',
189
- content: tr.content,
190
- toolCallId: tr.toolCallId,
191
- });
242
+ content: [tr],
243
+ } as ModelMessage);
192
244
  }
245
+
246
+ if (abortedByCallback) {
247
+ break;
248
+ }
249
+ }
250
+
251
+ // Distinguish user-driven abort from max-iterations exhaustion in logs
252
+ if (abortedByCallback) {
253
+ this.logger.warn('[AI] chatWithTools aborted by onToolError callback', { toolErrors });
254
+ } else {
255
+ this.logger.warn('[AI] chatWithTools max iterations reached, forcing final response', {
256
+ toolErrors: toolErrors.length > 0 ? toolErrors : undefined,
257
+ });
193
258
  }
194
259
 
195
- // If we exhausted the loop without a final response, make one last
196
- // call *without* tools so the model is forced to produce text.
197
- this.logger.warn('[AI] chatWithTools max iterations reached, forcing final response');
260
+ // Make one last call *without* tools so the model is forced to produce text.
198
261
  const finalResult = await this.adapter.chat(conversation, {
199
262
  ...chatOptions,
200
263
  tools: undefined,
@@ -202,4 +265,93 @@ export class AIService implements IAIService {
202
265
  });
203
266
  return finalResult;
204
267
  }
268
+
269
+ /**
270
+ * Stream chat with automatic tool call resolution.
271
+ *
272
+ * Works like {@link chatWithTools} but yields SSE events. When the model
273
+ * requests tool calls during streaming, they are executed and the results
274
+ * fed back until a final text stream is produced.
275
+ */
276
+ async *streamChatWithTools(
277
+ messages: ModelMessage[],
278
+ options?: ChatWithToolsOptions,
279
+ ): AsyncIterable<TextStreamPart<ToolSet>> {
280
+ const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
281
+ const maxIterations = maxIter ?? AIService.DEFAULT_MAX_ITERATIONS;
282
+ const registeredTools = this.toolRegistry.getAll();
283
+
284
+ const mergedTools = [
285
+ ...registeredTools,
286
+ ...(restOptions.tools ?? []),
287
+ ];
288
+
289
+ const chatOptions: AIRequestOptions = {
290
+ ...restOptions,
291
+ tools: mergedTools.length > 0 ? mergedTools : undefined,
292
+ toolChoice: mergedTools.length > 0 ? (restOptions.toolChoice ?? 'auto') : undefined,
293
+ };
294
+
295
+ const conversation = [...messages];
296
+ let abortedByCallback = false;
297
+
298
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
299
+ // Use non-streaming chat for intermediate tool-call rounds
300
+ const result = await this.adapter.chat(conversation, chatOptions);
301
+
302
+ if (!result.toolCalls || result.toolCalls.length === 0) {
303
+ // Final round — return the probed result without an extra model call
304
+ yield textDeltaPart('stream', result.content);
305
+ yield finishPart(result);
306
+ return;
307
+ }
308
+
309
+ // Emit tool-call events so the client can see tool execution progress
310
+ for (const tc of result.toolCalls) {
311
+ yield { type: 'tool-call', toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input } as TextStreamPart<ToolSet>;
312
+ }
313
+
314
+ const assistantContent: Array<{ type: 'text'; text: string } | ToolCallPart> = [];
315
+ if (result.content) assistantContent.push({ type: 'text', text: result.content });
316
+ assistantContent.push(...result.toolCalls);
317
+ conversation.push({
318
+ role: 'assistant',
319
+ content: assistantContent,
320
+ } as ModelMessage);
321
+
322
+ const toolResults: ToolExecutionResult[] = await this.toolRegistry.executeAll(result.toolCalls);
323
+
324
+ for (const tr of toolResults) {
325
+ if (tr.isError && onToolError) {
326
+ const matchedCall = result.toolCalls!.find(tc => tc.toolCallId === tr.toolCallId);
327
+ if (matchedCall) {
328
+ const errorText = AIService.extractOutputText(tr);
329
+ const action = onToolError(matchedCall, errorText);
330
+ if (action === 'abort') {
331
+ abortedByCallback = true;
332
+ }
333
+ }
334
+ }
335
+ conversation.push({
336
+ role: 'tool',
337
+ content: [tr],
338
+ } as ModelMessage);
339
+ }
340
+
341
+ if (abortedByCallback) {
342
+ break;
343
+ }
344
+ }
345
+
346
+ // Forced final response (no tools) — either aborted or max iterations
347
+ if (abortedByCallback) {
348
+ this.logger.warn('[AI] streamChatWithTools aborted by onToolError callback');
349
+ } else {
350
+ this.logger.warn('[AI] streamChatWithTools max iterations reached');
351
+ }
352
+ const finalOptions = { ...chatOptions, tools: undefined, toolChoice: undefined };
353
+ const result = await this.adapter.chat(conversation, finalOptions);
354
+ yield textDeltaPart('stream', result.content);
355
+ yield finishPart(result);
356
+ }
205
357
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type {
4
4
  AIConversation,
5
- AIMessage,
5
+ ModelMessage,
6
6
  IAIConversationService,
7
7
  } from '@objectstack/spec/contracts';
8
8
 
@@ -75,7 +75,7 @@ export class InMemoryConversationService implements IAIConversationService {
75
75
  return results;
76
76
  }
77
77
 
78
- async addMessage(conversationId: string, message: AIMessage): Promise<AIConversation> {
78
+ async addMessage(conversationId: string, message: ModelMessage): Promise<AIConversation> {
79
79
  const conversation = this.store.get(conversationId);
80
80
  if (!conversation) {
81
81
  throw new Error(`Conversation "${conversationId}" not found`);
@@ -3,7 +3,9 @@
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import type {
5
5
  AIConversation,
6
- AIMessage,
6
+ ModelMessage,
7
+ ToolCallPart,
8
+ ToolResultPart,
7
9
  IAIConversationService,
8
10
  IDataEngine,
9
11
  } from '@objectstack/spec/contracts';
@@ -157,7 +159,7 @@ export class ObjectQLConversationService implements IAIConversationService {
157
159
  return conversations;
158
160
  }
159
161
 
160
- async addMessage(conversationId: string, message: AIMessage): Promise<AIConversation> {
162
+ async addMessage(conversationId: string, message: ModelMessage): Promise<AIConversation> {
161
163
  // Verify conversation exists
162
164
  const row: DbConversationRow | null = await this.engine.findOne(CONVERSATIONS_OBJECT, {
163
165
  where: { id: conversationId },
@@ -169,14 +171,39 @@ export class ObjectQLConversationService implements IAIConversationService {
169
171
  const now = new Date().toISOString();
170
172
  const msgId = `msg_${randomUUID()}`;
171
173
 
174
+ // Extract flat fields from the discriminated union
175
+ let contentStr: string;
176
+ let toolCallsJson: string | null = null;
177
+ let toolCallId: string | null = null;
178
+
179
+ if (message.role === 'system' || message.role === 'user') {
180
+ contentStr = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
181
+ } else if (message.role === 'assistant') {
182
+ if (typeof message.content === 'string') {
183
+ contentStr = message.content;
184
+ } else {
185
+ const parts = message.content;
186
+ const textParts = parts.filter((p): p is { type: 'text'; text: string } => p.type === 'text').map(p => p.text);
187
+ const toolCalls = parts.filter(p => p.type === 'tool-call');
188
+ contentStr = textParts.join('');
189
+ if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
190
+ }
191
+ } else if (message.role === 'tool') {
192
+ contentStr = JSON.stringify(message.content);
193
+ const firstResult = Array.isArray(message.content) ? message.content[0] : undefined;
194
+ if (firstResult && 'toolCallId' in firstResult) toolCallId = firstResult.toolCallId;
195
+ } else {
196
+ contentStr = '';
197
+ }
198
+
172
199
  // Insert the message
173
200
  await this.engine.insert(MESSAGES_OBJECT, {
174
201
  id: msgId,
175
202
  conversation_id: conversationId,
176
203
  role: message.role,
177
- content: message.content,
178
- tool_calls: message.toolCalls ? JSON.stringify(message.toolCalls) : null,
179
- tool_call_id: message.toolCallId ?? null,
204
+ content: contentStr,
205
+ tool_calls: toolCallsJson,
206
+ tool_call_id: toolCallId,
180
207
  created_at: now,
181
208
  });
182
209
 
@@ -233,20 +260,42 @@ export class ObjectQLConversationService implements IAIConversationService {
233
260
  }
234
261
 
235
262
  /**
236
- * Map a database row to an AIMessage.
263
+ * Map a database row to a ModelMessage.
237
264
  */
238
- private toMessage(row: DbMessageRow): AIMessage {
239
- const msg: AIMessage = {
240
- role: row.role,
241
- content: row.content,
242
- };
243
- const toolCalls = this.safeParse<any[]>(row.tool_calls);
244
- if (toolCalls) {
245
- msg.toolCalls = toolCalls;
246
- }
247
- if (row.tool_call_id) {
248
- msg.toolCallId = row.tool_call_id;
265
+ private toMessage(row: DbMessageRow): ModelMessage {
266
+ switch (row.role) {
267
+ case 'system':
268
+ return { role: 'system', content: row.content };
269
+ case 'user':
270
+ return { role: 'user', content: row.content };
271
+ case 'assistant': {
272
+ const toolCalls = this.safeParse<ToolCallPart[]>(row.tool_calls);
273
+ if (toolCalls && toolCalls.length > 0) {
274
+ const content: Array<{ type: 'text'; text: string } | ToolCallPart> = [];
275
+ if (row.content) content.push({ type: 'text', text: row.content });
276
+ content.push(...toolCalls);
277
+ return { role: 'assistant', content };
278
+ }
279
+ return { role: 'assistant', content: row.content };
280
+ }
281
+ case 'tool': {
282
+ const toolResults = this.safeParse<ToolResultPart[]>(row.content);
283
+ if (toolResults && toolResults.length > 0 && toolResults[0]?.type === 'tool-result') {
284
+ return { role: 'tool', content: toolResults };
285
+ }
286
+ // Backward compat: old format was a plain string
287
+ return {
288
+ role: 'tool',
289
+ content: [{
290
+ type: 'tool-result' as const,
291
+ toolCallId: row.tool_call_id ?? '',
292
+ toolName: 'unknown',
293
+ output: { type: 'text' as const, value: row.content },
294
+ }],
295
+ };
296
+ }
297
+ default:
298
+ return { role: 'user', content: row.content };
249
299
  }
250
- return msg;
251
300
  }
252
301
  }
package/src/index.ts CHANGED
@@ -10,26 +10,45 @@ export type { AIServicePluginOptions } from './plugin.js';
10
10
 
11
11
  // Adapters
12
12
  export { MemoryLLMAdapter } from './adapters/memory-adapter.js';
13
+ export { VercelLLMAdapter } from './adapters/vercel-adapter.js';
14
+ export type { VercelLLMAdapterConfig } from './adapters/vercel-adapter.js';
13
15
  export type { LLMAdapter } from '@objectstack/spec/contracts';
14
16
 
17
+ // Vercel Data Stream encoder
18
+ export { encodeStreamPart, encodeVercelDataStream } from './stream/vercel-stream-encoder.js';
19
+
15
20
  // Conversation
16
21
  export { InMemoryConversationService } from './conversation/in-memory-conversation-service.js';
17
22
  export { ObjectQLConversationService } from './conversation/objectql-conversation-service.js';
18
23
 
19
24
  // Tool registry
20
25
  export { ToolRegistry } from './tools/tool-registry.js';
21
- export type { ToolHandler } from './tools/tool-registry.js';
26
+ export type { ToolHandler, ToolExecutionResult } from './tools/tool-registry.js';
22
27
 
23
28
  // Data tools
24
29
  export { registerDataTools, DATA_TOOL_DEFINITIONS } from './tools/data-tools.js';
25
30
  export type { DataToolContext } from './tools/data-tools.js';
26
31
 
32
+ // Metadata tools
33
+ export { registerMetadataTools, METADATA_TOOL_DEFINITIONS } from './tools/metadata-tools.js';
34
+ export type { MetadataToolContext } from './tools/metadata-tools.js';
35
+
36
+ // Individual tool metadata (first-class Tool definitions via defineTool)
37
+ export {
38
+ createObjectTool,
39
+ addFieldTool,
40
+ modifyFieldTool,
41
+ deleteFieldTool,
42
+ listMetadataObjectsTool,
43
+ describeMetadataObjectTool,
44
+ } from './tools/metadata-tools.js';
45
+
27
46
  // Agent runtime
28
47
  export { AgentRuntime } from './agent-runtime.js';
29
48
  export type { AgentChatContext } from './agent-runtime.js';
30
49
 
31
50
  // Built-in agents
32
- export { DATA_CHAT_AGENT } from './agents/index.js';
51
+ export { DATA_CHAT_AGENT, METADATA_ASSISTANT_AGENT } from './agents/index.js';
33
52
 
34
53
  // Object definitions
35
54
  export { AiConversationObject, AiMessageObject } from './objects/index.js';
@@ -37,4 +56,4 @@ export { AiConversationObject, AiMessageObject } from './objects/index.js';
37
56
  // Routes
38
57
  export { buildAIRoutes } from './routes/ai-routes.js';
39
58
  export { buildAgentRoutes } from './routes/agent-routes.js';
40
- export type { RouteDefinition, RouteRequest, RouteResponse } from './routes/ai-routes.js';
59
+ export type { RouteDefinition, RouteRequest, RouteResponse, RouteUserContext } from './routes/ai-routes.js';