@objectstack/service-ai 4.0.1 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +9 -0
  3. package/dist/index.cjs +1120 -66
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +316 -78
  6. package/dist/index.d.ts +316 -78
  7. package/dist/index.js +1105 -63
  8. package/dist/index.js.map +1 -1
  9. package/package.json +26 -4
  10. package/src/__tests__/ai-service.test.ts +248 -27
  11. package/src/__tests__/auth-and-toolcalling.test.ts +30 -28
  12. package/src/__tests__/chatbot-features.test.ts +229 -82
  13. package/src/__tests__/metadata-tools.test.ts +964 -0
  14. package/src/__tests__/objectql-conversation-service.test.ts +34 -16
  15. package/src/__tests__/vercel-stream-encoder.test.ts +263 -0
  16. package/src/adapters/index.ts +2 -0
  17. package/src/adapters/memory-adapter.ts +17 -9
  18. package/src/adapters/vercel-adapter.ts +148 -0
  19. package/src/agent-runtime.ts +27 -3
  20. package/src/agents/index.ts +1 -0
  21. package/src/agents/metadata-assistant-agent.ts +87 -0
  22. package/src/ai-service.ts +68 -36
  23. package/src/conversation/in-memory-conversation-service.ts +2 -2
  24. package/src/conversation/objectql-conversation-service.ts +67 -18
  25. package/src/index.ts +21 -2
  26. package/src/plugin.ts +166 -9
  27. package/src/routes/agent-routes.ts +26 -3
  28. package/src/routes/ai-routes.ts +156 -13
  29. package/src/stream/index.ts +3 -0
  30. package/src/stream/vercel-stream-encoder.ts +129 -0
  31. package/src/tools/add-field.tool.ts +70 -0
  32. package/src/tools/create-object.tool.ts +66 -0
  33. package/src/tools/delete-field.tool.ts +38 -0
  34. package/src/tools/describe-metadata-object.tool.ts +32 -0
  35. package/src/tools/index.ts +12 -1
  36. package/src/tools/list-metadata-objects.tool.ts +34 -0
  37. package/src/tools/metadata-tools.ts +430 -0
  38. package/src/tools/modify-field.tool.ts +44 -0
  39. package/src/tools/tool-registry.ts +32 -9
package/dist/index.js CHANGED
@@ -8,7 +8,9 @@ var MemoryLLMAdapter = class {
8
8
  }
