@objectstack/service-ai 4.0.1 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +17 -0
  3. package/dist/index.cjs +1632 -355
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +330 -87
  6. package/dist/index.d.ts +330 -87
  7. package/dist/index.js +1623 -352
  8. package/dist/index.js.map +1 -1
  9. package/package.json +27 -5
  10. package/src/__tests__/ai-service.test.ts +260 -27
  11. package/src/__tests__/auth-and-toolcalling.test.ts +81 -29
  12. package/src/__tests__/chatbot-features.test.ts +397 -102
  13. package/src/__tests__/metadata-tools.test.ts +970 -0
  14. package/src/__tests__/objectql-conversation-service.test.ts +34 -16
  15. package/src/__tests__/tool-routes.test.ts +191 -0
  16. package/src/__tests__/vercel-stream-encoder.test.ts +310 -0
  17. package/src/adapters/index.ts +2 -0
  18. package/src/adapters/memory-adapter.ts +17 -9
  19. package/src/adapters/vercel-adapter.ts +148 -0
  20. package/src/agent-runtime.ts +27 -3
  21. package/src/agents/index.ts +1 -0
  22. package/src/agents/metadata-assistant-agent.ts +87 -0
  23. package/src/ai-service.ts +75 -36
  24. package/src/conversation/in-memory-conversation-service.ts +2 -2
  25. package/src/conversation/objectql-conversation-service.ts +67 -18
  26. package/src/index.ts +22 -2
  27. package/src/plugin.ts +237 -30
  28. package/src/routes/agent-routes.ts +68 -12
  29. package/src/routes/ai-routes.ts +93 -14
  30. package/src/routes/index.ts +1 -0
  31. package/src/routes/message-utils.ts +90 -0
  32. package/src/routes/tool-routes.ts +142 -0
  33. package/src/stream/index.ts +3 -0
  34. package/src/stream/vercel-stream-encoder.ts +153 -0
  35. package/src/tools/add-field.tool.ts +70 -0
  36. package/src/tools/create-object.tool.ts +66 -0
  37. package/src/tools/data-tools.ts +4 -101
  38. package/src/tools/delete-field.tool.ts +38 -0
  39. package/src/tools/describe-object.tool.ts +31 -0
  40. package/src/tools/index.ts +12 -1
  41. package/src/tools/list-objects.tool.ts +34 -0
  42. package/src/tools/metadata-tools.ts +430 -0
  43. package/src/tools/modify-field.tool.ts +44 -0
  44. package/src/tools/tool-registry.ts +32 -9
