@objectstack/service-ai 4.0.2 → 4.0.4

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/dist/index.cjs CHANGED
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
6
9
  var __export = (target, all) => {
7
10
  for (var name in all)
8
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -17,330 +20,1077 @@ var __copyProps = (to, from, except, desc) => {
17
20
  };
18
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
22
 
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- AIService: () => AIService,
24
- AIServicePlugin: () => AIServicePlugin,
25
- AgentRuntime: () => AgentRuntime,
26
- AiConversationObject: () => AiConversationObject,
27
- AiMessageObject: () => AiMessageObject,
28
- DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
23
+ // src/tools/data-tools.ts
24
+ var data_tools_exports = {};
25
+ __export(data_tools_exports, {
26
+ AGGREGATE_DATA_TOOL: () => AGGREGATE_DATA_TOOL,
29
27
  DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
30
- InMemoryConversationService: () => InMemoryConversationService,
31
- METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
32
- METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
33
- MemoryLLMAdapter: () => MemoryLLMAdapter,
34
- ObjectQLConversationService: () => ObjectQLConversationService,
35
- ToolRegistry: () => ToolRegistry,
36
- VercelLLMAdapter: () => VercelLLMAdapter,
37
- addFieldTool: () => addFieldTool,
38
- buildAIRoutes: () => buildAIRoutes,
39
- buildAgentRoutes: () => buildAgentRoutes,
40
- createObjectTool: () => createObjectTool,
41
- deleteFieldTool: () => deleteFieldTool,
42
- describeMetadataObjectTool: () => describeMetadataObjectTool,
43
- encodeStreamPart: () => encodeStreamPart,
44
- encodeVercelDataStream: () => encodeVercelDataStream,
45
- listMetadataObjectsTool: () => listMetadataObjectsTool,
46
- modifyFieldTool: () => modifyFieldTool,
47
- registerDataTools: () => registerDataTools,
48
- registerMetadataTools: () => registerMetadataTools
28
+ GET_RECORD_TOOL: () => GET_RECORD_TOOL,
29
+ QUERY_RECORDS_TOOL: () => QUERY_RECORDS_TOOL,
30
+ registerDataTools: () => registerDataTools
49
31
  });
50
- module.exports = __toCommonJS(index_exports);
51
-
52
- // src/ai-service.ts
53
- var import_core = require("@objectstack/core");
54
-
55
- // src/adapters/memory-adapter.ts
56
- var MemoryLLMAdapter = class {
57
- constructor() {
58
- this.name = "memory";
59
- }
60
- async chat(messages, options) {
61
- const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
62
- const userContent = lastUserMessage?.content;
63
- const text = typeof userContent === "string" ? userContent : "(complex content)";
64
- const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
65
- return {
66
- content,
67
- model: options?.model ?? "memory",
68
- usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
32
+ function createQueryRecordsHandler(ctx) {
33
+ return async (args) => {
34
+ const {
35
+ objectName,
36
+ where,
37
+ fields,
38
+ orderBy,
39
+ limit,
40
+ offset
41
+ } = args;
42
+ const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
43
+ const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
44
+ const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
45
+ const records = await ctx.dataEngine.find(objectName, {
46
+ where,
47
+ fields,
48
+ orderBy,
49
+ limit: safeLimit,
50
+ offset: safeOffset
51
+ });
52
+ return JSON.stringify({ count: records.length, records });
53
+ };
54
+ }
55
+ function createGetRecordHandler(ctx) {
56
+ return async (args) => {
57
+ const { objectName, recordId, fields } = args;
58
+ const record = await ctx.dataEngine.findOne(objectName, {
59
+ where: { id: recordId },
60
+ fields
61
+ });
62
+ if (!record) {
63
+ return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
64
+ }
65
+ return JSON.stringify(record);
66
+ };
67
+ }
68
+ function createAggregateDataHandler(ctx) {
69
+ return async (args) => {
70
+ const { objectName, aggregations, groupBy, where } = args;
71
+ for (const a of aggregations) {
72
+ if (!VALID_AGG_FUNCTIONS.has(a.function)) {
73
+ return JSON.stringify({
74
+ error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
75
+ });
76
+ }
77
+ }
78
+ const result = await ctx.dataEngine.aggregate(objectName, {
79
+ where,
80
+ groupBy,
81
+ aggregations: aggregations.map((a) => ({
82
+ function: a.function,
83
+ field: a.field,
84
+ alias: a.alias
85
+ }))
86
+ });
87
+ return JSON.stringify(result);
88
+ };
89
+ }
90
+ function registerDataTools(registry, context) {
91
+ registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
92
+ registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
93
+ registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
94
+ }
95
+ var MAX_QUERY_LIMIT, DEFAULT_QUERY_LIMIT, QUERY_RECORDS_TOOL, GET_RECORD_TOOL, AGGREGATE_DATA_TOOL, DATA_TOOL_DEFINITIONS, VALID_AGG_FUNCTIONS;
96
+ var init_data_tools = __esm({
97
+ "src/tools/data-tools.ts"() {
98
+ "use strict";
99
+ MAX_QUERY_LIMIT = 200;
100
+ DEFAULT_QUERY_LIMIT = 20;
101
+ QUERY_RECORDS_TOOL = {
102
+ name: "query_records",
103
+ description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
104
+ parameters: {
105
+ type: "object",
106
+ properties: {
107
+ objectName: {
108
+ type: "string",
109
+ description: "The snake_case name of the object to query"
110
+ },
111
+ where: {
112
+ type: "object",
113
+ description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
114
+ },
115
+ fields: {
116
+ type: "array",
117
+ items: { type: "string" },
118
+ description: "List of field names to return (omit for all fields)"
119
+ },
120
+ orderBy: {
121
+ type: "array",
122
+ items: {
123
+ type: "object",
124
+ properties: {
125
+ field: { type: "string" },
126
+ order: { type: "string", enum: ["asc", "desc"] }
127
+ }
128
+ },
129
+ description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
130
+ },
131
+ limit: {
132
+ type: "number",
133
+ description: `Maximum number of records to return (default ${DEFAULT_QUERY_LIMIT}, max ${MAX_QUERY_LIMIT})`
134
+ },
135
+ offset: {
136
+ type: "number",
137
+ description: "Number of records to skip for pagination"
138
+ }
139
+ },
140
+ required: ["objectName"],
141
+ additionalProperties: false
142
+ }
69
143
  };
70
- }
71
- async complete(prompt, options) {
72
- return {
73
- content: `[memory] ${prompt}`,
74
- model: options?.model ?? "memory",
75
- usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
144
+ GET_RECORD_TOOL = {
145
+ name: "get_record",
146
+ description: "Get a single record by its ID from a data object.",
147
+ parameters: {
148
+ type: "object",
149
+ properties: {
150
+ objectName: {
151
+ type: "string",
152
+ description: "The snake_case name of the object"
153
+ },
154
+ recordId: {
155
+ type: "string",
156
+ description: "The unique ID of the record"
157
+ },
158
+ fields: {
159
+ type: "array",
160
+ items: { type: "string" },
161
+ description: "List of field names to return (omit for all fields)"
162
+ }
163
+ },
164
+ required: ["objectName", "recordId"],
165
+ additionalProperties: false
166
+ }
76
167
  };
77
- }
78
- async *streamChat(messages, _options) {
79
- const result = await this.chat(messages);
80
- const words = result.content.split(" ");
81
- for (let i = 0; i < words.length; i++) {
82
- const wordText = i === 0 ? words[i] : ` ${words[i]}`;
83
- yield { type: "text-delta", id: `delta_${i}`, text: wordText };
84
- }
85
- yield {
86
- type: "finish",
87
- finishReason: "stop",
88
- totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
89
- rawFinishReason: "stop"
168
+ AGGREGATE_DATA_TOOL = {
169
+ name: "aggregate_data",
170
+ description: "Perform aggregation/statistical operations on a data object. Supports count, sum, avg, min, max with optional groupBy and where filters.",
171
+ parameters: {
172
+ type: "object",
173
+ properties: {
174
+ objectName: {
175
+ type: "string",
176
+ description: "The snake_case name of the object to aggregate"
177
+ },
178
+ aggregations: {
179
+ type: "array",
180
+ items: {
181
+ type: "object",
182
+ properties: {
183
+ function: {
184
+ type: "string",
185
+ enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
186
+ description: "Aggregation function"
187
+ },
188
+ field: {
189
+ type: "string",
190
+ description: "Field to aggregate (optional for count)"
191
+ },
192
+ alias: {
193
+ type: "string",
194
+ description: "Result column alias"
195
+ }
196
+ },
197
+ required: ["function", "alias"]
198
+ },
199
+ description: "Aggregation definitions"
200
+ },
201
+ groupBy: {
202
+ type: "array",
203
+ items: { type: "string" },
204
+ description: "Fields to group by"
205
+ },
206
+ where: {
207
+ type: "object",
208
+ description: "Filter conditions applied before aggregation"
209
+ }
210
+ },
211
+ required: ["objectName", "aggregations"],
212
+ additionalProperties: false
213
+ }
90
214
  };
215
+ DATA_TOOL_DEFINITIONS = [
216
+ QUERY_RECORDS_TOOL,
217
+ GET_RECORD_TOOL,
218
+ AGGREGATE_DATA_TOOL
219
+ ];
220
+ VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
221
+ "count",
222
+ "sum",
223
+ "avg",
224
+ "min",
225
+ "max",
226
+ "count_distinct"
227
+ ]);
91
228
  }
92
- async embed(input) {
93
- const texts = Array.isArray(input) ? input : [input];
94
- return texts.map(() => [0, 0, 0]);
95
- }
96
- async listModels() {
97
- return ["memory"];
229
+ });
230
+
231
+ // src/tools/create-object.tool.ts
232
+ var import_ai, createObjectTool;
233
+ var init_create_object_tool = __esm({
234
+ "src/tools/create-object.tool.ts"() {
235
+ "use strict";
236
+ import_ai = require("@objectstack/spec/ai");
237
+ createObjectTool = (0, import_ai.defineTool)({
238
+ name: "create_object",
239
+ label: "Create Object",
240
+ description: "Creates a new data object (table) with the specified name, label, and optional field definitions. Use this when the user wants to create a new entity, table, or data model.",
241
+ category: "data",
242
+ builtIn: true,
243
+ // NOTE: requiresConfirmation is intentionally false (default) because the
244
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
245
+ // executes tool calls immediately without checking this flag. The flag
246
+ // should only be set once server-side approval gating is implemented to
247
+ // avoid giving users a false sense of safety.
248
+ parameters: {
249
+ type: "object",
250
+ properties: {
251
+ name: {
252
+ type: "string",
253
+ description: "Machine name for the object (snake_case, e.g. project_task)"
254
+ },
255
+ label: {
256
+ type: "string",
257
+ description: "Human-readable display name (e.g. Project Task)"
258
+ },
259
+ fields: {
260
+ type: "array",
261
+ description: "Initial fields to create with the object",
262
+ items: {
263
+ type: "object",
264
+ properties: {
265
+ name: { type: "string", description: "Field machine name (snake_case)" },
266
+ label: { type: "string", description: "Field display name" },
267
+ type: {
268
+ type: "string",
269
+ description: "Field data type",
270
+ enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
271
+ },
272
+ required: { type: "boolean", description: "Whether the field is required" }
273
+ },
274
+ required: ["name", "type"]
275
+ }
276
+ },
277
+ enableFeatures: {
278
+ type: "object",
279
+ description: "Object capability flags",
280
+ properties: {
281
+ trackHistory: { type: "boolean" },
282
+ apiEnabled: { type: "boolean" }
283
+ }
284
+ }
285
+ },
286
+ required: ["name", "label"],
287
+ additionalProperties: false
288
+ }
289
+ });
98
290
  }
99
- };
291
+ });
100
292
 
101
- // src/tools/tool-registry.ts
102
- var ToolRegistry = class {
103
- constructor() {
104
- this.definitions = /* @__PURE__ */ new Map();
105
- this.handlers = /* @__PURE__ */ new Map();
293
+ // src/tools/add-field.tool.ts
294
+ var import_ai2, addFieldTool;
295
+ var init_add_field_tool = __esm({
296
+ "src/tools/add-field.tool.ts"() {
297
+ "use strict";
298
+ import_ai2 = require("@objectstack/spec/ai");
299
+ addFieldTool = (0, import_ai2.defineTool)({
300
+ name: "add_field",
301
+ label: "Add Field",
302
+ description: "Adds a new field (column) to an existing data object. Use this when the user wants to add a property, column, or attribute to a table.",
303
+ category: "data",
304
+ builtIn: true,
305
+ parameters: {
306
+ type: "object",
307
+ properties: {
308
+ objectName: {
309
+ type: "string",
310
+ description: "Target object machine name (snake_case)"
311
+ },
312
+ name: {
313
+ type: "string",
314
+ description: "Field machine name (snake_case, e.g. due_date)"
315
+ },
316
+ label: {
317
+ type: "string",
318
+ description: "Human-readable field label (e.g. Due Date)"
319
+ },
320
+ type: {
321
+ type: "string",
322
+ description: "Field data type",
323
+ enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
324
+ },
325
+ required: {
326
+ type: "boolean",
327
+ description: "Whether the field is required"
328
+ },
329
+ defaultValue: {
330
+ description: "Default value for the field"
331
+ },
332
+ options: {
333
+ type: "array",
334
+ description: "Options for select/picklist fields",
335
+ items: {
336
+ type: "object",
337
+ properties: {
338
+ label: { type: "string" },
339
+ value: {
340
+ type: "string",
341
+ description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
342
+ pattern: "^[a-z_][a-z0-9_]*$"
343
+ }
344
+ }
345
+ }
346
+ },
347
+ reference: {
348
+ type: "string",
349
+ description: "Referenced object name for lookup fields (snake_case, e.g. account)"
350
+ }
351
+ },
352
+ required: ["objectName", "name", "type"],
353
+ additionalProperties: false
354
+ }
355
+ });
106
356
  }
107
- /**
108
- * Register a tool with its definition and handler.
109
- * @param definition - Tool definition (name, description, parameters schema)
110
- * @param handler - Async function that executes the tool
111
- */
112
- register(definition, handler) {
113
- this.definitions.set(definition.name, definition);
114
- this.handlers.set(definition.name, handler);
357
+ });
358
+
359
+ // src/tools/modify-field.tool.ts
360
+ var import_ai3, modifyFieldTool;
361
+ var init_modify_field_tool = __esm({
362
+ "src/tools/modify-field.tool.ts"() {
363
+ "use strict";
364
+ import_ai3 = require("@objectstack/spec/ai");
365
+ modifyFieldTool = (0, import_ai3.defineTool)({
366
+ name: "modify_field",
367
+ label: "Modify Field",
368
+ description: "Modifies an existing field definition (label, type, required, default value, etc.) on a data object. Use this when the user wants to change or reconfigure an existing column or attribute (not rename it).",
369
+ category: "data",
370
+ builtIn: true,
371
+ parameters: {
372
+ type: "object",
373
+ properties: {
374
+ objectName: {
375
+ type: "string",
376
+ description: "Target object machine name (snake_case)"
377
+ },
378
+ fieldName: {
379
+ type: "string",
380
+ description: "Existing field machine name to modify (snake_case)"
381
+ },
382
+ changes: {
383
+ type: "object",
384
+ description: "Field properties to update (partial patch)",
385
+ properties: {
386
+ label: { type: "string", description: "New display label" },
387
+ type: { type: "string", description: "New field type" },
388
+ required: { type: "boolean", description: "Update required constraint" },
389
+ defaultValue: { description: "New default value" }
390
+ }
391
+ }
392
+ },
393
+ required: ["objectName", "fieldName", "changes"],
394
+ additionalProperties: false
395
+ }
396
+ });
115
397
  }
116
- /**
117
- * Unregister a tool by name.
118
- */
119
- unregister(name) {
120
- this.definitions.delete(name);
121
- this.handlers.delete(name);
398
+ });
399
+
400
+ // src/tools/delete-field.tool.ts
401
+ var import_ai4, deleteFieldTool;
402
+ var init_delete_field_tool = __esm({
403
+ "src/tools/delete-field.tool.ts"() {
404
+ "use strict";
405
+ import_ai4 = require("@objectstack/spec/ai");
406
+ deleteFieldTool = (0, import_ai4.defineTool)({
407
+ name: "delete_field",
408
+ label: "Delete Field",
409
+ description: "Removes a field (column) from an existing data object. This is a destructive operation. Use this when the user explicitly wants to remove an attribute or column from a table.",
410
+ category: "data",
411
+ builtIn: true,
412
+ // NOTE: requiresConfirmation is intentionally false (default) because the
413
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
414
+ // executes tool calls immediately without checking this flag. The flag
415
+ // should only be set once server-side approval gating is implemented.
416
+ parameters: {
417
+ type: "object",
418
+ properties: {
419
+ objectName: {
420
+ type: "string",
421
+ description: "Target object machine name (snake_case)"
422
+ },
423
+ fieldName: {
424
+ type: "string",
425
+ description: "Field machine name to delete (snake_case)"
426
+ }
427
+ },
428
+ required: ["objectName", "fieldName"],
429
+ additionalProperties: false
430
+ }
431
+ });
122
432
  }
123
- /**
124
- * Check whether a tool is registered.
125
- */
126
- has(name) {
127
- return this.definitions.has(name);
433
+ });
434
+
435
+ // src/tools/list-objects.tool.ts
436
+ var import_ai5, listObjectsTool;
437
+ var init_list_objects_tool = __esm({
438
+ "src/tools/list-objects.tool.ts"() {
439
+ "use strict";
440
+ import_ai5 = require("@objectstack/spec/ai");
441
+ listObjectsTool = (0, import_ai5.defineTool)({
442
+ name: "list_objects",
443
+ label: "List Objects",
444
+ description: "Lists all registered data objects (tables) in the current environment. Use this when the user wants to see what tables, entities, or data models are available.",
445
+ category: "data",
446
+ builtIn: true,
447
+ parameters: {
448
+ type: "object",
449
+ properties: {
450
+ filter: {
451
+ type: "string",
452
+ description: "Optional name or label substring to filter objects"
453
+ },
454
+ includeFields: {
455
+ type: "boolean",
456
+ description: "Whether to include field summaries for each object (default: false)"
457
+ }
458
+ },
459
+ additionalProperties: false
460
+ }
461
+ });
128
462
  }
129
- /**
130
- * Get the definition for a registered tool.
131
- */
132
- getDefinition(name) {
133
- return this.definitions.get(name);
134
- }
135
- /**
136
- * Return all registered tool definitions.
137
- */
138
- getAll() {
139
- return Array.from(this.definitions.values());
140
- }
141
- /** Number of registered tools. */
142
- get size() {
143
- return this.definitions.size;
144
- }
145
- /** All registered tool names. */
146
- names() {
147
- return Array.from(this.definitions.keys());
463
+ });
464
+
465
+ // src/tools/describe-object.tool.ts
466
+ var import_ai6, describeObjectTool;
467
+ var init_describe_object_tool = __esm({
468
+ "src/tools/describe-object.tool.ts"() {
469
+ "use strict";
470
+ import_ai6 = require("@objectstack/spec/ai");
471
+ describeObjectTool = (0, import_ai6.defineTool)({
472
+ name: "describe_object",
473
+ label: "Describe Object",
474
+ description: "Returns the full schema details of a data object, including all fields, types, relationships, and configuration. Use this to understand the structure of a table before querying or modifying it.",
475
+ category: "data",
476
+ builtIn: true,
477
+ parameters: {
478
+ type: "object",
479
+ properties: {
480
+ objectName: {
481
+ type: "string",
482
+ description: "Object machine name to describe (snake_case)"
483
+ }
484
+ },
485
+ required: ["objectName"],
486
+ additionalProperties: false
487
+ }
488
+ });
148
489
  }
149
- /**
150
- * Execute a tool call and return the result.
151
- */
152
- async execute(toolCall) {
153
- const handler = this.handlers.get(toolCall.toolName);
154
- if (!handler) {
155
- return {
156
- type: "tool-result",
157
- toolCallId: toolCall.toolCallId,
158
- toolName: toolCall.toolName,
159
- output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
160
- isError: true
161
- };
490
+ });
491
+
492
+ // src/tools/metadata-tools.ts
493
+ var metadata_tools_exports = {};
494
+ __export(metadata_tools_exports, {
495
+ METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
496
+ addFieldTool: () => addFieldTool,
497
+ createObjectTool: () => createObjectTool,
498
+ deleteFieldTool: () => deleteFieldTool,
499
+ describeObjectTool: () => describeObjectTool,
500
+ listObjectsTool: () => listObjectsTool,
501
+ modifyFieldTool: () => modifyFieldTool,
502
+ registerMetadataTools: () => registerMetadataTools
503
+ });
504
+ function isSnakeCase(value) {
505
+ return SNAKE_CASE_RE.test(value);
506
+ }
507
+ function createCreateObjectHandler(ctx) {
508
+ return async (args) => {
509
+ const { name, label, fields, enableFeatures } = args;
510
+ if (!name || !label) {
511
+ return JSON.stringify({ error: 'Both "name" and "label" are required' });
162
512
  }
163
- try {
164
- const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
165
- const content = await handler(args);
166
- return {
167
- type: "tool-result",
168
- toolCallId: toolCall.toolCallId,
169
- toolName: toolCall.toolName,
170
- output: { type: "text", value: content }
171
- };
172
- } catch (err) {
173
- const message = err instanceof Error ? err.message : String(err);
174
- return {
175
- type: "tool-result",
176
- toolCallId: toolCall.toolCallId,
177
- toolName: toolCall.toolName,
178
- output: { type: "text", value: message },
179
- isError: true
180
- };
513
+ if (!isSnakeCase(name)) {
514
+ return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
181
515
  }
182
- }
183
- /**
184
- * Execute multiple tool calls in parallel.
185
- */
186
- async executeAll(toolCalls) {
187
- return Promise.all(toolCalls.map((tc) => this.execute(tc)));
188
- }
189
- /**
190
- * Clear all registered tools.
191
- */
192
- clear() {
193
- this.definitions.clear();
194
- this.handlers.clear();
195
- }
196
- };
197
-
198
- // src/conversation/in-memory-conversation-service.ts
199
- var InMemoryConversationService = class {
200
- constructor() {
201
- this.store = /* @__PURE__ */ new Map();
202
- this.counter = 0;
203
- }
204
- async create(options = {}) {
205
- const now = (/* @__PURE__ */ new Date()).toISOString();
206
- const id = `conv_${++this.counter}`;
207
- const conversation = {
208
- id,
209
- title: options.title,
210
- agentId: options.agentId,
211
- userId: options.userId,
212
- messages: [],
213
- createdAt: now,
214
- updatedAt: now,
215
- metadata: options.metadata
516
+ const existing = await ctx.metadataService.getObject(name);
517
+ if (existing) {
518
+ return JSON.stringify({ error: `Object "${name}" already exists` });
519
+ }
520
+ const fieldMap = {};
521
+ if (fields && Array.isArray(fields)) {
522
+ const seenNames = /* @__PURE__ */ new Set();
523
+ for (const f of fields) {
524
+ if (!f.name) {
525
+ return JSON.stringify({ error: 'Each field must have a "name" property' });
526
+ }
527
+ if (!isSnakeCase(f.name)) {
528
+ return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
529
+ }
530
+ if (seenNames.has(f.name)) {
531
+ return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
532
+ }
533
+ seenNames.add(f.name);
534
+ fieldMap[f.name] = {
535
+ type: f.type,
536
+ ...f.label ? { label: f.label } : {},
537
+ ...f.required !== void 0 ? { required: f.required } : {}
538
+ };
539
+ }
540
+ }
541
+ const objectDef = {
542
+ name,
543
+ label,
544
+ ...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
545
+ ...enableFeatures ? { enable: enableFeatures } : {}
216
546
  };
217
- this.store.set(id, conversation);
218
- return conversation;
219
- }
220
- async get(conversationId) {
221
- return this.store.get(conversationId) ?? null;
222
- }
223
- async list(options = {}) {
224
- let results = Array.from(this.store.values());
225
- if (options.userId) {
226
- results = results.filter((c) => c.userId === options.userId);
547
+ await ctx.metadataService.register("object", name, objectDef);
548
+ return JSON.stringify({
549
+ name,
550
+ label,
551
+ fieldCount: Object.keys(fieldMap).length
552
+ });
553
+ };
554
+ }
555
+ function createAddFieldHandler(ctx) {
556
+ return async (args) => {
557
+ const { objectName, name, label, type, required, defaultValue, options, reference } = args;
558
+ if (!objectName || !name || !type) {
559
+ return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
227
560
  }
228
- if (options.agentId) {
229
- results = results.filter((c) => c.agentId === options.agentId);
561
+ if (!isSnakeCase(objectName)) {
562
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
230
563
  }
231
- if (options.cursor) {
232
- const idx = results.findIndex((c) => c.id === options.cursor);
233
- if (idx >= 0) {
234
- results = results.slice(idx + 1);
564
+ if (!isSnakeCase(name)) {
565
+ return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
566
+ }
567
+ if (reference && !isSnakeCase(reference)) {
568
+ return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
569
+ }
570
+ if (options && Array.isArray(options)) {
571
+ for (const opt of options) {
572
+ if (opt.value && !isSnakeCase(opt.value)) {
573
+ return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
574
+ }
235
575
  }
236
576
  }
237
- if (options.limit && options.limit > 0) {
238
- results = results.slice(0, options.limit);
577
+ const objectDef = await ctx.metadataService.getObject(objectName);
578
+ if (!objectDef) {
579
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
239
580
  }
240
- return results;
241
- }
242
- async addMessage(conversationId, message) {
243
- const conversation = this.store.get(conversationId);
244
- if (!conversation) {
245
- throw new Error(`Conversation "${conversationId}" not found`);
581
+ const def = objectDef;
582
+ if (def.fields && def.fields[name]) {
583
+ return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
246
584
  }
247
- conversation.messages.push(message);
248
- conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
249
- return conversation;
250
- }
251
- async delete(conversationId) {
252
- this.store.delete(conversationId);
253
- }
254
- /** Total number of stored conversations. */
255
- get size() {
256
- return this.store.size;
257
- }
258
- /** Clear all conversations. */
259
- clear() {
260
- this.store.clear();
261
- this.counter = 0;
262
- }
263
- };
264
-
265
- // src/ai-service.ts
266
- function textDeltaPart(id, text) {
267
- return { type: "text-delta", id, text };
268
- }
269
- function finishPart(result) {
270
- return {
271
- type: "finish",
272
- finishReason: "stop",
273
- totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
274
- rawFinishReason: "stop"
585
+ const fieldDef = {
586
+ type,
587
+ ...label ? { label } : {},
588
+ ...required !== void 0 ? { required } : {},
589
+ ...defaultValue !== void 0 ? { defaultValue } : {},
590
+ ...options ? { options } : {},
591
+ ...reference ? { reference } : {}
592
+ };
593
+ const updatedFields = { ...def.fields ?? {}, [name]: fieldDef };
594
+ await ctx.metadataService.register("object", objectName, {
595
+ ...def,
596
+ fields: updatedFields
597
+ });
598
+ return JSON.stringify({
599
+ objectName,
600
+ fieldName: name,
601
+ fieldType: type
602
+ });
275
603
  };
276
604
  }
277
- var _AIService = class _AIService {
278
- constructor(config = {}) {
279
- this.adapter = config.adapter ?? new MemoryLLMAdapter();
280
- this.logger = config.logger ?? (0, import_core.createLogger)({ level: "info", format: "pretty" });
281
- this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
282
- this.conversationService = config.conversationService ?? new InMemoryConversationService();
283
- this.logger.info(
284
- `[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}`
285
- );
286
- }
287
- /** The name of the active LLM adapter. */
288
- get adapterName() {
289
- return this.adapter.name;
290
- }
291
- // ── IAIService implementation ──────────────────────────────────
605
+ function createModifyFieldHandler(ctx) {
606
+ return async (args) => {
607
+ const { objectName, fieldName, changes } = args;
608
+ if (!objectName || !fieldName || !changes) {
609
+ return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
610
+ }
611
+ if (!isSnakeCase(objectName)) {
612
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
613
+ }
614
+ if (!isSnakeCase(fieldName)) {
615
+ return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
616
+ }
617
+ const objectDef = await ctx.metadataService.getObject(objectName);
618
+ if (!objectDef) {
619
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
620
+ }
621
+ const def = objectDef;
622
+ if (!def.fields || !def.fields[fieldName]) {
623
+ return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
624
+ }
625
+ const existingField = def.fields[fieldName];
626
+ const updatedField = { ...existingField, ...changes };
627
+ const updatedFields = { ...def.fields, [fieldName]: updatedField };
628
+ await ctx.metadataService.register("object", objectName, {
629
+ ...def,
630
+ fields: updatedFields
631
+ });
632
+ return JSON.stringify({
633
+ objectName,
634
+ fieldName,
635
+ updatedProperties: Object.keys(changes)
636
+ });
637
+ };
638
+ }
639
+ function createDeleteFieldHandler(ctx) {
640
+ return async (args) => {
641
+ const { objectName, fieldName } = args;
642
+ if (!objectName || !fieldName) {
643
+ return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
644
+ }
645
+ if (!isSnakeCase(objectName)) {
646
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
647
+ }
648
+ if (!isSnakeCase(fieldName)) {
649
+ return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
650
+ }
651
+ const objectDef = await ctx.metadataService.getObject(objectName);
652
+ if (!objectDef) {
653
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
654
+ }
655
+ const def = objectDef;
656
+ if (!def.fields || !def.fields[fieldName]) {
657
+ return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
658
+ }
659
+ const { [fieldName]: _removed, ...remainingFields } = def.fields;
660
+ await ctx.metadataService.register("object", objectName, {
661
+ ...def,
662
+ fields: remainingFields
663
+ });
664
+ return JSON.stringify({
665
+ objectName,
666
+ fieldName,
667
+ success: true
668
+ });
669
+ };
670
+ }
671
+ function createListObjectsHandler(ctx) {
672
+ return async (args) => {
673
+ const { filter, includeFields } = args ?? {};
674
+ const objects = await ctx.metadataService.listObjects();
675
+ let result = objects.map((o) => {
676
+ const base = {
677
+ name: o.name,
678
+ label: o.label ?? o.name,
679
+ fieldCount: o.fields ? Object.keys(o.fields).length : 0
680
+ };
681
+ if (includeFields && o.fields) {
682
+ base.fields = Object.entries(o.fields).map(([key, f]) => ({
683
+ name: key,
684
+ type: f.type,
685
+ label: f.label ?? key
686
+ }));
687
+ }
688
+ return base;
689
+ });
690
+ if (filter) {
691
+ const lower = filter.toLowerCase();
692
+ result = result.filter(
693
+ (o) => o.name.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
694
+ );
695
+ }
696
+ return JSON.stringify({
697
+ objects: result,
698
+ totalCount: result.length
699
+ });
700
+ };
701
+ }
702
+ function createDescribeObjectHandler(ctx) {
703
+ return async (args) => {
704
+ const { objectName } = args;
705
+ if (!objectName) {
706
+ return JSON.stringify({ error: '"objectName" is required' });
707
+ }
708
+ if (!isSnakeCase(objectName)) {
709
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
710
+ }
711
+ const objectDef = await ctx.metadataService.getObject(objectName);
712
+ if (!objectDef) {
713
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
714
+ }
715
+ const def = objectDef;
716
+ const fields = def.fields ?? {};
717
+ const fieldSummary = Object.entries(fields).map(([key, f]) => ({
718
+ name: key,
719
+ type: f.type,
720
+ label: f.label ?? key,
721
+ required: f.required ?? false,
722
+ ...f.reference ? { reference: f.reference } : {},
723
+ ...f.options ? { options: f.options } : {}
724
+ }));
725
+ return JSON.stringify({
726
+ name: def.name,
727
+ label: def.label ?? def.name,
728
+ fields: fieldSummary,
729
+ enableFeatures: def.enable ?? {}
730
+ });
731
+ };
732
+ }
733
+ function registerMetadataTools(registry, context) {
734
+ registry.register(createObjectTool, createCreateObjectHandler(context));
735
+ registry.register(addFieldTool, createAddFieldHandler(context));
736
+ registry.register(modifyFieldTool, createModifyFieldHandler(context));
737
+ registry.register(deleteFieldTool, createDeleteFieldHandler(context));
738
+ registry.register(listObjectsTool, createListObjectsHandler(context));
739
+ registry.register(describeObjectTool, createDescribeObjectHandler(context));
740
+ }
741
+ var METADATA_TOOL_DEFINITIONS, SNAKE_CASE_RE;
742
+ var init_metadata_tools = __esm({
743
+ "src/tools/metadata-tools.ts"() {
744
+ "use strict";
745
+ init_create_object_tool();
746
+ init_add_field_tool();
747
+ init_modify_field_tool();
748
+ init_delete_field_tool();
749
+ init_list_objects_tool();
750
+ init_describe_object_tool();
751
+ init_create_object_tool();
752
+ init_add_field_tool();
753
+ init_modify_field_tool();
754
+ init_delete_field_tool();
755
+ init_list_objects_tool();
756
+ init_describe_object_tool();
757
+ METADATA_TOOL_DEFINITIONS = [
758
+ createObjectTool,
759
+ addFieldTool,
760
+ modifyFieldTool,
761
+ deleteFieldTool,
762
+ listObjectsTool,
763
+ describeObjectTool
764
+ ];
765
+ SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
766
+ }
767
+ });
768
+
769
+ // src/index.ts
770
+ var index_exports = {};
771
+ __export(index_exports, {
772
+ AIService: () => AIService,
773
+ AIServicePlugin: () => AIServicePlugin,
774
+ AgentRuntime: () => AgentRuntime,
775
+ AiConversationObject: () => AiConversationObject,
776
+ AiMessageObject: () => AiMessageObject,
777
+ DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
778
+ DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
779
+ InMemoryConversationService: () => InMemoryConversationService,
780
+ METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
781
+ METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
782
+ MemoryLLMAdapter: () => MemoryLLMAdapter,
783
+ ObjectQLConversationService: () => ObjectQLConversationService,
784
+ ToolRegistry: () => ToolRegistry,
785
+ VercelLLMAdapter: () => VercelLLMAdapter,
786
+ addFieldTool: () => addFieldTool,
787
+ buildAIRoutes: () => buildAIRoutes,
788
+ buildAgentRoutes: () => buildAgentRoutes,
789
+ buildToolRoutes: () => buildToolRoutes,
790
+ createObjectTool: () => createObjectTool,
791
+ deleteFieldTool: () => deleteFieldTool,
792
+ describeObjectTool: () => describeObjectTool,
793
+ encodeStreamPart: () => encodeStreamPart,
794
+ encodeVercelDataStream: () => encodeVercelDataStream,
795
+ listObjectsTool: () => listObjectsTool,
796
+ modifyFieldTool: () => modifyFieldTool,
797
+ registerDataTools: () => registerDataTools,
798
+ registerMetadataTools: () => registerMetadataTools
799
+ });
800
+ module.exports = __toCommonJS(index_exports);
801
+
802
+ // src/ai-service.ts
803
+ var import_core = require("@objectstack/core");
804
+
805
+ // src/adapters/memory-adapter.ts
806
+ var MemoryLLMAdapter = class {
807
+ constructor() {
808
+ this.name = "memory";
809
+ }
292
810
  async chat(messages, options) {
293
- this.logger.debug("[AI] chat", { messageCount: messages.length, model: options?.model });
294
- return this.adapter.chat(messages, options);
811
+ const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
812
+ const userContent = lastUserMessage?.content;
813
+ const text = typeof userContent === "string" ? userContent : "(complex content)";
814
+ const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
815
+ return {
816
+ content,
817
+ model: options?.model ?? "memory",
818
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
819
+ };
295
820
  }
296
821
  async complete(prompt, options) {
297
- this.logger.debug("[AI] complete", { promptLength: prompt.length, model: options?.model });
298
- return this.adapter.complete(prompt, options);
822
+ return {
823
+ content: `[memory] ${prompt}`,
824
+ model: options?.model ?? "memory",
825
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
826
+ };
299
827
  }
300
- async *streamChat(messages, options) {
301
- this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
302
- if (!this.adapter.streamChat) {
303
- const result = await this.adapter.chat(messages, options);
304
- yield textDeltaPart("fallback", result.content);
305
- yield finishPart(result);
306
- return;
828
+ async *streamChat(messages, _options) {
829
+ const result = await this.chat(messages);
830
+ const words = result.content.split(" ");
831
+ for (let i = 0; i < words.length; i++) {
832
+ const wordText = i === 0 ? words[i] : ` ${words[i]}`;
833
+ yield { type: "text-delta", id: `delta_${i}`, text: wordText };
307
834
  }
308
- yield* this.adapter.streamChat(messages, options);
835
+ yield {
836
+ type: "finish",
837
+ finishReason: "stop",
838
+ totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
839
+ rawFinishReason: "stop"
840
+ };
309
841
  }
310
- async embed(input, model) {
311
- if (!this.adapter.embed) {
312
- throw new Error(`[AI] Adapter "${this.adapter.name}" does not support embeddings`);
313
- }
314
- return this.adapter.embed(input, model);
842
+ async embed(input) {
843
+ const texts = Array.isArray(input) ? input : [input];
844
+ return texts.map(() => [0, 0, 0]);
315
845
  }
316
846
  async listModels() {
317
- if (!this.adapter.listModels) {
318
- return [];
319
- }
320
- return this.adapter.listModels();
847
+ return ["memory"];
321
848
  }
322
- /** Extract the text value from a ToolExecutionResult's output. */
323
- static extractOutputText(tr) {
324
- return tr.output && typeof tr.output === "object" && "value" in tr.output ? String(tr.output.value) : "unknown error";
849
+ };
850
+
851
+ // src/tools/tool-registry.ts
852
+ var ToolRegistry = class {
853
+ constructor() {
854
+ this.definitions = /* @__PURE__ */ new Map();
855
+ this.handlers = /* @__PURE__ */ new Map();
325
856
  }
326
857
  /**
327
- * Chat with automatic tool call resolution.
328
- *
329
- * 1. Merges registered tool definitions into `options.tools`.
330
- * 2. Calls the LLM adapter.
331
- * 3. If the response contains `toolCalls`, executes them via the
332
- * {@link ToolRegistry}, appends tool results as `role: 'tool'`
333
- * messages, and loops back to step 2.
334
- * 4. Repeats until the model produces a final text response or the
335
- * maximum number of iterations (`maxIterations`) is reached.
858
+ * Register a tool with its definition and handler.
859
+ * @param definition - Tool definition (name, description, parameters schema)
860
+ * @param handler - Async function that executes the tool
336
861
  */
337
- async chatWithTools(messages, options) {
338
- const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
339
- const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
340
- const registeredTools = this.toolRegistry.getAll();
341
- const mergedTools = [
342
- ...registeredTools,
343
- ...restOptions.tools ?? []
862
+ register(definition, handler) {
863
+ this.definitions.set(definition.name, definition);
864
+ this.handlers.set(definition.name, handler);
865
+ }
866
+ /**
867
+ * Unregister a tool by name.
868
+ */
869
+ unregister(name) {
870
+ this.definitions.delete(name);
871
+ this.handlers.delete(name);
872
+ }
873
+ /**
874
+ * Check whether a tool is registered.
875
+ */
876
+ has(name) {
877
+ return this.definitions.has(name);
878
+ }
879
+ /**
880
+ * Get the definition for a registered tool.
881
+ */
882
+ getDefinition(name) {
883
+ return this.definitions.get(name);
884
+ }
885
+ /**
886
+ * Return all registered tool definitions.
887
+ */
888
+ getAll() {
889
+ return Array.from(this.definitions.values());
890
+ }
891
+ /** Number of registered tools. */
892
+ get size() {
893
+ return this.definitions.size;
894
+ }
895
+ /** All registered tool names. */
896
+ names() {
897
+ return Array.from(this.definitions.keys());
898
+ }
899
+ /**
900
+ * Execute a tool call and return the result.
901
+ */
902
+ async execute(toolCall) {
903
+ const handler = this.handlers.get(toolCall.toolName);
904
+ if (!handler) {
905
+ return {
906
+ type: "tool-result",
907
+ toolCallId: toolCall.toolCallId,
908
+ toolName: toolCall.toolName,
909
+ output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
910
+ isError: true
911
+ };
912
+ }
913
+ try {
914
+ const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
915
+ const content = await handler(args);
916
+ return {
917
+ type: "tool-result",
918
+ toolCallId: toolCall.toolCallId,
919
+ toolName: toolCall.toolName,
920
+ output: { type: "text", value: content }
921
+ };
922
+ } catch (err) {
923
+ const message = err instanceof Error ? err.message : String(err);
924
+ return {
925
+ type: "tool-result",
926
+ toolCallId: toolCall.toolCallId,
927
+ toolName: toolCall.toolName,
928
+ output: { type: "text", value: message },
929
+ isError: true
930
+ };
931
+ }
932
+ }
933
+ /**
934
+ * Execute multiple tool calls in parallel.
935
+ */
936
+ async executeAll(toolCalls) {
937
+ return Promise.all(toolCalls.map((tc) => this.execute(tc)));
938
+ }
939
+ /**
940
+ * Clear all registered tools.
941
+ */
942
+ clear() {
943
+ this.definitions.clear();
944
+ this.handlers.clear();
945
+ }
946
+ };
947
+
948
+ // src/conversation/in-memory-conversation-service.ts
949
+ var InMemoryConversationService = class {
950
+ constructor() {
951
+ this.store = /* @__PURE__ */ new Map();
952
+ this.counter = 0;
953
+ }
954
+ async create(options = {}) {
955
+ const now = (/* @__PURE__ */ new Date()).toISOString();
956
+ const id = `conv_${++this.counter}`;
957
+ const conversation = {
958
+ id,
959
+ title: options.title,
960
+ agentId: options.agentId,
961
+ userId: options.userId,
962
+ messages: [],
963
+ createdAt: now,
964
+ updatedAt: now,
965
+ metadata: options.metadata
966
+ };
967
+ this.store.set(id, conversation);
968
+ return conversation;
969
+ }
970
+ async get(conversationId) {
971
+ return this.store.get(conversationId) ?? null;
972
+ }
973
+ async list(options = {}) {
974
+ let results = Array.from(this.store.values());
975
+ if (options.userId) {
976
+ results = results.filter((c) => c.userId === options.userId);
977
+ }
978
+ if (options.agentId) {
979
+ results = results.filter((c) => c.agentId === options.agentId);
980
+ }
981
+ if (options.cursor) {
982
+ const idx = results.findIndex((c) => c.id === options.cursor);
983
+ if (idx >= 0) {
984
+ results = results.slice(idx + 1);
985
+ }
986
+ }
987
+ if (options.limit && options.limit > 0) {
988
+ results = results.slice(0, options.limit);
989
+ }
990
+ return results;
991
+ }
992
+ async addMessage(conversationId, message) {
993
+ const conversation = this.store.get(conversationId);
994
+ if (!conversation) {
995
+ throw new Error(`Conversation "${conversationId}" not found`);
996
+ }
997
+ conversation.messages.push(message);
998
+ conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
999
+ return conversation;
1000
+ }
1001
+ async delete(conversationId) {
1002
+ this.store.delete(conversationId);
1003
+ }
1004
+ /** Total number of stored conversations. */
1005
+ get size() {
1006
+ return this.store.size;
1007
+ }
1008
+ /** Clear all conversations. */
1009
+ clear() {
1010
+ this.store.clear();
1011
+ this.counter = 0;
1012
+ }
1013
+ };
1014
+
1015
+ // src/ai-service.ts
1016
+ function textDeltaPart(id, text) {
1017
+ return { type: "text-delta", id, text };
1018
+ }
1019
+ function finishPart(result) {
1020
+ return {
1021
+ type: "finish",
1022
+ finishReason: "stop",
1023
+ totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
1024
+ rawFinishReason: "stop"
1025
+ };
1026
+ }
1027
+ var _AIService = class _AIService {
1028
+ constructor(config = {}) {
1029
+ this.adapter = config.adapter ?? new MemoryLLMAdapter();
1030
+ this.logger = config.logger ?? (0, import_core.createLogger)({ level: "info", format: "pretty" });
1031
+ this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
1032
+ this.conversationService = config.conversationService ?? new InMemoryConversationService();
1033
+ this.logger.info(
1034
+ `[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}`
1035
+ );
1036
+ }
1037
+ /** The name of the active LLM adapter. */
1038
+ get adapterName() {
1039
+ return this.adapter.name;
1040
+ }
1041
+ // ── IAIService implementation ──────────────────────────────────
1042
+ async chat(messages, options) {
1043
+ this.logger.debug("[AI] chat", { messageCount: messages.length, model: options?.model });
1044
+ return this.adapter.chat(messages, options);
1045
+ }
1046
+ async complete(prompt, options) {
1047
+ this.logger.debug("[AI] complete", { promptLength: prompt.length, model: options?.model });
1048
+ return this.adapter.complete(prompt, options);
1049
+ }
1050
+ async *streamChat(messages, options) {
1051
+ this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
1052
+ if (!this.adapter.streamChat) {
1053
+ const result = await this.adapter.chat(messages, options);
1054
+ yield textDeltaPart("fallback", result.content);
1055
+ yield finishPart(result);
1056
+ return;
1057
+ }
1058
+ yield* this.adapter.streamChat(messages, options);
1059
+ }
1060
+ async embed(input, model) {
1061
+ if (!this.adapter.embed) {
1062
+ throw new Error(`[AI] Adapter "${this.adapter.name}" does not support embeddings`);
1063
+ }
1064
+ return this.adapter.embed(input, model);
1065
+ }
1066
+ async listModels() {
1067
+ if (!this.adapter.listModels) {
1068
+ return [];
1069
+ }
1070
+ return this.adapter.listModels();
1071
+ }
1072
+ /** Extract the text value from a ToolExecutionResult's output. */
1073
+ static extractOutputText(tr) {
1074
+ return tr.output && typeof tr.output === "object" && "value" in tr.output ? String(tr.output.value) : "unknown error";
1075
+ }
1076
+ /**
1077
+ * Chat with automatic tool call resolution.
1078
+ *
1079
+ * 1. Merges registered tool definitions into `options.tools`.
1080
+ * 2. Calls the LLM adapter.
1081
+ * 3. If the response contains `toolCalls`, executes them via the
1082
+ * {@link ToolRegistry}, appends tool results as `role: 'tool'`
1083
+ * messages, and loops back to step 2.
1084
+ * 4. Repeats until the model produces a final text response or the
1085
+ * maximum number of iterations (`maxIterations`) is reached.
1086
+ */
1087
+ async chatWithTools(messages, options) {
1088
+ const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
1089
+ const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1090
+ const registeredTools = this.toolRegistry.getAll();
1091
+ const mergedTools = [
1092
+ ...registeredTools,
1093
+ ...restOptions.tools ?? []
344
1094
  ];
345
1095
  const chatOptions = {
346
1096
  ...restOptions,
@@ -462,6 +1212,12 @@ var _AIService = class _AIService {
462
1212
  }
463
1213
  }
464
1214
  }
1215
+ yield {
1216
+ type: "tool-result",
1217
+ toolCallId: tr.toolCallId,
1218
+ toolName: tr.toolName,
1219
+ output: tr.output
1220
+ };
465
1221
  conversation.push({
466
1222
  role: "tool",
467
1223
  content: [tr]
@@ -493,6 +1249,10 @@ function sse(data) {
493
1249
 
494
1250
  `;
495
1251
  }
1252
+ function dataStreamLine(prefix, data) {
1253
+ return `${prefix}:${JSON.stringify(data)}
1254
+ `;
1255
+ }
496
1256
  function encodeStreamPart(part) {
497
1257
  switch (part.type) {
498
1258
  case "text-delta":
@@ -527,8 +1287,20 @@ function encodeStreamPart(part) {
527
1287
  type: "error",
528
1288
  errorText: String(part.error)
529
1289
  });
1290
+ // Handle reasoning/thinking streams (DeepSeek R1, o1-style models)
1291
+ // Use 'g:' prefix for reasoning content per Vercel AI SDK protocol
1292
+ case "reasoning-start":
1293
+ return dataStreamLine("g", { text: "" });
1294
+ case "reasoning-delta":
1295
+ return dataStreamLine("g", { text: part.text });
1296
+ case "reasoning-end":
1297
+ return "";
1298
+ // No specific end marker needed for reasoning
530
1299
  // finish-step and finish are handled by the generator, not here
531
1300
  default:
1301
+ if (part.type?.startsWith("step-")) {
1302
+ return sse(part);
1303
+ }
532
1304
  return "";
533
1305
  }
534
1306
  }
@@ -562,8 +1334,7 @@ async function* encodeVercelDataStream(events) {
562
1334
  yield "data: [DONE]\n\n";
563
1335
  }
564
1336
 
565
- // src/routes/ai-routes.ts
566
- var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
1337
+ // src/routes/message-utils.ts
567
1338
  function normalizeMessage(raw) {
568
1339
  const role = raw.role;
569
1340
  if (typeof raw.content === "string") {
@@ -580,14 +1351,7 @@ function normalizeMessage(raw) {
580
1351
  }
581
1352
  return { role, content: "" };
582
1353
  }
583
- function validateMessage(raw) {
584
- if (typeof raw !== "object" || raw === null) {
585
- return "each message must be an object";
586
- }
587
- const msg = raw;
588
- if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
589
- return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
590
- }
1354
+ function validateMessageContent(msg, opts) {
591
1355
  const content = msg.content;
592
1356
  if (Array.isArray(msg.parts)) {
593
1357
  return null;
@@ -610,20 +1374,32 @@ function validateMessage(raw) {
610
1374
  }
611
1375
  return null;
612
1376
  }
613
- if (content === null || content === void 0) {
614
- if (msg.role === "assistant" || msg.role === "tool") {
615
- return null;
616
- }
1377
+ if ((content === null || content === void 0) && opts?.allowEmptyContent) {
1378
+ return null;
617
1379
  }
618
1380
  return "message.content must be a string, an array, or include parts";
619
1381
  }
620
- function buildAIRoutes(aiService, conversationService, logger) {
621
- return [
622
- // ── Chat ────────────────────────────────────────────────────
623
- //
624
- // Dual-mode endpoint compatible with both the legacy ObjectStack
625
- // format (`{ messages, options }`) and the Vercel AI SDK useChat
626
- // flat format (`{ messages, system, model, stream, … }`).
1382
+
1383
+ // src/routes/ai-routes.ts
1384
+ var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
1385
+ function validateMessage(raw) {
1386
+ if (typeof raw !== "object" || raw === null) {
1387
+ return "each message must be an object";
1388
+ }
1389
+ const msg = raw;
1390
+ if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
1391
+ return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
1392
+ }
1393
+ const allowEmpty = msg.role === "assistant" || msg.role === "tool";
1394
+ return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
1395
+ }
1396
+ function buildAIRoutes(aiService, conversationService, logger) {
1397
+ return [
1398
+ // ── Chat ────────────────────────────────────────────────────
1399
+ //
1400
+ // Dual-mode endpoint compatible with both the legacy ObjectStack
1401
+ // format (`{ messages, options }`) and the Vercel AI SDK useChat
1402
+ // flat format (`{ messages, system, model, stream, … }`).
627
1403
  //
628
1404
  // Behaviour:
629
1405
  // • `stream !== false` → Vercel Data Stream Protocol (SSE)
@@ -705,1358 +1481,764 @@ function buildAIRoutes(aiService, conversationService, logger) {
705
1481
  handler: async (req) => {
706
1482
  const { messages, options } = req.body ?? {};
707
1483
  if (!Array.isArray(messages) || messages.length === 0) {
708
- return { status: 400, body: { error: "messages array is required" } };
709
- }
710
- for (const msg of messages) {
711
- const err = validateMessage(msg);
712
- if (err) return { status: 400, body: { error: err } };
713
- }
714
- try {
715
- if (!aiService.streamChat) {
716
- return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
717
- }
718
- const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
719
- return { status: 200, stream: true, events };
720
- } catch (err) {
721
- logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
722
- return { status: 500, body: { error: "Internal AI service error" } };
723
- }
724
- }
725
- },
726
- // ── Complete ────────────────────────────────────────────────
727
- {
728
- method: "POST",
729
- path: "/api/v1/ai/complete",
730
- description: "Text completion",
731
- auth: true,
732
- permissions: ["ai:complete"],
733
- handler: async (req) => {
734
- const { prompt, options } = req.body ?? {};
735
- if (!prompt || typeof prompt !== "string") {
736
- return { status: 400, body: { error: "prompt string is required" } };
737
- }
738
- try {
739
- const result = await aiService.complete(prompt, options);
740
- return { status: 200, body: result };
741
- } catch (err) {
742
- logger.error("[AI Route] /complete error", err instanceof Error ? err : void 0);
743
- return { status: 500, body: { error: "Internal AI service error" } };
744
- }
745
- }
746
- },
747
- // ── Models ──────────────────────────────────────────────────
748
- {
749
- method: "GET",
750
- path: "/api/v1/ai/models",
751
- description: "List available models",
752
- auth: true,
753
- permissions: ["ai:read"],
754
- handler: async () => {
755
- try {
756
- const models = aiService.listModels ? await aiService.listModels() : [];
757
- return { status: 200, body: { models } };
758
- } catch (err) {
759
- logger.error("[AI Route] /models error", err instanceof Error ? err : void 0);
760
- return { status: 500, body: { error: "Internal AI service error" } };
761
- }
762
- }
763
- },
764
- // ── Conversations ──────────────────────────────────────────
765
- {
766
- method: "POST",
767
- path: "/api/v1/ai/conversations",
768
- description: "Create a conversation",
769
- auth: true,
770
- permissions: ["ai:conversations"],
771
- handler: async (req) => {
772
- try {
773
- if (req.body !== void 0 && req.body !== null && (typeof req.body !== "object" || Array.isArray(req.body))) {
774
- return { status: 400, body: { error: "Invalid request payload" } };
775
- }
776
- const options = { ...req.body ?? {} };
777
- if (req.user?.userId) {
778
- options.userId = req.user.userId;
779
- }
780
- const conversation = await conversationService.create(options);
781
- return { status: 201, body: conversation };
782
- } catch (err) {
783
- logger.error("[AI Route] POST /conversations error", err instanceof Error ? err : void 0);
784
- return { status: 500, body: { error: "Internal AI service error" } };
785
- }
786
- }
787
- },
788
- {
789
- method: "GET",
790
- path: "/api/v1/ai/conversations",
791
- description: "List conversations",
792
- auth: true,
793
- permissions: ["ai:conversations"],
794
- handler: async (req) => {
795
- try {
796
- const rawQuery = req.query ?? {};
797
- const options = { ...rawQuery };
798
- if (typeof rawQuery.limit === "string") {
799
- const parsedLimit = Number(rawQuery.limit);
800
- if (!Number.isFinite(parsedLimit) || parsedLimit <= 0 || !Number.isInteger(parsedLimit)) {
801
- return { status: 400, body: { error: "Invalid limit parameter" } };
802
- }
803
- options.limit = parsedLimit;
804
- }
805
- if (req.user?.userId) {
806
- options.userId = req.user.userId;
807
- }
808
- const conversations = await conversationService.list(options);
809
- return { status: 200, body: { conversations } };
810
- } catch (err) {
811
- logger.error("[AI Route] GET /conversations error", err instanceof Error ? err : void 0);
812
- return { status: 500, body: { error: "Internal AI service error" } };
813
- }
814
- }
815
- },
816
- {
817
- method: "POST",
818
- path: "/api/v1/ai/conversations/:id/messages",
819
- description: "Add message to a conversation",
820
- auth: true,
821
- permissions: ["ai:conversations"],
822
- handler: async (req) => {
823
- const id = req.params?.id;
824
- if (!id) {
825
- return { status: 400, body: { error: "conversation id is required" } };
826
- }
827
- const message = req.body;
828
- const validationError = validateMessage(message);
829
- if (validationError) {
830
- return { status: 400, body: { error: validationError } };
831
- }
832
- try {
833
- if (req.user?.userId) {
834
- const existing = await conversationService.get(id);
835
- if (!existing) {
836
- return { status: 404, body: { error: `Conversation "${id}" not found` } };
837
- }
838
- if (existing.userId && existing.userId !== req.user.userId) {
839
- return { status: 403, body: { error: "You do not have access to this conversation" } };
840
- }
841
- }
842
- const conversation = await conversationService.addMessage(id, message);
843
- return { status: 200, body: conversation };
844
- } catch (err) {
845
- const msg = err instanceof Error ? err.message : String(err);
846
- if (msg.includes("not found")) {
847
- return { status: 404, body: { error: msg } };
848
- }
849
- logger.error("[AI Route] POST /conversations/:id/messages error", err instanceof Error ? err : void 0);
850
- return { status: 500, body: { error: "Internal AI service error" } };
851
- }
852
- }
853
- },
854
- {
855
- method: "DELETE",
856
- path: "/api/v1/ai/conversations/:id",
857
- description: "Delete a conversation",
858
- auth: true,
859
- permissions: ["ai:conversations"],
860
- handler: async (req) => {
861
- const id = req.params?.id;
862
- if (!id) {
863
- return { status: 400, body: { error: "conversation id is required" } };
864
- }
865
- try {
866
- if (req.user?.userId) {
867
- const existing = await conversationService.get(id);
868
- if (!existing) {
869
- return { status: 404, body: { error: `Conversation "${id}" not found` } };
870
- }
871
- if (existing.userId && existing.userId !== req.user.userId) {
872
- return { status: 403, body: { error: "You do not have access to this conversation" } };
873
- }
874
- }
875
- await conversationService.delete(id);
876
- return { status: 204 };
877
- } catch (err) {
878
- logger.error("[AI Route] DELETE /conversations/:id error", err instanceof Error ? err : void 0);
879
- return { status: 500, body: { error: "Internal AI service error" } };
880
- }
881
- }
882
- }
883
- ];
884
- }
885
-
886
- // src/routes/agent-routes.ts
887
- var ALLOWED_AGENT_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
888
- function validateAgentMessage(raw) {
889
- if (typeof raw !== "object" || raw === null) {
890
- return "each message must be an object";
891
- }
892
- const msg = raw;
893
- if (typeof msg.role !== "string" || !ALLOWED_AGENT_ROLES.has(msg.role)) {
894
- return `message.role must be one of ${[...ALLOWED_AGENT_ROLES].map((r) => `"${r}"`).join(", ")} for agent chat`;
895
- }
896
- if (typeof msg.content !== "string") {
897
- return "message.content must be a string";
898
- }
899
- return null;
900
- }
901
- function buildAgentRoutes(aiService, agentRuntime, logger) {
902
- return [
903
- // ── List active agents ──────────────────────────────────────
904
- {
905
- method: "GET",
906
- path: "/api/v1/ai/agents",
907
- description: "List all active AI agents",
908
- auth: true,
909
- permissions: ["ai:chat"],
910
- handler: async () => {
911
- try {
912
- const agents = await agentRuntime.listAgents();
913
- return { status: 200, body: { agents } };
914
- } catch (err) {
915
- logger.error(
916
- "[AI Route] /agents list error",
917
- err instanceof Error ? err : void 0
918
- );
919
- return { status: 500, body: { error: "Internal AI service error" } };
920
- }
921
- }
922
- },
923
- // ── Chat with a specific agent ──────────────────────────────
924
- {
925
- method: "POST",
926
- path: "/api/v1/ai/agents/:agentName/chat",
927
- description: "Chat with a specific AI agent",
928
- auth: true,
929
- permissions: ["ai:chat", "ai:agents"],
930
- handler: async (req) => {
931
- const agentName = req.params?.agentName;
932
- if (!agentName) {
933
- return { status: 400, body: { error: "agentName parameter is required" } };
934
- }
935
- const {
936
- messages: rawMessages,
937
- context: chatContext,
938
- options: extraOptions
939
- } = req.body ?? {};
940
- if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
941
- return { status: 400, body: { error: "messages array is required" } };
942
- }
943
- for (const msg of rawMessages) {
944
- const err = validateAgentMessage(msg);
945
- if (err) return { status: 400, body: { error: err } };
946
- }
947
- const agent = await agentRuntime.loadAgent(agentName);
948
- if (!agent) {
949
- return { status: 404, body: { error: `Agent "${agentName}" not found` } };
950
- }
951
- if (!agent.active) {
952
- return { status: 403, body: { error: `Agent "${agentName}" is not active` } };
953
- }
954
- try {
955
- const systemMessages = agentRuntime.buildSystemMessages(agent, chatContext);
956
- const agentOptions = agentRuntime.buildRequestOptions(
957
- agent,
958
- aiService.toolRegistry.getAll()
959
- );
960
- const safeOverrides = {};
961
- if (extraOptions) {
962
- const ALLOWED_KEYS = /* @__PURE__ */ new Set(["temperature", "maxTokens", "stop"]);
963
- for (const key of Object.keys(extraOptions)) {
964
- if (ALLOWED_KEYS.has(key)) {
965
- safeOverrides[key] = extraOptions[key];
966
- }
967
- }
968
- }
969
- const mergedOptions = { ...agentOptions, ...safeOverrides };
970
- const fullMessages = [
971
- ...systemMessages,
972
- ...rawMessages
973
- ];
974
- const result = await aiService.chatWithTools(fullMessages, {
975
- ...mergedOptions,
976
- maxIterations: agent.planning?.maxIterations
977
- });
978
- return { status: 200, body: result };
979
- } catch (err) {
980
- logger.error(
981
- "[AI Route] /agents/:agentName/chat error",
982
- err instanceof Error ? err : void 0
983
- );
984
- return { status: 500, body: { error: "Internal AI service error" } };
985
- }
986
- }
987
- }
988
- ];
989
- }
990
-
991
- // src/conversation/objectql-conversation-service.ts
992
- var import_node_crypto = require("crypto");
993
- var CONVERSATIONS_OBJECT = "ai_conversations";
994
- var MESSAGES_OBJECT = "ai_messages";
995
- var CONVERSATION_ORDER = [
996
- { field: "created_at", order: "asc" },
997
- { field: "id", order: "asc" }
998
- ];
999
- var MESSAGE_ORDER = [
1000
- { field: "created_at", order: "asc" },
1001
- { field: "id", order: "asc" }
1002
- ];
1003
- var ObjectQLConversationService = class {
1004
- constructor(engine) {
1005
- this.engine = engine;
1006
- }
1007
- async create(options = {}) {
1008
- const now = (/* @__PURE__ */ new Date()).toISOString();
1009
- const id = `conv_${(0, import_node_crypto.randomUUID)()}`;
1010
- const record = {
1011
- id,
1012
- title: options.title ?? null,
1013
- agent_id: options.agentId ?? null,
1014
- user_id: options.userId ?? null,
1015
- metadata: options.metadata ? JSON.stringify(options.metadata) : null,
1016
- created_at: now,
1017
- updated_at: now
1018
- };
1019
- await this.engine.insert(CONVERSATIONS_OBJECT, record);
1020
- return {
1021
- id,
1022
- title: options.title,
1023
- agentId: options.agentId,
1024
- userId: options.userId,
1025
- messages: [],
1026
- createdAt: now,
1027
- updatedAt: now,
1028
- metadata: options.metadata
1029
- };
1030
- }
1031
- async get(conversationId) {
1032
- const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
1033
- where: { id: conversationId }
1034
- });
1035
- if (!row) return null;
1036
- const messages = await this.engine.find(MESSAGES_OBJECT, {
1037
- where: { conversation_id: conversationId },
1038
- orderBy: MESSAGE_ORDER
1039
- });
1040
- return this.toConversation(row, messages);
1041
- }
1042
- async list(options = {}) {
1043
- const where = {};
1044
- if (options.userId) where.user_id = options.userId;
1045
- if (options.agentId) where.agent_id = options.agentId;
1046
- if (options.cursor) {
1047
- const cursorRow = await this.engine.findOne(CONVERSATIONS_OBJECT, {
1048
- where: { id: options.cursor },
1049
- fields: ["created_at", "id"]
1050
- });
1051
- if (cursorRow) {
1052
- where.$or = [
1053
- { created_at: { $gt: cursorRow.created_at } },
1054
- { created_at: cursorRow.created_at, id: { $gt: cursorRow.id } }
1055
- ];
1056
- }
1057
- }
1058
- const rows = await this.engine.find(CONVERSATIONS_OBJECT, {
1059
- where: Object.keys(where).length > 0 ? where : void 0,
1060
- orderBy: CONVERSATION_ORDER,
1061
- limit: options.limit && options.limit > 0 ? options.limit : void 0
1062
- });
1063
- const conversations = await Promise.all(
1064
- rows.map(async (row) => {
1065
- const messages = await this.engine.find(MESSAGES_OBJECT, {
1066
- where: { conversation_id: row.id },
1067
- orderBy: MESSAGE_ORDER
1068
- });
1069
- return this.toConversation(row, messages);
1070
- })
1071
- );
1072
- return conversations;
1073
- }
1074
- async addMessage(conversationId, message) {
1075
- const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
1076
- where: { id: conversationId }
1077
- });
1078
- if (!row) {
1079
- throw new Error(`Conversation "${conversationId}" not found`);
1080
- }
1081
- const now = (/* @__PURE__ */ new Date()).toISOString();
1082
- const msgId = `msg_${(0, import_node_crypto.randomUUID)()}`;
1083
- let contentStr;
1084
- let toolCallsJson = null;
1085
- let toolCallId = null;
1086
- if (message.role === "system" || message.role === "user") {
1087
- contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
1088
- } else if (message.role === "assistant") {
1089
- if (typeof message.content === "string") {
1090
- contentStr = message.content;
1091
- } else {
1092
- const parts = message.content;
1093
- const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
1094
- const toolCalls = parts.filter((p) => p.type === "tool-call");
1095
- contentStr = textParts.join("");
1096
- if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
1097
- }
1098
- } else if (message.role === "tool") {
1099
- contentStr = JSON.stringify(message.content);
1100
- const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
1101
- if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
1102
- } else {
1103
- contentStr = "";
1104
- }
1105
- await this.engine.insert(MESSAGES_OBJECT, {
1106
- id: msgId,
1107
- conversation_id: conversationId,
1108
- role: message.role,
1109
- content: contentStr,
1110
- tool_calls: toolCallsJson,
1111
- tool_call_id: toolCallId,
1112
- created_at: now
1113
- });
1114
- await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
1115
- where: { id: conversationId }
1116
- });
1117
- return await this.get(conversationId);
1118
- }
1119
- async delete(conversationId) {
1120
- await this.engine.delete(MESSAGES_OBJECT, {
1121
- where: { conversation_id: conversationId },
1122
- multi: true
1123
- });
1124
- await this.engine.delete(CONVERSATIONS_OBJECT, {
1125
- where: { id: conversationId }
1126
- });
1127
- }
1128
- // ── Private helpers ──────────────────────────────────────────────
1129
- /**
1130
- * Safely parse a JSON string, returning `undefined` on failure.
1131
- */
1132
- safeParse(value, fallback) {
1133
- if (!value) return void 0;
1134
- try {
1135
- return JSON.parse(value);
1136
- } catch {
1137
- return fallback;
1138
- }
1139
- }
1140
- /**
1141
- * Map a database row + message rows to an AIConversation.
1142
- */
1143
- toConversation(row, messageRows) {
1144
- return {
1145
- id: row.id,
1146
- title: row.title ?? void 0,
1147
- agentId: row.agent_id ?? void 0,
1148
- userId: row.user_id ?? void 0,
1149
- messages: messageRows.map((m) => this.toMessage(m)),
1150
- createdAt: row.created_at,
1151
- updatedAt: row.updated_at,
1152
- metadata: this.safeParse(row.metadata)
1153
- };
1154
- }
1155
- /**
1156
- * Map a database row to a ModelMessage.
1157
- */
1158
- toMessage(row) {
1159
- switch (row.role) {
1160
- case "system":
1161
- return { role: "system", content: row.content };
1162
- case "user":
1163
- return { role: "user", content: row.content };
1164
- case "assistant": {
1165
- const toolCalls = this.safeParse(row.tool_calls);
1166
- if (toolCalls && toolCalls.length > 0) {
1167
- const content = [];
1168
- if (row.content) content.push({ type: "text", text: row.content });
1169
- content.push(...toolCalls);
1170
- return { role: "assistant", content };
1171
- }
1172
- return { role: "assistant", content: row.content };
1173
- }
1174
- case "tool": {
1175
- const toolResults = this.safeParse(row.content);
1176
- if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
1177
- return { role: "tool", content: toolResults };
1178
- }
1179
- return {
1180
- role: "tool",
1181
- content: [{
1182
- type: "tool-result",
1183
- toolCallId: row.tool_call_id ?? "",
1184
- toolName: "unknown",
1185
- output: { type: "text", value: row.content }
1186
- }]
1187
- };
1188
- }
1189
- default:
1190
- return { role: "user", content: row.content };
1191
- }
1192
- }
1193
- };
1194
-
1195
- // src/objects/ai-conversation.object.ts
1196
- var import_data = require("@objectstack/spec/data");
1197
- var AiConversationObject = import_data.ObjectSchema.create({
1198
- namespace: "ai",
1199
- name: "conversations",
1200
- label: "AI Conversation",
1201
- pluralLabel: "AI Conversations",
1202
- icon: "message-square",
1203
- isSystem: true,
1204
- description: "Persistent AI conversation metadata",
1205
- fields: {
1206
- id: import_data.Field.text({
1207
- label: "Conversation ID",
1208
- required: true,
1209
- readonly: true
1210
- }),
1211
- title: import_data.Field.text({
1212
- label: "Title",
1213
- required: false,
1214
- maxLength: 500,
1215
- description: "Conversation title or summary"
1216
- }),
1217
- agent_id: import_data.Field.text({
1218
- label: "Agent ID",
1219
- required: false,
1220
- maxLength: 255,
1221
- description: "Associated AI agent identifier"
1222
- }),
1223
- user_id: import_data.Field.text({
1224
- label: "User ID",
1225
- required: false,
1226
- maxLength: 255,
1227
- description: "User who owns the conversation"
1228
- }),
1229
- metadata: import_data.Field.textarea({
1230
- label: "Metadata",
1231
- required: false,
1232
- description: "JSON-serialized conversation metadata"
1233
- }),
1234
- created_at: import_data.Field.datetime({
1235
- label: "Created At",
1236
- required: true,
1237
- defaultValue: "NOW()",
1238
- readonly: true
1239
- }),
1240
- updated_at: import_data.Field.datetime({
1241
- label: "Updated At",
1242
- required: true,
1243
- defaultValue: "NOW()",
1244
- readonly: true
1245
- })
1246
- },
1247
- indexes: [
1248
- { fields: ["user_id"] },
1249
- { fields: ["agent_id"] },
1250
- { fields: ["created_at"] }
1251
- ],
1252
- enable: {
1253
- trackHistory: false,
1254
- searchable: false,
1255
- apiEnabled: true,
1256
- apiMethods: ["get", "list", "create", "update", "delete"],
1257
- trash: false,
1258
- mru: false
1259
- }
1260
- });
1261
-
1262
- // src/objects/ai-message.object.ts
1263
- var import_data2 = require("@objectstack/spec/data");
1264
- var AiMessageObject = import_data2.ObjectSchema.create({
1265
- namespace: "ai",
1266
- name: "messages",
1267
- label: "AI Message",
1268
- pluralLabel: "AI Messages",
1269
- icon: "message-circle",
1270
- isSystem: true,
1271
- description: "Individual messages within AI conversations",
1272
- fields: {
1273
- id: import_data2.Field.text({
1274
- label: "Message ID",
1275
- required: true,
1276
- readonly: true
1277
- }),
1278
- conversation_id: import_data2.Field.text({
1279
- label: "Conversation ID",
1280
- required: true,
1281
- description: "Foreign key to ai_conversations"
1282
- }),
1283
- role: import_data2.Field.select({
1284
- label: "Role",
1285
- required: true,
1286
- options: [
1287
- { label: "System", value: "system" },
1288
- { label: "User", value: "user" },
1289
- { label: "Assistant", value: "assistant" },
1290
- { label: "Tool", value: "tool" }
1291
- ]
1292
- }),
1293
- content: import_data2.Field.textarea({
1294
- label: "Content",
1295
- required: true,
1296
- description: "Message content"
1297
- }),
1298
- tool_calls: import_data2.Field.textarea({
1299
- label: "Tool Calls",
1300
- required: false,
1301
- description: "JSON-serialized tool calls (when role=assistant)"
1302
- }),
1303
- tool_call_id: import_data2.Field.text({
1304
- label: "Tool Call ID",
1305
- required: false,
1306
- maxLength: 255,
1307
- description: "ID of the tool call this message responds to (when role=tool)"
1308
- }),
1309
- created_at: import_data2.Field.datetime({
1310
- label: "Created At",
1311
- required: true,
1312
- defaultValue: "NOW()",
1313
- readonly: true
1314
- })
1315
- },
1316
- indexes: [
1317
- { fields: ["conversation_id"] },
1318
- { fields: ["conversation_id", "created_at"] }
1319
- ],
1320
- enable: {
1321
- trackHistory: false,
1322
- searchable: false,
1323
- apiEnabled: true,
1324
- apiMethods: ["get", "list", "create"],
1325
- trash: false,
1326
- mru: false
1327
- }
1328
- });
1329
-
1330
- // src/tools/data-tools.ts
1331
- var MAX_QUERY_LIMIT = 200;
1332
- var DEFAULT_QUERY_LIMIT = 20;
1333
- var LIST_OBJECTS_TOOL = {
1334
- name: "list_objects",
1335
- description: "List all available data objects (tables) in the system. Returns object names and labels.",
1336
- parameters: {
1337
- type: "object",
1338
- properties: {},
1339
- additionalProperties: false
1340
- }
1341
- };
1342
- var DESCRIBE_OBJECT_TOOL = {
1343
- name: "describe_object",
1344
- description: "Get the schema (fields, types, labels) of a specific data object. Use this to understand the structure of a table before querying it.",
1345
- parameters: {
1346
- type: "object",
1347
- properties: {
1348
- objectName: {
1349
- type: "string",
1350
- description: "The snake_case name of the object to describe"
1351
- }
1352
- },
1353
- required: ["objectName"],
1354
- additionalProperties: false
1355
- }
1356
- };
1357
- var QUERY_RECORDS_TOOL = {
1358
- name: "query_records",
1359
- description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
1360
- parameters: {
1361
- type: "object",
1362
- properties: {
1363
- objectName: {
1364
- type: "string",
1365
- description: "The snake_case name of the object to query"
1366
- },
1367
- where: {
1368
- type: "object",
1369
- description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
1370
- },
1371
- fields: {
1372
- type: "array",
1373
- items: { type: "string" },
1374
- description: "List of field names to return (omit for all fields)"
1375
- },
1376
- orderBy: {
1377
- type: "array",
1378
- items: {
1379
- type: "object",
1380
- properties: {
1381
- field: { type: "string" },
1382
- order: { type: "string", enum: ["asc", "desc"] }
1484
+ return { status: 400, body: { error: "messages array is required" } };
1485
+ }
1486
+ for (const msg of messages) {
1487
+ const err = validateMessage(msg);
1488
+ if (err) return { status: 400, body: { error: err } };
1489
+ }
1490
+ try {
1491
+ if (!aiService.streamChat) {
1492
+ return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
1383
1493
  }
1384
- },
1385
- description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
1386
- },
1387
- limit: {
1388
- type: "number",
1389
- description: `Maximum number of records to return (default ${DEFAULT_QUERY_LIMIT}, max ${MAX_QUERY_LIMIT})`
1390
- },
1391
- offset: {
1392
- type: "number",
1393
- description: "Number of records to skip for pagination"
1494
+ const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
1495
+ return { status: 200, stream: true, events };
1496
+ } catch (err) {
1497
+ logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
1498
+ return { status: 500, body: { error: "Internal AI service error" } };
1499
+ }
1394
1500
  }
1395
1501
  },
1396
- required: ["objectName"],
1397
- additionalProperties: false
1398
- }
1399
- };
1400
- var GET_RECORD_TOOL = {
1401
- name: "get_record",
1402
- description: "Get a single record by its ID from a data object.",
1403
- parameters: {
1404
- type: "object",
1405
- properties: {
1406
- objectName: {
1407
- type: "string",
1408
- description: "The snake_case name of the object"
1409
- },
1410
- recordId: {
1411
- type: "string",
1412
- description: "The unique ID of the record"
1413
- },
1414
- fields: {
1415
- type: "array",
1416
- items: { type: "string" },
1417
- description: "List of field names to return (omit for all fields)"
1502
+ // ── Complete ────────────────────────────────────────────────
1503
+ {
1504
+ method: "POST",
1505
+ path: "/api/v1/ai/complete",
1506
+ description: "Text completion",
1507
+ auth: true,
1508
+ permissions: ["ai:complete"],
1509
+ handler: async (req) => {
1510
+ const { prompt, options } = req.body ?? {};
1511
+ if (!prompt || typeof prompt !== "string") {
1512
+ return { status: 400, body: { error: "prompt string is required" } };
1513
+ }
1514
+ try {
1515
+ const result = await aiService.complete(prompt, options);
1516
+ return { status: 200, body: result };
1517
+ } catch (err) {
1518
+ logger.error("[AI Route] /complete error", err instanceof Error ? err : void 0);
1519
+ return { status: 500, body: { error: "Internal AI service error" } };
1520
+ }
1418
1521
  }
1419
1522
  },
1420
- required: ["objectName", "recordId"],
1421
- additionalProperties: false
1422
- }
1423
- };
1424
- var AGGREGATE_DATA_TOOL = {
1425
- name: "aggregate_data",
1426
- description: "Perform aggregation/statistical operations on a data object. Supports count, sum, avg, min, max with optional groupBy and where filters.",
1427
- parameters: {
1428
- type: "object",
1429
- properties: {
1430
- objectName: {
1431
- type: "string",
1432
- description: "The snake_case name of the object to aggregate"
1433
- },
1434
- aggregations: {
1435
- type: "array",
1436
- items: {
1437
- type: "object",
1438
- properties: {
1439
- function: {
1440
- type: "string",
1441
- enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
1442
- description: "Aggregation function"
1443
- },
1444
- field: {
1445
- type: "string",
1446
- description: "Field to aggregate (optional for count)"
1447
- },
1448
- alias: {
1449
- type: "string",
1450
- description: "Result column alias"
1451
- }
1452
- },
1453
- required: ["function", "alias"]
1454
- },
1455
- description: "Aggregation definitions"
1456
- },
1457
- groupBy: {
1458
- type: "array",
1459
- items: { type: "string" },
1460
- description: "Fields to group by"
1461
- },
1462
- where: {
1463
- type: "object",
1464
- description: "Filter conditions applied before aggregation"
1523
+ // ── Models ──────────────────────────────────────────────────
1524
+ {
1525
+ method: "GET",
1526
+ path: "/api/v1/ai/models",
1527
+ description: "List available models",
1528
+ auth: true,
1529
+ permissions: ["ai:read"],
1530
+ handler: async () => {
1531
+ try {
1532
+ const models = aiService.listModels ? await aiService.listModels() : [];
1533
+ return { status: 200, body: { models } };
1534
+ } catch (err) {
1535
+ logger.error("[AI Route] /models error", err instanceof Error ? err : void 0);
1536
+ return { status: 500, body: { error: "Internal AI service error" } };
1537
+ }
1465
1538
  }
1466
1539
  },
1467
- required: ["objectName", "aggregations"],
1468
- additionalProperties: false
1469
- }
1470
- };
1471
- var DATA_TOOL_DEFINITIONS = [
1472
- LIST_OBJECTS_TOOL,
1473
- DESCRIBE_OBJECT_TOOL,
1474
- QUERY_RECORDS_TOOL,
1475
- GET_RECORD_TOOL,
1476
- AGGREGATE_DATA_TOOL
1477
- ];
1478
- function createListObjectsHandler(ctx) {
1479
- return async () => {
1480
- const objects = await ctx.metadataService.listObjects();
1481
- const summary = objects.map((o) => ({
1482
- name: o.name,
1483
- label: o.label ?? o.name
1484
- }));
1485
- return JSON.stringify(summary);
1486
- };
1487
- }
1488
- function createDescribeObjectHandler(ctx) {
1489
- return async (args) => {
1490
- const { objectName } = args;
1491
- const objectDef = await ctx.metadataService.getObject(objectName);
1492
- if (!objectDef) {
1493
- return JSON.stringify({ error: `Object "${objectName}" not found` });
1494
- }
1495
- const def = objectDef;
1496
- const fields = def.fields ?? {};
1497
- const fieldSummary = {};
1498
- for (const [key, f] of Object.entries(fields)) {
1499
- fieldSummary[key] = {
1500
- type: f.type,
1501
- label: f.label ?? key,
1502
- required: f.required ?? false,
1503
- ...f.reference ? { reference: f.reference } : {},
1504
- ...f.options ? { options: f.options } : {}
1505
- };
1506
- }
1507
- return JSON.stringify({
1508
- name: def.name,
1509
- label: def.label ?? def.name,
1510
- fields: fieldSummary
1511
- });
1512
- };
1513
- }
1514
- function createQueryRecordsHandler(ctx) {
1515
- return async (args) => {
1516
- const {
1517
- objectName,
1518
- where,
1519
- fields,
1520
- orderBy,
1521
- limit,
1522
- offset
1523
- } = args;
1524
- const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
1525
- const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
1526
- const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
1527
- const records = await ctx.dataEngine.find(objectName, {
1528
- where,
1529
- fields,
1530
- orderBy,
1531
- limit: safeLimit,
1532
- offset: safeOffset
1533
- });
1534
- return JSON.stringify({ count: records.length, records });
1535
- };
1536
- }
1537
- function createGetRecordHandler(ctx) {
1538
- return async (args) => {
1539
- const { objectName, recordId, fields } = args;
1540
- const record = await ctx.dataEngine.findOne(objectName, {
1541
- where: { id: recordId },
1542
- fields
1543
- });
1544
- if (!record) {
1545
- return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
1546
- }
1547
- return JSON.stringify(record);
1548
- };
1549
- }
1550
- var VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
1551
- "count",
1552
- "sum",
1553
- "avg",
1554
- "min",
1555
- "max",
1556
- "count_distinct"
1557
- ]);
1558
- function createAggregateDataHandler(ctx) {
1559
- return async (args) => {
1560
- const { objectName, aggregations, groupBy, where } = args;
1561
- for (const a of aggregations) {
1562
- if (!VALID_AGG_FUNCTIONS.has(a.function)) {
1563
- return JSON.stringify({
1564
- error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
1565
- });
1566
- }
1567
- }
1568
- const result = await ctx.dataEngine.aggregate(objectName, {
1569
- where,
1570
- groupBy,
1571
- aggregations: aggregations.map((a) => ({
1572
- function: a.function,
1573
- field: a.field,
1574
- alias: a.alias
1575
- }))
1576
- });
1577
- return JSON.stringify(result);
1578
- };
1579
- }
1580
- function registerDataTools(registry, context) {
1581
- registry.register(LIST_OBJECTS_TOOL, createListObjectsHandler(context));
1582
- registry.register(DESCRIBE_OBJECT_TOOL, createDescribeObjectHandler(context));
1583
- registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
1584
- registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
1585
- registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
1586
- }
1587
-
1588
- // src/tools/create-object.tool.ts
1589
- var import_ai = require("@objectstack/spec/ai");
1590
- var createObjectTool = (0, import_ai.defineTool)({
1591
- name: "create_object",
1592
- label: "Create Object",
1593
- description: "Creates a new data object (table) with the specified name, label, and optional field definitions. Use this when the user wants to create a new entity, table, or data model.",
1594
- category: "data",
1595
- builtIn: true,
1596
- // NOTE: requiresConfirmation is intentionally false (default) because the
1597
- // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
1598
- // executes tool calls immediately without checking this flag. The flag
1599
- // should only be set once server-side approval gating is implemented to
1600
- // avoid giving users a false sense of safety.
1601
- parameters: {
1602
- type: "object",
1603
- properties: {
1604
- name: {
1605
- type: "string",
1606
- description: "Machine name for the object (snake_case, e.g. project_task)"
1607
- },
1608
- label: {
1609
- type: "string",
1610
- description: "Human-readable display name (e.g. Project Task)"
1611
- },
1612
- fields: {
1613
- type: "array",
1614
- description: "Initial fields to create with the object",
1615
- items: {
1616
- type: "object",
1617
- properties: {
1618
- name: { type: "string", description: "Field machine name (snake_case)" },
1619
- label: { type: "string", description: "Field display name" },
1620
- type: {
1621
- type: "string",
1622
- description: "Field data type",
1623
- enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
1624
- },
1625
- required: { type: "boolean", description: "Whether the field is required" }
1626
- },
1627
- required: ["name", "type"]
1540
+ // ── Conversations ──────────────────────────────────────────
1541
+ {
1542
+ method: "POST",
1543
+ path: "/api/v1/ai/conversations",
1544
+ description: "Create a conversation",
1545
+ auth: true,
1546
+ permissions: ["ai:conversations"],
1547
+ handler: async (req) => {
1548
+ try {
1549
+ if (req.body !== void 0 && req.body !== null && (typeof req.body !== "object" || Array.isArray(req.body))) {
1550
+ return { status: 400, body: { error: "Invalid request payload" } };
1551
+ }
1552
+ const options = { ...req.body ?? {} };
1553
+ if (req.user?.userId) {
1554
+ options.userId = req.user.userId;
1555
+ }
1556
+ const conversation = await conversationService.create(options);
1557
+ return { status: 201, body: conversation };
1558
+ } catch (err) {
1559
+ logger.error("[AI Route] POST /conversations error", err instanceof Error ? err : void 0);
1560
+ return { status: 500, body: { error: "Internal AI service error" } };
1628
1561
  }
1629
- },
1630
- enableFeatures: {
1631
- type: "object",
1632
- description: "Object capability flags",
1633
- properties: {
1634
- trackHistory: { type: "boolean" },
1635
- apiEnabled: { type: "boolean" }
1562
+ }
1563
+ },
1564
+ {
1565
+ method: "GET",
1566
+ path: "/api/v1/ai/conversations",
1567
+ description: "List conversations",
1568
+ auth: true,
1569
+ permissions: ["ai:conversations"],
1570
+ handler: async (req) => {
1571
+ try {
1572
+ const rawQuery = req.query ?? {};
1573
+ const options = { ...rawQuery };
1574
+ if (typeof rawQuery.limit === "string") {
1575
+ const parsedLimit = Number(rawQuery.limit);
1576
+ if (!Number.isFinite(parsedLimit) || parsedLimit <= 0 || !Number.isInteger(parsedLimit)) {
1577
+ return { status: 400, body: { error: "Invalid limit parameter" } };
1578
+ }
1579
+ options.limit = parsedLimit;
1580
+ }
1581
+ if (req.user?.userId) {
1582
+ options.userId = req.user.userId;
1583
+ }
1584
+ const conversations = await conversationService.list(options);
1585
+ return { status: 200, body: { conversations } };
1586
+ } catch (err) {
1587
+ logger.error("[AI Route] GET /conversations error", err instanceof Error ? err : void 0);
1588
+ return { status: 500, body: { error: "Internal AI service error" } };
1636
1589
  }
1637
1590
  }
1638
1591
  },
1639
- required: ["name", "label"],
1640
- additionalProperties: false
1641
- }
1642
- });
1643
-
1644
- // src/tools/add-field.tool.ts
1645
- var import_ai2 = require("@objectstack/spec/ai");
1646
- var addFieldTool = (0, import_ai2.defineTool)({
1647
- name: "add_field",
1648
- label: "Add Field",
1649
- description: "Adds a new field (column) to an existing data object. Use this when the user wants to add a property, column, or attribute to a table.",
1650
- category: "data",
1651
- builtIn: true,
1652
- parameters: {
1653
- type: "object",
1654
- properties: {
1655
- objectName: {
1656
- type: "string",
1657
- description: "Target object machine name (snake_case)"
1658
- },
1659
- name: {
1660
- type: "string",
1661
- description: "Field machine name (snake_case, e.g. due_date)"
1662
- },
1663
- label: {
1664
- type: "string",
1665
- description: "Human-readable field label (e.g. Due Date)"
1666
- },
1667
- type: {
1668
- type: "string",
1669
- description: "Field data type",
1670
- enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
1671
- },
1672
- required: {
1673
- type: "boolean",
1674
- description: "Whether the field is required"
1675
- },
1676
- defaultValue: {
1677
- description: "Default value for the field"
1678
- },
1679
- options: {
1680
- type: "array",
1681
- description: "Options for select/picklist fields",
1682
- items: {
1683
- type: "object",
1684
- properties: {
1685
- label: { type: "string" },
1686
- value: {
1687
- type: "string",
1688
- description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
1689
- pattern: "^[a-z_][a-z0-9_]*$"
1592
+ {
1593
+ method: "POST",
1594
+ path: "/api/v1/ai/conversations/:id/messages",
1595
+ description: "Add message to a conversation",
1596
+ auth: true,
1597
+ permissions: ["ai:conversations"],
1598
+ handler: async (req) => {
1599
+ const id = req.params?.id;
1600
+ if (!id) {
1601
+ return { status: 400, body: { error: "conversation id is required" } };
1602
+ }
1603
+ const message = req.body;
1604
+ const validationError = validateMessage(message);
1605
+ if (validationError) {
1606
+ return { status: 400, body: { error: validationError } };
1607
+ }
1608
+ try {
1609
+ if (req.user?.userId) {
1610
+ const existing = await conversationService.get(id);
1611
+ if (!existing) {
1612
+ return { status: 404, body: { error: `Conversation "${id}" not found` } };
1613
+ }
1614
+ if (existing.userId && existing.userId !== req.user.userId) {
1615
+ return { status: 403, body: { error: "You do not have access to this conversation" } };
1690
1616
  }
1691
1617
  }
1618
+ const conversation = await conversationService.addMessage(id, message);
1619
+ return { status: 200, body: conversation };
1620
+ } catch (err) {
1621
+ const msg = err instanceof Error ? err.message : String(err);
1622
+ if (msg.includes("not found")) {
1623
+ return { status: 404, body: { error: msg } };
1624
+ }
1625
+ logger.error("[AI Route] POST /conversations/:id/messages error", err instanceof Error ? err : void 0);
1626
+ return { status: 500, body: { error: "Internal AI service error" } };
1692
1627
  }
1693
- },
1694
- reference: {
1695
- type: "string",
1696
- description: "Referenced object name for lookup fields (snake_case, e.g. account)"
1697
1628
  }
1698
1629
  },
1699
- required: ["objectName", "name", "type"],
1700
- additionalProperties: false
1701
- }
1702
- });
1703
-
1704
- // src/tools/modify-field.tool.ts
1705
- var import_ai3 = require("@objectstack/spec/ai");
1706
- var modifyFieldTool = (0, import_ai3.defineTool)({
1707
- name: "modify_field",
1708
- label: "Modify Field",
1709
- description: "Modifies an existing field definition (label, type, required, default value, etc.) on a data object. Use this when the user wants to change or reconfigure an existing column or attribute (not rename it).",
1710
- category: "data",
1711
- builtIn: true,
1712
- parameters: {
1713
- type: "object",
1714
- properties: {
1715
- objectName: {
1716
- type: "string",
1717
- description: "Target object machine name (snake_case)"
1718
- },
1719
- fieldName: {
1720
- type: "string",
1721
- description: "Existing field machine name to modify (snake_case)"
1722
- },
1723
- changes: {
1724
- type: "object",
1725
- description: "Field properties to update (partial patch)",
1726
- properties: {
1727
- label: { type: "string", description: "New display label" },
1728
- type: { type: "string", description: "New field type" },
1729
- required: { type: "boolean", description: "Update required constraint" },
1730
- defaultValue: { description: "New default value" }
1630
+ {
1631
+ method: "DELETE",
1632
+ path: "/api/v1/ai/conversations/:id",
1633
+ description: "Delete a conversation",
1634
+ auth: true,
1635
+ permissions: ["ai:conversations"],
1636
+ handler: async (req) => {
1637
+ const id = req.params?.id;
1638
+ if (!id) {
1639
+ return { status: 400, body: { error: "conversation id is required" } };
1640
+ }
1641
+ try {
1642
+ if (req.user?.userId) {
1643
+ const existing = await conversationService.get(id);
1644
+ if (!existing) {
1645
+ return { status: 404, body: { error: `Conversation "${id}" not found` } };
1646
+ }
1647
+ if (existing.userId && existing.userId !== req.user.userId) {
1648
+ return { status: 403, body: { error: "You do not have access to this conversation" } };
1649
+ }
1650
+ }
1651
+ await conversationService.delete(id);
1652
+ return { status: 204 };
1653
+ } catch (err) {
1654
+ logger.error("[AI Route] DELETE /conversations/:id error", err instanceof Error ? err : void 0);
1655
+ return { status: 500, body: { error: "Internal AI service error" } };
1731
1656
  }
1732
1657
  }
1733
- },
1734
- required: ["objectName", "fieldName", "changes"],
1735
- additionalProperties: false
1736
- }
1737
- });
1658
+ }
1659
+ ];
1660
+ }
1738
1661
 
1739
- // src/tools/delete-field.tool.ts
1740
- var import_ai4 = require("@objectstack/spec/ai");
1741
- var deleteFieldTool = (0, import_ai4.defineTool)({
1742
- name: "delete_field",
1743
- label: "Delete Field",
1744
- description: "Removes a field (column) from an existing data object. This is a destructive operation. Use this when the user explicitly wants to remove an attribute or column from a table.",
1745
- category: "data",
1746
- builtIn: true,
1747
- // NOTE: requiresConfirmation is intentionally false (default) because the
1748
- // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
1749
- // executes tool calls immediately without checking this flag. The flag
1750
- // should only be set once server-side approval gating is implemented.
1751
- parameters: {
1752
- type: "object",
1753
- properties: {
1754
- objectName: {
1755
- type: "string",
1756
- description: "Target object machine name (snake_case)"
1757
- },
1758
- fieldName: {
1759
- type: "string",
1760
- description: "Field machine name to delete (snake_case)"
1761
- }
1762
- },
1763
- required: ["objectName", "fieldName"],
1764
- additionalProperties: false
1662
+ // src/routes/agent-routes.ts
1663
+ var ALLOWED_AGENT_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
1664
+ function validateAgentMessage(raw) {
1665
+ if (typeof raw !== "object" || raw === null) {
1666
+ return "each message must be an object";
1765
1667
  }
1766
- });
1767
-
1768
- // src/tools/list-metadata-objects.tool.ts
1769
- var import_ai5 = require("@objectstack/spec/ai");
1770
- var listMetadataObjectsTool = (0, import_ai5.defineTool)({
1771
- name: "list_metadata_objects",
1772
- label: "List Metadata Objects",
1773
- description: "Lists all registered metadata objects (tables) in the current environment. Use this when the user wants to see what tables, entities, or data models are defined in metadata.",
1774
- category: "data",
1775
- builtIn: true,
1776
- parameters: {
1777
- type: "object",
1778
- properties: {
1779
- filter: {
1780
- type: "string",
1781
- description: "Optional name or label substring to filter objects"
1782
- },
1783
- includeFields: {
1784
- type: "boolean",
1785
- description: "Whether to include field summaries for each object (default: false)"
1786
- }
1787
- },
1788
- additionalProperties: false
1668
+ const msg = raw;
1669
+ if (typeof msg.role !== "string" || !ALLOWED_AGENT_ROLES.has(msg.role)) {
1670
+ return `message.role must be one of ${[...ALLOWED_AGENT_ROLES].map((r) => `"${r}"`).join(", ")} for agent chat`;
1789
1671
  }
1790
- });
1791
-
1792
- // src/tools/describe-metadata-object.tool.ts
1793
- var import_ai6 = require("@objectstack/spec/ai");
1794
- var describeMetadataObjectTool = (0, import_ai6.defineTool)({
1795
- name: "describe_metadata_object",
1796
- label: "Describe Metadata Object",
1797
- description: "Returns the full metadata schema details of a data object, including all fields, types, relationships, and configuration. Use this when the user wants to inspect or understand the metadata structure of a specific table or entity.",
1798
- category: "data",
1799
- builtIn: true,
1800
- parameters: {
1801
- type: "object",
1802
- properties: {
1803
- objectName: {
1804
- type: "string",
1805
- description: "Object machine name to describe (snake_case)"
1672
+ const allowEmpty = msg.role === "assistant";
1673
+ return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
1674
+ }
1675
+ function buildAgentRoutes(aiService, agentRuntime, logger) {
1676
+ return [
1677
+ // ── List active agents ──────────────────────────────────────
1678
+ {
1679
+ method: "GET",
1680
+ path: "/api/v1/ai/agents",
1681
+ description: "List all active AI agents",
1682
+ auth: true,
1683
+ permissions: ["ai:chat"],
1684
+ handler: async () => {
1685
+ try {
1686
+ const agents = await agentRuntime.listAgents();
1687
+ return { status: 200, body: { agents } };
1688
+ } catch (err) {
1689
+ logger.error(
1690
+ "[AI Route] /agents list error",
1691
+ err instanceof Error ? err : void 0
1692
+ );
1693
+ return { status: 500, body: { error: "Internal AI service error" } };
1694
+ }
1806
1695
  }
1807
1696
  },
1808
- required: ["objectName"],
1809
- additionalProperties: false
1810
- }
1811
- });
1812
-
1813
- // src/tools/metadata-tools.ts
1814
- var METADATA_TOOL_DEFINITIONS = [
1815
- createObjectTool,
1816
- addFieldTool,
1817
- modifyFieldTool,
1818
- deleteFieldTool,
1819
- listMetadataObjectsTool,
1820
- describeMetadataObjectTool
1821
- ];
1822
- var SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
1823
- function isSnakeCase(value) {
1824
- return SNAKE_CASE_RE.test(value);
1825
- }
1826
- function createCreateObjectHandler(ctx) {
1827
- return async (args) => {
1828
- const { name, label, fields, enableFeatures } = args;
1829
- if (!name || !label) {
1830
- return JSON.stringify({ error: 'Both "name" and "label" are required' });
1831
- }
1832
- if (!isSnakeCase(name)) {
1833
- return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
1834
- }
1835
- const existing = await ctx.metadataService.getObject(name);
1836
- if (existing) {
1837
- return JSON.stringify({ error: `Object "${name}" already exists` });
1838
- }
1839
- const fieldMap = {};
1840
- if (fields && Array.isArray(fields)) {
1841
- const seenNames = /* @__PURE__ */ new Set();
1842
- for (const f of fields) {
1843
- if (!f.name) {
1844
- return JSON.stringify({ error: 'Each field must have a "name" property' });
1697
+ // ── Chat with a specific agent ──────────────────────────────
1698
+ //
1699
+ // Dual-mode endpoint matching the general chat route behaviour:
1700
+ // • `stream !== false` → Vercel Data Stream Protocol (SSE)
1701
+ // • `stream === false` → JSON response (legacy)
1702
+ //
1703
+ {
1704
+ method: "POST",
1705
+ path: "/api/v1/ai/agents/:agentName/chat",
1706
+ description: "Chat with a specific AI agent (supports Vercel AI Data Stream Protocol)",
1707
+ auth: true,
1708
+ permissions: ["ai:chat", "ai:agents"],
1709
+ handler: async (req) => {
1710
+ const agentName = req.params?.agentName;
1711
+ if (!agentName) {
1712
+ return { status: 400, body: { error: "agentName parameter is required" } };
1845
1713
  }
1846
- if (!isSnakeCase(f.name)) {
1847
- return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
1714
+ const body = req.body ?? {};
1715
+ const {
1716
+ messages: rawMessages,
1717
+ context: chatContext,
1718
+ options: extraOptions
1719
+ } = body;
1720
+ if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
1721
+ return { status: 400, body: { error: "messages array is required" } };
1848
1722
  }
1849
- if (seenNames.has(f.name)) {
1850
- return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
1723
+ for (const msg of rawMessages) {
1724
+ const err = validateAgentMessage(msg);
1725
+ if (err) return { status: 400, body: { error: err } };
1726
+ }
1727
+ const agent = await agentRuntime.loadAgent(agentName);
1728
+ if (!agent) {
1729
+ return { status: 404, body: { error: `Agent "${agentName}" not found` } };
1730
+ }
1731
+ if (!agent.active) {
1732
+ return { status: 403, body: { error: `Agent "${agentName}" is not active` } };
1733
+ }
1734
+ try {
1735
+ const systemMessages = agentRuntime.buildSystemMessages(agent, chatContext);
1736
+ const agentOptions = agentRuntime.buildRequestOptions(
1737
+ agent,
1738
+ aiService.toolRegistry.getAll()
1739
+ );
1740
+ const safeOverrides = {};
1741
+ if (extraOptions) {
1742
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set(["temperature", "maxTokens", "stop"]);
1743
+ for (const key of Object.keys(extraOptions)) {
1744
+ if (ALLOWED_KEYS.has(key)) {
1745
+ safeOverrides[key] = extraOptions[key];
1746
+ }
1747
+ }
1748
+ }
1749
+ const mergedOptions = { ...agentOptions, ...safeOverrides };
1750
+ const fullMessages = [
1751
+ ...systemMessages,
1752
+ ...rawMessages.map((m) => normalizeMessage(m))
1753
+ ];
1754
+ const chatWithToolsOptions = {
1755
+ ...mergedOptions,
1756
+ maxIterations: agent.planning?.maxIterations
1757
+ };
1758
+ const wantStream = body.stream !== false;
1759
+ if (wantStream) {
1760
+ if (!aiService.streamChatWithTools) {
1761
+ return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
1762
+ }
1763
+ const events = aiService.streamChatWithTools(fullMessages, chatWithToolsOptions);
1764
+ return {
1765
+ status: 200,
1766
+ stream: true,
1767
+ vercelDataStream: true,
1768
+ contentType: "text/event-stream",
1769
+ headers: {
1770
+ "Content-Type": "text/event-stream",
1771
+ "Cache-Control": "no-cache",
1772
+ "Connection": "keep-alive",
1773
+ "x-vercel-ai-ui-message-stream": "v1"
1774
+ },
1775
+ events: encodeVercelDataStream(events)
1776
+ };
1777
+ }
1778
+ const result = await aiService.chatWithTools(fullMessages, chatWithToolsOptions);
1779
+ return { status: 200, body: result };
1780
+ } catch (err) {
1781
+ logger.error(
1782
+ "[AI Route] /agents/:agentName/chat error",
1783
+ err instanceof Error ? err : void 0
1784
+ );
1785
+ return { status: 500, body: { error: "Internal AI service error" } };
1851
1786
  }
1852
- seenNames.add(f.name);
1853
- fieldMap[f.name] = {
1854
- type: f.type,
1855
- ...f.label ? { label: f.label } : {},
1856
- ...f.required !== void 0 ? { required: f.required } : {}
1857
- };
1858
1787
  }
1859
1788
  }
1860
- const objectDef = {
1861
- name,
1862
- label,
1863
- ...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
1864
- ...enableFeatures ? { enable: enableFeatures } : {}
1865
- };
1866
- await ctx.metadataService.register("object", name, objectDef);
1867
- return JSON.stringify({
1868
- name,
1869
- label,
1870
- fieldCount: Object.keys(fieldMap).length
1871
- });
1872
- };
1789
+ ];
1873
1790
  }
1874
- function createAddFieldHandler(ctx) {
1875
- return async (args) => {
1876
- const { objectName, name, label, type, required, defaultValue, options, reference } = args;
1877
- if (!objectName || !name || !type) {
1878
- return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
1879
- }
1880
- if (!isSnakeCase(objectName)) {
1881
- return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1882
- }
1883
- if (!isSnakeCase(name)) {
1884
- return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
1885
- }
1886
- if (reference && !isSnakeCase(reference)) {
1887
- return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
1888
- }
1889
- if (options && Array.isArray(options)) {
1890
- for (const opt of options) {
1891
- if (opt.value && !isSnakeCase(opt.value)) {
1892
- return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
1791
+
1792
+ // src/routes/tool-routes.ts
1793
+ function extractOutputValue(output) {
1794
+ if (!output) return "";
1795
+ if (typeof output === "string") return output;
1796
+ if (typeof output === "object" && "value" in output) {
1797
+ return String(output.value ?? "");
1798
+ }
1799
+ return JSON.stringify(output);
1800
+ }
1801
+ function buildToolRoutes(aiService, logger) {
1802
+ return [
1803
+ // ── List registered tools ──────────────────────────────────────
1804
+ {
1805
+ method: "GET",
1806
+ path: "/api/v1/ai/tools",
1807
+ description: "List all registered AI tools",
1808
+ auth: true,
1809
+ permissions: ["ai:tools"],
1810
+ handler: async () => {
1811
+ try {
1812
+ const tools = aiService.toolRegistry.getAll();
1813
+ return {
1814
+ status: 200,
1815
+ body: {
1816
+ tools: tools.map((t) => ({
1817
+ name: t.name,
1818
+ description: t.description,
1819
+ category: t.category
1820
+ }))
1821
+ }
1822
+ };
1823
+ } catch (err) {
1824
+ logger.error(
1825
+ "[AI Route] /tools list error",
1826
+ err instanceof Error ? err : void 0
1827
+ );
1828
+ return { status: 500, body: { error: "Internal AI service error" } };
1829
+ }
1830
+ }
1831
+ },
1832
+ // ── Execute a tool ──────────────────────────────────────────────
1833
+ //
1834
+ // Executes a tool with the provided parameters.
1835
+ // This is intended for testing/playground use.
1836
+ //
1837
+ {
1838
+ method: "POST",
1839
+ path: "/api/v1/ai/tools/:toolName/execute",
1840
+ description: "Execute a tool with parameters (playground/testing)",
1841
+ auth: true,
1842
+ permissions: ["ai:tools", "ai:execute"],
1843
+ handler: async (req) => {
1844
+ const toolName = req.params?.toolName;
1845
+ if (!toolName) {
1846
+ return { status: 400, body: { error: "toolName parameter is required" } };
1847
+ }
1848
+ const body = req.body ?? {};
1849
+ const { parameters } = body;
1850
+ if (!parameters || typeof parameters !== "object") {
1851
+ return { status: 400, body: { error: "parameters object is required" } };
1852
+ }
1853
+ try {
1854
+ if (!aiService.toolRegistry.has(toolName)) {
1855
+ return { status: 404, body: { error: `Tool "${toolName}" not found` } };
1856
+ }
1857
+ const startTime = Date.now();
1858
+ const toolCallPart = {
1859
+ type: "tool-call",
1860
+ toolCallId: `playground-${Date.now()}`,
1861
+ toolName,
1862
+ input: parameters
1863
+ };
1864
+ const result = await aiService.toolRegistry.execute(toolCallPart);
1865
+ const duration = Date.now() - startTime;
1866
+ if (result.isError) {
1867
+ const errorMsg = extractOutputValue(result.output);
1868
+ logger.error(
1869
+ `[AI Route] Tool execution error: ${toolName}`,
1870
+ new Error(errorMsg)
1871
+ );
1872
+ return {
1873
+ status: 500,
1874
+ body: {
1875
+ error: errorMsg,
1876
+ duration
1877
+ }
1878
+ };
1879
+ }
1880
+ return {
1881
+ status: 200,
1882
+ body: {
1883
+ result: extractOutputValue(result.output),
1884
+ duration,
1885
+ toolName
1886
+ }
1887
+ };
1888
+ } catch (err) {
1889
+ logger.error(
1890
+ "[AI Route] /tools/:toolName/execute error",
1891
+ err instanceof Error ? err : void 0
1892
+ );
1893
+ return { status: 500, body: { error: "Internal AI service error" } };
1893
1894
  }
1894
1895
  }
1895
1896
  }
1896
- const objectDef = await ctx.metadataService.getObject(objectName);
1897
- if (!objectDef) {
1898
- return JSON.stringify({ error: `Object "${objectName}" not found` });
1899
- }
1900
- const def = objectDef;
1901
- if (def.fields && def.fields[name]) {
1902
- return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
1903
- }
1904
- const fieldDef = {
1905
- type,
1906
- ...label ? { label } : {},
1907
- ...required !== void 0 ? { required } : {},
1908
- ...defaultValue !== void 0 ? { defaultValue } : {},
1909
- ...options ? { options } : {},
1910
- ...reference ? { reference } : {}
1897
+ ];
1898
+ }
1899
+
1900
+ // src/conversation/objectql-conversation-service.ts
1901
+ var import_node_crypto = require("crypto");
1902
+ var CONVERSATIONS_OBJECT = "ai_conversations";
1903
+ var MESSAGES_OBJECT = "ai_messages";
1904
+ var CONVERSATION_ORDER = [
1905
+ { field: "created_at", order: "asc" },
1906
+ { field: "id", order: "asc" }
1907
+ ];
1908
+ var MESSAGE_ORDER = [
1909
+ { field: "created_at", order: "asc" },
1910
+ { field: "id", order: "asc" }
1911
+ ];
1912
+ var ObjectQLConversationService = class {
1913
+ constructor(engine) {
1914
+ this.engine = engine;
1915
+ }
1916
+ async create(options = {}) {
1917
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1918
+ const id = `conv_${(0, import_node_crypto.randomUUID)()}`;
1919
+ const record = {
1920
+ id,
1921
+ title: options.title ?? null,
1922
+ agent_id: options.agentId ?? null,
1923
+ user_id: options.userId ?? null,
1924
+ metadata: options.metadata ? JSON.stringify(options.metadata) : null,
1925
+ created_at: now,
1926
+ updated_at: now
1911
1927
  };
1912
- const updatedFields = { ...def.fields ?? {}, [name]: fieldDef };
1913
- await ctx.metadataService.register("object", objectName, {
1914
- ...def,
1915
- fields: updatedFields
1928
+ await this.engine.insert(CONVERSATIONS_OBJECT, record);
1929
+ return {
1930
+ id,
1931
+ title: options.title,
1932
+ agentId: options.agentId,
1933
+ userId: options.userId,
1934
+ messages: [],
1935
+ createdAt: now,
1936
+ updatedAt: now,
1937
+ metadata: options.metadata
1938
+ };
1939
+ }
1940
+ async get(conversationId) {
1941
+ const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
1942
+ where: { id: conversationId }
1916
1943
  });
1917
- return JSON.stringify({
1918
- objectName,
1919
- fieldName: name,
1920
- fieldType: type
1944
+ if (!row) return null;
1945
+ const messages = await this.engine.find(MESSAGES_OBJECT, {
1946
+ where: { conversation_id: conversationId },
1947
+ orderBy: MESSAGE_ORDER
1921
1948
  });
1922
- };
1923
- }
1924
- function createModifyFieldHandler(ctx) {
1925
- return async (args) => {
1926
- const { objectName, fieldName, changes } = args;
1927
- if (!objectName || !fieldName || !changes) {
1928
- return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
1929
- }
1930
- if (!isSnakeCase(objectName)) {
1931
- return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1932
- }
1933
- if (!isSnakeCase(fieldName)) {
1934
- return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
1935
- }
1936
- const objectDef = await ctx.metadataService.getObject(objectName);
1937
- if (!objectDef) {
1938
- return JSON.stringify({ error: `Object "${objectName}" not found` });
1939
- }
1940
- const def = objectDef;
1941
- if (!def.fields || !def.fields[fieldName]) {
1942
- return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
1949
+ return this.toConversation(row, messages);
1950
+ }
1951
+ async list(options = {}) {
1952
+ const where = {};
1953
+ if (options.userId) where.user_id = options.userId;
1954
+ if (options.agentId) where.agent_id = options.agentId;
1955
+ if (options.cursor) {
1956
+ const cursorRow = await this.engine.findOne(CONVERSATIONS_OBJECT, {
1957
+ where: { id: options.cursor },
1958
+ fields: ["created_at", "id"]
1959
+ });
1960
+ if (cursorRow) {
1961
+ where.$or = [
1962
+ { created_at: { $gt: cursorRow.created_at } },
1963
+ { created_at: cursorRow.created_at, id: { $gt: cursorRow.id } }
1964
+ ];
1965
+ }
1943
1966
  }
1944
- const existingField = def.fields[fieldName];
1945
- const updatedField = { ...existingField, ...changes };
1946
- const updatedFields = { ...def.fields, [fieldName]: updatedField };
1947
- await ctx.metadataService.register("object", objectName, {
1948
- ...def,
1949
- fields: updatedFields
1967
+ const rows = await this.engine.find(CONVERSATIONS_OBJECT, {
1968
+ where: Object.keys(where).length > 0 ? where : void 0,
1969
+ orderBy: CONVERSATION_ORDER,
1970
+ limit: options.limit && options.limit > 0 ? options.limit : void 0
1950
1971
  });
1951
- return JSON.stringify({
1952
- objectName,
1953
- fieldName,
1954
- updatedProperties: Object.keys(changes)
1972
+ const conversations = await Promise.all(
1973
+ rows.map(async (row) => {
1974
+ const messages = await this.engine.find(MESSAGES_OBJECT, {
1975
+ where: { conversation_id: row.id },
1976
+ orderBy: MESSAGE_ORDER
1977
+ });
1978
+ return this.toConversation(row, messages);
1979
+ })
1980
+ );
1981
+ return conversations;
1982
+ }
1983
+ async addMessage(conversationId, message) {
1984
+ const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
1985
+ where: { id: conversationId }
1955
1986
  });
1956
- };
1957
- }
1958
- function createDeleteFieldHandler(ctx) {
1959
- return async (args) => {
1960
- const { objectName, fieldName } = args;
1961
- if (!objectName || !fieldName) {
1962
- return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
1963
- }
1964
- if (!isSnakeCase(objectName)) {
1965
- return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1966
- }
1967
- if (!isSnakeCase(fieldName)) {
1968
- return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
1969
- }
1970
- const objectDef = await ctx.metadataService.getObject(objectName);
1971
- if (!objectDef) {
1972
- return JSON.stringify({ error: `Object "${objectName}" not found` });
1973
- }
1974
- const def = objectDef;
1975
- if (!def.fields || !def.fields[fieldName]) {
1976
- return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
1987
+ if (!row) {
1988
+ throw new Error(`Conversation "${conversationId}" not found`);
1977
1989
  }
1978
- const { [fieldName]: _removed, ...remainingFields } = def.fields;
1979
- await ctx.metadataService.register("object", objectName, {
1980
- ...def,
1981
- fields: remainingFields
1982
- });
1983
- return JSON.stringify({
1984
- objectName,
1985
- fieldName,
1986
- success: true
1987
- });
1988
- };
1989
- }
1990
- function createListObjectsHandler2(ctx) {
1991
- return async (args) => {
1992
- const { filter, includeFields } = args ?? {};
1993
- const objects = await ctx.metadataService.listObjects();
1994
- let result = objects.map((o) => {
1995
- const base = {
1996
- name: o.name,
1997
- label: o.label ?? o.name,
1998
- fieldCount: o.fields ? Object.keys(o.fields).length : 0
1999
- };
2000
- if (includeFields && o.fields) {
2001
- base.fields = Object.entries(o.fields).map(([key, f]) => ({
2002
- name: key,
2003
- type: f.type,
2004
- label: f.label ?? key
2005
- }));
1990
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1991
+ const msgId = `msg_${(0, import_node_crypto.randomUUID)()}`;
1992
+ let contentStr;
1993
+ let toolCallsJson = null;
1994
+ let toolCallId = null;
1995
+ if (message.role === "system" || message.role === "user") {
1996
+ contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
1997
+ } else if (message.role === "assistant") {
1998
+ if (typeof message.content === "string") {
1999
+ contentStr = message.content;
2000
+ } else {
2001
+ const parts = message.content;
2002
+ const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
2003
+ const toolCalls = parts.filter((p) => p.type === "tool-call");
2004
+ contentStr = textParts.join("");
2005
+ if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
2006
2006
  }
2007
- return base;
2008
- });
2009
- if (filter) {
2010
- const lower = filter.toLowerCase();
2011
- result = result.filter(
2012
- (o) => o.name.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
2013
- );
2007
+ } else if (message.role === "tool") {
2008
+ contentStr = JSON.stringify(message.content);
2009
+ const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
2010
+ if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
2011
+ } else {
2012
+ contentStr = "";
2014
2013
  }
2015
- return JSON.stringify({
2016
- objects: result,
2017
- totalCount: result.length
2014
+ await this.engine.insert(MESSAGES_OBJECT, {
2015
+ id: msgId,
2016
+ conversation_id: conversationId,
2017
+ role: message.role,
2018
+ content: contentStr,
2019
+ tool_calls: toolCallsJson,
2020
+ tool_call_id: toolCallId,
2021
+ created_at: now
2018
2022
  });
2019
- };
2020
- }
2021
- function createDescribeObjectHandler2(ctx) {
2022
- return async (args) => {
2023
- const { objectName } = args;
2024
- if (!objectName) {
2025
- return JSON.stringify({ error: '"objectName" is required' });
2026
- }
2027
- if (!isSnakeCase(objectName)) {
2028
- return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
2023
+ await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
2024
+ where: { id: conversationId }
2025
+ });
2026
+ return await this.get(conversationId);
2027
+ }
2028
+ async delete(conversationId) {
2029
+ await this.engine.delete(MESSAGES_OBJECT, {
2030
+ where: { conversation_id: conversationId },
2031
+ multi: true
2032
+ });
2033
+ await this.engine.delete(CONVERSATIONS_OBJECT, {
2034
+ where: { id: conversationId }
2035
+ });
2036
+ }
2037
+ // ── Private helpers ──────────────────────────────────────────────
2038
+ /**
2039
+ * Safely parse a JSON string, returning `undefined` on failure.
2040
+ */
2041
+ safeParse(value, fallback) {
2042
+ if (!value) return void 0;
2043
+ try {
2044
+ return JSON.parse(value);
2045
+ } catch {
2046
+ return fallback;
2029
2047
  }
2030
- const objectDef = await ctx.metadataService.getObject(objectName);
2031
- if (!objectDef) {
2032
- return JSON.stringify({ error: `Object "${objectName}" not found` });
2048
+ }
2049
+ /**
2050
+ * Map a database row + message rows to an AIConversation.
2051
+ */
2052
+ toConversation(row, messageRows) {
2053
+ return {
2054
+ id: row.id,
2055
+ title: row.title ?? void 0,
2056
+ agentId: row.agent_id ?? void 0,
2057
+ userId: row.user_id ?? void 0,
2058
+ messages: messageRows.map((m) => this.toMessage(m)),
2059
+ createdAt: row.created_at,
2060
+ updatedAt: row.updated_at,
2061
+ metadata: this.safeParse(row.metadata)
2062
+ };
2063
+ }
2064
+ /**
2065
+ * Map a database row to a ModelMessage.
2066
+ */
2067
+ toMessage(row) {
2068
+ switch (row.role) {
2069
+ case "system":
2070
+ return { role: "system", content: row.content };
2071
+ case "user":
2072
+ return { role: "user", content: row.content };
2073
+ case "assistant": {
2074
+ const toolCalls = this.safeParse(row.tool_calls);
2075
+ if (toolCalls && toolCalls.length > 0) {
2076
+ const content = [];
2077
+ if (row.content) content.push({ type: "text", text: row.content });
2078
+ content.push(...toolCalls);
2079
+ return { role: "assistant", content };
2080
+ }
2081
+ return { role: "assistant", content: row.content };
2082
+ }
2083
+ case "tool": {
2084
+ const toolResults = this.safeParse(row.content);
2085
+ if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
2086
+ return { role: "tool", content: toolResults };
2087
+ }
2088
+ return {
2089
+ role: "tool",
2090
+ content: [{
2091
+ type: "tool-result",
2092
+ toolCallId: row.tool_call_id ?? "",
2093
+ toolName: "unknown",
2094
+ output: { type: "text", value: row.content }
2095
+ }]
2096
+ };
2097
+ }
2098
+ default:
2099
+ return { role: "user", content: row.content };
2033
2100
  }
2034
- const def = objectDef;
2035
- const fields = def.fields ?? {};
2036
- const fieldSummary = Object.entries(fields).map(([key, f]) => ({
2037
- name: key,
2038
- type: f.type,
2039
- label: f.label ?? key,
2040
- required: f.required ?? false,
2041
- ...f.reference ? { reference: f.reference } : {},
2042
- ...f.options ? { options: f.options } : {}
2043
- }));
2044
- return JSON.stringify({
2045
- name: def.name,
2046
- label: def.label ?? def.name,
2047
- fields: fieldSummary,
2048
- enableFeatures: def.enable ?? {}
2049
- });
2050
- };
2051
- }
2052
- function registerMetadataTools(registry, context) {
2053
- registry.register(createObjectTool, createCreateObjectHandler(context));
2054
- registry.register(addFieldTool, createAddFieldHandler(context));
2055
- registry.register(modifyFieldTool, createModifyFieldHandler(context));
2056
- registry.register(deleteFieldTool, createDeleteFieldHandler(context));
2057
- registry.register(listMetadataObjectsTool, createListObjectsHandler2(context));
2058
- registry.register(describeMetadataObjectTool, createDescribeObjectHandler2(context));
2059
- }
2101
+ }
2102
+ };
2103
+
2104
+ // src/objects/ai-conversation.object.ts
2105
+ var import_data = require("@objectstack/spec/data");
2106
+ var AiConversationObject = import_data.ObjectSchema.create({
2107
+ namespace: "ai",
2108
+ name: "conversations",
2109
+ label: "AI Conversation",
2110
+ pluralLabel: "AI Conversations",
2111
+ icon: "message-square",
2112
+ isSystem: true,
2113
+ description: "Persistent AI conversation metadata",
2114
+ fields: {
2115
+ id: import_data.Field.text({
2116
+ label: "Conversation ID",
2117
+ required: true,
2118
+ readonly: true
2119
+ }),
2120
+ title: import_data.Field.text({
2121
+ label: "Title",
2122
+ required: false,
2123
+ maxLength: 500,
2124
+ description: "Conversation title or summary"
2125
+ }),
2126
+ agent_id: import_data.Field.text({
2127
+ label: "Agent ID",
2128
+ required: false,
2129
+ maxLength: 255,
2130
+ description: "Associated AI agent identifier"
2131
+ }),
2132
+ user_id: import_data.Field.text({
2133
+ label: "User ID",
2134
+ required: false,
2135
+ maxLength: 255,
2136
+ description: "User who owns the conversation"
2137
+ }),
2138
+ metadata: import_data.Field.textarea({
2139
+ label: "Metadata",
2140
+ required: false,
2141
+ description: "JSON-serialized conversation metadata"
2142
+ }),
2143
+ created_at: import_data.Field.datetime({
2144
+ label: "Created At",
2145
+ required: true,
2146
+ defaultValue: "NOW()",
2147
+ readonly: true
2148
+ }),
2149
+ updated_at: import_data.Field.datetime({
2150
+ label: "Updated At",
2151
+ required: true,
2152
+ defaultValue: "NOW()",
2153
+ readonly: true
2154
+ })
2155
+ },
2156
+ indexes: [
2157
+ { fields: ["user_id"] },
2158
+ { fields: ["agent_id"] },
2159
+ { fields: ["created_at"] }
2160
+ ],
2161
+ enable: {
2162
+ trackHistory: false,
2163
+ searchable: false,
2164
+ apiEnabled: true,
2165
+ apiMethods: ["get", "list", "create", "update", "delete"],
2166
+ trash: false,
2167
+ mru: false
2168
+ }
2169
+ });
2170
+
2171
+ // src/objects/ai-message.object.ts
2172
+ var import_data2 = require("@objectstack/spec/data");
2173
+ var AiMessageObject = import_data2.ObjectSchema.create({
2174
+ namespace: "ai",
2175
+ name: "messages",
2176
+ label: "AI Message",
2177
+ pluralLabel: "AI Messages",
2178
+ icon: "message-circle",
2179
+ isSystem: true,
2180
+ description: "Individual messages within AI conversations",
2181
+ fields: {
2182
+ id: import_data2.Field.text({
2183
+ label: "Message ID",
2184
+ required: true,
2185
+ readonly: true
2186
+ }),
2187
+ conversation_id: import_data2.Field.text({
2188
+ label: "Conversation ID",
2189
+ required: true,
2190
+ description: "Foreign key to ai_conversations"
2191
+ }),
2192
+ role: import_data2.Field.select({
2193
+ label: "Role",
2194
+ required: true,
2195
+ options: [
2196
+ { label: "System", value: "system" },
2197
+ { label: "User", value: "user" },
2198
+ { label: "Assistant", value: "assistant" },
2199
+ { label: "Tool", value: "tool" }
2200
+ ]
2201
+ }),
2202
+ content: import_data2.Field.textarea({
2203
+ label: "Content",
2204
+ required: true,
2205
+ description: "Message content"
2206
+ }),
2207
+ tool_calls: import_data2.Field.textarea({
2208
+ label: "Tool Calls",
2209
+ required: false,
2210
+ description: "JSON-serialized tool calls (when role=assistant)"
2211
+ }),
2212
+ tool_call_id: import_data2.Field.text({
2213
+ label: "Tool Call ID",
2214
+ required: false,
2215
+ maxLength: 255,
2216
+ description: "ID of the tool call this message responds to (when role=tool)"
2217
+ }),
2218
+ created_at: import_data2.Field.datetime({
2219
+ label: "Created At",
2220
+ required: true,
2221
+ defaultValue: "NOW()",
2222
+ readonly: true
2223
+ })
2224
+ },
2225
+ indexes: [
2226
+ { fields: ["conversation_id"] },
2227
+ { fields: ["conversation_id", "created_at"] }
2228
+ ],
2229
+ enable: {
2230
+ trackHistory: false,
2231
+ searchable: false,
2232
+ apiEnabled: true,
2233
+ apiMethods: ["get", "list", "create"],
2234
+ trash: false,
2235
+ mru: false
2236
+ }
2237
+ });
2238
+
2239
+ // src/plugin.ts
2240
+ init_data_tools();
2241
+ init_metadata_tools();
2060
2242
 
2061
2243
  // src/agent-runtime.ts
2062
2244
  var import_ai7 = require("@objectstack/spec/ai");
@@ -2230,8 +2412,8 @@ Capabilities:
2230
2412
  - Describe the full schema of a specific object
2231
2413
 
2232
2414
  Guidelines:
2233
- 1. Before creating a new object, use list_metadata_objects to check if a similar one already exists.
2234
- 2. Before modifying or deleting fields, use describe_metadata_object to understand the current schema.
2415
+ 1. Before creating a new object, use list_objects to check if a similar one already exists.
2416
+ 2. Before modifying or deleting fields, use describe_object to understand the current schema.
2235
2417
  3. Always use snake_case for object names and field names (e.g. project_task, due_date).
2236
2418
  4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
2237
2419
  5. When creating objects, propose a reasonable set of initial fields based on the entity type.
@@ -2251,8 +2433,8 @@ Guidelines:
2251
2433
  { type: "action", name: "add_field", description: "Add a field to an existing object" },
2252
2434
  { type: "action", name: "modify_field", description: "Modify an existing field definition" },
2253
2435
  { type: "action", name: "delete_field", description: "Delete a field from an object" },
2254
- { type: "query", name: "list_metadata_objects", description: "List all metadata objects" },
2255
- { type: "query", name: "describe_metadata_object", description: "Describe an object schema" }
2436
+ { type: "query", name: "list_objects", description: "List all data objects" },
2437
+ { type: "query", name: "describe_object", description: "Describe an object schema" }
2256
2438
  ],
2257
2439
  active: true,
2258
2440
  visibility: "global",
@@ -2503,8 +2685,8 @@ var AIServicePlugin = class {
2503
2685
  setupNav.contribute({
2504
2686
  areaId: "area_ai",
2505
2687
  items: [
2506
- { id: "nav_ai_conversations", type: "object", label: { key: "setup.nav.ai_conversations", defaultValue: "Conversations" }, objectName: "conversations", icon: "message-square", order: 10 },
2507
- { id: "nav_ai_messages", type: "object", label: { key: "setup.nav.ai_messages", defaultValue: "Messages" }, objectName: "messages", icon: "messages-square", order: 20 }
2688
+ { id: "nav_ai_conversations", type: "object", label: "Conversations", objectName: "conversations", icon: "message-square", order: 10 },
2689
+ { id: "nav_ai_messages", type: "object", label: "Messages", objectName: "messages", icon: "messages-square", order: 20 }
2508
2690
  ]
2509
2691
  });
2510
2692
  ctx.logger.info("[AI] Navigation items contributed to Setup App");
@@ -2518,20 +2700,40 @@ var AIServicePlugin = class {
2518
2700
  let metadataService;
2519
2701
  try {
2520
2702
  metadataService = ctx.getService("metadata");
2521
- } catch {
2703
+ console.log("[AI Plugin] Retrieved metadata service:", !!metadataService, "has getRegisteredTypes:", typeof metadataService?.getRegisteredTypes);
2704
+ } catch (e) {
2705
+ console.log("[AI] Metadata service not available:", e.message);
2522
2706
  ctx.logger.debug("[AI] Metadata service not available");
2523
2707
  }
2524
2708
  try {
2525
2709
  const dataEngine = ctx.getService("data");
2526
- if (dataEngine && metadataService) {
2527
- registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
2710
+ if (dataEngine) {
2711
+ registerDataTools(this.service.toolRegistry, { dataEngine });
2528
2712
  ctx.logger.info("[AI] Built-in data tools registered");
2529
- const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", DATA_CHAT_AGENT.name) : false;
2530
- if (!agentExists) {
2531
- await metadataService.register("agent", DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
2532
- ctx.logger.info("[AI] data_chat agent registered");
2533
- } else {
2534
- ctx.logger.debug("[AI] data_chat agent already exists, skipping auto-registration");
2713
+ if (metadataService) {
2714
+ const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
2715
+ for (const toolDef of DATA_TOOL_DEFINITIONS2) {
2716
+ const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
2717
+ if (!toolExists) {
2718
+ await metadataService.register("tool", toolDef.name, toolDef);
2719
+ }
2720
+ }
2721
+ ctx.logger.info(`[AI] ${DATA_TOOL_DEFINITIONS2.length} data tools registered as metadata`);
2722
+ }
2723
+ if (metadataService) {
2724
+ try {
2725
+ const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", DATA_CHAT_AGENT.name) : false;
2726
+ if (!agentExists) {
2727
+ await metadataService.register("agent", DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
2728
+ console.log("[AI] Registered data_chat agent to metadataService");
2729
+ ctx.logger.info("[AI] data_chat agent registered");
2730
+ } else {
2731
+ console.log("[AI] data_chat agent already exists, skipping");
2732
+ ctx.logger.debug("[AI] data_chat agent already exists, skipping auto-registration");
2733
+ }
2734
+ } catch (err) {
2735
+ ctx.logger.warn("[AI] Failed to register data_chat agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
2736
+ }
2535
2737
  }
2536
2738
  }
2537
2739
  } catch {
@@ -2541,12 +2743,26 @@ var AIServicePlugin = class {
2541
2743
  try {
2542
2744
  registerMetadataTools(this.service.toolRegistry, { metadataService });
2543
2745
  ctx.logger.info("[AI] Built-in metadata tools registered");
2544
- const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
2545
- if (!agentExists) {
2546
- await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
2547
- ctx.logger.info("[AI] metadata_assistant agent registered");
2548
- } else {
2549
- ctx.logger.debug("[AI] metadata_assistant agent already exists, skipping auto-registration");
2746
+ const { METADATA_TOOL_DEFINITIONS: METADATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_metadata_tools(), metadata_tools_exports));
2747
+ for (const toolDef of METADATA_TOOL_DEFINITIONS2) {
2748
+ const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
2749
+ if (!toolExists) {
2750
+ await metadataService.register("tool", toolDef.name, toolDef);
2751
+ }
2752
+ }
2753
+ ctx.logger.info(`[AI] ${METADATA_TOOL_DEFINITIONS2.length} metadata tools registered as metadata`);
2754
+ try {
2755
+ const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
2756
+ if (!agentExists) {
2757
+ await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
2758
+ console.log("[AI] Registered metadata_assistant agent to metadataService");
2759
+ ctx.logger.info("[AI] metadata_assistant agent registered");
2760
+ } else {
2761
+ console.log("[AI] metadata_assistant agent already exists, skipping");
2762
+ ctx.logger.debug("[AI] metadata_assistant agent already exists, skipping auto-registration");
2763
+ }
2764
+ } catch (err) {
2765
+ ctx.logger.warn("[AI] Failed to register metadata_assistant agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
2550
2766
  }
2551
2767
  } catch (err) {
2552
2768
  ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
@@ -2554,14 +2770,15 @@ var AIServicePlugin = class {
2554
2770
  }
2555
2771
  await ctx.trigger("ai:ready", this.service);
2556
2772
  const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
2557
- try {
2558
- const metadataService2 = ctx.getService("metadata");
2559
- if (metadataService2) {
2560
- const agentRuntime = new AgentRuntime(metadataService2);
2561
- const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
2562
- routes.push(...agentRoutes);
2563
- }
2564
- } catch {
2773
+ const toolRoutes = buildToolRoutes(this.service, ctx.logger);
2774
+ routes.push(...toolRoutes);
2775
+ ctx.logger.info(`[AI] Tool routes registered (${toolRoutes.length} routes)`);
2776
+ if (metadataService) {
2777
+ const agentRuntime = new AgentRuntime(metadataService);
2778
+ const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
2779
+ routes.push(...agentRoutes);
2780
+ ctx.logger.info(`[AI] Agent routes registered (${agentRoutes.length} routes)`);
2781
+ } else {
2565
2782
  ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
2566
2783
  }
2567
2784
  await ctx.trigger("ai:routes", routes);
@@ -2577,6 +2794,11 @@ var AIServicePlugin = class {
2577
2794
  this.service = void 0;
2578
2795
  }
2579
2796
  };
2797
+
2798
+ // src/index.ts
2799
+ init_data_tools();
2800
+ init_metadata_tools();
2801
+ init_metadata_tools();
2580
2802
  // Annotate the CommonJS export names for ESM import in node:
2581
2803
  0 && (module.exports = {
2582
2804
  AIService,
@@ -2596,12 +2818,13 @@ var AIServicePlugin = class {
2596
2818
  addFieldTool,
2597
2819
  buildAIRoutes,
2598
2820
  buildAgentRoutes,
2821
+ buildToolRoutes,
2599
2822
  createObjectTool,
2600
2823
  deleteFieldTool,
2601
- describeMetadataObjectTool,
2824
+ describeObjectTool,
2602
2825
  encodeStreamPart,
2603
2826
  encodeVercelDataStream,
2604
- listMetadataObjectsTool,
2827
+ listObjectsTool,
2605
2828
  modifyFieldTool,
2606
2829
  registerDataTools,
2607
2830
  registerMetadataTools