9
9
  async chat(messages, options) {
10
10
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
11
- const content = lastUserMessage ? `[memory] ${lastUserMessage.content}` : "[memory] (no user message)";
11
+ const userContent = lastUserMessage?.content;
12
+ const text = typeof userContent === "string" ? userContent : "(complex content)";
13
+ const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
12
14
  return {
13
15
  content,
14
16
  model: options?.model ?? "memory",
@@ -26,10 +28,15 @@ var MemoryLLMAdapter = class {
26
28
  const result = await this.chat(messages);
27
29
  const words = result.content.split(" ");
28
30
  for (let i = 0; i < words.length; i++) {
29
- const textDelta = i === 0 ? words[i] : ` ${words[i]}`;
30
- yield { type: "text-delta", textDelta };
31
+ const wordText = i === 0 ? words[i] : ` ${words[i]}`;
32
+ yield { type: "text-delta", id: `delta_${i}`, text: wordText };
31
33
  }
32
- yield { type: "finish", result };
34
+ yield {
35
+ type: "finish",
36
+ finishReason: "stop",
37
+ totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
38
+ rawFinishReason: "stop"
39
+ };
33
40
  }
34
41
  async embed(input) {
35
42
  const texts = Array.isArray(input) ? input : [input];
@@ -92,21 +99,34 @@ var ToolRegistry = class {
92
99
  * Execute a tool call and return the result.
93
100
  */
94
101
  async execute(toolCall) {
95
- const handler = this.handlers.get(toolCall.name);
102
+ const handler = this.handlers.get(toolCall.toolName);
96
103
  if (!handler) {
97
104
  return {
98
- toolCallId: toolCall.id,
99
- content: `Tool "${toolCall.name}" is not registered`,
105
+ type: "tool-result",
106
+ toolCallId: toolCall.toolCallId,
107
+ toolName: toolCall.toolName,
108
+ output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
100
109
  isError: true
101
110
  };
102
111
  }
103
112
  try {
104
- const args = JSON.parse(toolCall.arguments);
113
+ const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
105
114
  const content = await handler(args);
106
- return { toolCallId: toolCall.id, content };
115
+ return {
116
+ type: "tool-result",
117
+ toolCallId: toolCall.toolCallId,
118
+ toolName: toolCall.toolName,
119
+ output: { type: "text", value: content }
120
+ };
107
121
  } catch (err) {
108
122
  const message = err instanceof Error ? err.message : String(err);
109
- return { toolCallId: toolCall.id, content: message, isError: true };
123
+ return {
124
+ type: "tool-result",
125
+ toolCallId: toolCall.toolCallId,
126
+ toolName: toolCall.toolName,
127
+ output: { type: "text", value: message },
128
+ isError: true
129
+ };
110
130
  }
111
131
  }
112
132
  /**
@@ -192,6 +212,17 @@ var InMemoryConversationService = class {
192
212
  };
193
213
 
194
214
  // src/ai-service.ts
215
+ function textDeltaPart(id, text) {
216
+ return { type: "text-delta", id, text };
217
+ }
218
+ function finishPart(result) {
219
+ return {
220
+ type: "finish",
221
+ finishReason: "stop",
222
+ totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
223
+ rawFinishReason: "stop"
224
+ };
225
+ }
195
226
  var _AIService = class _AIService {
196
227
  constructor(config = {}) {
197
228
  this.adapter = config.adapter ?? new MemoryLLMAdapter();
@@ -219,8 +250,8 @@ var _AIService = class _AIService {
219
250
  this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
220
251
  if (!this.adapter.streamChat) {
221
252
  const result = await this.adapter.chat(messages, options);
222
- yield { type: "text-delta", textDelta: result.content };
223
- yield { type: "finish", result };
253
+ yield textDeltaPart("fallback", result.content);
254
+ yield finishPart(result);
224
255
  return;
225
256
  }
226
257
  yield* this.adapter.streamChat(messages, options);
@@ -237,6 +268,10 @@ var _AIService = class _AIService {
237
268
  }
238
269
  return this.adapter.listModels();
239
270
  }
271
+ /** Extract the text value from a ToolExecutionResult's output. */
272
+ static extractOutputText(tr) {
273
+ return tr.output && typeof tr.output === "object" && "value" in tr.output ? String(tr.output.value) : "unknown error";
274
+ }
240
275
  /**
241
276
  * Chat with automatic tool call resolution.
242
277
  *
@@ -277,23 +312,26 @@ var _AIService = class _AIService {
277
312
  }
278
313
  this.logger.debug("[AI] chatWithTools tool calls", {
279
314
  iteration,
280
- calls: result.toolCalls.map((tc) => tc.name)
315
+ calls: result.toolCalls.map((tc) => tc.toolName)
281
316
  });
317
+ const assistantContent = [];
318
+ if (result.content) assistantContent.push({ type: "text", text: result.content });
319
+ assistantContent.push(...result.toolCalls);
282
320
  conversation.push({
283
321
  role: "assistant",
284
- content: result.content ?? "",
285
- toolCalls: result.toolCalls
322
+ content: assistantContent
286
323
  });
287
324
  const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
288
325
  for (const tr of toolResults) {
289
326
  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 };
327
+ const matchedCall = result.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
328
+ const toolName = matchedCall?.toolName ?? "unknown";
329
+ const errorText = _AIService.extractOutputText(tr);
330
+ const errorEntry = { iteration, toolName, error: errorText };
293
331
  toolErrors.push(errorEntry);
294
332
  this.logger.warn("[AI] chatWithTools tool error", errorEntry);
295
333
  if (onToolError && matchedCall) {
296
- const action = onToolError(matchedCall, tr.content);
334
+ const action = onToolError(matchedCall, errorText);
297
335
  if (action === "abort") {
298
336
  abortedByCallback = true;
299
337
  }
@@ -301,8 +339,7 @@ var _AIService = class _AIService {
301
339
  }
302
340
  conversation.push({
303
341
  role: "tool",
304
- content: tr.content,
305
- toolCallId: tr.toolCallId
342
+ content: [tr]
306
343
  });
307
344
  }
308
345
  if (abortedByCallback) {
@@ -348,24 +385,27 @@ var _AIService = class _AIService {
348
385
  for (let iteration = 0; iteration < maxIterations; iteration++) {
349
386
  const result2 = await this.adapter.chat(conversation, chatOptions);
350
387
  if (!result2.toolCalls || result2.toolCalls.length === 0) {
351
- yield { type: "text-delta", textDelta: result2.content };
352
- yield { type: "finish", result: result2 };
388
+ yield textDeltaPart("stream", result2.content);
389
+ yield finishPart(result2);
353
390
  return;
354
391
  }
355
392
  for (const tc of result2.toolCalls) {
356
- yield { type: "tool-call", toolCall: tc };
393
+ yield { type: "tool-call", toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input };
357
394
  }
395
+ const assistantContent = [];
396
+ if (result2.content) assistantContent.push({ type: "text", text: result2.content });
397
+ assistantContent.push(...result2.toolCalls);
358
398
  conversation.push({
359
399
  role: "assistant",
360
- content: result2.content ?? "",
361
- toolCalls: result2.toolCalls
400
+ content: assistantContent
362
401
  });
363
402
  const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
364
403
  for (const tr of toolResults) {
365
404
  if (tr.isError && onToolError) {
366
- const matchedCall = result2.toolCalls.find((tc) => tc.id === tr.toolCallId);
405
+ const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
367
406
  if (matchedCall) {
368
- const action = onToolError(matchedCall, tr.content);
407
+ const errorText = _AIService.extractOutputText(tr);
408
+ const action = onToolError(matchedCall, errorText);
369
409
  if (action === "abort") {
370
410
  abortedByCallback = true;
371
411
  }
@@ -373,8 +413,7 @@ var _AIService = class _AIService {
373
413
  }
374
414
  conversation.push({
375
415
  role: "tool",
376
- content: tr.content,
377
- toolCallId: tr.toolCallId
416
+ content: [tr]
378
417
  });
379
418
  }
380
419
  if (abortedByCallback) {
@@ -388,8 +427,8 @@ var _AIService = class _AIService {
388
427
  }
389
428
  const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
390
429
  const result = await this.adapter.chat(conversation, finalOptions);
391
- yield { type: "text-delta", textDelta: result.content };
392
- yield { type: "finish", result };
430
+ yield textDeltaPart("stream", result.content);
431
+ yield finishPart(result);
393
432
  }
394
433
  };
395
434
  // ── Tool Call Loop ────────────────────────────────────────────
@@ -397,8 +436,99 @@ var _AIService = class _AIService {
397
436
  _AIService.DEFAULT_MAX_ITERATIONS = 10;
398
437
  var AIService = _AIService;
399
438
 
439
+ // src/stream/vercel-stream-encoder.ts
440
+ function sse(data) {
441
+ return `data: ${JSON.stringify(data)}
442
+
443
+ `;
444
+ }
445
+ function encodeStreamPart(part) {
446
+ switch (part.type) {
447
+ case "text-delta":
448
+ return sse({ type: "text-delta", id: "0", delta: part.text });
449
+ case "tool-input-start":
450
+ return sse({
451
+ type: "tool-input-start",
452
+ toolCallId: part.id,
453
+ toolName: part.toolName
454
+ });
455
+ case "tool-input-delta":
456
+ return sse({
457
+ type: "tool-input-delta",
458
+ toolCallId: part.id,
459
+ inputTextDelta: part.delta
460
+ });
461
+ case "tool-call":
462
+ return sse({
463
+ type: "tool-input-available",
464
+ toolCallId: part.toolCallId,
465
+ toolName: part.toolName,
466
+ input: part.input
467
+ });
468
+ case "tool-result":
469
+ return sse({
470
+ type: "tool-output-available",
471
+ toolCallId: part.toolCallId,
472
+ output: part.output
473
+ });
474
+ case "error":
475
+ return sse({
476
+ type: "error",
477
+ errorText: String(part.error)
478
+ });
479
+ // finish-step and finish are handled by the generator, not here
480
+ default:
481
+ return "";
482
+ }
483
+ }
484
+ async function* encodeVercelDataStream(events) {
485
+ yield sse({ type: "start" });
486
+ yield sse({ type: "start-step" });
487
+ yield sse({ type: "text-start", id: "0" });
488
+ let textOpen = true;
489
+ let finishReason = "stop";
490
+ for await (const part of events) {
491
+ if (part.type === "finish") {
492
+ finishReason = part.finishReason ?? "stop";
493
+ }
494
+ if (part.type === "finish-step" || part.type === "finish") {
495
+ if (textOpen) {
496
+ yield sse({ type: "text-end", id: "0" });
497
+ textOpen = false;
498
+ }
499
+ continue;
500
+ }
501
+ const frame = encodeStreamPart(part);
502
+ if (frame) {
503
+ yield frame;
504
+ }
505
+ }
506
+ if (textOpen) {
507
+ yield sse({ type: "text-end", id: "0" });
508
+ }
509
+ yield sse({ type: "finish-step" });
510
+ yield sse({ type: "finish", finishReason });
511
+ yield "data: [DONE]\n\n";
512
+ }
513
+
400
514
  // src/routes/ai-routes.ts
401
515
  var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
516
+ function normalizeMessage(raw) {
517
+ const role = raw.role;
518
+ if (typeof raw.content === "string") {
519
+ return { role, content: raw.content };
520
+ }
521
+ if (Array.isArray(raw.content)) {
522
+ return { role, content: raw.content };
523
+ }
524
+ if (Array.isArray(raw.parts)) {
525
+ const textParts = raw.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text);
526
+ if (textParts.length > 0) {
527
+ return { role, content: textParts.join("") };
528
+ }
529
+ }
530
+ return { role, content: "" };
531
+ }
402
532
  function validateMessage(raw) {
403
533
  if (typeof raw !== "object" || raw === null) {
404
534
  return "each message must be an object";
@@ -407,22 +537,56 @@ function validateMessage(raw) {
407
537
  if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
408
538
  return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
409
539
  }
410
- if (typeof msg.content !== "string") {
411
- return "message.content must be a string";
540
+ const content = msg.content;
541
+ if (Array.isArray(msg.parts)) {
542
+ return null;
412
543
  }
413
- return null;
544
+ if (typeof content === "string") {
545
+ return null;
546
+ }
547
+ if (Array.isArray(content)) {
548
+ for (const part of content) {
549
+ if (typeof part !== "object" || part === null) {
550
+ return "message.content array elements must be non-null objects";
551
+ }
552
+ const partObj = part;
553
+ if (typeof partObj.type !== "string") {
554
+ return 'each message.content array element must have a string "type" property';
555
+ }
556
+ if (partObj.type === "text" && typeof partObj.text !== "string") {
557
+ return 'message.content elements with type "text" must have a string "text" property';
558
+ }
559
+ }
560
+ return null;
561
+ }
562
+ if (content === null || content === void 0) {
563
+ if (msg.role === "assistant" || msg.role === "tool") {
564
+ return null;
565
+ }
566
+ }
567
+ return "message.content must be a string, an array, or include parts";
414
568
  }
415
569
  function buildAIRoutes(aiService, conversationService, logger) {
416
570
  return [
417
571
  // ── Chat ────────────────────────────────────────────────────
572
+ //
573
+ // Dual-mode endpoint compatible with both the legacy ObjectStack
574
+ // format (`{ messages, options }`) and the Vercel AI SDK useChat
575
+ // flat format (`{ messages, system, model, stream, … }`).
576
+ //
577
+ // Behaviour:
578
+ // • `stream !== false` → Vercel Data Stream Protocol (SSE)
579
+ // • `stream === false` → JSON response (legacy)
580
+ //
418
581
  {
419
582
  method: "POST",
420
583
  path: "/api/v1/ai/chat",
421
- description: "Synchronous chat completion",
584
+ description: "Chat completion (supports Vercel AI Data Stream Protocol)",
422
585
  auth: true,
423
586
  permissions: ["ai:chat"],
424
587
  handler: async (req) => {
425
- const { messages, options } = req.body ?? {};
588
+ const body = req.body ?? {};
589
+ const messages = body.messages;
426
590
  if (!Array.isArray(messages) || messages.length === 0) {
427
591
  return { status: 400, body: { error: "messages array is required" } };
428
592
  }
@@ -430,8 +594,49 @@ function buildAIRoutes(aiService, conversationService, logger) {
430
594
  const err = validateMessage(msg);
431
595
  if (err) return { status: 400, body: { error: err } };
432
596
  }
597
+ const nested = body.options ?? {};
598
+ const resolvedOptions = {
599
+ ...nested,
600
+ ...body.model != null && { model: body.model },
601
+ ...body.temperature != null && { temperature: body.temperature },
602
+ ...body.maxTokens != null && { maxTokens: body.maxTokens }
603
+ };
604
+ const rawSystemPrompt = body.system ?? body.systemPrompt;
605
+ if (rawSystemPrompt != null && typeof rawSystemPrompt !== "string") {
606
+ return { status: 400, body: { error: "system/systemPrompt must be a string" } };
607
+ }
608
+ const systemPrompt = rawSystemPrompt;
609
+ const finalMessages = [
610
+ ...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
611
+ ...messages.map((m) => normalizeMessage(m))
612
+ ];
613
+ const wantStream = body.stream !== false;
614
+ if (wantStream) {
615
+ try {
616
+ if (!aiService.streamChatWithTools) {
617
+ return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
618
+ }
619
+ const events = aiService.streamChatWithTools(finalMessages, resolvedOptions);
620
+ return {
621
+ status: 200,
622
+ stream: true,
623
+ vercelDataStream: true,
624
+ contentType: "text/event-stream",
625
+ headers: {
626
+ "Content-Type": "text/event-stream",
627
+ "Cache-Control": "no-cache",
628
+ "Connection": "keep-alive",
629
+ "x-vercel-ai-ui-message-stream": "v1"
630
+ },
631
+ events: encodeVercelDataStream(events)
632
+ };
633
+ } catch (err) {
634
+ logger.error("[AI Route] /chat stream error", err instanceof Error ? err : void 0);
635
+ return { status: 500, body: { error: "Internal AI service error" } };
636
+ }
637
+ }
433
638
  try {
434
- const result = await aiService.chat(messages, options);
639
+ const result = await aiService.chatWithTools(finalMessages, resolvedOptions);
435
640
  return { status: 200, body: result };
436
641
  } catch (err) {
437
642
  logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
@@ -459,7 +664,7 @@ function buildAIRoutes(aiService, conversationService, logger) {
459
664
  if (!aiService.streamChat) {
460
665
  return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
461
666
  }
462
- const events = aiService.streamChat(messages, options);
667
+ const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
463
668
  return { status: 200, stream: true, events };
464
669
  } catch (err) {
465
670
  logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
@@ -644,6 +849,27 @@ function validateAgentMessage(raw) {
644
849
  }
645
850
  function buildAgentRoutes(aiService, agentRuntime, logger) {
646
851
  return [
852
+ // ── List active agents ──────────────────────────────────────
853
+ {
854
+ method: "GET",
855
+ path: "/api/v1/ai/agents",
856
+ description: "List all active AI agents",
857
+ auth: true,
858
+ permissions: ["ai:chat"],
859
+ handler: async () => {
860
+ try {
861
+ const agents = await agentRuntime.listAgents();
862
+ return { status: 200, body: { agents } };
863
+ } catch (err) {
864
+ logger.error(
865
+ "[AI Route] /agents list error",
866
+ err instanceof Error ? err : void 0
867
+ );
868
+ return { status: 500, body: { error: "Internal AI service error" } };
869
+ }
870
+ }
871
+ },
872
+ // ── Chat with a specific agent ──────────────────────────────
647
873
  {
648
874
  method: "POST",
649
875
  path: "/api/v1/ai/agents/:agentName/chat",
@@ -803,13 +1029,35 @@ var ObjectQLConversationService = class {
803
1029
  }
804
1030
  const now = (/* @__PURE__ */ new Date()).toISOString();
805
1031
  const msgId = `msg_${randomUUID()}`;
1032
+ let contentStr;
1033
+ let toolCallsJson = null;
1034
+ let toolCallId = null;
1035
+ if (message.role === "system" || message.role === "user") {
1036
+ contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
1037
+ } else if (message.role === "assistant") {
1038
+ if (typeof message.content === "string") {
1039
+ contentStr = message.content;
1040
+ } else {
1041
+ const parts = message.content;
1042
+ const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
1043
+ const toolCalls = parts.filter((p) => p.type === "tool-call");
1044
+ contentStr = textParts.join("");
1045
+ if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
1046
+ }
1047
+ } else if (message.role === "tool") {
1048
+ contentStr = JSON.stringify(message.content);
1049
+ const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
1050
+ if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
1051
+ } else {
1052
+ contentStr = "";
1053
+ }
806
1054
  await this.engine.insert(MESSAGES_OBJECT, {
807
1055
  id: msgId,
808
1056
  conversation_id: conversationId,
809
1057
  role: message.role,
810
- content: message.content,
811
- tool_calls: message.toolCalls ? JSON.stringify(message.toolCalls) : null,
812
- tool_call_id: message.toolCallId ?? null,
1058
+ content: contentStr,
1059
+ tool_calls: toolCallsJson,
1060
+ tool_call_id: toolCallId,
813
1061
  created_at: now
814
1062
  });
815
1063
  await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
@@ -854,21 +1102,42 @@ var ObjectQLConversationService = class {
854
1102
  };
855
1103
  }
856
1104
  /**
857
- * Map a database row to an AIMessage.
1105
+ * Map a database row to a ModelMessage.
858
1106
  */
859
1107
  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;
1108
+ switch (row.role) {
1109
+ case "system":
1110
+ return { role: "system", content: row.content };
1111
+ case "user":
1112
+ return { role: "user", content: row.content };
1113
+ case "assistant": {
1114
+ const toolCalls = this.safeParse(row.tool_calls);
1115
+ if (toolCalls && toolCalls.length > 0) {
1116
+ const content = [];
1117
+ if (row.content) content.push({ type: "text", text: row.content });
1118
+ content.push(...toolCalls);
1119
+ return { role: "assistant", content };
1120
+ }
1121
+ return { role: "assistant", content: row.content };
1122
+ }
1123
+ case "tool": {
1124
+ const toolResults = this.safeParse(row.content);
1125
+ if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
1126
+ return { role: "tool", content: toolResults };
1127
+ }
1128
+ return {
1129
+ role: "tool",
1130
+ content: [{
1131
+ type: "tool-result",
1132
+ toolCallId: row.tool_call_id ?? "",
1133
+ toolName: "unknown",
1134
+ output: { type: "text", value: row.content }
1135
+ }]
1136
+ };
1137
+ }
1138
+ default:
1139
+ return { role: "user", content: row.content };
870
1140
  }
871
- return msg;
872
1141
  }