package/dist/index.js CHANGED
@@ -1,3 +1,759 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/tools/data-tools.ts
12
+ var data_tools_exports = {};
13
+ __export(data_tools_exports, {
14
+ AGGREGATE_DATA_TOOL: () => AGGREGATE_DATA_TOOL,
15
+ DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
16
+ GET_RECORD_TOOL: () => GET_RECORD_TOOL,
17
+ QUERY_RECORDS_TOOL: () => QUERY_RECORDS_TOOL,
18
+ registerDataTools: () => registerDataTools
19
+ });
20
+ function createQueryRecordsHandler(ctx) {
21
+ return async (args) => {
22
+ const {
23
+ objectName,
24
+ where,
25
+ fields,
26
+ orderBy,
27
+ limit,
28
+ offset
29
+ } = args;
30
+ const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
31
+ const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
32
+ const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
33
+ const records = await ctx.dataEngine.find(objectName, {
34
+ where,
35
+ fields,
36
+ orderBy,
37
+ limit: safeLimit,
38
+ offset: safeOffset
39
+ });
40
+ return JSON.stringify({ count: records.length, records });
41
+ };
42
+ }
43
+ function createGetRecordHandler(ctx) {
44
+ return async (args) => {
45
+ const { objectName, recordId, fields } = args;
46
+ const record = await ctx.dataEngine.findOne(objectName, {
47
+ where: { id: recordId },
48
+ fields
49
+ });
50
+ if (!record) {
51
+ return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
52
+ }
53
+ return JSON.stringify(record);
54
+ };
55
+ }
56
+ function createAggregateDataHandler(ctx) {
57
+ return async (args) => {
58
+ const { objectName, aggregations, groupBy, where } = args;
59
+ for (const a of aggregations) {
60
+ if (!VALID_AGG_FUNCTIONS.has(a.function)) {
61
+ return JSON.stringify({
62
+ error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
63
+ });
64
+ }
65
+ }
66
+ const result = await ctx.dataEngine.aggregate(objectName, {
67
+ where,
68
+ groupBy,
69
+ aggregations: aggregations.map((a) => ({
70
+ function: a.function,
71
+ field: a.field,
72
+ alias: a.alias
73
+ }))
74
+ });
75
+ return JSON.stringify(result);
76
+ };
77
+ }
78
+ function registerDataTools(registry, context) {
79
+ registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
80
+ registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
81
+ registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
82
+ }
83
+ var MAX_QUERY_LIMIT, DEFAULT_QUERY_LIMIT, QUERY_RECORDS_TOOL, GET_RECORD_TOOL, AGGREGATE_DATA_TOOL, DATA_TOOL_DEFINITIONS, VALID_AGG_FUNCTIONS;
84
+ var init_data_tools = __esm({
85
+ "src/tools/data-tools.ts"() {
86
+ "use strict";
87
+ MAX_QUERY_LIMIT = 200;
88
+ DEFAULT_QUERY_LIMIT = 20;
89
+ QUERY_RECORDS_TOOL = {
90
+ name: "query_records",
91
+ description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
92
+ parameters: {
93
+ type: "object",
94
+ properties: {
95
+ objectName: {
96
+ type: "string",
97
+ description: "The snake_case name of the object to query"
98
+ },
99
+ where: {
100
+ type: "object",
101
+ description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
102
+ },
103
+ fields: {
104
+ type: "array",
105
+ items: { type: "string" },
106
+ description: "List of field names to return (omit for all fields)"
107
+ },
108
+ orderBy: {
109
+ type: "array",
110
+ items: {
111
+ type: "object",
112
+ properties: {
113
+ field: { type: "string" },
114
+ order: { type: "string", enum: ["asc", "desc"] }
115
+ }
116
+ },
117
+ description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
118
+ },
119
+ limit: {
120
+ type: "number",
121
+ description: `Maximum number of records to return (default ${DEFAULT_QUERY_LIMIT}, max ${MAX_QUERY_LIMIT})`
122
+ },
123
+ offset: {
124
+ type: "number",
125
+ description: "Number of records to skip for pagination"
126
+ }
127
+ },
128
+ required: ["objectName"],
129
+ additionalProperties: false
130
+ }
131
+ };
132
+ GET_RECORD_TOOL = {
133
+ name: "get_record",
134
+ description: "Get a single record by its ID from a data object.",
135
+ parameters: {
136
+ type: "object",
137
+ properties: {
138
+ objectName: {
139
+ type: "string",
140
+ description: "The snake_case name of the object"
141
+ },
142
+ recordId: {
143
+ type: "string",
144
+ description: "The unique ID of the record"
145
+ },
146
+ fields: {
147
+ type: "array",
148
+ items: { type: "string" },
149
+ description: "List of field names to return (omit for all fields)"
150
+ }
151
+ },
152
+ required: ["objectName", "recordId"],
153
+ additionalProperties: false
154
+ }
155
+ };
156
+ AGGREGATE_DATA_TOOL = {
157
+ name: "aggregate_data",
158
+ description: "Perform aggregation/statistical operations on a data object. Supports count, sum, avg, min, max with optional groupBy and where filters.",
159
+ parameters: {
160
+ type: "object",
161
+ properties: {
162
+ objectName: {
163
+ type: "string",
164
+ description: "The snake_case name of the object to aggregate"
165
+ },
166
+ aggregations: {
167
+ type: "array",
168
+ items: {
169
+ type: "object",
170
+ properties: {
171
+ function: {
172
+ type: "string",
173
+ enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
174
+ description: "Aggregation function"
175
+ },
176
+ field: {
177
+ type: "string",
178
+ description: "Field to aggregate (optional for count)"
179
+ },
180
+ alias: {
181
+ type: "string",
182
+ description: "Result column alias"
183
+ }
184
+ },
185
+ required: ["function", "alias"]
186
+ },
187
+ description: "Aggregation definitions"
188
+ },
189
+ groupBy: {
190
+ type: "array",
191
+ items: { type: "string" },
192
+ description: "Fields to group by"
193
+ },
194
+ where: {
195
+ type: "object",
196
+ description: "Filter conditions applied before aggregation"
197
+ }
198
+ },
199
+ required: ["objectName", "aggregations"],
200
+ additionalProperties: false
201
+ }
202
+ };
203
+ DATA_TOOL_DEFINITIONS = [
204
+ QUERY_RECORDS_TOOL,
205
+ GET_RECORD_TOOL,
206
+ AGGREGATE_DATA_TOOL
207
+ ];
208
+ VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
209
+ "count",
210
+ "sum",
211
+ "avg",
212
+ "min",
213
+ "max",
214
+ "count_distinct"
215
+ ]);
216
+ }
217
+ });
218
+
219
+ // src/tools/create-object.tool.ts
220
+ import { defineTool } from "@objectstack/spec/ai";
221
+ var createObjectTool;
222
+ var init_create_object_tool = __esm({
223
+ "src/tools/create-object.tool.ts"() {
224
+ "use strict";
225
+ createObjectTool = defineTool({
226
+ name: "create_object",
227
+ label: "Create Object",
228
+ 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.",
229
+ category: "data",
230
+ builtIn: true,
231
+ // NOTE: requiresConfirmation is intentionally false (default) because the
232
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
233
+ // executes tool calls immediately without checking this flag. The flag
234
+ // should only be set once server-side approval gating is implemented to
235
+ // avoid giving users a false sense of safety.
236
+ parameters: {
237
+ type: "object",
238
+ properties: {
239
+ name: {
240
+ type: "string",
241
+ description: "Machine name for the object (snake_case, e.g. project_task)"
242
+ },
243
+ label: {
244
+ type: "string",
245
+ description: "Human-readable display name (e.g. Project Task)"
246
+ },
247
+ fields: {
248
+ type: "array",
249
+ description: "Initial fields to create with the object",
250
+ items: {
251
+ type: "object",
252
+ properties: {
253
+ name: { type: "string", description: "Field machine name (snake_case)" },
254
+ label: { type: "string", description: "Field display name" },
255
+ type: {
256
+ type: "string",
257
+ description: "Field data type",
258
+ enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
259
+ },
260
+ required: { type: "boolean", description: "Whether the field is required" }
261
+ },
262
+ required: ["name", "type"]
263
+ }
264
+ },
265
+ enableFeatures: {
266
+ type: "object",
267
+ description: "Object capability flags",
268
+ properties: {
269
+ trackHistory: { type: "boolean" },
270
+ apiEnabled: { type: "boolean" }
271
+ }
272
+ }
273
+ },
274
+ required: ["name", "label"],
275
+ additionalProperties: false
276
+ }
277
+ });
278
+ }
279
+ });
280
+
281
+ // src/tools/add-field.tool.ts
282
+ import { defineTool as defineTool2 } from "@objectstack/spec/ai";
283
+ var addFieldTool;
284
+ var init_add_field_tool = __esm({
285
+ "src/tools/add-field.tool.ts"() {
286
+ "use strict";
287
+ addFieldTool = defineTool2({
288
+ name: "add_field",
289
+ label: "Add Field",
290
+ 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.",
291
+ category: "data",
292
+ builtIn: true,
293
+ parameters: {
294
+ type: "object",
295
+ properties: {
296
+ objectName: {
297
+ type: "string",
298
+ description: "Target object machine name (snake_case)"
299
+ },
300
+ name: {
301
+ type: "string",
302
+ description: "Field machine name (snake_case, e.g. due_date)"
303
+ },
304
+ label: {
305
+ type: "string",
306
+ description: "Human-readable field label (e.g. Due Date)"
307
+ },
308
+ type: {
309
+ type: "string",
310
+ description: "Field data type",
311
+ enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
312
+ },
313
+ required: {
314
+ type: "boolean",
315
+ description: "Whether the field is required"
316
+ },
317
+ defaultValue: {
318
+ description: "Default value for the field"
319
+ },
320
+ options: {
321
+ type: "array",
322
+ description: "Options for select/picklist fields",
323
+ items: {
324
+ type: "object",
325
+ properties: {
326
+ label: { type: "string" },
327
+ value: {
328
+ type: "string",
329
+ description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
330
+ pattern: "^[a-z_][a-z0-9_]*$"
331
+ }
332
+ }
333
+ }
334
+ },
335
+ reference: {
336
+ type: "string",
337
+ description: "Referenced object name for lookup fields (snake_case, e.g. account)"
338
+ }
339
+ },
340
+ required: ["objectName", "name", "type"],
341
+ additionalProperties: false
342
+ }
343
+ });
344
+ }
345
+ });
346
+
347
+ // src/tools/modify-field.tool.ts
348
+ import { defineTool as defineTool3 } from "@objectstack/spec/ai";
349
+ var modifyFieldTool;
350
+ var init_modify_field_tool = __esm({
351
+ "src/tools/modify-field.tool.ts"() {
352
+ "use strict";
353
+ modifyFieldTool = defineTool3({
354
+ name: "modify_field",
355
+ label: "Modify Field",
356
+ 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).",
357
+ category: "data",
358
+ builtIn: true,
359
+ parameters: {
360
+ type: "object",
361
+ properties: {
362
+ objectName: {
363
+ type: "string",
364
+ description: "Target object machine name (snake_case)"
365
+ },
366
+ fieldName: {
367
+ type: "string",
368
+ description: "Existing field machine name to modify (snake_case)"
369
+ },
370
+ changes: {
371
+ type: "object",
372
+ description: "Field properties to update (partial patch)",
373
+ properties: {
374
+ label: { type: "string", description: "New display label" },
375
+ type: { type: "string", description: "New field type" },
376
+ required: { type: "boolean", description: "Update required constraint" },
377
+ defaultValue: { description: "New default value" }
378
+ }
379
+ }
380
+ },
381
+ required: ["objectName", "fieldName", "changes"],
382
+ additionalProperties: false
383
+ }
384
+ });
385
+ }
386
+ });
387
+
388
+ // src/tools/delete-field.tool.ts
389
+ import { defineTool as defineTool4 } from "@objectstack/spec/ai";
390
+ var deleteFieldTool;
391
+ var init_delete_field_tool = __esm({
392
+ "src/tools/delete-field.tool.ts"() {
393
+ "use strict";
394
+ deleteFieldTool = defineTool4({
395
+ name: "delete_field",
396
+ label: "Delete Field",
397
+ 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.",
398
+ category: "data",
399
+ builtIn: true,
400
+ // NOTE: requiresConfirmation is intentionally false (default) because the
401
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
402
+ // executes tool calls immediately without checking this flag. The flag
403
+ // should only be set once server-side approval gating is implemented.
404
+ parameters: {
405
+ type: "object",
406
+ properties: {
407
+ objectName: {
408
+ type: "string",
409
+ description: "Target object machine name (snake_case)"
410
+ },
411
+ fieldName: {
412
+ type: "string",
413
+ description: "Field machine name to delete (snake_case)"
414
+ }
415
+ },
416
+ required: ["objectName", "fieldName"],
417
+ additionalProperties: false
418
+ }
419
+ });
420
+ }
421
+ });
422
+
423
+ // src/tools/list-objects.tool.ts
424
+ import { defineTool as defineTool5 } from "@objectstack/spec/ai";
425
+ var listObjectsTool;
426
+ var init_list_objects_tool = __esm({
427
+ "src/tools/list-objects.tool.ts"() {
428
+ "use strict";
429
+ listObjectsTool = defineTool5({
430
+ name: "list_objects",
431
+ label: "List Objects",
432
+ 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.",
433
+ category: "data",
434
+ builtIn: true,
435
+ parameters: {
436
+ type: "object",
437
+ properties: {
438
+ filter: {
439
+ type: "string",
440
+ description: "Optional name or label substring to filter objects"
441
+ },
442
+ includeFields: {
443
+ type: "boolean",
444
+ description: "Whether to include field summaries for each object (default: false)"
445
+ }
446
+ },
447
+ additionalProperties: false
448
+ }
449
+ });
450
+ }
451
+ });
452
+
453
+ // src/tools/describe-object.tool.ts
454
+ import { defineTool as defineTool6 } from "@objectstack/spec/ai";
455
+ var describeObjectTool;
456
+ var init_describe_object_tool = __esm({
457
+ "src/tools/describe-object.tool.ts"() {
458
+ "use strict";
459
+ describeObjectTool = defineTool6({
460
+ name: "describe_object",
461
+ label: "Describe Object",
462
+ 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.",
463
+ category: "data",
464
+ builtIn: true,
465
+ parameters: {
466
+ type: "object",
467
+ properties: {
468
+ objectName: {
469
+ type: "string",
470
+ description: "Object machine name to describe (snake_case)"
471
+ }
472
+ },
473
+ required: ["objectName"],
474
+ additionalProperties: false
475
+ }
476
+ });
477
+ }
478
+ });
479
+
480
+ // src/tools/metadata-tools.ts
481
+ var metadata_tools_exports = {};
482
+ __export(metadata_tools_exports, {
483
+ METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
484
+ addFieldTool: () => addFieldTool,
485
+ createObjectTool: () => createObjectTool,
486
+ deleteFieldTool: () => deleteFieldTool,
487
+ describeObjectTool: () => describeObjectTool,
488
+ listObjectsTool: () => listObjectsTool,
489
+ modifyFieldTool: () => modifyFieldTool,
490
+ registerMetadataTools: () => registerMetadataTools
491
+ });
492
+ function isSnakeCase(value) {
493
+ return SNAKE_CASE_RE.test(value);
494
+ }
495
+ function createCreateObjectHandler(ctx) {
496
+ return async (args) => {
497
+ const { name, label, fields, enableFeatures } = args;
498
+ if (!name || !label) {
499
+ return JSON.stringify({ error: 'Both "name" and "label" are required' });
500
+ }
501
+ if (!isSnakeCase(name)) {
502
+ return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
503
+ }
504
+ const existing = await ctx.metadataService.getObject(name);
505
+ if (existing) {
506
+ return JSON.stringify({ error: `Object "${name}" already exists` });
507
+ }
508
+ const fieldMap = {};
509
+ if (fields && Array.isArray(fields)) {
510
+ const seenNames = /* @__PURE__ */ new Set();
511
+ for (const f of fields) {
512
+ if (!f.name) {
513
+ return JSON.stringify({ error: 'Each field must have a "name" property' });
514
+ }
515
+ if (!isSnakeCase(f.name)) {
516
+ return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
517
+ }
518
+ if (seenNames.has(f.name)) {
519
+ return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
520
+ }
521
+ seenNames.add(f.name);
522
+ fieldMap[f.name] = {
523
+ type: f.type,
524
+ ...f.label ? { label: f.label } : {},
525
+ ...f.required !== void 0 ? { required: f.required } : {}
526
+ };
527
+ }
528
+ }
529
+ const objectDef = {
530
+ name,
531
+ label,
532
+ ...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
533
+ ...enableFeatures ? { enable: enableFeatures } : {}
534
+ };
535
+ await ctx.metadataService.register("object", name, objectDef);
536
+ return JSON.stringify({
537
+ name,
538
+ label,
539
+ fieldCount: Object.keys(fieldMap).length
540
+ });
541
+ };
542
+ }
543
+ function createAddFieldHandler(ctx) {
544
+ return async (args) => {
545
+ const { objectName, name, label, type, required, defaultValue, options, reference } = args;
546
+ if (!objectName || !name || !type) {
547
+ return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
548
+ }
549
+ if (!isSnakeCase(objectName)) {
550
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
551
+ }
552
+ if (!isSnakeCase(name)) {
553
+ return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
554
+ }
555
+ if (reference && !isSnakeCase(reference)) {
556
+ return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
557
+ }
558
+ if (options && Array.isArray(options)) {
559
+ for (const opt of options) {
560
+ if (opt.value && !isSnakeCase(opt.value)) {
561
+ return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
562
+ }
563
+ }
564
+ }
565
+ const objectDef = await ctx.metadataService.getObject(objectName);
566
+ if (!objectDef) {
567
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
568
+ }
569
+ const def = objectDef;
570
+ if (def.fields && def.fields[name]) {
571
+ return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
572
+ }
573
+ const fieldDef = {
574
+ type,
575
+ ...label ? { label } : {},
576
+ ...required !== void 0 ? { required } : {},
577
+ ...defaultValue !== void 0 ? { defaultValue } : {},
578
+ ...options ? { options } : {},
579
+ ...reference ? { reference } : {}
580
+ };
581
+ const updatedFields = { ...def.fields ?? {}, [name]: fieldDef };
582
+ await ctx.metadataService.register("object", objectName, {
583
+ ...def,
584
+ fields: updatedFields
585
+ });
586
+ return JSON.stringify({
587
+ objectName,
588
+ fieldName: name,
589
+ fieldType: type
590
+ });
591
+ };
592
+ }
593
+ function createModifyFieldHandler(ctx) {
594
+ return async (args) => {
595
+ const { objectName, fieldName, changes } = args;
596
+ if (!objectName || !fieldName || !changes) {
597
+ return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
598
+ }
599
+ if (!isSnakeCase(objectName)) {
600
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
601
+ }
602
+ if (!isSnakeCase(fieldName)) {
603
+ return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
604
+ }
605
+ const objectDef = await ctx.metadataService.getObject(objectName);
606
+ if (!objectDef) {
607
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
608
+ }
609
+ const def = objectDef;
610
+ if (!def.fields || !def.fields[fieldName]) {
611
+ return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
612
+ }
613
+ const existingField = def.fields[fieldName];
614
+ const updatedField = { ...existingField, ...changes };
615
+ const updatedFields = { ...def.fields, [fieldName]: updatedField };
616
+ await ctx.metadataService.register("object", objectName, {
617
+ ...def,
618
+ fields: updatedFields
619
+ });
620
+ return JSON.stringify({
621
+ objectName,
622
+ fieldName,
623
+ updatedProperties: Object.keys(changes)
624
+ });
625
+ };
626
+ }
627
+ function createDeleteFieldHandler(ctx) {
628
+ return async (args) => {
629
+ const { objectName, fieldName } = args;
630
+ if (!objectName || !fieldName) {
631
+ return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
632
+ }
633
+ if (!isSnakeCase(objectName)) {
634
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
635
+ }
636
+ if (!isSnakeCase(fieldName)) {
637
+ return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
638
+ }
639
+ const objectDef = await ctx.metadataService.getObject(objectName);
640
+ if (!objectDef) {
641
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
642
+ }
643
+ const def = objectDef;
644
+ if (!def.fields || !def.fields[fieldName]) {
645
+ return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
646
+ }
647
+ const { [fieldName]: _removed, ...remainingFields } = def.fields;
648
+ await ctx.metadataService.register("object", objectName, {
649
+ ...def,
650
+ fields: remainingFields
651
+ });
652
+ return JSON.stringify({
653
+ objectName,
654
+ fieldName,
655
+ success: true
656
+ });
657
+ };
658
+ }
659
+ function createListObjectsHandler(ctx) {
660
+ return async (args) => {
661
+ const { filter, includeFields } = args ?? {};
662
+ const objects = await ctx.metadataService.listObjects();
663
+ let result = objects.map((o) => {
664
+ const base = {
665
+ name: o.name,
666
+ label: o.label ?? o.name,
667
+ fieldCount: o.fields ? Object.keys(o.fields).length : 0
668
+ };
669
+ if (includeFields && o.fields) {
670
+ base.fields = Object.entries(o.fields).map(([key, f]) => ({
671
+ name: key,
672
+ type: f.type,
673
+ label: f.label ?? key
674
+ }));
675
+ }
676
+ return base;
677
+ });
678
+ if (filter) {
679
+ const lower = filter.toLowerCase();
680
+ result = result.filter(
681
+ (o) => o.name.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
682
+ );
683
+ }
684
+ return JSON.stringify({
685
+ objects: result,
686
+ totalCount: result.length
687
+ });
688
+ };
689
+ }
690
+ function createDescribeObjectHandler(ctx) {
691
+ return async (args) => {
692
+ const { objectName } = args;
693
+ if (!objectName) {
694
+ return JSON.stringify({ error: '"objectName" is required' });
695
+ }
696
+ if (!isSnakeCase(objectName)) {
697
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
698
+ }
699
+ const objectDef = await ctx.metadataService.getObject(objectName);
700
+ if (!objectDef) {
701
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
702
+ }
703
+ const def = objectDef;
704
+ const fields = def.fields ?? {};
705
+ const fieldSummary = Object.entries(fields).map(([key, f]) => ({
706
+ name: key,
707
+ type: f.type,
708
+ label: f.label ?? key,
709
+ required: f.required ?? false,
710
+ ...f.reference ? { reference: f.reference } : {},
711
+ ...f.options ? { options: f.options } : {}
712
+ }));
713
+ return JSON.stringify({
714
+ name: def.name,
715
+ label: def.label ?? def.name,
716
+ fields: fieldSummary,
717
+ enableFeatures: def.enable ?? {}
718
+ });
719
+ };
720
+ }
721
+ function registerMetadataTools(registry, context) {
722
+ registry.register(createObjectTool, createCreateObjectHandler(context));
723
+ registry.register(addFieldTool, createAddFieldHandler(context));
724
+ registry.register(modifyFieldTool, createModifyFieldHandler(context));
725
+ registry.register(deleteFieldTool, createDeleteFieldHandler(context));
726
+ registry.register(listObjectsTool, createListObjectsHandler(context));
727
+ registry.register(describeObjectTool, createDescribeObjectHandler(context));
728
+ }
729
+ var METADATA_TOOL_DEFINITIONS, SNAKE_CASE_RE;
730
+ var init_metadata_tools = __esm({
731
+ "src/tools/metadata-tools.ts"() {
732
+ "use strict";
733
+ init_create_object_tool();
734
+ init_add_field_tool();
735
+ init_modify_field_tool();
736
+ init_delete_field_tool();
737
+ init_list_objects_tool();
738
+ init_describe_object_tool();
739
+ init_create_object_tool();
740
+ init_add_field_tool();
741
+ init_modify_field_tool();
742
+ init_delete_field_tool();
743
+ init_list_objects_tool();
744
+ init_describe_object_tool();
745
+ METADATA_TOOL_DEFINITIONS = [
746
+ createObjectTool,
747
+ addFieldTool,
748
+ modifyFieldTool,
749
+ deleteFieldTool,
750
+ listObjectsTool,
751
+ describeObjectTool
752
+ ];
753
+ SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
754
+ }
755
+ });
756
+
1
757
  // src/ai-service.ts
