@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.cjs CHANGED
@@ -28,12 +28,24 @@ __export(index_exports, {
28
28
  DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
29
29
  DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
30
30
  InMemoryConversationService: () => InMemoryConversationService,
31
+ METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
32
+ METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
31
33
  MemoryLLMAdapter: () => MemoryLLMAdapter,
32
34
  ObjectQLConversationService: () => ObjectQLConversationService,
33
35
  ToolRegistry: () => ToolRegistry,
36
+ VercelLLMAdapter: () => VercelLLMAdapter,
37
+ addFieldTool: () => addFieldTool,
34
38
  buildAIRoutes: () => buildAIRoutes,
35
39
  buildAgentRoutes: () => buildAgentRoutes,
36
- registerDataTools: () => registerDataTools
40
+ createObjectTool: () => createObjectTool,
41
+ deleteFieldTool: () => deleteFieldTool,
42
+ describeMetadataObjectTool: () => describeMetadataObjectTool,
43
+ encodeStreamPart: () => encodeStreamPart,
44
+ encodeVercelDataStream: () => encodeVercelDataStream,
45
+ listMetadataObjectsTool: () => listMetadataObjectsTool,
46
+ modifyFieldTool: () => modifyFieldTool,
47
+ registerDataTools: () => registerDataTools,
48
+ registerMetadataTools: () => registerMetadataTools
37
49
  });
38
50
  module.exports = __toCommonJS(index_exports);
39
51
 
@@ -47,7 +59,9 @@ var MemoryLLMAdapter = class {
47
59
  }