873
1142
  };
874
1143
 
@@ -1265,6 +1534,479 @@ function registerDataTools(registry, context) {
1265
1534
  registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
1266
1535
  }
1267
1536
 
1537
+ // src/tools/create-object.tool.ts
1538
+ import { defineTool } from "@objectstack/spec/ai";
1539
+ var createObjectTool = defineTool({
1540
+ name: "create_object",
1541
+ label: "Create Object",
1542
+ 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.",
1543
+ category: "data",
1544
+ builtIn: true,
1545
+ // NOTE: requiresConfirmation is intentionally false (default) because the
1546
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
1547
+ // executes tool calls immediately without checking this flag. The flag
1548
+ // should only be set once server-side approval gating is implemented to
1549
+ // avoid giving users a false sense of safety.
1550
+ parameters: {
1551
+ type: "object",
1552
+ properties: {
1553
+ name: {
1554
+ type: "string",
1555
+ description: "Machine name for the object (snake_case, e.g. project_task)"
1556
+ },
1557
+ label: {
1558
+ type: "string",
1559
+ description: "Human-readable display name (e.g. Project Task)"
1560
+ },
1561
+ fields: {
1562
+ type: "array",
1563
+ description: "Initial fields to create with the object",
1564
+ items: {
1565
+ type: "object",
1566
+ properties: {
1567
+ name: { type: "string", description: "Field machine name (snake_case)" },
1568
+ label: { type: "string", description: "Field display name" },
1569
+ type: {
1570
+ type: "string",
1571
+ description: "Field data type",
1572
+ enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
1573
+ },
1574
+ required: { type: "boolean", description: "Whether the field is required" }
1575
+ },
1576
+ required: ["name", "type"]
1577
+ }
1578
+ },
1579
+ enableFeatures: {
1580
+ type: "object",
1581
+ description: "Object capability flags",
1582
+ properties: {
1583
+ trackHistory: { type: "boolean" },
1584
+ apiEnabled: { type: "boolean" }
1585
+ }
1586
+ }
1587
+ },
1588
+ required: ["name", "label"],
1589
+ additionalProperties: false
1590
+ }
1591
+ });
1592
+
1593
+ // src/tools/add-field.tool.ts
1594
+ import { defineTool as defineTool2 } from "@objectstack/spec/ai";
1595
+ var addFieldTool = defineTool2({
1596
+ name: "add_field",
1597
+ label: "Add Field",
1598
+ 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.",
1599
+ category: "data",
1600
+ builtIn: true,
1601
+ parameters: {
1602
+ type: "object",
1603
+ properties: {
1604
+ objectName: {
1605
+ type: "string",
1606
+ description: "Target object machine name (snake_case)"
1607
+ },
1608
+ name: {
1609
+ type: "string",
1610
+ description: "Field machine name (snake_case, e.g. due_date)"
1611
+ },
1612
+ label: {
1613
+ type: "string",
1614
+ description: "Human-readable field label (e.g. Due Date)"
1615
+ },
1616
+ type: {
1617
+ type: "string",
1618
+ description: "Field data type",
1619
+ enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
1620
+ },
1621
+ required: {
1622
+ type: "boolean",
1623
+ description: "Whether the field is required"
1624
+ },
1625
+ defaultValue: {
1626
+ description: "Default value for the field"
1627
+ },
1628
+ options: {
1629
+ type: "array",
1630
+ description: "Options for select/picklist fields",
1631
+ items: {
1632
+ type: "object",
1633
+ properties: {
1634
+ label: { type: "string" },
1635
+ value: {
1636
+ type: "string",
1637
+ description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
1638
+ pattern: "^[a-z_][a-z0-9_]*$"
1639
+ }
1640
+ }
1641
+ }
1642
+ },
1643
+ reference: {
1644
+ type: "string",
1645
+ description: "Referenced object name for lookup fields (snake_case, e.g. account)"
1646
+ }
1647
+ },
1648
+ required: ["objectName", "name", "type"],
1649
+ additionalProperties: false
1650
+ }
1651
+ });
1652
+
1653
+ // src/tools/modify-field.tool.ts
1654
+ import { defineTool as defineTool3 } from "@objectstack/spec/ai";
1655
+ var modifyFieldTool = defineTool3({
1656
+ name: "modify_field",
1657
+ label: "Modify Field",
1658
+ 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).",
1659
+ category: "data",
1660
+ builtIn: true,
1661
+ parameters: {
1662
+ type: "object",
1663
+ properties: {
1664
+ objectName: {
1665
+ type: "string",
1666
+ description: "Target object machine name (snake_case)"
1667
+ },
1668
+ fieldName: {
1669
+ type: "string",
1670
+ description: "Existing field machine name to modify (snake_case)"
1671
+ },
1672
+ changes: {
1673
+ type: "object",
1674
+ description: "Field properties to update (partial patch)",
1675
+ properties: {
1676
+ label: { type: "string", description: "New display label" },
1677
+ type: { type: "string", description: "New field type" },
1678
+ required: { type: "boolean", description: "Update required constraint" },
1679
+ defaultValue: { description: "New default value" }
1680
+ }
1681
+ }
1682
+ },
1683
+ required: ["objectName", "fieldName", "changes"],
1684
+ additionalProperties: false
1685
+ }
1686
+ });
1687
+
1688
+ // src/tools/delete-field.tool.ts
1689
+ import { defineTool as defineTool4 } from "@objectstack/spec/ai";
1690
+ var deleteFieldTool = defineTool4({
1691
+ name: "delete_field",
1692
+ label: "Delete Field",
1693
+ 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.",
1694
+ category: "data",
1695
+ builtIn: true,
1696
+ // NOTE: requiresConfirmation is intentionally false (default) because the
1697
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
1698
+ // executes tool calls immediately without checking this flag. The flag
1699
+ // should only be set once server-side approval gating is implemented.
1700
+ parameters: {
1701
+ type: "object",
1702
+ properties: {
1703
+ objectName: {
1704
+ type: "string",
1705
+ description: "Target object machine name (snake_case)"
1706
+ },
1707
+ fieldName: {
1708
+ type: "string",
1709
+ description: "Field machine name to delete (snake_case)"
1710
+ }
1711
+ },
1712
+ required: ["objectName", "fieldName"],
1713
+ additionalProperties: false
1714
+ }
1715
+ });
1716
+
1717
+ // src/tools/list-metadata-objects.tool.ts
1718
+ import { defineTool as defineTool5 } from "@objectstack/spec/ai";
1719
+ var listMetadataObjectsTool = defineTool5({
1720
+ name: "list_metadata_objects",
1721
+ label: "List Metadata Objects",
1722
+ 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.",
1723
+ category: "data",
1724
+ builtIn: true,
1725
+ parameters: {
1726
+ type: "object",
1727
+ properties: {
1728
+ filter: {
1729
+ type: "string",
1730
+ description: "Optional name or label substring to filter objects"
1731
+ },
1732
+ includeFields: {
1733
+ type: "boolean",
1734
+ description: "Whether to include field summaries for each object (default: false)"
1735
+ }
1736
+ },
1737
+ additionalProperties: false
1738
+ }
1739
+ });
1740
+
1741
+ // src/tools/describe-metadata-object.tool.ts
1742
+ import { defineTool as defineTool6 } from "@objectstack/spec/ai";
1743
+ var describeMetadataObjectTool = defineTool6({
1744
+ name: "describe_metadata_object",
1745
+ label: "Describe Metadata Object",
1746
+ 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.",
1747
+ category: "data",
1748
+ builtIn: true,
1749
+ parameters: {
1750
+ type: "object",
1751
+ properties: {
1752
+ objectName: {
1753
+ type: "string",
1754
+ description: "Object machine name to describe (snake_case)"
1755
+ }
1756
+ },
1757
+ required: ["objectName"],
1758
+ additionalProperties: false
1759
+ }
1760
+ });
1761
+
1762
+ // src/tools/metadata-tools.ts
1763
+ var METADATA_TOOL_DEFINITIONS = [
1764
+ createObjectTool,
1765
+ addFieldTool,
1766
+ modifyFieldTool,
1767
+ deleteFieldTool,
1768
+ listMetadataObjectsTool,
1769
+ describeMetadataObjectTool
1770
+ ];
1771
+ var SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
1772
+ function isSnakeCase(value) {
1773
+ return SNAKE_CASE_RE.test(value);
1774
+ }
1775
+ function createCreateObjectHandler(ctx) {
1776
+ return async (args) => {
1777
+ const { name, label, fields, enableFeatures } = args;
1778
+ if (!name || !label) {
1779
+ return JSON.stringify({ error: 'Both "name" and "label" are required' });
1780
+ }
1781
+ if (!isSnakeCase(name)) {
1782
+ return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
1783
+ }
1784
+ const existing = await ctx.metadataService.getObject(name);
1785
+ if (existing) {
1786
+ return JSON.stringify({ error: `Object "${name}" already exists` });
1787
+ }
1788
+ const fieldMap = {};
1789
+ if (fields && Array.isArray(fields)) {
1790
+ const seenNames = /* @__PURE__ */ new Set();
1791
+ for (const f of fields) {
1792
+ if (!f.name) {
1793
+ return JSON.stringify({ error: 'Each field must have a "name" property' });
1794
+ }
1795
+ if (!isSnakeCase(f.name)) {
1796
+ return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
1797
+ }
1798
+ if (seenNames.has(f.name)) {
1799
+ return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
1800
+ }
1801
+ seenNames.add(f.name);
1802
+ fieldMap[f.name] = {
1803
+ type: f.type,
1804
+ ...f.label ? { label: f.label } : {},
1805
+ ...f.required !== void 0 ? { required: f.required } : {}
1806
+ };
1807
+ }
1808
+ }
1809
+ const objectDef = {
1810
+ name,
1811
+ label,
1812
+ ...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
1813
+ ...enableFeatures ? { enable: enableFeatures } : {}
1814
+ };
1815
+ await ctx.metadataService.register("object", name, objectDef);
1816
+ return JSON.stringify({
1817
+ name,
1818
+ label,
1819
+ fieldCount: Object.keys(fieldMap).length
1820
+ });
1821
+ };
1822
+ }
1823
+ function createAddFieldHandler(ctx) {
1824
+ return async (args) => {
1825
+ const { objectName, name, label, type, required, defaultValue, options, reference } = args;
1826
+ if (!objectName || !name || !type) {
1827
+ return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
1828
+ }
1829
+ if (!isSnakeCase(objectName)) {
1830
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1831
+ }
1832
+ if (!isSnakeCase(name)) {
1833
+ return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
1834
+ }
1835
+ if (reference && !isSnakeCase(reference)) {
1836
+ return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
1837
+ }
1838
+ if (options && Array.isArray(options)) {
1839
+ for (const opt of options) {
1840
+ if (opt.value && !isSnakeCase(opt.value)) {
1841
+ return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
1842
+ }
1843
+ }
1844
+ }
1845
+ const objectDef = await ctx.metadataService.getObject(objectName);
1846
+ if (!objectDef) {
1847
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
1848
+ }
1849
+ const def = objectDef;
1850
+ if (def.fields && def.fields[name]) {
1851
+ return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
1852
+ }
1853
+ const fieldDef = {
1854
+ type,
1855
+ ...label ? { label } : {},
1856
+ ...required !== void 0 ? { required } : {},
1857
+ ...defaultValue !== void 0 ? { defaultValue } : {},
1858
+ ...options ? { options } : {},
1859
+ ...reference ? { reference } : {}
1860
+ };
1861
+ const updatedFields = { ...def.fields ?? {}, [name]: fieldDef };
1862
+ await ctx.metadataService.register("object", objectName, {
1863
+ ...def,
1864
+ fields: updatedFields
1865
+ });
1866
+ return JSON.stringify({
1867
+ objectName,
1868
+ fieldName: name,
1869
+ fieldType: type
1870
+ });
1871
+ };
1872
+ }
1873
+ function createModifyFieldHandler(ctx) {
1874
+ return async (args) => {
1875
+ const { objectName, fieldName, changes } = args;
1876
+ if (!objectName || !fieldName || !changes) {
1877
+ return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
1878
+ }
1879
+ if (!isSnakeCase(objectName)) {
1880
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1881
+ }
1882
+ if (!isSnakeCase(fieldName)) {
1883
+ return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
1884
+ }
1885
+ const objectDef = await ctx.metadataService.getObject(objectName);
1886
+ if (!objectDef) {
1887
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
1888
+ }
1889
+ const def = objectDef;
1890
+ if (!def.fields || !def.fields[fieldName]) {
1891
+ return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
1892
+ }
1893
+ const existingField = def.fields[fieldName];
1894
+ const updatedField = { ...existingField, ...changes };
1895
+ const updatedFields = { ...def.fields, [fieldName]: updatedField };
1896
+ await ctx.metadataService.register("object", objectName, {
1897
+ ...def,
1898
+ fields: updatedFields
1899
+ });
1900
+ return JSON.stringify({
1901
+ objectName,
1902
+ fieldName,
1903
+ updatedProperties: Object.keys(changes)
1904
+ });
1905
+ };
1906
+ }
1907
+ function createDeleteFieldHandler(ctx) {
1908
+ return async (args) => {
1909
+ const { objectName, fieldName } = args;
1910
+ if (!objectName || !fieldName) {
1911
+ return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
1912
+ }
1913
+ if (!isSnakeCase(objectName)) {
1914
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1915
+ }
1916
+ if (!isSnakeCase(fieldName)) {
1917
+ return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
1918
+ }
1919
+ const objectDef = await ctx.metadataService.getObject(objectName);
1920
+ if (!objectDef) {
1921
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
1922
+ }
1923
+ const def = objectDef;
1924
+ if (!def.fields || !def.fields[fieldName]) {
1925
+ return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
1926
+ }
1927
+ const { [fieldName]: _removed, ...remainingFields } = def.fields;
1928
+ await ctx.metadataService.register("object", objectName, {
1929
+ ...def,
1930
+ fields: remainingFields
1931
+ });
1932
+ return JSON.stringify({
1933
+ objectName,
1934
+ fieldName,
1935
+ success: true
1936
+ });
1937
+ };
1938
+ }
1939
+ function createListObjectsHandler2(ctx) {
1940
+ return async (args) => {
1941
+ const { filter, includeFields } = args ?? {};
1942
+ const objects = await ctx.metadataService.listObjects();
1943
+ let result = objects.map((o) => {
1944
+ const base = {
1945
+ name: o.name,
1946
+ label: o.label ?? o.name,
1947
+ fieldCount: o.fields ? Object.keys(o.fields).length : 0
1948
+ };
1949
+ if (includeFields && o.fields) {
1950
+ base.fields = Object.entries(o.fields).map(([key, f]) => ({
1951
+ name: key,
1952
+ type: f.type,
1953
+ label: f.label ?? key
1954
+ }));
1955
+ }
1956
+ return base;
1957
+ });
1958
+ if (filter) {
1959
+ const lower = filter.toLowerCase();
1960
+ result = result.filter(
1961
+ (o) => o.name.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
1962
+ );
1963
+ }
1964
+ return JSON.stringify({
1965
+ objects: result,
1966
+ totalCount: result.length
1967
+ });
1968
+ };
1969
+ }
1970
+ function createDescribeObjectHandler2(ctx) {
1971
+ return async (args) => {
1972
+ const { objectName } = args;
1973
+ if (!objectName) {
1974
+ return JSON.stringify({ error: '"objectName" is required' });
1975
+ }
1976
+ if (!isSnakeCase(objectName)) {
1977
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1978
+ }
1979
+ const objectDef = await ctx.metadataService.getObject(objectName);
1980
+ if (!objectDef) {
1981
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
1982
+ }
1983
+ const def = objectDef;
1984
+ const fields = def.fields ?? {};
1985
+ const fieldSummary = Object.entries(fields).map(([key, f]) => ({
1986
+ name: key,
1987
+ type: f.type,
1988
+ label: f.label ?? key,
1989
+ required: f.required ?? false,
1990
+ ...f.reference ? { reference: f.reference } : {},
1991
+ ...f.options ? { options: f.options } : {}
1992
+ }));
1993
+ return JSON.stringify({
1994
+ name: def.name,
1995
+ label: def.label ?? def.name,
1996
+ fields: fieldSummary,
1997
+ enableFeatures: def.enable ?? {}
1998
+ });
1999
+ };
2000
+ }
2001
+ function registerMetadataTools(registry, context) {
2002
+ registry.register(createObjectTool, createCreateObjectHandler(context));
2003
+ registry.register(addFieldTool, createAddFieldHandler(context));
2004
+ registry.register(modifyFieldTool, createModifyFieldHandler(context));
2005
+ registry.register(deleteFieldTool, createDeleteFieldHandler(context));
2006
+ registry.register(listMetadataObjectsTool, createListObjectsHandler2(context));
2007
+ registry.register(describeMetadataObjectTool, createDescribeObjectHandler2(context));
2008
+ }
2009
+
1268
2010
  // src/agent-runtime.ts