2
758
  import { createLogger } from "@objectstack/core";
3
759
 
@@ -8,7 +764,9 @@ var MemoryLLMAdapter = class {
8
764
  }
9
765
  async chat(messages, options) {
10
766
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
11
- const content = lastUserMessage ? `[memory] ${lastUserMessage.content}` : "[memory] (no user message)";
767
+ const userContent = lastUserMessage?.content;
768
+ const text = typeof userContent === "string" ? userContent : "(complex content)";
769
+ const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
12
770
  return {
13
771
  content,
14
772
  model: options?.model ?? "memory",
@@ -26,10 +784,15 @@ var MemoryLLMAdapter = class {
26
784
  const result = await this.chat(messages);
27
785
  const words = result.content.split(" ");
28
786
  for (let i = 0; i < words.length; i++) {
29
- const textDelta = i === 0 ? words[i] : ` ${words[i]}`;
30
- yield { type: "text-delta", textDelta };
787
+ const wordText = i === 0 ? words[i] : ` ${words[i]}`;
788
+ yield { type: "text-delta", id: `delta_${i}`, text: wordText };
31
789
  }
32
- yield { type: "finish", result };
790
+ yield {
791
+ type: "finish",
792
+ finishReason: "stop",
793
+ totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
794
+ rawFinishReason: "stop"
795
+ };
33
796
  }
34
797
  async embed(input) {
35
798
  const texts = Array.isArray(input) ? input : [input];
@@ -92,21 +855,34 @@ var ToolRegistry = class {
92
855
  * Execute a tool call and return the result.
93
856
  */
94
857
  async execute(toolCall) {
95
- const handler = this.handlers.get(toolCall.name);
858
+ const handler = this.handlers.get(toolCall.toolName);
96
859
  if (!handler) {
97
860
  return {
98
- toolCallId: toolCall.id,
99
- content: `Tool "${toolCall.name}" is not registered`,
861
+ type: "tool-result",
862
+ toolCallId: toolCall.toolCallId,
863
+ toolName: toolCall.toolName,
864
+ output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
100
865
  isError: true
101
866
  };
102
867
  }
103
868
  try {
104
- const args = JSON.parse(toolCall.arguments);
869
+ const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
105
870
  const content = await handler(args);
106
- return { toolCallId: toolCall.id, content };
871
+ return {
872
+ type: "tool-result",
873
+ toolCallId: toolCall.toolCallId,
874
+ toolName: toolCall.toolName,
875
+ output: { type: "text", value: content }
876
+ };
107
877
  } catch (err) {
108
878
  const message = err instanceof Error ? err.message : String(err);
109
- return { toolCallId: toolCall.id, content: message, isError: true };
879
+ return {
880
+ type: "tool-result",
881
+ toolCallId: toolCall.toolCallId,
882
+ toolName: toolCall.toolName,
883
+ output: { type: "text", value: message },
884
+ isError: true
885
+ };
110
886
  }
111
887
  }
112
888
  /**
@@ -192,6 +968,17 @@ var InMemoryConversationService = class {
192
968
  };
193
969
 
194
970
  // src/ai-service.ts
971
+ function textDeltaPart(id, text) {
972
+ return { type: "text-delta", id, text };
973
+ }
974
+ function finishPart(result) {
975
+ return {
976
+ type: "finish",
977
+ finishReason: "stop",
978
+ totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
979
+ rawFinishReason: "stop"
980
+ };
981
+ }
195
982
  var _AIService = class _AIService {
196
983
  constructor(config = {}) {
197
984
  this.adapter = config.adapter ?? new MemoryLLMAdapter();
@@ -219,8 +1006,8 @@ var _AIService = class _AIService {
219
1006
  this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
220
1007
  if (!this.adapter.streamChat) {
221
1008
  const result = await this.adapter.chat(messages, options);
222
- yield { type: "text-delta", textDelta: result.content };
223
- yield { type: "finish", result };
1009
+ yield textDeltaPart("fallback", result.content);
1010
+ yield finishPart(result);
224
1011
  return;
225
1012
  }
226
1013
  yield* this.adapter.streamChat(messages, options);
@@ -237,6 +1024,10 @@ var _AIService = class _AIService {
237
1024
  }
238
1025
  return this.adapter.listModels();
239
1026
  }
1027
+ /** Extract the text value from a ToolExecutionResult's output. */
1028
+ static extractOutputText(tr) {
1029
+ return tr.output && typeof tr.output === "object" && "value" in tr.output ? String(tr.output.value) : "unknown error";
1030
+ }
240
1031
  /**
241
1032
  * Chat with automatic tool call resolution.
242
1033
  *
@@ -277,23 +1068,26 @@ var _AIService = class _AIService {
277
1068
  }
278
1069
  this.logger.debug("[AI] chatWithTools tool calls", {
279
1070
  iteration,
280
- calls: result.toolCalls.map((tc) => tc.name)
1071
+ calls: result.toolCalls.map((tc) => tc.toolName)
281
1072
  });
1073
+ const assistantContent = [];
1074
+ if (result.content) assistantContent.push({ type: "text", text: result.content });
1075
+ assistantContent.push(...result.toolCalls);
282
1076
  conversation.push({
283
1077
  role: "assistant",
284
- content: result.content ?? "",
285
- toolCalls: result.toolCalls
1078
+ content: assistantContent
286
1079
  });
287
1080
  const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
288
1081
  for (const tr of toolResults) {
289
1082
  if (tr.isError) {
290
- const matchedCall = result.toolCalls.find((tc) => tc.id === tr.toolCallId);
291
- const toolName = matchedCall?.name ?? "unknown";
292
- const errorEntry = { iteration, toolName, error: tr.content };
1083
+ const matchedCall = result.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
1084
+ const toolName = matchedCall?.toolName ?? "unknown";
1085
+ const errorText = _AIService.extractOutputText(tr);
1086
+ const errorEntry = { iteration, toolName, error: errorText };
293
1087
  toolErrors.push(errorEntry);
294
1088
  this.logger.warn("[AI] chatWithTools tool error", errorEntry);
295
1089
  if (onToolError && matchedCall) {
296
- const action = onToolError(matchedCall, tr.content);
1090
+ const action = onToolError(matchedCall, errorText);
297
1091
  if (action === "abort") {
298
1092
  abortedByCallback = true;
299
1093
  }
@@ -301,8 +1095,7 @@ var _AIService = class _AIService {
301
1095
  }
302
1096
  conversation.push({
303
1097
  role: "tool",
304
- content: tr.content,
305
- toolCallId: tr.toolCallId
1098
+ content: [tr]
306
1099
  });
307
1100
  }
308
1101
  if (abortedByCallback) {
@@ -348,33 +1141,41 @@ var _AIService = class _AIService {
348
1141
  for (let iteration = 0; iteration < maxIterations; iteration++) {
349
1142
  const result2 = await this.adapter.chat(conversation, chatOptions);
350
1143
  if (!result2.toolCalls || result2.toolCalls.length === 0) {
351
- yield { type: "text-delta", textDelta: result2.content };
352
- yield { type: "finish", result: result2 };
1144
+ yield textDeltaPart("stream", result2.content);
1145
+ yield finishPart(result2);
353
1146
  return;
354
1147
  }
355
1148
  for (const tc of result2.toolCalls) {
356
- yield { type: "tool-call", toolCall: tc };
1149
+ yield { type: "tool-call", toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input };
357
1150
  }
1151
+ const assistantContent = [];
1152
+ if (result2.content) assistantContent.push({ type: "text", text: result2.content });
1153
+ assistantContent.push(...result2.toolCalls);
358
1154
  conversation.push({
359
1155
  role: "assistant",
360
- content: result2.content ?? "",
361
- toolCalls: result2.toolCalls
1156
+ content: assistantContent
362
1157
  });
363
1158
  const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
364
1159
  for (const tr of toolResults) {
365
1160
  if (tr.isError && onToolError) {
366
- const matchedCall = result2.toolCalls.find((tc) => tc.id === tr.toolCallId);
1161
+ const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
367
1162
  if (matchedCall) {
368
- const action = onToolError(matchedCall, tr.content);
1163
+ const errorText = _AIService.extractOutputText(tr);
1164
+ const action = onToolError(matchedCall, errorText);
369
1165
  if (action === "abort") {
370
1166
  abortedByCallback = true;
371
1167
  }
372
1168
  }
373
1169
  }
1170
+ yield {
1171
+ type: "tool-result",
1172
+ toolCallId: tr.toolCallId,
1173
+ toolName: tr.toolName,
1174
+ output: tr.output
1175
+ };
374
1176
  conversation.push({
375
1177
  role: "tool",
376
- content: tr.content,
377
- toolCallId: tr.toolCallId
1178
+ content: [tr]
378
1179
  });
379
1180
  }
380
1181
  if (abortedByCallback) {
@@ -388,8 +1189,8 @@ var _AIService = class _AIService {
388
1189
  }
389
1190
  const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
390
1191
  const result = await this.adapter.chat(conversation, finalOptions);
391
- yield { type: "text-delta", textDelta: result.content };
392
- yield { type: "finish", result };
1192
+ yield textDeltaPart("stream", result.content);
1193
+ yield finishPart(result);
393
1194
  }
394
1195
  };
395
1196
  // ── Tool Call Loop ────────────────────────────────────────────
@@ -397,6 +1198,143 @@ var _AIService = class _AIService {
397
1198
  _AIService.DEFAULT_MAX_ITERATIONS = 10;
398
1199
  var AIService = _AIService;
399
1200
 
1201
+ // src/stream/vercel-stream-encoder.ts
1202
+ function sse(data) {
1203
+ return `data: ${JSON.stringify(data)}
1204
+
1205
+ `;
1206
+ }
1207
+ function dataStreamLine(prefix, data) {
1208
+ return `${prefix}:${JSON.stringify(data)}
1209
+ `;
1210
+ }
1211
+ function encodeStreamPart(part) {
1212
+ switch (part.type) {
1213
+ case "text-delta":
1214
+ return sse({ type: "text-delta", id: "0", delta: part.text });
1215
+ case "tool-input-start":
1216
+ return sse({
1217
+ type: "tool-input-start",
1218
+ toolCallId: part.id,
1219
+ toolName: part.toolName
1220
+ });
1221
+ case "tool-input-delta":
1222
+ return sse({
1223
+ type: "tool-input-delta",
1224
+ toolCallId: part.id,
1225
+ inputTextDelta: part.delta
1226
+ });
1227
+ case "tool-call":
1228
+ return sse({
1229
+ type: "tool-input-available",
1230
+ toolCallId: part.toolCallId,
1231
+ toolName: part.toolName,
1232
+ input: part.input
1233
+ });
1234
+ case "tool-result":
1235
+ return sse({
1236
+ type: "tool-output-available",
1237
+ toolCallId: part.toolCallId,
1238
+ output: part.output
1239
+ });
1240
+ case "error":
1241
+ return sse({
1242
+ type: "error",
1243
+ errorText: String(part.error)
1244
+ });
1245
+ // Handle reasoning/thinking streams (DeepSeek R1, o1-style models)
1246
+ // Use 'g:' prefix for reasoning content per Vercel AI SDK protocol
1247
+ case "reasoning-start":
1248
+ return dataStreamLine("g", { text: "" });
1249
+ case "reasoning-delta":
1250
+ return dataStreamLine("g", { text: part.text });
1251
+ case "reasoning-end":
1252
+ return "";
1253
+ // No specific end marker needed for reasoning
1254
+ // finish-step and finish are handled by the generator, not here
1255
+ default:
1256
+ if (part.type?.startsWith("step-")) {
1257
+ return sse(part);
1258
+ }
1259
+ return "";
1260
+ }
1261
+ }
1262
+ async function* encodeVercelDataStream(events) {
1263
+ yield sse({ type: "start" });
1264
+ yield sse({ type: "start-step" });
1265
+ yield sse({ type: "text-start", id: "0" });
1266
+ let textOpen = true;
1267
+ let finishReason = "stop";
1268
+ for await (const part of events) {
1269
+ if (part.type === "finish") {
1270
+ finishReason = part.finishReason ?? "stop";
1271
+ }
1272
+ if (part.type === "finish-step" || part.type === "finish") {
1273
+ if (textOpen) {
1274
+ yield sse({ type: "text-end", id: "0" });
1275
+ textOpen = false;
1276
+ }
1277
+ continue;
1278
+ }
1279
+ const frame = encodeStreamPart(part);
1280
+ if (frame) {
1281
+ yield frame;
1282
+ }
1283
+ }
1284
+ if (textOpen) {
1285
+ yield sse({ type: "text-end", id: "0" });
1286
+ }
1287
+ yield sse({ type: "finish-step" });
1288
+ yield sse({ type: "finish", finishReason });
1289
+ yield "data: [DONE]\n\n";
1290
+ }
1291
+
1292
+ // src/routes/message-utils.ts
1293
+ function normalizeMessage(raw) {
1294
+ const role = raw.role;
1295
+ if (typeof raw.content === "string") {
1296
+ return { role, content: raw.content };
1297
+ }
1298
+ if (Array.isArray(raw.content)) {
1299
+ return { role, content: raw.content };
1300
+ }
1301
+ if (Array.isArray(raw.parts)) {
1302
+ const textParts = raw.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text);
1303
+ if (textParts.length > 0) {
1304
+ return { role, content: textParts.join("") };
1305
+ }
1306
+ }
1307
+ return { role, content: "" };
1308
+ }
1309
+ function validateMessageContent(msg, opts) {
1310
+ const content = msg.content;
1311
+ if (Array.isArray(msg.parts)) {
1312
+ return null;
1313
+ }
1314
+ if (typeof content === "string") {
1315
+ return null;
1316
+ }
1317
+ if (Array.isArray(content)) {
1318
+ for (const part of content) {
1319
+ if (typeof part !== "object" || part === null) {
1320
+ return "message.content array elements must be non-null objects";
1321
+ }
1322
+ const partObj = part;
1323
+ if (typeof partObj.type !== "string") {
1324
+ return 'each message.content array element must have a string "type" property';
1325
+ }
1326
+ if (partObj.type === "text" && typeof partObj.text !== "string") {
1327
+ return 'message.content elements with type "text" must have a string "text" property';
1328
+ }
1329
+ }
1330
+ return null;
1331
+ }
1332
+ if ((content === null || content === void 0) && opts?.allowEmptyContent) {
1333
+ return null;
1334
+ }
1335
+ return "message.content must be a string, an array, or include parts";
1336
+ }
1337
+
400
1338
  // src/routes/ai-routes.ts
401
1339
  var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
402
1340
  function validateMessage(raw) {
@@ -407,22 +1345,30 @@ function validateMessage(raw) {
407
1345
  if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
408
1346
  return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
409
1347
  }
410
- if (typeof msg.content !== "string") {
411
- return "message.content must be a string";
412
- }
413
- return null;
1348
+ const allowEmpty = msg.role === "assistant" || msg.role === "tool";
1349
+ return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
414
1350
  }
415
1351
  function buildAIRoutes(aiService, conversationService, logger) {
416
1352
  return [
417
1353
  // ── Chat ────────────────────────────────────────────────────
1354
+ //
1355
+ // Dual-mode endpoint compatible with both the legacy ObjectStack
1356
+ // format (`{ messages, options }`) and the Vercel AI SDK useChat
1357
+ // flat format (`{ messages, system, model, stream, … }`).
1358
+ //
1359
+ // Behaviour:
1360
+ // • `stream !== false` → Vercel Data Stream Protocol (SSE)
1361
+ // • `stream === false` → JSON response (legacy)
1362
+ //
418
1363
  {
419
1364
  method: "POST",
420
1365
  path: "/api/v1/ai/chat",
421
- description: "Synchronous chat completion",
1366
+ description: "Chat completion (supports Vercel AI Data Stream Protocol)",
422
1367
  auth: true,
423
1368
  permissions: ["ai:chat"],
424
1369
  handler: async (req) => {
425
- const { messages, options } = req.body ?? {};
1370
+ const body = req.body ?? {};
1371
+ const messages = body.messages;
426
1372
  if (!Array.isArray(messages) || messages.length === 0) {
427
1373
  return { status: 400, body: { error: "messages array is required" } };
428
1374
  }
@@ -430,8 +1376,49 @@ function buildAIRoutes(aiService, conversationService, logger) {
430
1376
  const err = validateMessage(msg);
431
1377
  if (err) return { status: 400, body: { error: err } };
432
1378
  }
1379
+ const nested = body.options ?? {};
1380
+ const resolvedOptions = {
1381
+ ...nested,
1382
+ ...body.model != null && { model: body.model },
1383
+ ...body.temperature != null && { temperature: body.temperature },
1384
+ ...body.maxTokens != null && { maxTokens: body.maxTokens }
1385
+ };
1386
+ const rawSystemPrompt = body.system ?? body.systemPrompt;
1387
+ if (rawSystemPrompt != null && typeof rawSystemPrompt !== "string") {
1388
+ return { status: 400, body: { error: "system/systemPrompt must be a string" } };
1389
+ }
1390
+ const systemPrompt = rawSystemPrompt;
1391
+ const finalMessages = [
1392
+ ...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
1393
+ ...messages.map((m) => normalizeMessage(m))
1394
+ ];
1395
+ const wantStream = body.stream !== false;
1396
+ if (wantStream) {
1397
+ try {
1398
+ if (!aiService.streamChatWithTools) {
1399
+ return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
1400
+ }
1401
+ const events = aiService.streamChatWithTools(finalMessages, resolvedOptions);
1402
+ return {
1403
+ status: 200,
1404
+ stream: true,
1405
+ vercelDataStream: true,
1406
+ contentType: "text/event-stream",
1407
+ headers: {
1408
+ "Content-Type": "text/event-stream",
1409
+ "Cache-Control": "no-cache",
1410
+ "Connection": "keep-alive",
1411
+ "x-vercel-ai-ui-message-stream": "v1"
1412
+ },
1413
+ events: encodeVercelDataStream(events)
1414
+ };
1415
+ } catch (err) {
1416
+ logger.error("[AI Route] /chat stream error", err instanceof Error ? err : void 0);
1417
+ return { status: 500, body: { error: "Internal AI service error" } };
1418
+ }
1419
+ }
433
1420
  try {
434
- const result = await aiService.chat(messages, options);
1421
+ const result = await aiService.chatWithTools(finalMessages, resolvedOptions);
435
1422
  return { status: 200, body: result };
436
1423
  } catch (err) {
437
1424
  logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
@@ -459,7 +1446,7 @@ function buildAIRoutes(aiService, conversationService, logger) {
459
1446
  if (!aiService.streamChat) {
460
1447
  return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
461
1448
  }
462
- const events = aiService.streamChat(messages, options);
1449
+ const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
463
1450
  return { status: 200, stream: true, events };
464
1451
  } catch (err) {
465
1452
  logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
@@ -637,17 +1624,41 @@ function validateAgentMessage(raw) {
637
1624
  if (typeof msg.role !== "string" || !ALLOWED_AGENT_ROLES.has(msg.role)) {
638
1625
  return `message.role must be one of ${[...ALLOWED_AGENT_ROLES].map((r) => `"${r}"`).join(", ")} for agent chat`;
639
1626
  }
640
- if (typeof msg.content !== "string") {
641
- return "message.content must be a string";
642
- }
643
- return null;
1627
+ const allowEmpty = msg.role === "assistant";
1628
+ return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
644
1629
  }
645
1630
  function buildAgentRoutes(aiService, agentRuntime, logger) {
646
1631
  return [
1632
+ // ── List active agents ──────────────────────────────────────
1633
+ {
1634
+ method: "GET",
1635
+ path: "/api/v1/ai/agents",
1636
+ description: "List all active AI agents",
1637
+ auth: true,
1638
+ permissions: ["ai:chat"],
1639
+ handler: async () => {
1640
+ try {
1641
+ const agents = await agentRuntime.listAgents();
1642
+ return { status: 200, body: { agents } };
1643
+ } catch (err) {
1644
+ logger.error(
1645
+ "[AI Route] /agents list error",
1646
+ err instanceof Error ? err : void 0
1647
+ );
1648
+ return { status: 500, body: { error: "Internal AI service error" } };
1649
+ }
1650
+ }
1651
+ },
1652
+ // ── Chat with a specific agent ──────────────────────────────
1653
+ //
1654
+ // Dual-mode endpoint matching the general chat route behaviour:
1655
+ // • `stream !== false` → Vercel Data Stream Protocol (SSE)
1656
+ // • `stream === false` → JSON response (legacy)
1657
+ //
647
1658
  {
648
1659
  method: "POST",
649
1660
  path: "/api/v1/ai/agents/:agentName/chat",
650
- description: "Chat with a specific AI agent",
1661
+ description: "Chat with a specific AI agent (supports Vercel AI Data Stream Protocol)",
651
1662
  auth: true,
652
1663
  permissions: ["ai:chat", "ai:agents"],
653
1664
  handler: async (req) => {
@@ -655,11 +1666,12 @@ function buildAgentRoutes(aiService, agentRuntime, logger) {
655
1666
  if (!agentName) {
656
1667
  return { status: 400, body: { error: "agentName parameter is required" } };
657
1668
  }
1669
+ const body = req.body ?? {};
658
1670
  const {
659
1671
  messages: rawMessages,
660
1672
  context: chatContext,
661
1673
  options: extraOptions
662
- } = req.body ?? {};
1674
+ } = body;
663
1675
  if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
664
1676
  return { status: 400, body: { error: "messages array is required" } };
665
1677
  }
@@ -687,21 +1699,150 @@ function buildAgentRoutes(aiService, agentRuntime, logger) {
687
1699
  if (ALLOWED_KEYS.has(key)) {
688
1700
  safeOverrides[key] = extraOptions[key];
689
1701
  }
690
- }
1702
+ }
1703
+ }
1704
+ const mergedOptions = { ...agentOptions, ...safeOverrides };
1705
+ const fullMessages = [
1706
+ ...systemMessages,
1707
+ ...rawMessages.map((m) => normalizeMessage(m))
1708
+ ];
1709
+ const chatWithToolsOptions = {
1710
+ ...mergedOptions,
1711
+ maxIterations: agent.planning?.maxIterations
1712
+ };
1713
+ const wantStream = body.stream !== false;
1714
+ if (wantStream) {
1715
+ if (!aiService.streamChatWithTools) {
1716
+ return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
1717
+ }
1718
+ const events = aiService.streamChatWithTools(fullMessages, chatWithToolsOptions);
1719
+ return {
1720
+ status: 200,
1721
+ stream: true,
1722
+ vercelDataStream: true,
1723
+ contentType: "text/event-stream",
1724
+ headers: {
1725
+ "Content-Type": "text/event-stream",
1726
+ "Cache-Control": "no-cache",
1727
+ "Connection": "keep-alive",
1728
+ "x-vercel-ai-ui-message-stream": "v1"
1729
+ },
1730
+ events: encodeVercelDataStream(events)
1731
+ };
1732
+ }
1733
+ const result = await aiService.chatWithTools(fullMessages, chatWithToolsOptions);
1734
+ return { status: 200, body: result };
1735
+ } catch (err) {
1736
+ logger.error(
1737
+ "[AI Route] /agents/:agentName/chat error",
1738
+ err instanceof Error ? err : void 0
1739
+ );
1740
+ return { status: 500, body: { error: "Internal AI service error" } };
1741
+ }
1742
+ }
1743
+ }
1744
+ ];
1745
+ }
1746
+
1747
+ // src/routes/tool-routes.ts
1748
+ function extractOutputValue(output) {
1749
+ if (!output) return "";
1750
+ if (typeof output === "string") return output;
1751
+ if (typeof output === "object" && "value" in output) {
1752
+ return String(output.value ?? "");
1753
+ }
1754
+ return JSON.stringify(output);
1755
+ }
1756
+ function buildToolRoutes(aiService, logger) {
1757
+ return [
1758
+ // ── List registered tools ──────────────────────────────────────
1759
+ {
1760
+ method: "GET",
1761
+ path: "/api/v1/ai/tools",
1762
+ description: "List all registered AI tools",
1763
+ auth: true,
1764
+ permissions: ["ai:tools"],
1765
+ handler: async () => {
1766
+ try {
1767
+ const tools = aiService.toolRegistry.getAll();
1768
+ return {
1769
+ status: 200,
1770
+ body: {
1771
+ tools: tools.map((t) => ({
1772
+ name: t.name,
1773
+ description: t.description,
1774
+ category: t.category
1775
+ }))
1776
+ }
1777
+ };
1778
+ } catch (err) {
1779
+ logger.error(
1780
+ "[AI Route] /tools list error",
1781
+ err instanceof Error ? err : void 0
1782
+ );
1783
+ return { status: 500, body: { error: "Internal AI service error" } };
1784
+ }
1785
+ }
1786
+ },
1787
+ // ── Execute a tool ──────────────────────────────────────────────
1788
+ //
1789
+ // Executes a tool with the provided parameters.
1790
+ // This is intended for testing/playground use.
1791
+ //
1792
+ {
1793
+ method: "POST",
1794
+ path: "/api/v1/ai/tools/:toolName/execute",
1795
+ description: "Execute a tool with parameters (playground/testing)",
1796
+ auth: true,
1797
+ permissions: ["ai:tools", "ai:execute"],
1798
+ handler: async (req) => {
1799
+ const toolName = req.params?.toolName;
1800
+ if (!toolName) {
1801
+ return { status: 400, body: { error: "toolName parameter is required" } };
1802
+ }
1803
+ const body = req.body ?? {};
1804
+ const { parameters } = body;
1805
+ if (!parameters || typeof parameters !== "object") {
1806
+ return { status: 400, body: { error: "parameters object is required" } };
1807
+ }
1808
+ try {
1809
+ if (!aiService.toolRegistry.has(toolName)) {
1810
+ return { status: 404, body: { error: `Tool "${toolName}" not found` } };
1811
+ }
1812
+ const startTime = Date.now();
1813
+ const toolCallPart = {
1814
+ type: "tool-call",
1815
+ toolCallId: `playground-${Date.now()}`,
1816
+ toolName,
1817
+ input: parameters
1818
+ };
1819
+ const result = await aiService.toolRegistry.execute(toolCallPart);
1820
+ const duration = Date.now() - startTime;
1821
+ if (result.isError) {
1822
+ const errorMsg = extractOutputValue(result.output);
1823
+ logger.error(
1824
+ `[AI Route] Tool execution error: ${toolName}`,
1825
+ new Error(errorMsg)
1826
+ );
1827
+ return {
1828
+ status: 500,
1829
+ body: {
1830
+ error: errorMsg,
1831
+ duration
1832
+ }
1833
+ };
691
1834
  }
692
- const mergedOptions = { ...agentOptions, ...safeOverrides };
693
- const fullMessages = [
694
- ...systemMessages,
695
- ...rawMessages
696
- ];
697
- const result = await aiService.chatWithTools(fullMessages, {
698
- ...mergedOptions,
699
- maxIterations: agent.planning?.maxIterations
700
- });
701
- return { status: 200, body: result };
1835
+ return {
1836
+ status: 200,
1837
+ body: {
1838
+ result: extractOutputValue(result.output),
1839
+ duration,
1840
+ toolName
1841
+ }
1842
+ };
702
1843
  } catch (err) {
703
1844
  logger.error(
704
- "[AI Route] /agents/:agentName/chat error",
1845
+ "[AI Route] /tools/:toolName/execute error",
705
1846
  err instanceof Error ? err : void 0
706
1847
  );
707
1848
  return { status: 500, body: { error: "Internal AI service error" } };
@@ -803,13 +1944,35 @@ var ObjectQLConversationService = class {
803
1944
  }
804
1945
  const now = (/* @__PURE__ */ new Date()).toISOString();
805
1946
  const msgId = `msg_${randomUUID()}`;
1947
+ let contentStr;
1948
+ let toolCallsJson = null;
1949
+ let toolCallId = null;
1950
+ if (message.role === "system" || message.role === "user") {
1951
+ contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
1952
+ } else if (message.role === "assistant") {
1953
+ if (typeof message.content === "string") {
1954
+ contentStr = message.content;
1955
+ } else {
1956
+ const parts = message.content;
1957
+ const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
1958
+ const toolCalls = parts.filter((p) => p.type === "tool-call");
1959
+ contentStr = textParts.join("");
1960
+ if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
1961
+ }
1962
+ } else if (message.role === "tool") {
1963
+ contentStr = JSON.stringify(message.content);
1964
+ const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
1965
+ if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
1966
+ } else {
1967
+ contentStr = "";
1968
+ }
806
1969
  await this.engine.insert(MESSAGES_OBJECT, {
807
1970
  id: msgId,
808
1971
  conversation_id: conversationId,
809
1972
  role: message.role,
810
- content: message.content,
811
- tool_calls: message.toolCalls ? JSON.stringify(message.toolCalls) : null,
812
- tool_call_id: message.toolCallId ?? null,
1973
+ content: contentStr,
1974
+ tool_calls: toolCallsJson,
1975
+ tool_call_id: toolCallId,
813
1976
  created_at: now
814
1977
  });
815
1978
  await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
@@ -854,21 +2017,42 @@ var ObjectQLConversationService = class {
854
2017
  };
855
2018
  }
856
2019
  /**
857
- * Map a database row to an AIMessage.
2020
+ * Map a database row to a ModelMessage.
858
2021
  */
859
2022
  toMessage(row) {
860
- const msg = {
861
- role: row.role,
862
- content: row.content
863
- };
864
- const toolCalls = this.safeParse(row.tool_calls);
865
- if (toolCalls) {
866
- msg.toolCalls = toolCalls;
867
- }
868
- if (row.tool_call_id) {
869
- msg.toolCallId = row.tool_call_id;
2023
+ switch (row.role) {
2024
+ case "system":
2025
+ return { role: "system", content: row.content };
2026
+ case "user":
2027
+ return { role: "user", content: row.content };
2028
+ case "assistant": {
2029
+ const toolCalls = this.safeParse(row.tool_calls);
2030
+ if (toolCalls && toolCalls.length > 0) {
2031
+ const content = [];
2032
+ if (row.content) content.push({ type: "text", text: row.content });
2033
+ content.push(...toolCalls);
2034
+ return { role: "assistant", content };
2035
+ }
2036
+ return { role: "assistant", content: row.content };
2037
+ }
2038
+ case "tool": {
2039
+ const toolResults = this.safeParse(row.content);
2040
+ if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
2041
+ return { role: "tool", content: toolResults };
2042
+ }
2043
+ return {
2044
+ role: "tool",
2045
+ content: [{
2046
+ type: "tool-result",
2047
+ toolCallId: row.tool_call_id ?? "",
2048
+ toolName: "unknown",
2049
+ output: { type: "text", value: row.content }
2050
+ }]
2051
+ };
2052
+ }
2053
+ default:
2054
+ return { role: "user", content: row.content };
870
2055
  }
871
- return msg;
872
2056
  }
873
2057
  };
874
2058
 
@@ -1007,263 +2191,9 @@ var AiMessageObject = ObjectSchema2.create({
1007
2191
  }
1008
2192
  });