48
60
  async chat(messages, options) {
49
61
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
50
- const content = lastUserMessage ? `[memory] ${lastUserMessage.content}` : "[memory] (no user message)";
62
+ const userContent = lastUserMessage?.content;
63
+ const text = typeof userContent === "string" ? userContent : "(complex content)";
64
+ const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
51
65
  return {
52
66
  content,
53
67
  model: options?.model ?? "memory",
@@ -65,10 +79,15 @@ var MemoryLLMAdapter = class {
65
79
  const result = await this.chat(messages);
66
80
  const words = result.content.split(" ");
67
81
  for (let i = 0; i < words.length; i++) {
68
- const textDelta = i === 0 ? words[i] : ` ${words[i]}`;
69
- yield { type: "text-delta", textDelta };
82
+ const wordText = i === 0 ? words[i] : ` ${words[i]}`;
83
+ yield { type: "text-delta", id: `delta_${i}`, text: wordText };
70
84
  }
71
- yield { type: "finish", result };
85
+ yield {
86
+ type: "finish",
87
+ finishReason: "stop",
88
+ totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
89
+ rawFinishReason: "stop"
90
+ };
72
91
  }
73
92
  async embed(input) {
74
93
  const texts = Array.isArray(input) ? input : [input];
@@ -131,21 +150,34 @@ var ToolRegistry = class {
131
150
  * Execute a tool call and return the result.
132
151
  */
133
152
  async execute(toolCall) {
134
- const handler = this.handlers.get(toolCall.name);
153
+ const handler = this.handlers.get(toolCall.toolName);
135
154
  if (!handler) {
136
155
  return {
137
- toolCallId: toolCall.id,
138
- content: `Tool "${toolCall.name}" is not registered`,
156
+ type: "tool-result",
157
+ toolCallId: toolCall.toolCallId,
158
+ toolName: toolCall.toolName,
159
+ output: { type: "text", value: `Tool "${toolCall.toolName}" is not registered` },
139
160
  isError: true
140
161
  };
141
162
  }
142
163
  try {
143
- const args = JSON.parse(toolCall.arguments);
164
+ const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
144
165
  const content = await handler(args);
145
- return { toolCallId: toolCall.id, content };
166
+ return {
167
+ type: "tool-result",
168
+ toolCallId: toolCall.toolCallId,
169
+ toolName: toolCall.toolName,
170
+ output: { type: "text", value: content }
171
+ };
146
172
  } catch (err) {
147
173
  const message = err instanceof Error ? err.message : String(err);
148
- return { toolCallId: toolCall.id, content: message, isError: true };
174
+ return {
175
+ type: "tool-result",
176
+ toolCallId: toolCall.toolCallId,
177
+ toolName: toolCall.toolName,
178
+ output: { type: "text", value: message },
179
+ isError: true
180
+ };
149
181
  }
150
182
  }
151
183
  /**
@@ -231,6 +263,17 @@ var InMemoryConversationService = class {
231
263
  };
232
264
 
233
265
  // src/ai-service.ts
266
+ function textDeltaPart(id, text) {
267
+ return { type: "text-delta", id, text };
268
+ }
269
+ function finishPart(result) {
270
+ return {
271
+ type: "finish",
272
+ finishReason: "stop",
273
+ totalUsage: result?.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
274
+ rawFinishReason: "stop"
275
+ };
276
+ }
234
277
  var _AIService = class _AIService {
235
278
  constructor(config = {}) {
236
279
  this.adapter = config.adapter ?? new MemoryLLMAdapter();
@@ -258,8 +301,8 @@ var _AIService = class _AIService {
258
301
  this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
259
302
  if (!this.adapter.streamChat) {
260
303
  const result = await this.adapter.chat(messages, options);
261
- yield { type: "text-delta", textDelta: result.content };
262
- yield { type: "finish", result };
304
+ yield textDeltaPart("fallback", result.content);
305
+ yield finishPart(result);
263
306
  return;
264
307
  }
265
308
  yield* this.adapter.streamChat(messages, options);
@@ -276,6 +319,10 @@ var _AIService = class _AIService {
276
319
  }
277
320
  return this.adapter.listModels();
278
321
  }
322
+ /** Extract the text value from a ToolExecutionResult's output. */
323
+ static extractOutputText(tr) {
324
+ return tr.output && typeof tr.output === "object" && "value" in tr.output ? String(tr.output.value) : "unknown error";
325
+ }
279
326
  /**
280
327
  * Chat with automatic tool call resolution.
281
328
  *
@@ -316,23 +363,26 @@ var _AIService = class _AIService {
316
363
  }
317
364
  this.logger.debug("[AI] chatWithTools tool calls", {
318
365
  iteration,
319
- calls: result.toolCalls.map((tc) => tc.name)
366
+ calls: result.toolCalls.map((tc) => tc.toolName)
320
367
  });
368
+ const assistantContent = [];
369
+ if (result.content) assistantContent.push({ type: "text", text: result.content });
370
+ assistantContent.push(...result.toolCalls);
321
371
  conversation.push({
322
372
  role: "assistant",
323
- content: result.content ?? "",
324
- toolCalls: result.toolCalls
373
+ content: assistantContent
325
374
  });
326
375
  const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
327
376
  for (const tr of toolResults) {
328
377
  if (tr.isError) {
329
- const matchedCall = result.toolCalls.find((tc) => tc.id === tr.toolCallId);
330
- const toolName = matchedCall?.name ?? "unknown";
331
- const errorEntry = { iteration, toolName, error: tr.content };
378
+ const matchedCall = result.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
379
+ const toolName = matchedCall?.toolName ?? "unknown";
380
+ const errorText = _AIService.extractOutputText(tr);
381
+ const errorEntry = { iteration, toolName, error: errorText };
332
382
  toolErrors.push(errorEntry);
333
383
  this.logger.warn("[AI] chatWithTools tool error", errorEntry);
334
384
  if (onToolError && matchedCall) {
335
- const action = onToolError(matchedCall, tr.content);
385
+ const action = onToolError(matchedCall, errorText);
336
386
  if (action === "abort") {
337
387
  abortedByCallback = true;
338
388
  }
@@ -340,8 +390,7 @@ var _AIService = class _AIService {
340
390
  }
341
391
  conversation.push({
342
392
  role: "tool",
343
- content: tr.content,
344
- toolCallId: tr.toolCallId
393
+ content: [tr]
345
394
  });
346
395
  }
347
396
  if (abortedByCallback) {
@@ -387,24 +436,27 @@ var _AIService = class _AIService {
387
436
  for (let iteration = 0; iteration < maxIterations; iteration++) {
388
437
  const result2 = await this.adapter.chat(conversation, chatOptions);
389
438
  if (!result2.toolCalls || result2.toolCalls.length === 0) {
390
- yield { type: "text-delta", textDelta: result2.content };
391
- yield { type: "finish", result: result2 };
439
+ yield textDeltaPart("stream", result2.content);
440
+ yield finishPart(result2);
392
441
  return;
393
442
  }
394
443
  for (const tc of result2.toolCalls) {
395
- yield { type: "tool-call", toolCall: tc };
444
+ yield { type: "tool-call", toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input };
396
445
  }
446
+ const assistantContent = [];
447
+ if (result2.content) assistantContent.push({ type: "text", text: result2.content });
448
+ assistantContent.push(...result2.toolCalls);
397
449
  conversation.push({
398
450
  role: "assistant",
399
- content: result2.content ?? "",
400
- toolCalls: result2.toolCalls
451
+ content: assistantContent
401
452
  });
402
453
  const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
403
454
  for (const tr of toolResults) {
404
455
  if (tr.isError && onToolError) {
405
- const matchedCall = result2.toolCalls.find((tc) => tc.id === tr.toolCallId);
456
+ const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
406
457
  if (matchedCall) {
407
- const action = onToolError(matchedCall, tr.content);
458
+ const errorText = _AIService.extractOutputText(tr);
459
+ const action = onToolError(matchedCall, errorText);
408
460
  if (action === "abort") {
409
461
  abortedByCallback = true;
410
462
  }
@@ -412,8 +464,7 @@ var _AIService = class _AIService {
412
464
  }
413
465
  conversation.push({
414
466
  role: "tool",
415
- content: tr.content,
416
- toolCallId: tr.toolCallId
467
+ content: [tr]
417
468
  });
418
469
  }
419
470
  if (abortedByCallback) {
@@ -427,8 +478,8 @@ var _AIService = class _AIService {
427
478
  }
428
479
  const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
429
480
  const result = await this.adapter.chat(conversation, finalOptions);
430
- yield { type: "text-delta", textDelta: result.content };
431
- yield { type: "finish", result };
481
+ yield textDeltaPart("stream", result.content);
482
+ yield finishPart(result);
432
483
  }
433
484
  };
434
485
  // ── Tool Call Loop ────────────────────────────────────────────
@@ -436,8 +487,99 @@ var _AIService = class _AIService {
436
487
  _AIService.DEFAULT_MAX_ITERATIONS = 10;
437
488
  var AIService = _AIService;
438
489
 
490
+ // src/stream/vercel-stream-encoder.ts
491
+ function sse(data) {
492
+ return `data: ${JSON.stringify(data)}
493
+
494
+ `;
495
+ }
496
+ function encodeStreamPart(part) {
497
+ switch (part.type) {
498
+ case "text-delta":
499
+ return sse({ type: "text-delta", id: "0", delta: part.text });
500
+ case "tool-input-start":
501
+ return sse({
502
+ type: "tool-input-start",
503
+ toolCallId: part.id,
504
+ toolName: part.toolName
505
+ });
506
+ case "tool-input-delta":
507
+ return sse({
508
+ type: "tool-input-delta",
509
+ toolCallId: part.id,
510
+ inputTextDelta: part.delta
511
+ });
512
+ case "tool-call":
513
+ return sse({
514
+ type: "tool-input-available",
515
+ toolCallId: part.toolCallId,
516
+ toolName: part.toolName,
517
+ input: part.input
518
+ });
519
+ case "tool-result":
520
+ return sse({
521
+ type: "tool-output-available",
522
+ toolCallId: part.toolCallId,
523
+ output: part.output
524
+ });
525
+ case "error":
526
+ return sse({
527
+ type: "error",
528
+ errorText: String(part.error)
529
+ });
530
+ // finish-step and finish are handled by the generator, not here
531
+ default:
532
+ return "";
533
+ }
534
+ }
535
+ async function* encodeVercelDataStream(events) {
536
+ yield sse({ type: "start" });
537
+ yield sse({ type: "start-step" });
538
+ yield sse({ type: "text-start", id: "0" });
539
+ let textOpen = true;
540
+ let finishReason = "stop";
541
+ for await (const part of events) {
542
+ if (part.type === "finish") {
543
+ finishReason = part.finishReason ?? "stop";
544
+ }
545
+ if (part.type === "finish-step" || part.type === "finish") {
546
+ if (textOpen) {
547
+ yield sse({ type: "text-end", id: "0" });
548
+ textOpen = false;
549
+ }
550
+ continue;
551
+ }
552
+ const frame = encodeStreamPart(part);
553
+ if (frame) {
554
+ yield frame;
555
+ }
556
+ }
557
+ if (textOpen) {
558
+ yield sse({ type: "text-end", id: "0" });
559
+ }
560
+ yield sse({ type: "finish-step" });
561
+ yield sse({ type: "finish", finishReason });
562
+ yield "data: [DONE]\n\n";
563
+ }
564
+
439
565
  // src/routes/ai-routes.ts
440
566
  var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
567
+ function normalizeMessage(raw) {
568
+ const role = raw.role;
569
+ if (typeof raw.content === "string") {
570
+ return { role, content: raw.content };
571
+ }
572
+ if (Array.isArray(raw.content)) {
573
+ return { role, content: raw.content };
574
+ }
575
+ if (Array.isArray(raw.parts)) {
576
+ const textParts = raw.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text);
577
+ if (textParts.length > 0) {
578
+ return { role, content: textParts.join("") };
579
+ }
580
+ }
581
+ return { role, content: "" };
582
+ }
441
583
  function validateMessage(raw) {
442
584
  if (typeof raw !== "object" || raw === null) {
443
585
  return "each message must be an object";
@@ -446,22 +588,56 @@ function validateMessage(raw) {
446
588
  if (typeof msg.role !== "string" || !VALID_ROLES.has(msg.role)) {
447
589
  return `message.role must be one of ${[...VALID_ROLES].map((r) => `"${r}"`).join(", ")}`;
448
590
  }
449
- if (typeof msg.content !== "string") {
450
- return "message.content must be a string";
591
+ const content = msg.content;
592
+ if (Array.isArray(msg.parts)) {
593
+ return null;
451
594
  }
452
- return null;
595
+ if (typeof content === "string") {
596
+ return null;
597
+ }
598
+ if (Array.isArray(content)) {
599
+ for (const part of content) {
600
+ if (typeof part !== "object" || part === null) {
601
+ return "message.content array elements must be non-null objects";
602
+ }
603
+ const partObj = part;
604
+ if (typeof partObj.type !== "string") {
605
+ return 'each message.content array element must have a string "type" property';
606
+ }
607
+ if (partObj.type === "text" && typeof partObj.text !== "string") {
608
+ return 'message.content elements with type "text" must have a string "text" property';
609
+ }
610
+ }
611
+ return null;
612
+ }
613
+ if (content === null || content === void 0) {
614
+ if (msg.role === "assistant" || msg.role === "tool") {
615
+ return null;
616
+ }
617
+ }
618
+ return "message.content must be a string, an array, or include parts";
453
619
  }
454
620
  function buildAIRoutes(aiService, conversationService, logger) {
455
621
  return [
456
622
  // ── Chat ────────────────────────────────────────────────────
623
+ //
624
+ // Dual-mode endpoint compatible with both the legacy ObjectStack
625
+ // format (`{ messages, options }`) and the Vercel AI SDK useChat
626
+ // flat format (`{ messages, system, model, stream, … }`).
627
+ //
628
+ // Behaviour:
629
+ // • `stream !== false` → Vercel Data Stream Protocol (SSE)
630
+ // • `stream === false` → JSON response (legacy)
631
+ //
457
632
  {
458
633
  method: "POST",
459
634
  path: "/api/v1/ai/chat",
460
- description: "Synchronous chat completion",
635
+ description: "Chat completion (supports Vercel AI Data Stream Protocol)",
461
636
  auth: true,
462
637
  permissions: ["ai:chat"],
463
638
  handler: async (req) => {
464
- const { messages, options } = req.body ?? {};
639
+ const body = req.body ?? {};
640
+ const messages = body.messages;
465
641
  if (!Array.isArray(messages) || messages.length === 0) {
466
642
  return { status: 400, body: { error: "messages array is required" } };
467
643
  }
@@ -469,8 +645,49 @@ function buildAIRoutes(aiService, conversationService, logger) {
469
645
  const err = validateMessage(msg);
470
646
  if (err) return { status: 400, body: { error: err } };
471
647
  }
648
+ const nested = body.options ?? {};
649
+ const resolvedOptions = {
650
+ ...nested,
651
+ ...body.model != null && { model: body.model },
652
+ ...body.temperature != null && { temperature: body.temperature },
653
+ ...body.maxTokens != null && { maxTokens: body.maxTokens }
654
+ };
655
+ const rawSystemPrompt = body.system ?? body.systemPrompt;
656
+ if (rawSystemPrompt != null && typeof rawSystemPrompt !== "string") {
657
+ return { status: 400, body: { error: "system/systemPrompt must be a string" } };
658
+ }
659
+ const systemPrompt = rawSystemPrompt;
660
+ const finalMessages = [
661
+ ...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
662
+ ...messages.map((m) => normalizeMessage(m))
663
+ ];
664
+ const wantStream = body.stream !== false;
665
+ if (wantStream) {
666
+ try {
667
+ if (!aiService.streamChatWithTools) {
668
+ return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
669
+ }
670
+ const events = aiService.streamChatWithTools(finalMessages, resolvedOptions);
671
+ return {
672
+ status: 200,
673
+ stream: true,
674
+ vercelDataStream: true,
675
+ contentType: "text/event-stream",
676
+ headers: {
677
+ "Content-Type": "text/event-stream",
678
+ "Cache-Control": "no-cache",
679
+ "Connection": "keep-alive",
680
+ "x-vercel-ai-ui-message-stream": "v1"
681
+ },
682
+ events: encodeVercelDataStream(events)
683
+ };
684
+ } catch (err) {
685
+ logger.error("[AI Route] /chat stream error", err instanceof Error ? err : void 0);
686
+ return { status: 500, body: { error: "Internal AI service error" } };
687
+ }
688
+ }
472
689
  try {
473
- const result = await aiService.chat(messages, options);
690
+ const result = await aiService.chatWithTools(finalMessages, resolvedOptions);
474
691
  return { status: 200, body: result };
475
692
  } catch (err) {
476
693
  logger.error("[AI Route] /chat error", err instanceof Error ? err : void 0);
@@ -498,7 +715,7 @@ function buildAIRoutes(aiService, conversationService, logger) {
498
715
  if (!aiService.streamChat) {
499
716
  return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
500
717
  }
501
- const events = aiService.streamChat(messages, options);
718
+ const events = aiService.streamChat(messages.map((m) => normalizeMessage(m)), options);
502
719
  return { status: 200, stream: true, events };
503
720
  } catch (err) {
504
721
  logger.error("[AI Route] /chat/stream error", err instanceof Error ? err : void 0);
@@ -683,6 +900,27 @@ function validateAgentMessage(raw) {
683
900
  }
684
901
  function buildAgentRoutes(aiService, agentRuntime, logger) {
685
902
  return [
903
+ // ── List active agents ──────────────────────────────────────
904
+ {
905
+ method: "GET",
906
+ path: "/api/v1/ai/agents",
907
+ description: "List all active AI agents",
908
+ auth: true,
909
+ permissions: ["ai:chat"],
910
+ handler: async () => {
911
+ try {
912
+ const agents = await agentRuntime.listAgents();
913
+ return { status: 200, body: { agents } };
914
+ } catch (err) {
915
+ logger.error(
916
+ "[AI Route] /agents list error",
917
+ err instanceof Error ? err : void 0
918
+ );
919
+ return { status: 500, body: { error: "Internal AI service error" } };
920
+ }
921
+ }
922
+ },
923
+ // ── Chat with a specific agent ──────────────────────────────
686
924
  {
687
925
  method: "POST",
688
926
  path: "/api/v1/ai/agents/:agentName/chat",
@@ -842,13 +1080,35 @@ var ObjectQLConversationService = class {
842
1080
  }
843
1081
  const now = (/* @__PURE__ */ new Date()).toISOString();
844
1082
  const msgId = `msg_${(0, import_node_crypto.randomUUID)()}`;
1083
+ let contentStr;
1084
+ let toolCallsJson = null;
1085
+ let toolCallId = null;
1086
+ if (message.role === "system" || message.role === "user") {
1087
+ contentStr = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
1088
+ } else if (message.role === "assistant") {
1089
+ if (typeof message.content === "string") {
1090
+ contentStr = message.content;
1091
+ } else {
1092
+ const parts = message.content;
1093
+ const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
1094
+ const toolCalls = parts.filter((p) => p.type === "tool-call");
1095
+ contentStr = textParts.join("");
1096
+ if (toolCalls.length > 0) toolCallsJson = JSON.stringify(toolCalls);
1097
+ }
1098
+ } else if (message.role === "tool") {
1099
+ contentStr = JSON.stringify(message.content);
1100
+ const firstResult = Array.isArray(message.content) ? message.content[0] : void 0;
1101
+ if (firstResult && "toolCallId" in firstResult) toolCallId = firstResult.toolCallId;
1102
+ } else {
1103
+ contentStr = "";
1104
+ }
845
1105
  await this.engine.insert(MESSAGES_OBJECT, {
846
1106
  id: msgId,
847
1107
  conversation_id: conversationId,
848
1108
  role: message.role,
849
- content: message.content,
850
- tool_calls: message.toolCalls ? JSON.stringify(message.toolCalls) : null,
851
- tool_call_id: message.toolCallId ?? null,
1109
+ content: contentStr,
1110
+ tool_calls: toolCallsJson,
1111
+ tool_call_id: toolCallId,
852
1112
  created_at: now
853
1113
  });
854
1114
  await this.engine.update(CONVERSATIONS_OBJECT, { id: conversationId, updated_at: now }, {
@@ -893,21 +1153,42 @@ var ObjectQLConversationService = class {
893
1153
  };
894
1154
  }
895
1155
  /**
896
- * Map a database row to an AIMessage.
1156
+ * Map a database row to a ModelMessage.
897
1157
  */
898
1158
  toMessage(row) {
899
- const msg = {
900
- role: row.role,
901
- content: row.content
902
- };
903
- const toolCalls = this.safeParse(row.tool_calls);
904
- if (toolCalls) {
905
- msg.toolCalls = toolCalls;
906
- }
907
- if (row.tool_call_id) {
908
- msg.toolCallId = row.tool_call_id;
1159
+ switch (row.role) {
1160
+ case "system":
1161
+ return { role: "system", content: row.content };
1162
+ case "user":
1163
+ return { role: "user", content: row.content };
1164
+ case "assistant": {
1165
+ const toolCalls = this.safeParse(row.tool_calls);
1166
+ if (toolCalls && toolCalls.length > 0) {
1167
+ const content = [];
1168
+ if (row.content) content.push({ type: "text", text: row.content });
1169
+ content.push(...toolCalls);
1170
+ return { role: "assistant", content };
1171
+ }
1172
+ return { role: "assistant", content: row.content };
1173
+ }
1174
+ case "tool": {
1175
+ const toolResults = this.safeParse(row.content);
1176
+ if (toolResults && toolResults.length > 0 && toolResults[0]?.type === "tool-result") {
1177
+ return { role: "tool", content: toolResults };
1178
+ }
1179
+ return {
1180
+ role: "tool",
1181
+ content: [{
1182
+ type: "tool-result",
1183
+ toolCallId: row.tool_call_id ?? "",
1184
+ toolName: "unknown",
1185
+ output: { type: "text", value: row.content }
1186
+ }]
1187
+ };
1188
+ }
1189
+ default:
1190
+ return { role: "user", content: row.content };
909
1191
  }
910
- return msg;
911
1192
  }
912
1193
  };
913
1194
 
@@ -1304,13 +1585,507 @@ function registerDataTools(registry, context) {
1304
1585
  registry.register(AGGREGATE_DATA_TOOL, createAggregateDataHandler(context));
1305
1586
  }
1306
1587
 
1307
- // src/agent-runtime.ts
1588
+ // src/tools/create-object.tool.ts
1308
1589
  var import_ai = require("@objectstack/spec/ai");
1590
+ var createObjectTool = (0, import_ai.defineTool)({
1591
+ name: "create_object",
1592
+ label: "Create Object",
1593
+ description: "Creates a new data object (table) with the specified name, label, and optional field definitions. Use this when the user wants to create a new entity, table, or data model.",
1594
+ category: "data",
1595
+ builtIn: true,
1596
+ // NOTE: requiresConfirmation is intentionally false (default) because the
1597
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
1598
+ // executes tool calls immediately without checking this flag. The flag
1599
+ // should only be set once server-side approval gating is implemented to
1600
+ // avoid giving users a false sense of safety.
1601
+ parameters: {
1602
+ type: "object",
1603
+ properties: {
1604
+ name: {
1605
+ type: "string",
1606
+ description: "Machine name for the object (snake_case, e.g. project_task)"
1607
+ },
1608
+ label: {
1609
+ type: "string",
1610
+ description: "Human-readable display name (e.g. Project Task)"
1611
+ },
1612
+ fields: {
1613
+ type: "array",
1614
+ description: "Initial fields to create with the object",
1615
+ items: {
1616
+ type: "object",
1617
+ properties: {
1618
+ name: { type: "string", description: "Field machine name (snake_case)" },
1619
+ label: { type: "string", description: "Field display name" },
1620
+ type: {
1621
+ type: "string",
1622
+ description: "Field data type",
1623
+ enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
1624
+ },
1625
+ required: { type: "boolean", description: "Whether the field is required" }
1626
+ },
1627
+ required: ["name", "type"]
1628
+ }
1629
+ },
1630
+ enableFeatures: {
1631
+ type: "object",
1632
+ description: "Object capability flags",
1633
+ properties: {
1634
+ trackHistory: { type: "boolean" },
1635
+ apiEnabled: { type: "boolean" }
1636
+ }
1637
+ }
1638
+ },
1639
+ required: ["name", "label"],
1640
+ additionalProperties: false
1641
+ }
1642
+ });
1643
+
1644
+ // src/tools/add-field.tool.ts
1645
+ var import_ai2 = require("@objectstack/spec/ai");
1646
+ var addFieldTool = (0, import_ai2.defineTool)({
1647
+ name: "add_field",
1648
+ label: "Add Field",
1649
+ description: "Adds a new field (column) to an existing data object. Use this when the user wants to add a property, column, or attribute to a table.",
1650
+ category: "data",
1651
+ builtIn: true,
1652
+ parameters: {
1653
+ type: "object",
1654
+ properties: {
1655
+ objectName: {
1656
+ type: "string",
1657
+ description: "Target object machine name (snake_case)"
1658
+ },
1659
+ name: {
1660
+ type: "string",
1661
+ description: "Field machine name (snake_case, e.g. due_date)"
1662
+ },
1663
+ label: {
1664
+ type: "string",
1665
+ description: "Human-readable field label (e.g. Due Date)"
1666
+ },
1667
+ type: {
1668
+ type: "string",
1669
+ description: "Field data type",
1670
+ enum: ["text", "textarea", "number", "boolean", "date", "datetime", "select", "lookup", "formula", "autonumber"]
1671
+ },
1672
+ required: {
1673
+ type: "boolean",
1674
+ description: "Whether the field is required"
1675
+ },
1676
+ defaultValue: {
1677
+ description: "Default value for the field"
1678
+ },
1679
+ options: {
1680
+ type: "array",
1681
+ description: "Options for select/picklist fields",
1682
+ items: {
1683
+ type: "object",
1684
+ properties: {
1685
+ label: { type: "string" },
1686
+ value: {
1687
+ type: "string",
1688
+ description: "Option machine identifier (lowercase snake_case, e.g. high_priority)",
1689
+ pattern: "^[a-z_][a-z0-9_]*$"
1690
+ }
1691
+ }
1692
+ }
1693
+ },
1694
+ reference: {
1695
+ type: "string",
1696
+ description: "Referenced object name for lookup fields (snake_case, e.g. account)"
1697
+ }
1698
+ },
1699
+ required: ["objectName", "name", "type"],
1700
+ additionalProperties: false
1701
+ }
1702
+ });
1703
+
1704
+ // src/tools/modify-field.tool.ts
1705
+ var import_ai3 = require("@objectstack/spec/ai");
1706
+ var modifyFieldTool = (0, import_ai3.defineTool)({
1707
+ name: "modify_field",
1708
+ label: "Modify Field",
1709
+ description: "Modifies an existing field definition (label, type, required, default value, etc.) on a data object. Use this when the user wants to change or reconfigure an existing column or attribute (not rename it).",
1710
+ category: "data",
1711
+ builtIn: true,
1712
+ parameters: {
1713
+ type: "object",
1714
+ properties: {
1715
+ objectName: {
1716
+ type: "string",
1717
+ description: "Target object machine name (snake_case)"
1718
+ },
1719
+ fieldName: {
1720
+ type: "string",
1721
+ description: "Existing field machine name to modify (snake_case)"
1722
+ },
1723
+ changes: {
1724
+ type: "object",
1725
+ description: "Field properties to update (partial patch)",
1726
+ properties: {
1727
+ label: { type: "string", description: "New display label" },
1728
+ type: { type: "string", description: "New field type" },
1729
+ required: { type: "boolean", description: "Update required constraint" },
1730
+ defaultValue: { description: "New default value" }
1731
+ }
1732
+ }
1733
+ },
1734
+ required: ["objectName", "fieldName", "changes"],
1735
+ additionalProperties: false
1736
+ }
1737
+ });
1738
+
1739
+ // src/tools/delete-field.tool.ts
1740
+ var import_ai4 = require("@objectstack/spec/ai");
1741
+ var deleteFieldTool = (0, import_ai4.defineTool)({
1742
+ name: "delete_field",
1743
+ label: "Delete Field",
1744
+ description: "Removes a field (column) from an existing data object. This is a destructive operation. Use this when the user explicitly wants to remove an attribute or column from a table.",
1745
+ category: "data",
1746
+ builtIn: true,
1747
+ // NOTE: requiresConfirmation is intentionally false (default) because the
1748
+ // server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
1749
+ // executes tool calls immediately without checking this flag. The flag
1750
+ // should only be set once server-side approval gating is implemented.
1751
+ parameters: {
1752
+ type: "object",
1753
+ properties: {
1754
+ objectName: {
1755
+ type: "string",
1756
+ description: "Target object machine name (snake_case)"
1757
+ },
1758
+ fieldName: {
1759
+ type: "string",
1760
+ description: "Field machine name to delete (snake_case)"
1761
+ }
1762
+ },
1763
+ required: ["objectName", "fieldName"],
1764
+ additionalProperties: false
1765
+ }
1766
+ });
1767
+
1768
+ // src/tools/list-metadata-objects.tool.ts
1769
+ var import_ai5 = require("@objectstack/spec/ai");
1770
+ var listMetadataObjectsTool = (0, import_ai5.defineTool)({
1771
+ name: "list_metadata_objects",
1772
+ label: "List Metadata Objects",
1773
+ description: "Lists all registered metadata objects (tables) in the current environment. Use this when the user wants to see what tables, entities, or data models are defined in metadata.",
1774
+ category: "data",
1775
+ builtIn: true,
1776
+ parameters: {
1777
+ type: "object",
1778
+ properties: {
1779
+ filter: {
1780
+ type: "string",
1781
+ description: "Optional name or label substring to filter objects"
1782
+ },
1783
+ includeFields: {
1784
+ type: "boolean",
1785
+ description: "Whether to include field summaries for each object (default: false)"
1786
+ }
1787
+ },
1788
+ additionalProperties: false
1789
+ }
1790
+ });
1791
+
1792
+ // src/tools/describe-metadata-object.tool.ts
1793
+ var import_ai6 = require("@objectstack/spec/ai");
1794
+ var describeMetadataObjectTool = (0, import_ai6.defineTool)({
1795
+ name: "describe_metadata_object",
1796
+ label: "Describe Metadata Object",
1797
+ description: "Returns the full metadata schema details of a data object, including all fields, types, relationships, and configuration. Use this when the user wants to inspect or understand the metadata structure of a specific table or entity.",
1798
+ category: "data",
1799
+ builtIn: true,
1800
+ parameters: {
1801
+ type: "object",
1802
+ properties: {
1803
+ objectName: {
1804
+ type: "string",
1805
+ description: "Object machine name to describe (snake_case)"
1806
+ }
1807
+ },
1808
+ required: ["objectName"],
1809
+ additionalProperties: false
1810
+ }
1811
+ });
1812
+
1813
+ // src/tools/metadata-tools.ts
1814
+ var METADATA_TOOL_DEFINITIONS = [
1815
+ createObjectTool,
1816
+ addFieldTool,
1817
+ modifyFieldTool,
1818
+ deleteFieldTool,
1819
+ listMetadataObjectsTool,
1820
+ describeMetadataObjectTool
1821
+ ];
1822
+ var SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
1823
+ function isSnakeCase(value) {
1824
+ return SNAKE_CASE_RE.test(value);
1825
+ }
1826
+ function createCreateObjectHandler(ctx) {
1827
+ return async (args) => {
1828
+ const { name, label, fields, enableFeatures } = args;
1829
+ if (!name || !label) {
1830
+ return JSON.stringify({ error: 'Both "name" and "label" are required' });
1831
+ }
1832
+ if (!isSnakeCase(name)) {
1833
+ return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
1834
+ }
1835
+ const existing = await ctx.metadataService.getObject(name);
1836
+ if (existing) {
1837
+ return JSON.stringify({ error: `Object "${name}" already exists` });
1838
+ }
1839
+ const fieldMap = {};
1840
+ if (fields && Array.isArray(fields)) {
1841
+ const seenNames = /* @__PURE__ */ new Set();
1842
+ for (const f of fields) {
1843
+ if (!f.name) {
1844
+ return JSON.stringify({ error: 'Each field must have a "name" property' });
1845
+ }
1846
+ if (!isSnakeCase(f.name)) {
1847
+ return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
1848
+ }
1849
+ if (seenNames.has(f.name)) {
1850
+ return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
1851
+ }
1852
+ seenNames.add(f.name);
1853
+ fieldMap[f.name] = {
1854
+ type: f.type,
1855
+ ...f.label ? { label: f.label } : {},
1856
+ ...f.required !== void 0 ? { required: f.required } : {}
1857
+ };
1858
+ }
1859
+ }
1860
+ const objectDef = {
1861
+ name,
1862
+ label,
1863
+ ...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
1864
+ ...enableFeatures ? { enable: enableFeatures } : {}
1865
+ };
1866
+ await ctx.metadataService.register("object", name, objectDef);
1867
+ return JSON.stringify({
1868
+ name,
1869
+ label,
1870
+ fieldCount: Object.keys(fieldMap).length
1871
+ });
1872
+ };
1873
+ }
1874
+ function createAddFieldHandler(ctx) {
1875
+ return async (args) => {
1876
+ const { objectName, name, label, type, required, defaultValue, options, reference } = args;
1877
+ if (!objectName || !name || !type) {
1878
+ return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
1879
+ }
1880
+ if (!isSnakeCase(objectName)) {
1881
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1882
+ }
1883
+ if (!isSnakeCase(name)) {
1884
+ return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
1885
+ }
1886
+ if (reference && !isSnakeCase(reference)) {
1887
+ return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
1888
+ }
1889
+ if (options && Array.isArray(options)) {
1890
+ for (const opt of options) {
1891
+ if (opt.value && !isSnakeCase(opt.value)) {
1892
+ return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
1893
+ }
1894
+ }
1895
+ }
1896
+ const objectDef = await ctx.metadataService.getObject(objectName);
1897
+ if (!objectDef) {
1898
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
1899
+ }
1900
+ const def = objectDef;
1901
+ if (def.fields && def.fields[name]) {
1902
+ return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
1903
+ }
1904
+ const fieldDef = {
1905
+ type,
1906
+ ...label ? { label } : {},
1907
+ ...required !== void 0 ? { required } : {},
1908
+ ...defaultValue !== void 0 ? { defaultValue } : {},
1909
+ ...options ? { options } : {},
1910
+ ...reference ? { reference } : {}
1911
+ };
1912
+ const updatedFields = { ...def.fields ?? {}, [name]: fieldDef };
1913
+ await ctx.metadataService.register("object", objectName, {
1914
+ ...def,
1915
+ fields: updatedFields
1916
+ });
1917
+ return JSON.stringify({
1918
+ objectName,
1919
+ fieldName: name,
1920
+ fieldType: type
1921
+ });
1922
+ };
1923
+ }
1924
+ function createModifyFieldHandler(ctx) {
1925
+ return async (args) => {
1926
+ const { objectName, fieldName, changes } = args;
1927
+ if (!objectName || !fieldName || !changes) {
1928
+ return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
1929
+ }
1930
+ if (!isSnakeCase(objectName)) {
1931
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1932
+ }
1933
+ if (!isSnakeCase(fieldName)) {
1934
+ return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
1935
+ }
1936
+ const objectDef = await ctx.metadataService.getObject(objectName);
1937
+ if (!objectDef) {
1938
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
1939
+ }
1940
+ const def = objectDef;
1941
+ if (!def.fields || !def.fields[fieldName]) {
1942
+ return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
1943
+ }
1944
+ const existingField = def.fields[fieldName];
1945
+ const updatedField = { ...existingField, ...changes };
1946
+ const updatedFields = { ...def.fields, [fieldName]: updatedField };
1947
+ await ctx.metadataService.register("object", objectName, {
1948
+ ...def,
1949
+ fields: updatedFields
1950
+ });
1951
+ return JSON.stringify({
1952
+ objectName,
1953
+ fieldName,
1954
+ updatedProperties: Object.keys(changes)
1955
+ });
1956
+ };
1957
+ }
1958
+ function createDeleteFieldHandler(ctx) {
1959
+ return async (args) => {
1960
+ const { objectName, fieldName } = args;
1961
+ if (!objectName || !fieldName) {
1962
+ return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
1963
+ }
1964
+ if (!isSnakeCase(objectName)) {
1965
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
1966
+ }
1967
+ if (!isSnakeCase(fieldName)) {
1968
+ return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
1969
+ }
1970
+ const objectDef = await ctx.metadataService.getObject(objectName);
1971
+ if (!objectDef) {
1972
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
1973
+ }
1974
+ const def = objectDef;
1975
+ if (!def.fields || !def.fields[fieldName]) {
1976
+ return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
1977
+ }
1978
+ const { [fieldName]: _removed, ...remainingFields } = def.fields;
1979
+ await ctx.metadataService.register("object", objectName, {
1980
+ ...def,
1981
+ fields: remainingFields
1982
+ });
1983
+ return JSON.stringify({
1984
+ objectName,
1985
+ fieldName,
1986
+ success: true
1987
+ });
1988
+ };
1989
+ }
1990
+ function createListObjectsHandler2(ctx) {
1991
+ return async (args) => {
1992
+ const { filter, includeFields } = args ?? {};
1993
+ const objects = await ctx.metadataService.listObjects();
1994
+ let result = objects.map((o) => {
1995
+ const base = {
1996
+ name: o.name,
1997
+ label: o.label ?? o.name,
1998
+ fieldCount: o.fields ? Object.keys(o.fields).length : 0
1999
+ };
2000
+ if (includeFields && o.fields) {
2001
+ base.fields = Object.entries(o.fields).map(([key, f]) => ({
2002
+ name: key,
2003
+ type: f.type,
2004
+ label: f.label ?? key
2005
+ }));
2006
+ }
2007
+ return base;
2008
+ });
2009
+ if (filter) {
2010
+ const lower = filter.toLowerCase();
2011
+ result = result.filter(
2012
+ (o) => o.name.toLowerCase().includes(lower) || o.label.toLowerCase().includes(lower)
2013
+ );
2014
+ }
2015
+ return JSON.stringify({
2016
+ objects: result,
2017
+ totalCount: result.length
2018
+ });
2019
+ };
2020
+ }
2021
+ function createDescribeObjectHandler2(ctx) {
2022
+ return async (args) => {
2023
+ const { objectName } = args;
2024
+ if (!objectName) {
2025
+ return JSON.stringify({ error: '"objectName" is required' });
2026
+ }
2027
+ if (!isSnakeCase(objectName)) {
2028
+ return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
2029
+ }
2030
+ const objectDef = await ctx.metadataService.getObject(objectName);
2031
+ if (!objectDef) {
2032
+ return JSON.stringify({ error: `Object "${objectName}" not found` });
2033
+ }
2034
+ const def = objectDef;
2035
+ const fields = def.fields ?? {};
2036
+ const fieldSummary = Object.entries(fields).map(([key, f]) => ({
2037
+ name: key,
2038
+ type: f.type,
2039
+ label: f.label ?? key,
2040
+ required: f.required ?? false,
2041
+ ...f.reference ? { reference: f.reference } : {},
2042
+ ...f.options ? { options: f.options } : {}
2043
+ }));
2044
+ return JSON.stringify({
2045
+ name: def.name,
2046
+ label: def.label ?? def.name,
2047
+ fields: fieldSummary,
2048
+ enableFeatures: def.enable ?? {}
2049
+ });
2050
+ };
2051
+ }
2052
+ function registerMetadataTools(registry, context) {
2053
+ registry.register(createObjectTool, createCreateObjectHandler(context));
2054
+ registry.register(addFieldTool, createAddFieldHandler(context));
2055
+ registry.register(modifyFieldTool, createModifyFieldHandler(context));
2056
+ registry.register(deleteFieldTool, createDeleteFieldHandler(context));
2057
+ registry.register(listMetadataObjectsTool, createListObjectsHandler2(context));
2058
+ registry.register(describeMetadataObjectTool, createDescribeObjectHandler2(context));
2059
+ }
2060
+
2061
+ // src/agent-runtime.ts
2062
+ var import_ai7 = require("@objectstack/spec/ai");
1309
2063
  var AgentRuntime = class {
1310
2064
  constructor(metadataService) {
1311
2065
  this.metadataService = metadataService;
1312
2066
  }
1313
2067
  // ── Public API ────────────────────────────────────────────────
2068
+ /**
2069
+ * List all active agents registered in the metadata service.
2070
+ *
2071
+ * Returns a summary for each agent (name, label, role) suitable
2072
+ * for populating an agent selector dropdown in the UI.
2073
+ */
2074
+ async listAgents() {
2075
+ const rawItems = await this.metadataService.list("agent");
2076
+ const agents = [];
2077
+ for (const raw of rawItems) {
2078
+ const result = import_ai7.AgentSchema.safeParse(raw);
2079
+ if (result.success && result.data.active) {
2080
+ agents.push({
2081
+ name: result.data.name,
2082
+ label: result.data.label,
2083
+ role: result.data.role
2084
+ });
2085
+ }
2086
+ }
2087
+ return agents;
2088
+ }
1314
2089
  /**
1315
2090
  * Load and validate an agent definition by name.
1316
2091
  *
@@ -1322,7 +2097,7 @@ var AgentRuntime = class {
1322
2097
  async loadAgent(agentName) {
1323
2098
  const raw = await this.metadataService.get("agent", agentName);
1324
2099
  if (!raw) return void 0;
1325
- const result = import_ai.AgentSchema.safeParse(raw);
2100
+ const result = import_ai7.AgentSchema.safeParse(raw);
1326
2101
  if (!result.success) {
1327
2102
  return void 0;
1328
2103
  }
@@ -1439,15 +2214,233 @@ Guidelines:
1439
2214
  }
1440
2215
  };
1441
2216
 
2217
+ // src/agents/metadata-assistant-agent.ts
2218
+ var METADATA_ASSISTANT_AGENT = {
2219
+ name: "metadata_assistant",
2220
+ label: "Metadata Assistant",
2221
+ role: "Schema Architect",
2222
+ instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
2223
+
2224
+ Capabilities:
2225
+ - Create new data objects (tables) with fields
2226
+ - Add fields (columns) to existing objects
2227
+ - Modify field properties (label, type, required, default value)
2228
+ - Delete fields from objects
2229
+ - List all registered metadata objects and their schemas
2230
+ - Describe the full schema of a specific object
2231
+
2232
+ Guidelines:
2233
+ 1. Before creating a new object, use list_metadata_objects to check if a similar one already exists.
2234
+ 2. Before modifying or deleting fields, use describe_metadata_object to understand the current schema.
2235
+ 3. Always use snake_case for object names and field names (e.g. project_task, due_date).
2236
+ 4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
2237
+ 5. When creating objects, propose a reasonable set of initial fields based on the entity type.
2238
+ 6. Explain what changes you are about to make before executing them.
2239
+ 7. After making changes, confirm the result by describing the updated schema.
2240
+ 8. For destructive operations (deleting fields), always warn the user about potential data loss.
2241
+ 9. Always answer in the same language the user is using.
2242
+ 10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
2243
+ model: {
2244
+ provider: "openai",
2245
+ model: "gpt-4",
2246
+ temperature: 0.2,
2247
+ maxTokens: 4096
2248
+ },
2249
+ tools: [
2250
+ { type: "action", name: "create_object", description: "Create a new data object (table)" },
2251
+ { type: "action", name: "add_field", description: "Add a field to an existing object" },
2252
+ { type: "action", name: "modify_field", description: "Modify an existing field definition" },
2253
+ { type: "action", name: "delete_field", description: "Delete a field from an object" },
2254
+ { type: "query", name: "list_metadata_objects", description: "List all metadata objects" },
2255
+ { type: "query", name: "describe_metadata_object", description: "Describe an object schema" }
2256
+ ],
2257
+ active: true,
2258
+ visibility: "global",
2259
+ guardrails: {
2260
+ maxTokensPerInvocation: 8192,
2261
+ maxExecutionTimeSec: 60,
2262
+ blockedTopics: ["drop_database", "raw_sql", "system_tables"]
2263
+ },
2264
+ planning: {
2265
+ strategy: "react",
2266
+ maxIterations: 10,
2267
+ allowReplan: true
2268
+ },
2269
+ memory: {
2270
+ shortTerm: {
2271
+ maxMessages: 30,
2272
+ maxTokens: 8192
2273
+ }
2274
+ }
2275
+ };
2276
+
2277
+ // src/adapters/vercel-adapter.ts
2278
+ var import_ai8 = require("ai");
2279
+ function buildVercelOptions(options) {
2280
+ if (!options) return {};
2281
+ const opts = {};
2282
+ if (options.temperature != null) opts.temperature = options.temperature;
2283
+ if (options.maxTokens != null) opts.maxTokens = options.maxTokens;
2284
+ if (options.stop?.length) opts.stopSequences = options.stop;
2285
+ if (options.tools?.length) {
2286
+ const tools = {};
2287
+ for (const t of options.tools) {
2288
+ tools[t.name] = (0, import_ai8.tool)({
2289
+ description: t.description,
2290
+ inputSchema: (0, import_ai8.jsonSchema)(t.parameters)
2291
+ });
2292
+ }
2293
+ opts.tools = tools;
2294
+ }
2295
+ if (options.toolChoice != null) {
2296
+ opts.toolChoice = options.toolChoice;
2297
+ }
2298
+ return opts;
2299
+ }
2300
+ var VercelLLMAdapter = class {
2301
+ constructor(config) {
2302
+ this.name = "vercel";
2303
+ this.model = config.model;
2304
+ }
2305
+ async chat(messages, options) {
2306
+ const result = await (0, import_ai8.generateText)({
2307
+ model: this.model,
2308
+ messages,
2309
+ ...buildVercelOptions(options)
2310
+ });
2311
+ return {
2312
+ content: result.text,
2313
+ model: result.response?.modelId,
2314
+ toolCalls: result.toolCalls?.length ? result.toolCalls : void 0,
2315
+ usage: result.usage ? {
2316
+ promptTokens: result.usage.inputTokens ?? 0,
2317
+ completionTokens: result.usage.outputTokens ?? 0,
2318
+ totalTokens: result.usage.totalTokens ?? 0
2319
+ } : void 0
2320
+ };
2321
+ }
2322
+ async complete(prompt, options) {
2323
+ const result = await (0, import_ai8.generateText)({
2324
+ model: this.model,
2325
+ prompt,
2326
+ ...buildVercelOptions(options)
2327
+ });
2328
+ return {
2329
+ content: result.text,
2330
+ model: result.response?.modelId,
2331
+ usage: result.usage ? {
2332
+ promptTokens: result.usage.inputTokens ?? 0,
2333
+ completionTokens: result.usage.outputTokens ?? 0,
2334
+ totalTokens: result.usage.totalTokens ?? 0
2335
+ } : void 0
2336
+ };
2337
+ }
2338
+ async *streamChat(messages, options) {
2339
+ const result = (0, import_ai8.streamText)({
2340
+ model: this.model,
2341
+ messages,
2342
+ ...buildVercelOptions(options)
2343
+ });
2344
+ for await (const part of result.fullStream) {
2345
+ yield part;
2346
+ }
2347
+ }
2348
+ async embed(_input) {
2349
+ throw new Error(
2350
+ "[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
2351
+ );
2352
+ }
2353
+ async listModels() {
2354
+ return [];
2355
+ }
2356
+ };
2357
+
1442
2358
  // src/plugin.ts
1443
2359
  var AIServicePlugin = class {
1444
2360
  constructor(options = {}) {
1445
2361
  this.name = "com.objectstack.service-ai";
1446
2362
  this.version = "1.0.0";
1447
2363
  this.type = "standard";
1448
- this.dependencies = [];
2364
+ this.dependencies = ["com.objectstack.engine.objectql"];
1449
2365
  this.options = options;
1450
2366
  }
2367
+ /**
2368
+ * Auto-detect LLM provider from environment variables.
2369
+ *
2370
+ * Priority order:
2371
+ * 1. AI_GATEWAY_MODEL → Vercel AI Gateway
2372
+ * 2. OPENAI_API_KEY → OpenAI
2373
+ * 3. ANTHROPIC_API_KEY → Anthropic
2374
+ * 4. GOOGLE_GENERATIVE_AI_API_KEY → Google
2375
+ * 5. Fallback → MemoryLLMAdapter
2376
+ *
2377
+ * Returns the adapter and a description for logging.
2378
+ */
2379
+ async detectAdapter(ctx) {
2380
+ const gatewayModel = process.env.AI_GATEWAY_MODEL;
2381
+ if (gatewayModel) {
2382
+ try {
2383
+ const gatewayPkg = "@ai-sdk/gateway";
2384
+ const { gateway } = await import(
2385
+ /* webpackIgnore: true */
2386
+ gatewayPkg
2387
+ );
2388
+ const adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) });
2389
+ return { adapter, description: `Vercel AI Gateway (model: ${gatewayModel})` };
2390
+ } catch (err) {
2391
+ ctx.logger.warn(
2392
+ `[AI] Failed to load @ai-sdk/gateway for AI_GATEWAY_MODEL=${gatewayModel}, trying next provider`,
2393
+ err instanceof Error ? { error: err.message } : void 0
2394
+ );
2395
+ }
2396
+ }
2397
+ const providerConfigs = [
2398
+ {
2399
+ envKey: "OPENAI_API_KEY",
2400
+ pkg: "@ai-sdk/openai",
2401
+ factory: "openai",
2402
+ defaultModel: "gpt-4o",
2403
+ displayName: "OpenAI"
2404
+ },
2405
+ {
2406
+ envKey: "ANTHROPIC_API_KEY",
2407
+ pkg: "@ai-sdk/anthropic",
2408
+ factory: "anthropic",
2409
+ defaultModel: "claude-sonnet-4-20250514",
2410
+ displayName: "Anthropic"
2411
+ },
2412
+ {
2413
+ envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
2414
+ pkg: "@ai-sdk/google",
2415
+ factory: "google",
2416
+ defaultModel: "gemini-2.0-flash",
2417
+ displayName: "Google"
2418
+ }
2419
+ ];
2420
+ for (const { envKey, pkg, factory, defaultModel, displayName } of providerConfigs) {
2421
+ if (process.env[envKey]) {
2422
+ try {
2423
+ const mod = await import(
2424
+ /* webpackIgnore: true */
2425
+ pkg
2426
+ );
2427
+ const createModel = mod[factory] ?? mod.default;
2428
+ if (typeof createModel === "function") {
2429
+ const modelId = process.env.AI_MODEL ?? defaultModel;
2430
+ const adapter = new VercelLLMAdapter({ model: createModel(modelId) });
2431
+ return { adapter, description: `${displayName} (model: ${modelId})` };
2432
+ }
2433
+ } catch (err) {
2434
+ ctx.logger.warn(
2435
+ `[AI] Failed to load ${pkg} for ${envKey}, trying next provider`,
2436
+ err instanceof Error ? { error: err.message } : void 0
2437
+ );
2438
+ }
2439
+ }
2440
+ }
2441
+ 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.");
2442
+ return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode - for testing only)" };
2443
+ }
1451
2444
  async init(ctx) {
1452
2445
  let hasExisting = false;
1453
2446
  try {
@@ -1469,8 +2462,19 @@ var AIServicePlugin = class {
1469
2462
  } catch {
1470
2463
  }
1471
2464
  }
2465
+ let adapter;
2466
+ let adapterDescription;
2467
+ if (this.options.adapter) {
2468
+ adapter = this.options.adapter;
2469
+ adapterDescription = `${adapter.name} (explicitly configured)`;
2470
+ } else {
2471
+ const detected = await this.detectAdapter(ctx);
2472
+ adapter = detected.adapter;
2473
+ adapterDescription = detected.description;
2474
+ }
2475
+ ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
1472
2476
  const config = {
1473
- adapter: this.options.adapter,
2477
+ adapter,
1474
2478
  logger: ctx.logger,
1475
2479
  conversationService
1476
2480
  };
@@ -1480,7 +2484,7 @@ var AIServicePlugin = class {
1480
2484
  } else {
1481
2485
  ctx.registerService("ai", this.service);
1482
2486
  }
1483
- ctx.registerService("app.com.objectstack.service-ai", {
2487
+ ctx.getService("manifest").register({
1484
2488
  id: "com.objectstack.service-ai",
1485
2489
  name: "AI Service",
1486
2490
  version: "1.0.0",
@@ -1493,13 +2497,32 @@ var AIServicePlugin = class {
1493
2497
  ctx.logger.debug("[AI] Before chat", { messages });
1494
2498
  });
1495
2499
  }
2500
+ try {
2501
+ const setupNav = ctx.getService("setupNav");
2502
+ if (setupNav) {
2503
+ setupNav.contribute({
2504
+ areaId: "area_ai",
2505
+ items: [
2506
+ { id: "nav_ai_conversations", type: "object", label: { key: "setup.nav.ai_conversations", defaultValue: "Conversations" }, objectName: "conversations", icon: "message-square", order: 10 },
2507
+ { id: "nav_ai_messages", type: "object", label: { key: "setup.nav.ai_messages", defaultValue: "Messages" }, objectName: "messages", icon: "messages-square", order: 20 }
2508
+ ]
2509
+ });
2510
+ ctx.logger.info("[AI] Navigation items contributed to Setup App");
2511
+ }
2512
+ } catch {
2513
+ }
1496
2514
  ctx.logger.info("[AI] Service initialized");
1497
2515
  }
1498
2516
  async start(ctx) {
1499
2517
  if (!this.service) return;
2518
+ let metadataService;
2519
+ try {
2520
+ metadataService = ctx.getService("metadata");
2521
+ } catch {
2522
+ ctx.logger.debug("[AI] Metadata service not available");
2523
+ }
1500
2524
  try {
1501
2525
  const dataEngine = ctx.getService("data");
1502
- const metadataService = ctx.getService("metadata");
1503
2526
  if (dataEngine && metadataService) {
1504
2527
  registerDataTools(this.service.toolRegistry, { dataEngine, metadataService });
1505
2528
  ctx.logger.info("[AI] Built-in data tools registered");
@@ -1512,14 +2535,29 @@ var AIServicePlugin = class {
1512
2535
  }
1513
2536
  }
1514
2537
  } catch {
1515
- ctx.logger.debug("[AI] Data engine or metadata service not available, skipping data tools");
2538
+ ctx.logger.debug("[AI] Data engine not available, skipping data tools");
2539
+ }
2540
+ if (metadataService) {
2541
+ try {
2542
+ registerMetadataTools(this.service.toolRegistry, { metadataService });
2543
+ ctx.logger.info("[AI] Built-in metadata tools registered");
2544
+ const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
2545
+ if (!agentExists) {
2546
+ await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
2547
+ ctx.logger.info("[AI] metadata_assistant agent registered");
2548
+ } else {
2549
+ ctx.logger.debug("[AI] metadata_assistant agent already exists, skipping auto-registration");
2550
+ }
2551
+ } catch (err) {
2552
+ ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
2553
+ }
1516
2554
  }
1517
2555
  await ctx.trigger("ai:ready", this.service);
1518
2556
  const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
1519
2557
  try {
1520
- const metadataService = ctx.getService("metadata");
1521
- if (metadataService) {
1522
- const agentRuntime = new AgentRuntime(metadataService);
2558
+ const metadataService2 = ctx.getService("metadata");
2559
+ if (metadataService2) {
2560
+ const agentRuntime = new AgentRuntime(metadataService2);
1523
2561
  const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
1524
2562
  routes.push(...agentRoutes);
1525
2563
  }
@@ -1527,6 +2565,10 @@ var AIServicePlugin = class {
1527
2565
  ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
1528
2566
  }
1529
2567
  await ctx.trigger("ai:routes", routes);
2568
+ const kernel = ctx.getKernel();
2569
+ if (kernel) {
2570
+ kernel.__aiRoutes = routes;
2571
+ }
1530
2572
  ctx.logger.info(
1531
2573
  `[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
1532
2574
  );
@@ -1545,11 +2587,23 @@ var AIServicePlugin = class {
1545
2587
  DATA_CHAT_AGENT,
1546
2588
  DATA_TOOL_DEFINITIONS,
1547
2589
  InMemoryConversationService,
2590
+ METADATA_ASSISTANT_AGENT,
2591
+ METADATA_TOOL_DEFINITIONS,
1548
2592
  MemoryLLMAdapter,
1549
2593
  ObjectQLConversationService,
1550
2594
  ToolRegistry,
2595
+ VercelLLMAdapter,
2596
+ addFieldTool,
1551
2597
  buildAIRoutes,
1552
2598
  buildAgentRoutes,
1553
- registerDataTools
2599
+ createObjectTool,
2600
+ deleteFieldTool,
2601
+ describeMetadataObjectTool,
2602
+ encodeStreamPart,
2603
+ encodeVercelDataStream,
2604
+ listMetadataObjectsTool,
2605
+ modifyFieldTool,
2606
+ registerDataTools,
2607
+ registerMetadataTools
1554
2608
  });
1555
2609
  //# sourceMappingURL=index.cjs.map