1269
2011
  import { AgentSchema } from "@objectstack/spec/ai";
1270
2012
  var AgentRuntime = class {
@@ -1272,6 +2014,27 @@ var AgentRuntime = class {
1272
2014
  this.metadataService = metadataService;
1273
2015
  }
1274
2016
  // ── Public API ────────────────────────────────────────────────
2017
+ /**
2018
+ * List all active agents registered in the metadata service.
2019
+ *
2020
+ * Returns a summary for each agent (name, label, role) suitable
2021
+ * for populating an agent selector dropdown in the UI.
2022
+ */
2023
+ async listAgents() {
2024
+ const rawItems = await this.metadataService.list("agent");
2025
+ const agents = [];
2026
+ for (const raw of rawItems) {
2027
+ const result = AgentSchema.safeParse(raw);
2028
+ if (result.success && result.data.active) {
2029
+ agents.push({
2030
+ name: result.data.name,
2031
+ label: result.data.label,
2032
+ role: result.data.role
2033
+ });
2034
+ }
2035
+ }
2036
+ return agents;
2037
+ }
1275
2038
  /**
1276
2039
  * Load and validate an agent definition by name.
1277
2040
  *
@@ -1400,15 +2163,233 @@ Guidelines:
1400
2163
  }
1401
2164
  };
1402
2165
 
2166
+ // src/agents/metadata-assistant-agent.ts
2167
+ var METADATA_ASSISTANT_AGENT = {
2168
+ name: "metadata_assistant",
2169
+ label: "Metadata Assistant",
2170
+ role: "Schema Architect",
2171
+ instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
2172
+
2173
+ Capabilities:
2174
+ - Create new data objects (tables) with fields
2175
+ - Add fields (columns) to existing objects
2176
+ - Modify field properties (label, type, required, default value)
2177
+ - Delete fields from objects
2178
+ - List all registered metadata objects and their schemas
2179
+ - Describe the full schema of a specific object
2180
+
2181
+ Guidelines:
2182
+ 1. Before creating a new object, use list_metadata_objects to check if a similar one already exists.
2183
+ 2. Before modifying or deleting fields, use describe_metadata_object to understand the current schema.
2184
+ 3. Always use snake_case for object names and field names (e.g. project_task, due_date).
2185
+ 4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
2186
+ 5. When creating objects, propose a reasonable set of initial fields based on the entity type.
2187
+ 6. Explain what changes you are about to make before executing them.
2188
+ 7. After making changes, confirm the result by describing the updated schema.
2189
+ 8. For destructive operations (deleting fields), always warn the user about potential data loss.
2190
+ 9. Always answer in the same language the user is using.
2191
+ 10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
2192
+ model: {
2193
+ provider: "openai",
2194
+ model: "gpt-4",
2195
+ temperature: 0.2,
2196
+ maxTokens: 4096
2197
+ },
2198
+ tools: [
2199
+ { type: "action", name: "create_object", description: "Create a new data object (table)" },
2200
+ { type: "action", name: "add_field", description: "Add a field to an existing object" },
2201
+ { type: "action", name: "modify_field", description: "Modify an existing field definition" },
2202
+ { type: "action", name: "delete_field", description: "Delete a field from an object" },
2203
+ { type: "query", name: "list_metadata_objects", description: "List all metadata objects" },
2204
+ { type: "query", name: "describe_metadata_object", description: "Describe an object schema" }
2205
+ ],
2206
+ active: true,
2207
+ visibility: "global",
2208
+ guardrails: {
2209
+ maxTokensPerInvocation: 8192,
2210
+ maxExecutionTimeSec: 60,
2211
+ blockedTopics: ["drop_database", "raw_sql", "system_tables"]
2212
+ },
2213
+ planning: {
2214
+ strategy: "react",
2215
+ maxIterations: 10,
2216
+ allowReplan: true
2217
+ },
2218
+ memory: {
2219
+ shortTerm: {
2220
+ maxMessages: 30,
2221
+ maxTokens: 8192
2222
+ }
2223
+ }
2224
+ };
2225
+
2226
+ // src/adapters/vercel-adapter.ts
2227
+ import { generateText, streamText, tool as vercelTool, jsonSchema } from "ai";
2228
+ function buildVercelOptions(options) {
2229
+ if (!options) return {};
2230
+ const opts = {};
2231
+ if (options.temperature != null) opts.temperature = options.temperature;
2232
+ if (options.maxTokens != null) opts.maxTokens = options.maxTokens;
2233
+ if (options.stop?.length) opts.stopSequences = options.stop;
2234
+ if (options.tools?.length) {
2235
+ const tools = {};
2236
+ for (const t of options.tools) {
2237
+ tools[t.name] = vercelTool({
2238
+ description: t.description,
2239
+ inputSchema: jsonSchema(t.parameters)
2240
+ });
2241
+ }
2242
+ opts.tools = tools;
2243
+ }
2244
+ if (options.toolChoice != null) {
2245
+ opts.toolChoice = options.toolChoice;
2246
+ }
2247
+ return opts;
2248
+ }
2249
+ var VercelLLMAdapter = class {
2250
+ constructor(config) {
2251
+ this.name = "vercel";
2252
+ this.model = config.model;
2253
+ }
2254
+ async chat(messages, options) {
2255
+ const result = await generateText({
2256
+ model: this.model,
2257
+ messages,
2258
+ ...buildVercelOptions(options)
2259
+ });
2260
+ return {
2261
+ content: result.text,
2262
+ model: result.response?.modelId,
2263
+ toolCalls: result.toolCalls?.length ? result.toolCalls : void 0,
2264
+ usage: result.usage ? {
2265
+ promptTokens: result.usage.inputTokens ?? 0,
2266
+ completionTokens: result.usage.outputTokens ?? 0,
2267
+ totalTokens: result.usage.totalTokens ?? 0
2268
+ } : void 0
2269
+ };
2270
+ }
2271
+ async complete(prompt, options) {
2272
+ const result = await generateText({
2273
+ model: this.model,
2274
+ prompt,
2275
+ ...buildVercelOptions(options)
2276
+ });
2277
+ return {
2278
+ content: result.text,
2279
+ model: result.response?.modelId,
2280
+ usage: result.usage ? {
2281
+ promptTokens: result.usage.inputTokens ?? 0,
2282
+ completionTokens: result.usage.outputTokens ?? 0,
2283
+ totalTokens: result.usage.totalTokens ?? 0
2284
+ } : void 0
2285
+ };
2286
+ }
2287
+ async *streamChat(messages, options) {
2288
+ const result = streamText({
2289
+ model: this.model,
2290
+ messages,
2291
+ ...buildVercelOptions(options)
2292
+ });
2293
+ for await (const part of result.fullStream) {
2294
+ yield part;
2295
+ }
2296
+ }
2297
+ async embed(_input) {
2298
+ throw new Error(
2299
+ "[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
2300
+ );
2301
+ }
2302
+ async listModels() {
2303
+ return [];
2304
+ }
2305
+ };
2306
+
1403
2307
  // src/plugin.ts
1404
2308
  var AIServicePlugin = class {
1405
2309
  constructor(options = {}) {
1406
2310
  this.name = "com.objectstack.service-ai";
1407
2311
  this.version = "1.0.0";
1408
2312
  this.type = "standard";
1409
- this.dependencies = [];
2313
+ this.dependencies = ["com.objectstack.engine.objectql"];
1410
2314
  this.options = options;
1411
2315
  }
2316
+ /**
2317
+ * Auto-detect LLM provider from environment variables.
2318
+ *
2319
+ * Priority order:
2320
+ * 1. AI_GATEWAY_MODEL → Vercel AI Gateway
2321
+ * 2. OPENAI_API_KEY → OpenAI
2322
+ * 3. ANTHROPIC_API_KEY → Anthropic
2323
+ * 4. GOOGLE_GENERATIVE_AI_API_KEY → Google
2324
+ * 5. Fallback → MemoryLLMAdapter
2325
+ *
2326
+ * Returns the adapter and a description for logging.
2327
+ */
2328
+ async detectAdapter(ctx) {
2329
+ const gatewayModel = process.env.AI_GATEWAY_MODEL;
2330
+ if (gatewayModel) {
2331
+ try {
2332
+ const gatewayPkg = "@ai-sdk/gateway";
2333
+ const { gateway } = await import(
2334
+ /* webpackIgnore: true */
2335
+ gatewayPkg
2336
+ );
2337
+ const adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) });
2338
+ return { adapter, description: `Vercel AI Gateway (model: ${gatewayModel})` };
2339
+ } catch (err) {
2340
+ ctx.logger.warn(
2341
+ `[AI] Failed to load @ai-sdk/gateway for AI_GATEWAY_MODEL=${gatewayModel}, trying next provider`,
2342
+ err instanceof Error ? { error: err.message } : void 0
2343
+ );
2344
+ }
2345
+ }
2346
+ const providerConfigs = [
2347
+ {
2348
+ envKey: "OPENAI_API_KEY",
2349
+ pkg: "@ai-sdk/openai",
2350
+ factory: "openai",
2351
+ defaultModel: "gpt-4o",
2352
+ displayName: "OpenAI"
2353
+ },
2354
+ {
2355
+ envKey: "ANTHROPIC_API_KEY",
2356
+ pkg: "@ai-sdk/anthropic",
2357
+ factory: "anthropic",
2358
+ defaultModel: "claude-sonnet-4-20250514",
2359
+ displayName: "Anthropic"
2360
+ },
2361
+ {
2362
+ envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
2363
+ pkg: "@ai-sdk/google",
2364
+ factory: "google",
2365
+ defaultModel: "gemini-2.0-flash",
2366
+ displayName: "Google"
2367
+ }
2368
+ ];
2369
+ for (const { envKey, pkg, factory, defaultModel, displayName } of providerConfigs) {
2370
+ if (process.env[envKey]) {
2371
+ try {
2372
+ const mod = await import(
2373
+ /* webpackIgnore: true */
2374
+ pkg
2375
+ );
2376
+ const createModel = mod[factory] ?? mod.default;
2377
+ if (typeof createModel === "function") {
2378
+ const modelId = process.env.AI_MODEL ?? defaultModel;
2379
+ const adapter = new VercelLLMAdapter({ model: createModel(modelId) });
2380
+ return { adapter, description: `${displayName} (model: ${modelId})` };
2381
+ }
2382
+ } catch (err) {
2383
+ ctx.logger.warn(
2384
+ `[AI] Failed to load ${pkg} for ${envKey}, trying next provider`,
2385
+ err instanceof Error ? { error: err.message } : void 0
2386
+ );
2387
+ }
2388
+ }
2389
+ }
2390
+ 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.");
2391
+ return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode - for testing only)" };
2392
+ }
1412
2393
  async init(ctx) {
1413
2394
  let hasExisting = false;
1414
2395
  try {
@@ -1430,8 +2411,19 @@ var AIServicePlugin = class {
1430
2411
  } catch {
1431
2412
  }
1432
2413
  }
2414
+ let adapter;
2415
+ let adapterDescription;
2416
+ if (this.options.adapter) {
2417
+ adapter = this.options.adapter;
2418
+ adapterDescription = `${adapter.name} (explicitly configured)`;
2419
+ } else {
2420
+ const detected = await this.detectAdapter(ctx);
2421
+ adapter = detected.adapter;
2422
+ adapterDescription = detected.description;
2423
+ }
2424
+ ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
1433
2425
  const config = {
1434
- adapter: this.options.adapter,
2426
+ adapter,
1435
2427
  logger: ctx.logger,
1436
2428
  conversationService
1437
2429
  };
@@ -1441,7 +2433,7 @@ var AIServicePlugin = class {
1441
2433
  } else {
1442
2434
  ctx.registerService("ai", this.service);
1443
2435
  }
1444
- ctx.registerService("app.com.objectstack.service-ai", {
2436
+ ctx.getService("manifest").register({
1445
2437
  id: "com.objectstack.service-ai",
1446
2438
  name: "AI Service",
1447
2439
  version: "1.0.0",
@@ -1454,13 +2446,32 @@ var AIServicePlugin = class {
1454
2446
  ctx.logger.debug("[AI] Before chat", { messages });
1455
2447
  });
1456
2448
  }
2449
+ try {
2450
+ const setupNav = ctx.getService("setupNav");
2451
+ if (setupNav) {
2452
+ setupNav.contribute({
2453
+ areaId: "area_ai",
2454
+ items: [
2455
+ { id: "nav_ai_conversations", type: "object", label: { key: "setup.nav.ai_conversations", defaultValue: "Conversations" }, objectName: "conversations", icon: "message-square", order: 10 },
2456
+ { id: "nav_ai_messages", type: "object", label: { key: "setup.nav.ai_messages", defaultValue: "Messages" }, objectName: "messages", icon: "messages-square", order: 20 }
2457
+ ]
2458
+ });
2459
+ ctx.logger.info("[AI] Navigation items contributed to Setup App");
2460
+ }
2461
+ } catch {
2462
+ }
1457
2463
  ctx.logger.info("[AI] Service initialized");
1458
2464
  }
1459
2465
  async start(ctx) {
1460
2466
  if (!this.service) return;
2467
+ let metadataService;
2468
+ try {
2469
+ metadataService = ctx.getService("metadata");
2470
+ } catch {
2471
+ ctx.logger.debug("[AI] Metadata service not available");
2472
+ }
1461
2473
  try {
1462
2474
  const dataEngine = ctx.getService("data");
1463
- const metadataService = ctx.getService("metadata");
1464
2475
  if (dataEngine && metadataService) {
1465
2476
  registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
1466
2477
  ctx.logger.info("[AI] Built-in data tools registered");
@@ -1473,14 +2484,29 @@ var AIServicePlugin = class {
1473
2484
  }
1474
2485
  }
1475
2486
  } catch {
1476
- ctx.logger.debug("[AI] Data engine or metadata service not available, skipping data tools");
2487
+ ctx.logger.debug("[AI] Data engine not available, skipping data tools");
2488
+ }
2489
+ if (metadataService) {
2490
+ try {
2491
+ registerMetadataTools(this.service.toolRegistry, { metadataService });
2492
+ ctx.logger.info("[AI] Built-in metadata tools registered");
2493
+ const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
2494
+ if (!agentExists) {
2495
+ await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
2496
+ ctx.logger.info("[AI] metadata_assistant agent registered");
2497
+ } else {
2498
+ ctx.logger.debug("[AI] metadata_assistant agent already exists, skipping auto-registration");
2499
+ }
2500
+ } catch (err) {
2501
+ ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
2502
+ }
1477
2503
  }
1478
2504
  await ctx.trigger("ai:ready", this.service);
1479
2505
  const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
1480
2506
  try {
1481
- const metadataService = ctx.getService("metadata");
1482
- if (metadataService) {
1483
- const agentRuntime = new AgentRuntime(metadataService);
2507
+ const metadataService2 = ctx.getService("metadata");
2508
+ if (metadataService2) {
2509
+ const agentRuntime = new AgentRuntime(metadataService2);
1484
2510
  const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
1485
2511
  routes.push(...agentRoutes);
1486
2512
  }
@@ -1488,6 +2514,10 @@ var AIServicePlugin = class {
1488
2514
  ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
1489
2515
  }
1490
2516
  await ctx.trigger("ai:routes", routes);
2517
+ const kernel = ctx.getKernel();
2518
+ if (kernel) {
2519
+ kernel.__aiRoutes = routes;
2520
+ }
1491
2521
  ctx.logger.info(
1492
2522
  `[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
1493
2523
  );
@@ -1505,11 +2535,23 @@ export {
1505
2535
  DATA_CHAT_AGENT,
1506
2536
  DATA_TOOL_DEFINITIONS,
1507
2537
  InMemoryConversationService,
2538
+ METADATA_ASSISTANT_AGENT,
2539
+ METADATA_TOOL_DEFINITIONS,
1508
2540
  MemoryLLMAdapter,
1509
2541
  ObjectQLConversationService,
1510
2542
  ToolRegistry,
2543
+ VercelLLMAdapter,
2544
+ addFieldTool,
1511
2545
  buildAIRoutes,
1512
2546
  buildAgentRoutes,
1513
- registerDataTools
2547
+ createObjectTool,
2548
+ deleteFieldTool,
2549
+ describeMetadataObjectTool,
2550
+ encodeStreamPart,
2551
+ encodeVercelDataStream,
2552
+ listMetadataObjectsTool,
2553
+ modifyFieldTool,
2554
+ registerDataTools,
2555
+ registerMetadataTools
1514
2556
  };
1515
2557
  //# sourceMappingURL=index.js.map