1009
2193
 
1010
- // src/tools/data-tools.ts
1011
- var MAX_QUERY_LIMIT = 200;
1012
- var DEFAULT_QUERY_LIMIT = 20;
1013
- var LIST_OBJECTS_TOOL = {
1014
- name: "list_objects",
1015
- description: "List all available data objects (tables) in the system. Returns object names and labels.",
1016
- parameters: {
1017
- type: "object",
1018
- properties: {},
1019
- additionalProperties: false
1020
- }
1021
- };
1022
- var DESCRIBE_OBJECT_TOOL = {
1023
- name: "describe_object",
1024
- description: "Get the schema (fields, types, labels) of a specific data object. Use this to understand the structure of a table before querying it.",
1025
- parameters: {
1026
- type: "object",
1027
- properties: {
1028
- objectName: {
1029
- type: "string",
1030
- description: "The snake_case name of the object to describe"
1031
- }
1032
- },
1033
- required: ["objectName"],
1034
- additionalProperties: false
1035
- }
1036
- };
1037
- var QUERY_RECORDS_TOOL = {
1038
- name: "query_records",
1039
- description: "Query records from a data object with optional filters, field selection, sorting, and pagination. Returns an array of matching records.",
1040
- parameters: {
1041
- type: "object",
1042
- properties: {
1043
- objectName: {
1044
- type: "string",
1045
- description: "The snake_case name of the object to query"
1046
- },
1047
- where: {
1048
- type: "object",
1049
- description: 'Filter conditions as key-value pairs (e.g. { "status": "active" }) or MongoDB-style operators (e.g. { "amount": { "$gt": 100 } })'
1050
- },
1051
- fields: {
1052
- type: "array",
1053
- items: { type: "string" },
1054
- description: "List of field names to return (omit for all fields)"
1055
- },
1056
- orderBy: {
1057
- type: "array",
1058
- items: {
1059
- type: "object",
1060
- properties: {
1061
- field: { type: "string" },
1062
- order: { type: "string", enum: ["asc", "desc"] }
1063
- }
1064
- },
1065
- description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
1066
- },
1067
- limit: {
1068
- type: "number",
1069
- description: `Maximum number of records to return (default ${DEFAULT_QUERY_LIMIT}, max ${MAX_QUERY_LIMIT})`
1070
- },
1071
- offset: {
1072
- type: "number",
1073
- description: "Number of records to skip for pagination"
1074
- }
1075
- },
1076
- required: ["objectName"],
1077
- additionalProperties: false
1078
- }
1079
- };
1080
- var GET_RECORD_TOOL = {
1081
- name: "get_record",
1082
- description: "Get a single record by its ID from a data object.",
1083
- parameters: {
1084
- type: "object",
1085
- properties: {
1086
- objectName: {
1087
- type: "string",
1088
- description: "The snake_case name of the object"
1089
- },
1090
- recordId: {
1091
- type: "string",
1092
- description: "The unique ID of the record"
1093
- },
1094
- fields: {
1095
- type: "array",
1096
- items: { type: "string" },
1097
- description: "List of field names to return (omit for all fields)"
1098
- }
1099
- },
1100
- required: ["objectName", "recordId"],
1101
- additionalProperties: false
1102
- }
1103
- };
1104
- var AGGREGATE_DATA_TOOL = {
1105
- name: "aggregate_data",
1106
- description: "Perform aggregation/statistical operations on a data object. Supports count, sum, avg, min, max with optional groupBy and where filters.",
1107
- parameters: {
1108
- type: "object",
1109
- properties: {
1110
- objectName: {
1111
- type: "string",
1112
- description: "The snake_case name of the object to aggregate"
1113
- },
1114
- aggregations: {
1115
- type: "array",
1116
- items: {
1117
- type: "object",
1118
- properties: {
1119
- function: {
1120
- type: "string",
1121
- enum: ["count", "sum", "avg", "min", "max", "count_distinct"],
1122
- description: "Aggregation function"
1123
- },
1124
- field: {
1125
- type: "string",
1126
- description: "Field to aggregate (optional for count)"
1127
- },
1128
- alias: {
1129
- type: "string",
1130
- description: "Result column alias"
1131
- }
1132
- },
1133
- required: ["function", "alias"]
1134
- },
1135
- description: "Aggregation definitions"
1136
- },
1137
- groupBy: {
1138
- type: "array",
1139
- items: { type: "string" },
1140
- description: "Fields to group by"
1141
- },
1142
- where: {
1143
- type: "object",
1144
- description: "Filter conditions applied before aggregation"
1145
- }
1146
- },
1147
- required: ["objectName", "aggregations"],
1148
- additionalProperties: false
1149
- }
1150
- };
1151
- var DATA_TOOL_DEFINITIONS = [
1152
- LIST_OBJECTS_TOOL,
1153
- DESCRIBE_OBJECT_TOOL,
1154
- QUERY_RECORDS_TOOL,
1155
- GET_RECORD_TOOL,
1156
- AGGREGATE_DATA_TOOL
1157
- ];
1158
- function createListObjectsHandler(ctx) {
1159
- return async () => {
1160
- const objects = await ctx.metadataService.listObjects();
1161
- const summary = objects.map((o) => ({
1162
- name: o.name,
1163
- label: o.label ?? o.name
1164
- }));
1165
- return JSON.stringify(summary);
1166
- };
1167
- }
1168
- function createDescribeObjectHandler(ctx) {
1169
- return async (args) => {
1170
- const { objectName } = args;
1171
- const objectDef = await ctx.metadataService.getObject(objectName);
1172
- if (!objectDef) {
1173
- return JSON.stringify({ error: `Object "${objectName}" not found` });
1174
- }
1175
- const def = objectDef;
1176
- const fields = def.fields ?? {};
1177
- const fieldSummary = {};
1178
- for (const [key, f] of Object.entries(fields)) {
1179
- fieldSummary[key] = {
1180
- type: f.type,
1181
- label: f.label ?? key,
1182
- required: f.required ?? false,
1183
- ...f.reference ? { reference: f.reference } : {},
1184
- ...f.options ? { options: f.options } : {}
1185
- };
1186
- }
1187
- return JSON.stringify({
1188
- name: def.name,
1189
- label: def.label ?? def.name,
1190
- fields: fieldSummary
1191
- });
1192
- };
1193
- }
1194
- function createQueryRecordsHandler(ctx) {
1195
- return async (args) => {
1196
- const {
1197
- objectName,
1198
- where,
1199
- fields,
1200
- orderBy,
1201
- limit,
1202
- offset
1203
- } = args;
1204
- const rawLimit = limit ?? DEFAULT_QUERY_LIMIT;
1205
- const safeLimit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.floor(rawLimit), MAX_QUERY_LIMIT) : DEFAULT_QUERY_LIMIT;
1206
- const safeOffset = Number.isFinite(offset) && offset >= 0 ? Math.floor(offset) : void 0;
1207
- const records = await ctx.dataEngine.find(objectName, {
1208
- where,
1209
- fields,
1210
- orderBy,
1211
- limit: safeLimit,
1212
- offset: safeOffset
1213
- });
1214
- return JSON.stringify({ count: records.length, records });
1215
- };
1216
- }
1217
- function createGetRecordHandler(ctx) {
1218
- return async (args) => {
1219
- const { objectName, recordId, fields } = args;
1220
- const record = await ctx.dataEngine.findOne(objectName, {
1221
- where: { id: recordId },
1222
- fields
1223
- });
1224
- if (!record) {
1225
- return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
1226
- }
1227
- return JSON.stringify(record);
1228
- };
1229
- }
1230
- var VALID_AGG_FUNCTIONS = /* @__PURE__ */ new Set([
1231
- "count",
1232
- "sum",
1233
- "avg",
1234
- "min",
1235
- "max",
1236
- "count_distinct"
1237
- ]);
1238
- function createAggregateDataHandler(ctx) {
1239
- return async (args) => {
1240
- const { objectName, aggregations, groupBy, where } = args;
1241
- for (const a of aggregations) {
1242
- if (!VALID_AGG_FUNCTIONS.has(a.function)) {
1243
- return JSON.stringify({
1244
- error: `Invalid aggregation function "${a.function}". Allowed: ${[...VALID_AGG_FUNCTIONS].join(", ")}`
1245
- });
1246
- }
1247
- }
1248
- const result = await ctx.dataEngine.aggregate(objectName, {
1249
- where,
1250
- groupBy,
1251
- aggregations: aggregations.map((a) => ({
1252
- function: a.function,
1253
- field: a.field,
1254
- alias: a.alias
1255
- }))
1256
- });
1257
- return JSON.stringify(result);
1258
- };
1259
- }
1260
- function registerDataTools(registry, context) {
1261
- registry.register(LIST_OBJECTS_TOOL, createListObjectsHandler(context));
1262
- registry.register(DESCRIBE_OBJECT_TOOL, createDescribeObjectHandler(context));
1263
- registry.register(QUERY_RECORDS_TOOL, createQueryRecordsHandler(context));
1264
- registry.register(GET_RECORD_TOOL, createGetRecordHandler(context));
1265
- registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
1266
- }
2194
+ // src/plugin.ts
2195
+ init_data_tools();
2196
+ init_metadata_tools();
1267
2197
 
1268
2198
  // src/agent-runtime.ts
1269
2199
  import { AgentSchema } from "@objectstack/spec/ai";
@@ -1272,6 +2202,27 @@ var AgentRuntime = class {
1272
2202
  this.metadataService = metadataService;
1273
2203
  }
1274
2204
  // ── Public API ────────────────────────────────────────────────
2205
+ /**
2206
+ * List all active agents registered in the metadata service.
2207
+ *
2208
+ * Returns a summary for each agent (name, label, role) suitable
2209
+ * for populating an agent selector dropdown in the UI.
2210
+ */
2211
+ async listAgents() {
2212
+ const rawItems = await this.metadataService.list("agent");
2213
+ const agents = [];
2214
+ for (const raw of rawItems) {
2215
+ const result = AgentSchema.safeParse(raw);
2216
+ if (result.success && result.data.active) {
2217
+ agents.push({
2218
+ name: result.data.name,
2219
+ label: result.data.label,
2220
+ role: result.data.role
2221
+ });
2222
+ }
2223
+ }
2224
+ return agents;
2225
+ }
1275
2226
  /**
1276
2227
  * Load and validate an agent definition by name.
1277
2228
  *
@@ -1400,15 +2351,233 @@ Guidelines:
1400
2351
  }
1401
2352
  };
1402
2353
 
2354
+ // src/agents/metadata-assistant-agent.ts
2355
+ var METADATA_ASSISTANT_AGENT = {
2356
+ name: "metadata_assistant",
2357
+ label: "Metadata Assistant",
2358
+ role: "Schema Architect",
2359
+ instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
2360
+
2361
+ Capabilities:
2362
+ - Create new data objects (tables) with fields
2363
+ - Add fields (columns) to existing objects
2364
+ - Modify field properties (label, type, required, default value)
2365
+ - Delete fields from objects
2366
+ - List all registered metadata objects and their schemas
2367
+ - Describe the full schema of a specific object
2368
+
2369
+ Guidelines:
2370
+ 1. Before creating a new object, use list_objects to check if a similar one already exists.
2371
+ 2. Before modifying or deleting fields, use describe_object to understand the current schema.
2372
+ 3. Always use snake_case for object names and field names (e.g. project_task, due_date).
2373
+ 4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
2374
+ 5. When creating objects, propose a reasonable set of initial fields based on the entity type.
2375
+ 6. Explain what changes you are about to make before executing them.
2376
+ 7. After making changes, confirm the result by describing the updated schema.
2377
+ 8. For destructive operations (deleting fields), always warn the user about potential data loss.
2378
+ 9. Always answer in the same language the user is using.
2379
+ 10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
2380
+ model: {
2381
+ provider: "openai",
2382
+ model: "gpt-4",
2383
+ temperature: 0.2,
2384
+ maxTokens: 4096
2385
+ },
2386
+ tools: [
2387
+ { type: "action", name: "create_object", description: "Create a new data object (table)" },
2388
+ { type: "action", name: "add_field", description: "Add a field to an existing object" },
2389
+ { type: "action", name: "modify_field", description: "Modify an existing field definition" },
2390
+ { type: "action", name: "delete_field", description: "Delete a field from an object" },
2391
+ { type: "query", name: "list_objects", description: "List all data objects" },
2392
+ { type: "query", name: "describe_object", description: "Describe an object schema" }
2393
+ ],
2394
+ active: true,
2395
+ visibility: "global",
2396
+ guardrails: {
2397
+ maxTokensPerInvocation: 8192,
2398
+ maxExecutionTimeSec: 60,
2399
+ blockedTopics: ["drop_database", "raw_sql", "system_tables"]
2400
+ },
2401
+ planning: {
2402
+ strategy: "react",
2403
+ maxIterations: 10,
2404
+ allowReplan: true
2405
+ },
2406
+ memory: {
2407
+ shortTerm: {
2408
+ maxMessages: 30,
2409
+ maxTokens: 8192
2410
+ }
2411
+ }
2412
+ };
2413
+
2414
+ // src/adapters/vercel-adapter.ts
2415
+ import { generateText, streamText, tool as vercelTool, jsonSchema } from "ai";
2416
+ function buildVercelOptions(options) {
2417
+ if (!options) return {};
2418
+ const opts = {};
2419
+ if (options.temperature != null) opts.temperature = options.temperature;
2420
+ if (options.maxTokens != null) opts.maxTokens = options.maxTokens;
2421
+ if (options.stop?.length) opts.stopSequences = options.stop;
2422
+ if (options.tools?.length) {
2423
+ const tools = {};
2424
+ for (const t of options.tools) {
2425
+ tools[t.name] = vercelTool({
2426
+ description: t.description,
2427
+ inputSchema: jsonSchema(t.parameters)
2428
+ });
2429
+ }
2430
+ opts.tools = tools;
2431
+ }
2432
+ if (options.toolChoice != null) {
2433
+ opts.toolChoice = options.toolChoice;
2434
+ }
2435
+ return opts;
2436
+ }
2437
+ var VercelLLMAdapter = class {
2438
+ constructor(config) {
2439
+ this.name = "vercel";
2440
+ this.model = config.model;
2441
+ }
2442
+ async chat(messages, options) {
2443
+ const result = await generateText({
2444
+ model: this.model,
2445
+ messages,
2446
+ ...buildVercelOptions(options)
2447
+ });
2448
+ return {
2449
+ content: result.text,
2450
+ model: result.response?.modelId,
2451
+ toolCalls: result.toolCalls?.length ? result.toolCalls : void 0,
2452
+ usage: result.usage ? {
2453
+ promptTokens: result.usage.inputTokens ?? 0,
2454
+ completionTokens: result.usage.outputTokens ?? 0,
2455
+ totalTokens: result.usage.totalTokens ?? 0
2456
+ } : void 0
2457
+ };
2458
+ }
2459
+ async complete(prompt, options) {
2460
+ const result = await generateText({
2461
+ model: this.model,
2462
+ prompt,
2463
+ ...buildVercelOptions(options)
2464
+ });
2465
+ return {
2466
+ content: result.text,
2467
+ model: result.response?.modelId,
2468
+ usage: result.usage ? {
2469
+ promptTokens: result.usage.inputTokens ?? 0,
2470
+ completionTokens: result.usage.outputTokens ?? 0,
2471
+ totalTokens: result.usage.totalTokens ?? 0
2472
+ } : void 0
2473
+ };
2474
+ }
2475
+ async *streamChat(messages, options) {
2476
+ const result = streamText({
2477
+ model: this.model,
2478
+ messages,
2479
+ ...buildVercelOptions(options)
2480
+ });
2481
+ for await (const part of result.fullStream) {
2482
+ yield part;
2483
+ }
2484
+ }
2485
+ async embed(_input) {
2486
+ throw new Error(
2487
+ "[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
2488
+ );
2489
+ }
2490
+ async listModels() {
2491
+ return [];
2492
+ }
2493
+ };
2494
+
1403
2495
  // src/plugin.ts
1404
2496
  var AIServicePlugin = class {
1405
2497
  constructor(options = {}) {
1406
2498
  this.name = "com.objectstack.service-ai";
1407
2499
  this.version = "1.0.0";
1408
2500
  this.type = "standard";
1409
- this.dependencies = [];
2501
+ this.dependencies = ["com.objectstack.engine.objectql"];
1410
2502
  this.options = options;
1411
2503
  }
2504
+ /**
2505
+ * Auto-detect LLM provider from environment variables.
2506
+ *
2507
+ * Priority order:
2508
+ * 1. AI_GATEWAY_MODEL → Vercel AI Gateway
2509
+ * 2. OPENAI_API_KEY → OpenAI
2510
+ * 3. ANTHROPIC_API_KEY → Anthropic
2511
+ * 4. GOOGLE_GENERATIVE_AI_API_KEY → Google
2512
+ * 5. Fallback → MemoryLLMAdapter
2513
+ *
2514
+ * Returns the adapter and a description for logging.
2515
+ */
2516
+ async detectAdapter(ctx) {
2517
+ const gatewayModel = process.env.AI_GATEWAY_MODEL;
2518
+ if (gatewayModel) {
2519
+ try {
2520
+ const gatewayPkg = "@ai-sdk/gateway";
2521
+ const { gateway } = await import(
2522
+ /* webpackIgnore: true */
2523
+ gatewayPkg
2524
+ );
2525
+ const adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) });
2526
+ return { adapter, description: `Vercel AI Gateway (model: ${gatewayModel})` };
2527
+ } catch (err) {
2528
+ ctx.logger.warn(
2529
+ `[AI] Failed to load @ai-sdk/gateway for AI_GATEWAY_MODEL=${gatewayModel}, trying next provider`,
2530
+ err instanceof Error ? { error: err.message } : void 0
2531
+ );
2532
+ }
2533
+ }
2534
+ const providerConfigs = [
2535
+ {
2536
+ envKey: "OPENAI_API_KEY",
2537
+ pkg: "@ai-sdk/openai",
2538
+ factory: "openai",
2539
+ defaultModel: "gpt-4o",
2540
+ displayName: "OpenAI"
2541
+ },
2542
+ {
2543
+ envKey: "ANTHROPIC_API_KEY",
2544
+ pkg: "@ai-sdk/anthropic",
2545
+ factory: "anthropic",
2546
+ defaultModel: "claude-sonnet-4-20250514",
2547
+ displayName: "Anthropic"
2548
+ },
2549
+ {
2550
+ envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
2551
+ pkg: "@ai-sdk/google",
2552
+ factory: "google",
2553
+ defaultModel: "gemini-2.0-flash",
2554
+ displayName: "Google"
2555
+ }
2556
+ ];
2557
+ for (const { envKey, pkg, factory, defaultModel, displayName } of providerConfigs) {
2558
+ if (process.env[envKey]) {
2559
+ try {
2560
+ const mod = await import(
2561
+ /* webpackIgnore: true */
2562
+ pkg
2563
+ );
2564
+ const createModel = mod[factory] ?? mod.default;
2565
+ if (typeof createModel === "function") {
2566
+ const modelId = process.env.AI_MODEL ?? defaultModel;
2567
+ const adapter = new VercelLLMAdapter({ model: createModel(modelId) });
2568
+ return { adapter, description: `${displayName} (model: ${modelId})` };
2569
+ }
2570
+ } catch (err) {
2571
+ ctx.logger.warn(
2572
+ `[AI] Failed to load ${pkg} for ${envKey}, trying next provider`,
2573
+ err instanceof Error ? { error: err.message } : void 0
2574
+ );
2575
+ }
2576
+ }
2577
+ }
2578
+ ctx.logger.warn("[AI] No LLM provider configured via environment variables. Falling back to MemoryLLMAdapter (echo mode). Set AI_GATEWAY_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY to use a real LLM.");
2579
+ return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode - for testing only)" };
2580
+ }
1412
2581
  async init(ctx) {
1413
2582
  let hasExisting = false;
1414
2583
  try {
@@ -1430,8 +2599,19 @@ var AIServicePlugin = class {
1430
2599
  } catch {
1431
2600
  }
1432
2601
  }
2602
+ let adapter;
2603
+ let adapterDescription;
2604
+ if (this.options.adapter) {
2605
+ adapter = this.options.adapter;
2606
+ adapterDescription = `${adapter.name} (explicitly configured)`;
2607
+ } else {
2608
+ const detected = await this.detectAdapter(ctx);
2609
+ adapter = detected.adapter;
2610
+ adapterDescription = detected.description;
2611
+ }
2612
+ ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
1433
2613
  const config = {
1434
- adapter: this.options.adapter,
2614
+ adapter,
1435
2615
  logger: ctx.logger,
1436
2616
  conversationService
1437
2617
  };
@@ -1441,7 +2621,7 @@ var AIServicePlugin = class {
1441
2621
  } else {
1442
2622
  ctx.registerService("ai", this.service);
1443
2623
  }
1444
- ctx.registerService("app.com.objectstack.service-ai", {
2624
+ ctx.getService("manifest").register({
1445
2625
  id: "com.objectstack.service-ai",
1446
2626
  name: "AI Service",
1447
2627
  version: "1.0.0",
@@ -1454,40 +2634,113 @@ var AIServicePlugin = class {
1454
2634
  ctx.logger.debug("[AI] Before chat", { messages });
1455
2635
  });
1456
2636
  }
2637
+ try {
2638
+ const setupNav = ctx.getService("setupNav");
2639
+ if (setupNav) {
2640
+ setupNav.contribute({
2641
+ areaId: "area_ai",
2642
+ items: [
2643
+ { id: "nav_ai_conversations", type: "object", label: { key: "setup.nav.ai_conversations", defaultValue: "Conversations" }, objectName: "conversations", icon: "message-square", order: 10 },
2644
+ { id: "nav_ai_messages", type: "object", label: { key: "setup.nav.ai_messages", defaultValue: "Messages" }, objectName: "messages", icon: "messages-square", order: 20 }
2645
+ ]
2646
+ });
2647
+ ctx.logger.info("[AI] Navigation items contributed to Setup App");
2648
+ }
2649
+ } catch {
2650
+ }
1457
2651
  ctx.logger.info("[AI] Service initialized");
1458
2652
  }
1459
2653
  async start(ctx) {
1460
2654
  if (!this.service) return;
2655
+ let metadataService;
2656
+ try {
2657
+ metadataService = ctx.getService("metadata");
2658
+ console.log("[AI Plugin] Retrieved metadata service:", !!metadataService, "has getRegisteredTypes:", typeof metadataService?.getRegisteredTypes);
2659
+ } catch (e) {
2660
+ console.log("[AI] Metadata service not available:", e.message);
2661
+ ctx.logger.debug("[AI] Metadata service not available");
2662
+ }
1461
2663
  try {
1462
2664
  const dataEngine = ctx.getService("data");
1463
- const metadataService = ctx.getService("metadata");
1464
- if (dataEngine && metadataService) {
1465
- registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
2665
+ if (dataEngine) {
2666
+ registerDataTools(this.service.toolRegistry, { dataEngine });
1466
2667
  ctx.logger.info("[AI] Built-in data tools registered");
1467
- const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", DATA_CHAT_AGENT.name) : false;
1468
- if (!agentExists) {
1469
- await metadataService.register("agent", DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
1470
- ctx.logger.info("[AI] data_chat agent registered");
1471
- } else {
1472
- ctx.logger.debug("[AI] data_chat agent already exists, skipping auto-registration");
2668
+ if (metadataService) {
2669
+ const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
2670
+ for (const toolDef of DATA_TOOL_DEFINITIONS2) {
2671
+ const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
2672
+ if (!toolExists) {
2673
+ await metadataService.register("tool", toolDef.name, toolDef);
2674
+ }
2675
+ }
2676
+ ctx.logger.info(`[AI] ${DATA_TOOL_DEFINITIONS2.length} data tools registered as metadata`);
2677
+ }
2678
+ if (metadataService) {
2679
+ try {
2680
+ const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", DATA_CHAT_AGENT.name) : false;
2681
+ if (!agentExists) {
2682
+ await metadataService.register("agent", DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
2683
+ console.log("[AI] Registered data_chat agent to metadataService");
2684
+ ctx.logger.info("[AI] data_chat agent registered");
2685
+ } else {
2686
+ console.log("[AI] data_chat agent already exists, skipping");
2687
+ ctx.logger.debug("[AI] data_chat agent already exists, skipping auto-registration");
2688
+ }
2689
+ } catch (err) {
2690
+ ctx.logger.warn("[AI] Failed to register data_chat agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
2691
+ }
1473
2692
  }
1474
2693
  }
1475
2694
  } catch {
1476
- ctx.logger.debug("[AI] Data engine or metadata service not available, skipping data tools");
2695
+ ctx.logger.debug("[AI] Data engine not available, skipping data tools");
2696
+ }
2697
+ if (metadataService) {
2698
+ try {
2699
+ registerMetadataTools(this.service.toolRegistry, { metadataService });
2700
+ ctx.logger.info("[AI] Built-in metadata tools registered");
2701
+ const { METADATA_TOOL_DEFINITIONS: METADATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_metadata_tools(), metadata_tools_exports));
2702
+ for (const toolDef of METADATA_TOOL_DEFINITIONS2) {
2703
+ const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
2704
+ if (!toolExists) {
2705
+ await metadataService.register("tool", toolDef.name, toolDef);
2706
+ }
2707
+ }
2708
+ ctx.logger.info(`[AI] ${METADATA_TOOL_DEFINITIONS2.length} metadata tools registered as metadata`);
2709
+ try {
2710
+ const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
2711
+ if (!agentExists) {
2712
+ await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
2713
+ console.log("[AI] Registered metadata_assistant agent to metadataService");
2714
+ ctx.logger.info("[AI] metadata_assistant agent registered");
2715
+ } else {
2716
+ console.log("[AI] metadata_assistant agent already exists, skipping");
2717
+ ctx.logger.debug("[AI] metadata_assistant agent already exists, skipping auto-registration");
2718
+ }
2719
+ } catch (err) {
2720
+ ctx.logger.warn("[AI] Failed to register metadata_assistant agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
2721
+ }
2722
+ } catch (err) {
2723
+ ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
2724
+ }
1477
2725
  }
1478
2726
  await ctx.trigger("ai:ready", this.service);
1479
2727
  const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
1480
- try {
1481
- const metadataService = ctx.getService("metadata");
1482
- if (metadataService) {
1483
- const agentRuntime = new AgentRuntime(metadataService);
1484
- const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
1485
- routes.push(...agentRoutes);
1486
- }
1487
- } catch {
2728
+ const toolRoutes = buildToolRoutes(this.service, ctx.logger);
2729
+ routes.push(...toolRoutes);
2730
+ ctx.logger.info(`[AI] Tool routes registered (${toolRoutes.length} routes)`);
2731
+ if (metadataService) {
2732
+ const agentRuntime = new AgentRuntime(metadataService);
2733
+ const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
2734
+ routes.push(...agentRoutes);
2735
+ ctx.logger.info(`[AI] Agent routes registered (${agentRoutes.length} routes)`);
2736
+ } else {
1488
2737
  ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
1489
2738
  }
1490
2739
  await ctx.trigger("ai:routes", routes);
2740
+ const kernel = ctx.getKernel();
2741
+ if (kernel) {
2742
+ kernel.__aiRoutes = routes;
2743
+ }
1491
2744
  ctx.logger.info(
1492
2745
  `[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
1493
2746
  );
@@ -1496,6 +2749,11 @@ var AIServicePlugin = class {
1496
2749
  this.service = void 0;
1497
2750
  }
1498
2751
  };
2752
+
2753
+ // src/index.ts
2754
+ init_data_tools();
2755
+ init_metadata_tools();
2756
+ init_metadata_tools();
1499
2757
  export {
1500
2758
  AIService,
1501
2759
  AIServicePlugin,
@@ -1505,11 +2763,24 @@ export {
1505
2763
  DATA_CHAT_AGENT,
1506
2764
  DATA_TOOL_DEFINITIONS,
1507
2765
  InMemoryConversationService,
2766
+ METADATA_ASSISTANT_AGENT,
2767
+ METADATA_TOOL_DEFINITIONS,
1508
2768
  MemoryLLMAdapter,
1509
2769
  ObjectQLConversationService,
1510
2770
  ToolRegistry,
2771
+ VercelLLMAdapter,
2772
+ addFieldTool,
1511
2773
  buildAIRoutes,
1512
2774
  buildAgentRoutes,
1513
- registerDataTools
2775
+ buildToolRoutes,
2776
+ createObjectTool,
2777
+ deleteFieldTool,
2778
+ describeObjectTool,
2779
+ encodeStreamPart,
2780
+ encodeVercelDataStream,
2781
+ listObjectsTool,
2782
+ modifyFieldTool,
2783
+ registerDataTools,
2784
+ registerMetadataTools
1514
2785
  };
1515
2786
  //# sourceMappingURL=index.js.map