@objectstack/service-ai 5.2.0 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -124,7 +124,9 @@ var init_data_tools = __esm({
124
124
  properties: {
125
125
  field: { type: "string" },
126
126
  order: { type: "string", enum: ["asc", "desc"] }
127
- }
127
+ },
128
+ required: ["field", "order"],
129
+ additionalProperties: false
128
130
  },
129
131
  description: 'Sort order (e.g. [{ "field": "created_at", "order": "desc" }])'
130
132
  },
@@ -194,7 +196,8 @@ var init_data_tools = __esm({
194
196
  description: "Result column alias"
195
197
  }
196
198
  },
197
- required: ["function", "alias"]
199
+ required: ["function", "alias"],
200
+ additionalProperties: false
198
201
  },
199
202
  description: "Aggregation definitions"
200
203
  },
@@ -845,29 +848,45 @@ var init_metadata_tools = __esm({
845
848
  // src/index.ts
846
849
  var index_exports = {};
847
850
  __export(index_exports, {
851
+ ACTIONS_EXECUTOR_SKILL: () => ACTIONS_EXECUTOR_SKILL,
848
852
  AIService: () => AIService,
849
853
  AIServicePlugin: () => AIServicePlugin,
850
854
  AgentRuntime: () => AgentRuntime,
851
855
  AiConversationObject: () => AiConversationObject,
852
856
  AiMessageObject: () => AiMessageObject,
857
+ AiTraceObject: () => AiTraceObject,
858
+ AiTraceView: () => AiTraceView,
853
859
  DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
860
+ DATA_EXPLORER_SKILL: () => DATA_EXPLORER_SKILL,
854
861
  DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
855
862
  InMemoryConversationService: () => InMemoryConversationService,
856
863
  METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
864
+ METADATA_AUTHORING_SKILL: () => METADATA_AUTHORING_SKILL,
857
865
  METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
858
866
  MemoryLLMAdapter: () => MemoryLLMAdapter,
867
+ ModelRegistry: () => ModelRegistry,
868
+ NullTraceRecorder: () => NullTraceRecorder,
859
869
  ObjectQLConversationService: () => ObjectQLConversationService,
870
+ ObjectQLTraceRecorder: () => ObjectQLTraceRecorder,
860
871
  PACKAGE_TOOL_DEFINITIONS: () => PACKAGE_TOOL_DEFINITIONS,
872
+ QUERY_DATA_TOOL: () => QUERY_DATA_TOOL,
873
+ SchemaRetriever: () => SchemaRetriever,
861
874
  SkillRegistry: () => SkillRegistry,
862
875
  ToolRegistry: () => ToolRegistry,
863
876
  VercelLLMAdapter: () => VercelLLMAdapter,
877
+ actionSkipReason: () => actionSkipReason,
878
+ actionToToolDefinition: () => actionToToolDefinition,
879
+ actionToolName: () => actionToolName,
864
880
  addFieldTool: () => addFieldTool,
865
881
  buildAIRoutes: () => buildAIRoutes,
866
882
  buildAgentRoutes: () => buildAgentRoutes,
867
883
  buildAssistantRoutes: () => buildAssistantRoutes,
868
884
  buildToolRoutes: () => buildToolRoutes,
885
+ buildTraceEvent: () => buildTraceEvent,
886
+ computeCost: () => computeCost,
869
887
  createObjectTool: () => createObjectTool,
870
888
  createPackageTool: () => createPackageTool,
889
+ createQueryDataHandler: () => createQueryDataHandler,
871
890
  deleteFieldTool: () => deleteFieldTool,
872
891
  describeObjectTool: () => describeObjectTool,
873
892
  encodeStreamPart: () => encodeStreamPart,
@@ -877,9 +896,11 @@ __export(index_exports, {
877
896
  listObjectsTool: () => listObjectsTool,
878
897
  listPackagesTool: () => listPackagesTool,
879
898
  modifyFieldTool: () => modifyFieldTool,
899
+ registerActionsAsTools: () => registerActionsAsTools,
880
900
  registerDataTools: () => registerDataTools,
881
901
  registerMetadataTools: () => registerMetadataTools,
882
902
  registerPackageTools: () => registerPackageTools,
903
+ registerQueryDataTool: () => registerQueryDataTool,
883
904
  setActivePackageTool: () => setActivePackageTool
884
905
  });
885
906
  module.exports = __toCommonJS(index_exports);
@@ -895,8 +916,112 @@ var MemoryLLMAdapter = class {
895
916
  async chat(messages, options) {
896
917
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
897
918
  const userContent = lastUserMessage?.content;
898
- const text = typeof userContent === "string" ? userContent : "(complex content)";
899
- const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
919
+ const userText = typeof userContent === "string" ? userContent : "(complex content)";
920
+ const tools = options?.tools;
921
+ const hasQueryDataTool = Array.isArray(tools) && tools.some((t) => t?.name === "query_data");
922
+ const alreadyCalledQueryData = messages.some(
923
+ (m) => m.role === "tool" && Array.isArray(m.content) && m.content.some((c) => c?.toolName === "query_data")
924
+ );
925
+ const alreadyCalledAction = messages.some(
926
+ (m) => m.role === "tool" && Array.isArray(m.content) && m.content.some(
927
+ (c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")
928
+ )
929
+ );
930
+ if (Array.isArray(tools) && !alreadyCalledAction && lastUserMessage) {
931
+ const actionTools = tools.filter((t) => typeof t?.name === "string" && t.name.startsWith("action_"));
932
+ const chosen = pickActionTool(userText, actionTools);
933
+ if (chosen) {
934
+ const recordId = extractRecordIdFromMessages(messages, userText);
935
+ if (recordId) {
936
+ const toolCallId = `memory_tc_${Date.now().toString(36)}`;
937
+ return {
938
+ content: "",
939
+ model: options?.model ?? "memory",
940
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
941
+ toolCalls: [
942
+ {
943
+ type: "tool-call",
944
+ toolCallId,
945
+ toolName: chosen.name,
946
+ input: { recordId }
947
+ }
948
+ ]
949
+ };
950
+ }
951
+ }
952
+ }
953
+ if (hasQueryDataTool && !alreadyCalledQueryData && lastUserMessage) {
954
+ const toolCallId = `memory_tc_${Date.now().toString(36)}`;
955
+ return {
956
+ content: "",
957
+ model: options?.model ?? "memory",
958
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
959
+ toolCalls: [
960
+ {
961
+ type: "tool-call",
962
+ toolCallId,
963
+ toolName: "query_data",
964
+ input: { request: userText }
965
+ }
966
+ ]
967
+ };
968
+ }
969
+ if (alreadyCalledAction) {
970
+ const lastTool = [...messages].reverse().find((m) => m.role === "tool");
971
+ const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")) : void 0;
972
+ const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
973
+ let payload = {};
974
+ if (typeof raw === "string") {
975
+ try {
976
+ payload = JSON.parse(raw);
977
+ } catch {
978
+ }
979
+ } else if (raw && typeof raw === "object") {
980
+ payload = raw;
981
+ }
982
+ if (payload.error) {
983
+ return {
984
+ content: `[memory] action ${payload.action ?? ""} failed: ${payload.error}`,
985
+ model: options?.model ?? "memory",
986
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
987
+ };
988
+ }
989
+ return {
990
+ content: `[memory] ${payload.message ?? "Action executed."} (${payload.action ?? "action"})`,
991
+ model: options?.model ?? "memory",
992
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
993
+ };
994
+ }
995
+ if (alreadyCalledQueryData) {
996
+ const lastTool = [...messages].reverse().find((m) => m.role === "tool");
997
+ const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => c?.toolName === "query_data") : void 0;
998
+ let payload = {};
999
+ const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
1000
+ if (typeof raw === "string") {
1001
+ try {
1002
+ payload = JSON.parse(raw);
1003
+ } catch {
1004
+ payload = {};
1005
+ }
1006
+ } else if (raw && typeof raw === "object") {
1007
+ payload = raw;
1008
+ }
1009
+ if (payload.error) {
1010
+ return {
1011
+ content: `[memory] query_data failed: ${payload.error}`,
1012
+ model: options?.model ?? "memory",
1013
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
1014
+ };
1015
+ }
1016
+ const records = payload.records ?? [];
1017
+ const count = payload.count ?? records.length;
1018
+ return {
1019
+ content: `[memory] Found ${count} record${count === 1 ? "" : "s"} for "${userText}".`,
1020
+ model: options?.model ?? "memory",
1021
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
1022
+ };
1023
+ }
1024
+ const content = lastUserMessage ? `[memory] ${userText}` : "[memory] (no user message)";
900
1025
  return {
901
1026
  content,
902
1027
  model: options?.model ?? "memory",
@@ -931,7 +1056,171 @@ var MemoryLLMAdapter = class {
931
1056
  async listModels() {
932
1057
  return ["memory"];
933
1058
  }
1059
+ /**
1060
+ * Heuristic structured-output for testing & demos — NOT a real LLM.
1061
+ *
1062
+ * Strategy:
1063
+ * 1. Extract candidate object names from the system messages by matching
1064
+ * schema-context headers (`### name — Label`) emitted by
1065
+ * {@link SchemaRetriever.renderSnippet}.
1066
+ * 2. Pick the candidate whose tokens overlap most with the last user
1067
+ * message (falls back to the first candidate).
1068
+ * 3. Try `schema.safeParse({ objectName, limit: 20 })` — this satisfies the
1069
+ * `QueryPlanSchema` used by the built-in `query_data` tool.
1070
+ * 4. If that fails, fall back to `schema.safeParse({})` for schemas that
1071
+ * accept defaults.
1072
+ * 5. Otherwise throw with a clear message — the demo needs a real provider.
1073
+ */
1074
+ async generateObject(messages, schema, options) {
1075
+ const sys = messages.filter((m) => m.role === "system").map((m) => typeof m.content === "string" ? m.content : "").join("\n");
1076
+ const headerRe = /^###\s+([a-z0-9_]+)(?:\s+—\s+([^\n]+))?/gim;
1077
+ const candidates = [];
1078
+ for (const match of sys.matchAll(headerRe)) {
1079
+ const machineName = match[1];
1080
+ if (!machineName) continue;
1081
+ const aliasText = match[2] ?? "";
1082
+ const aliasTokens = /* @__PURE__ */ new Set();
1083
+ for (const t of machineName.split(/[^a-z0-9]+/)) {
1084
+ if (t) aliasTokens.add(t);
1085
+ }
1086
+ for (const t of aliasText.toLowerCase().split(/[^a-z0-9]+/)) {
1087
+ if (t) aliasTokens.add(t);
1088
+ }
1089
+ for (const t of [...aliasTokens]) {
1090
+ if (t.length > 3 && t.endsWith("s")) aliasTokens.add(t.slice(0, -1));
1091
+ }
1092
+ candidates.push({ name: machineName, aliasTokens });
1093
+ }
1094
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
1095
+ const userText = typeof lastUser?.content === "string" ? lastUser.content.toLowerCase() : "";
1096
+ const userTokens = new Set(
1097
+ userText.split(/[^a-z0-9_]+/).filter((t) => t.length > 1)
1098
+ );
1099
+ for (const t of [...userTokens]) {
1100
+ if (t.length > 3 && t.endsWith("s")) userTokens.add(t.slice(0, -1));
1101
+ }
1102
+ let chosen = candidates[0]?.name;
1103
+ let bestScore = -1;
1104
+ for (const cand of candidates) {
1105
+ let score = 0;
1106
+ for (const tok of cand.aliasTokens) {
1107
+ if (userTokens.has(tok)) score += 1;
1108
+ }
1109
+ if (score > bestScore) {
1110
+ bestScore = score;
1111
+ chosen = cand.name;
1112
+ }
1113
+ }
1114
+ const attempts = [];
1115
+ if (chosen) attempts.push({ objectName: chosen, limit: 20 });
1116
+ attempts.push({});
1117
+ for (const attempt of attempts) {
1118
+ const result = schema.safeParse(attempt);
1119
+ if (result.success) {
1120
+ return {
1121
+ object: result.data,
1122
+ model: options?.model ?? "memory",
1123
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
1124
+ };
1125
+ }
1126
+ }
1127
+ throw new Error(
1128
+ "MemoryLLMAdapter.generateObject: unable to synthesise a value for the requested schema. The memory adapter only handles QueryPlan-shaped schemas \u2014 wire a real LLM adapter (OpenAI / Anthropic / Google) for arbitrary structured output."
1129
+ );
1130
+ }
934
1131
  };
1132
+ function pickActionTool(userText, actionTools) {
1133
+ if (actionTools.length === 0 || !userText) return null;
1134
+ const userTokens = new Set(
1135
+ userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2)
1136
+ );
1137
+ const ACTION_VERBS = /* @__PURE__ */ new Set([
1138
+ "complete",
1139
+ "finish",
1140
+ "done",
1141
+ "close",
1142
+ "start",
1143
+ "begin",
1144
+ "resume",
1145
+ "clone",
1146
+ "copy",
1147
+ "duplicate",
1148
+ "cancel",
1149
+ "abort",
1150
+ "archive",
1151
+ "restore",
1152
+ "approve",
1153
+ "reject",
1154
+ "assign",
1155
+ "unassign",
1156
+ "export",
1157
+ "import",
1158
+ "send",
1159
+ "notify",
1160
+ "publish",
1161
+ "unpublish",
1162
+ "mark"
1163
+ ]);
1164
+ const hasActionVerb = [...userTokens].some((t) => ACTION_VERBS.has(t));
1165
+ if (!hasActionVerb) return null;
1166
+ let best = null;
1167
+ let bestScore = 0;
1168
+ for (const tool of actionTools) {
1169
+ const nameTokens = tool.name.replace(/^action_/, "").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
1170
+ let score = 0;
1171
+ for (const tok of nameTokens) {
1172
+ if (!userTokens.has(tok)) continue;
1173
+ score += ACTION_VERBS.has(tok) ? 3 : 1;
1174
+ }
1175
+ if (score > bestScore) {
1176
+ bestScore = score;
1177
+ best = tool;
1178
+ }
1179
+ }
1180
+ return bestScore >= 3 ? best : null;
1181
+ }
1182
+ function extractRecordIdFromMessages(messages, userText) {
1183
+ const userTokens = userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
1184
+ for (let i = messages.length - 1; i >= 0; i--) {
1185
+ const m = messages[i];
1186
+ if (m.role !== "tool" || !Array.isArray(m.content)) continue;
1187
+ const parts = m.content;
1188
+ for (const part of parts) {
1189
+ if (part?.toolName !== "query_data") continue;
1190
+ const raw = part.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part.result;
1191
+ let payload = {};
1192
+ if (typeof raw === "string") {
1193
+ try {
1194
+ payload = JSON.parse(raw);
1195
+ } catch {
1196
+ }
1197
+ } else if (raw && typeof raw === "object") {
1198
+ payload = raw;
1199
+ }
1200
+ const records = payload.records ?? [];
1201
+ if (records.length === 0) continue;
1202
+ let bestId;
1203
+ let bestScore = -1;
1204
+ for (const rec of records) {
1205
+ if (!rec || typeof rec !== "object") continue;
1206
+ const id = rec.id;
1207
+ if (typeof id !== "string" && typeof id !== "number") continue;
1208
+ const hay = Object.values(rec).filter((v) => typeof v === "string").join(" ").toLowerCase();
1209
+ const hayTokens = hay.split(/[^a-z0-9]+/).filter(Boolean);
1210
+ let score = 0;
1211
+ for (const ut of userTokens) {
1212
+ if (hayTokens.includes(ut)) score += 1;
1213
+ }
1214
+ if (score > bestScore) {
1215
+ bestScore = score;
1216
+ bestId = String(id);
1217
+ }
1218
+ }
1219
+ return bestId ?? String(records[0].id);
1220
+ }
1221
+ }
1222
+ return void 0;
1223
+ }
935
1224
 
936
1225
  // src/tools/tool-registry.ts
937
1226
  var ToolRegistry = class {
@@ -1097,6 +1386,70 @@ var InMemoryConversationService = class {
1097
1386
  }
1098
1387
  };
1099
1388
 
1389
+ // src/trace-recorder.ts
1390
+ var import_node_crypto = require("crypto");
1391
+ var TRACE_OBJECT = "ai_traces";
1392
+ var NullTraceRecorder = class {
1393
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1394
+ record(_event) {
1395
+ }
1396
+ };
1397
+ var ObjectQLTraceRecorder = class {
1398
+ constructor(engine, options = {}) {
1399
+ this.engine = engine;
1400
+ this.logger = options.logger;
1401
+ }
1402
+ async record(event) {
1403
+ const row = {
1404
+ id: `trace_${(0, import_node_crypto.randomUUID)()}`,
1405
+ conversation_id: event.conversationId ?? null,
1406
+ agent_id: event.agentId ?? null,
1407
+ operation: event.operation,
1408
+ model: event.model ?? null,
1409
+ adapter: event.adapter,
1410
+ prompt_tokens: event.promptTokens,
1411
+ completion_tokens: event.completionTokens,
1412
+ total_tokens: event.totalTokens,
1413
+ input_cost: event.cost?.inputCost ?? null,
1414
+ output_cost: event.cost?.outputCost ?? null,
1415
+ total_cost: event.cost?.totalCost ?? null,
1416
+ currency: event.cost?.currency ?? null,
1417
+ latency_ms: event.latencyMs,
1418
+ status: event.status,
1419
+ error: event.error ?? null,
1420
+ metadata: event.metadata ? JSON.stringify(event.metadata) : null,
1421
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1422
+ };
1423
+ try {
1424
+ await this.engine.insert(TRACE_OBJECT, row);
1425
+ } catch (err) {
1426
+ this.logger?.warn(
1427
+ "[AI] Failed to record trace (non-fatal)",
1428
+ err instanceof Error ? { error: err.message } : { error: String(err) }
1429
+ );
1430
+ }
1431
+ }
1432
+ };
1433
+ function buildTraceEvent(input) {
1434
+ const usage = input.usage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1435
+ const cost = input.model && input.registry ? input.registry.estimateCost(input.model, usage) : void 0;
1436
+ return {
1437
+ operation: input.operation,
1438
+ adapter: input.adapter,
1439
+ model: input.model,
1440
+ agentId: input.agentId,
1441
+ conversationId: input.conversationId,
1442
+ promptTokens: usage.promptTokens,
1443
+ completionTokens: usage.completionTokens,
1444
+ totalTokens: usage.totalTokens,
1445
+ latencyMs: input.latencyMs,
1446
+ status: input.status,
1447
+ error: input.error,
1448
+ cost,
1449
+ metadata: input.metadata
1450
+ };
1451
+ }
1452
+
1100
1453
  // src/ai-service.ts
1101
1454
  function textDeltaPart(id, text) {
1102
1455
  return { type: "text-delta", id, text };
@@ -1115,22 +1468,83 @@ var _AIService = class _AIService {
1115
1468
  this.logger = config.logger ?? (0, import_core.createLogger)({ level: "info", format: "pretty" });
1116
1469
  this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
1117
1470
  this.conversationService = config.conversationService ?? new InMemoryConversationService();
1471
+ this.modelRegistry = config.modelRegistry;
1472
+ this.traceRecorder = config.traceRecorder ?? new NullTraceRecorder();
1118
1473
  this.logger.info(
1119
- `[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}`
1474
+ `[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}, models=${this.modelRegistry?.size ?? 0}`
1120
1475
  );
1121
1476
  }
1122
1477
  /** The name of the active LLM adapter. */
1123
1478
  get adapterName() {
1124
1479
  return this.adapter.name;
1125
1480
  }
1481
+ /**
1482
+ * Run an adapter call and emit a trace event.
1483
+ *
1484
+ * Records both success and failure. Tracing failures never escape — the
1485
+ * recorder is expected to be defensive.
1486
+ */
1487
+ async instrument(operation, options, fn) {
1488
+ const started = Date.now();
1489
+ try {
1490
+ const result = await fn();
1491
+ void this.traceRecorder.record(buildTraceEvent({
1492
+ operation,
1493
+ adapter: this.adapter.name,
1494
+ model: result.model ?? options?.model,
1495
+ usage: result.usage,
1496
+ latencyMs: Date.now() - started,
1497
+ status: "success",
1498
+ registry: this.modelRegistry
1499
+ }));
1500
+ return result;
1501
+ } catch (err) {
1502
+ void this.traceRecorder.record(buildTraceEvent({
1503
+ operation,
1504
+ adapter: this.adapter.name,
1505
+ model: options?.model,
1506
+ latencyMs: Date.now() - started,
1507
+ status: "error",
1508
+ error: err instanceof Error ? err.message : String(err),
1509
+ registry: this.modelRegistry
1510
+ }));
1511
+ throw err;
1512
+ }
1513
+ }
1126
1514
  // ── IAIService implementation ──────────────────────────────────
1127
1515
  async chat(messages, options) {
1128
1516
  this.logger.debug("[AI] chat", { messageCount: messages.length, model: options?.model });
1129
- return this.adapter.chat(messages, options);
1517
+ return this.instrument("chat", options, () => this.adapter.chat(messages, options));
1130
1518
  }
1131
1519
  async complete(prompt, options) {
1132
1520
  this.logger.debug("[AI] complete", { promptLength: prompt.length, model: options?.model });
1133
- return this.adapter.complete(prompt, options);
1521
+ return this.instrument("complete", options, () => this.adapter.complete(prompt, options));
1522
+ }
1523
+ /**
1524
+ * Generate a strongly-typed object validated against a Zod schema.
1525
+ *
1526
+ * Delegates to the adapter's `generateObject` when supported; throws a
1527
+ * descriptive error when the adapter does not implement structured output.
1528
+ *
1529
+ * @example
1530
+ * ```ts
1531
+ * import { z } from 'zod';
1532
+ * const Schema = z.object({ name: z.string(), priority: z.number().int() });
1533
+ * const { object } = await ai.generateObject(messages, Schema);
1534
+ * ```
1535
+ */
1536
+ async generateObject(messages, schema, options) {
1537
+ this.logger.debug("[AI] generateObject", { messageCount: messages.length, model: options?.model });
1538
+ if (!this.adapter.generateObject) {
1539
+ throw new Error(
1540
+ `[AI] Adapter "${this.adapter.name}" does not support generateObject. Use VercelLLMAdapter with a structured-output-capable model.`
1541
+ );
1542
+ }
1543
+ return this.instrument(
1544
+ "generate_object",
1545
+ options,
1546
+ () => this.adapter.generateObject(messages, schema, options)
1547
+ );
1134
1548
  }
1135
1549
  async *streamChat(messages, options) {
1136
1550
  this.logger.debug("[AI] streamChat", { messageCount: messages.length, model: options?.model });
@@ -1170,6 +1584,13 @@ var _AIService = class _AIService {
1170
1584
  * maximum number of iterations (`maxIterations`) is reached.
1171
1585
  */
1172
1586
  async chatWithTools(messages, options) {
1587
+ return this.instrument(
1588
+ "chat_with_tools",
1589
+ options,
1590
+ () => this.chatWithToolsImpl(messages, options)
1591
+ );
1592
+ }
1593
+ async chatWithToolsImpl(messages, options) {
1173
1594
  const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
1174
1595
  const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1175
1596
  const registeredTools = this.toolRegistry.getAll();
@@ -2212,7 +2633,7 @@ function buildToolRoutes(aiService, logger) {
2212
2633
  }
2213
2634
 
2214
2635
  // src/conversation/objectql-conversation-service.ts
2215
- var import_node_crypto = require("crypto");
2636
+ var import_node_crypto2 = require("crypto");
2216
2637
  var CONVERSATIONS_OBJECT = "ai_conversations";
2217
2638
  var MESSAGES_OBJECT = "ai_messages";
2218
2639
  var CONVERSATION_ORDER = [
@@ -2229,7 +2650,7 @@ var ObjectQLConversationService = class {
2229
2650
  }
2230
2651
  async create(options = {}) {
2231
2652
  const now = (/* @__PURE__ */ new Date()).toISOString();
2232
- const id = `conv_${(0, import_node_crypto.randomUUID)()}`;
2653
+ const id = `conv_${(0, import_node_crypto2.randomUUID)()}`;
2233
2654
  const record = {
2234
2655
  id,
2235
2656
  title: options.title ?? null,
@@ -2302,7 +2723,7 @@ var ObjectQLConversationService = class {
2302
2723
  throw new Error(`Conversation "${conversationId}" not found`);
2303
2724
  }
2304
2725
  const now = (/* @__PURE__ */ new Date()).toISOString();
2305
- const msgId = `msg_${(0, import_node_crypto.randomUUID)()}`;
2726
+ const msgId = `msg_${(0, import_node_crypto2.randomUUID)()}`;
2306
2727
  let contentStr;
2307
2728
  let toolCallsJson = null;
2308
2729
  let toolCallId = null;
@@ -2547,10 +2968,677 @@ var AiMessageObject = import_data2.ObjectSchema.create({
2547
2968
  }
2548
2969
  });
2549
2970
 
2971
+ // src/objects/ai-trace.object.ts
2972
+ var import_data3 = require("@objectstack/spec/data");
2973
+ var AiTraceObject = import_data3.ObjectSchema.create({
2974
+ name: "ai_traces",
2975
+ label: "AI Trace",
2976
+ pluralLabel: "AI Traces",
2977
+ icon: "activity",
2978
+ isSystem: true,
2979
+ description: "Per-call LLM invocation trace with token usage and cost",
2980
+ fields: {
2981
+ id: import_data3.Field.text({
2982
+ label: "Trace ID",
2983
+ required: true,
2984
+ readonly: true
2985
+ }),
2986
+ conversation_id: import_data3.Field.lookup("ai_conversations", {
2987
+ label: "Conversation",
2988
+ required: false,
2989
+ description: "Parent conversation, if any"
2990
+ }),
2991
+ agent_id: import_data3.Field.text({
2992
+ label: "Agent",
2993
+ required: false,
2994
+ maxLength: 128,
2995
+ description: "Agent metadata name that originated the call"
2996
+ }),
2997
+ operation: import_data3.Field.select({
2998
+ label: "Operation",
2999
+ required: true,
3000
+ options: [
3001
+ { label: "Chat", value: "chat" },
3002
+ { label: "Complete", value: "complete" },
3003
+ { label: "Stream Chat", value: "stream_chat" },
3004
+ { label: "Chat With Tools", value: "chat_with_tools" },
3005
+ { label: "Generate Object", value: "generate_object" },
3006
+ { label: "Embed", value: "embed" }
3007
+ ]
3008
+ }),
3009
+ model: import_data3.Field.text({
3010
+ label: "Model",
3011
+ required: false,
3012
+ maxLength: 128,
3013
+ description: "Model identifier reported by the adapter"
3014
+ }),
3015
+ adapter: import_data3.Field.text({
3016
+ label: "Adapter",
3017
+ required: false,
3018
+ maxLength: 64,
3019
+ description: 'LLM adapter name (e.g. "vercel", "memory")'
3020
+ }),
3021
+ prompt_tokens: import_data3.Field.number({
3022
+ label: "Prompt Tokens",
3023
+ required: false,
3024
+ defaultValue: 0
3025
+ }),
3026
+ completion_tokens: import_data3.Field.number({
3027
+ label: "Completion Tokens",
3028
+ required: false,
3029
+ defaultValue: 0
3030
+ }),
3031
+ total_tokens: import_data3.Field.number({
3032
+ label: "Total Tokens",
3033
+ required: false,
3034
+ defaultValue: 0
3035
+ }),
3036
+ input_cost: import_data3.Field.number({
3037
+ label: "Input Cost",
3038
+ required: false,
3039
+ description: "Cost attributable to prompt tokens (currency in `currency` field)"
3040
+ }),
3041
+ output_cost: import_data3.Field.number({
3042
+ label: "Output Cost",
3043
+ required: false,
3044
+ description: "Cost attributable to completion tokens"
3045
+ }),
3046
+ total_cost: import_data3.Field.number({
3047
+ label: "Total Cost",
3048
+ required: false,
3049
+ description: "input_cost + output_cost"
3050
+ }),
3051
+ currency: import_data3.Field.text({
3052
+ label: "Currency",
3053
+ required: false,
3054
+ maxLength: 8,
3055
+ defaultValue: "USD"
3056
+ }),
3057
+ latency_ms: import_data3.Field.number({
3058
+ label: "Latency (ms)",
3059
+ required: true,
3060
+ defaultValue: 0,
3061
+ description: "Wall-clock duration of the LLM call"
3062
+ }),
3063
+ status: import_data3.Field.select({
3064
+ label: "Status",
3065
+ required: true,
3066
+ options: [
3067
+ { label: "Success", value: "success" },
3068
+ { label: "Error", value: "error" }
3069
+ ]
3070
+ }),
3071
+ error: import_data3.Field.textarea({
3072
+ label: "Error",
3073
+ required: false,
3074
+ description: "Error message when status=error"
3075
+ }),
3076
+ metadata: import_data3.Field.textarea({
3077
+ label: "Metadata",
3078
+ required: false,
3079
+ description: "JSON-serialized extra fields (request id, user id, \u2026)"
3080
+ }),
3081
+ created_at: import_data3.Field.datetime({
3082
+ label: "Created At",
3083
+ required: true,
3084
+ defaultValue: "NOW()",
3085
+ readonly: true
3086
+ })
3087
+ },
3088
+ indexes: [
3089
+ { fields: ["conversation_id"] },
3090
+ { fields: ["agent_id"] },
3091
+ { fields: ["model"] },
3092
+ { fields: ["status"] },
3093
+ { fields: ["created_at"] }
3094
+ ],
3095
+ enable: {
3096
+ trackHistory: false,
3097
+ searchable: false,
3098
+ apiEnabled: true,
3099
+ apiMethods: ["get", "list"],
3100
+ trash: false,
3101
+ mru: false
3102
+ }
3103
+ });
3104
+
3105
+ // src/views/ai-trace.view.ts
3106
+ var import_spec = require("@objectstack/spec");
3107
+ var AiTraceView = (0, import_spec.defineView)({
3108
+ list: {
3109
+ type: "grid",
3110
+ data: { provider: "object", object: "ai_traces" },
3111
+ columns: [
3112
+ { field: "created_at", label: "Time" },
3113
+ { field: "operation" },
3114
+ { field: "model" },
3115
+ { field: "agent_id", label: "Agent" },
3116
+ { field: "latency_ms", label: "Latency (ms)" },
3117
+ { field: "total_tokens", label: "Tokens" },
3118
+ { field: "cost_total", label: "Cost" },
3119
+ { field: "status" }
3120
+ ],
3121
+ sort: [{ field: "created_at", order: "desc" }],
3122
+ pagination: { pageSize: 50 },
3123
+ searchableFields: ["conversation_id", "agent_id", "model", "error"],
3124
+ filterableFields: ["operation", "model", "status"]
3125
+ },
3126
+ listViews: {
3127
+ errors: {
3128
+ label: "Errors",
3129
+ type: "grid",
3130
+ data: { provider: "object", object: "ai_traces" },
3131
+ columns: [
3132
+ { field: "created_at", label: "Time" },
3133
+ { field: "operation" },
3134
+ { field: "model" },
3135
+ { field: "latency_ms", label: "Latency (ms)" },
3136
+ { field: "error" }
3137
+ ],
3138
+ filter: [
3139
+ { field: "status", operator: "=", value: "error" }
3140
+ ],
3141
+ sort: [{ field: "created_at", order: "desc" }]
3142
+ },
3143
+ by_model: {
3144
+ label: "By Model",
3145
+ type: "grid",
3146
+ data: { provider: "object", object: "ai_traces" },
3147
+ columns: [
3148
+ { field: "model" },
3149
+ { field: "operation" },
3150
+ { field: "latency_ms", label: "Latency (ms)" },
3151
+ { field: "total_tokens", label: "Tokens" },
3152
+ { field: "cost_total", label: "Cost" },
3153
+ { field: "status" },
3154
+ { field: "created_at", label: "Time" }
3155
+ ],
3156
+ grouping: { fields: [{ field: "model" }] },
3157
+ sort: [{ field: "created_at", order: "desc" }]
3158
+ }
3159
+ }
3160
+ });
3161
+
2550
3162
  // src/plugin.ts
2551
3163
  init_data_tools();
2552
3164
  init_metadata_tools();
2553
3165
 
3166
+ // src/tools/query-data.tool.ts
3167
+ var import_zod = require("zod");
3168
+
3169
+ // src/schema-retriever.ts
3170
+ var SchemaRetriever = class {
3171
+ constructor(metadata, options = {}) {
3172
+ this.metadata = metadata;
3173
+ this.options = {
3174
+ limit: options.limit ?? 3,
3175
+ minScore: options.minScore ?? 1,
3176
+ maxFieldsPerObject: options.maxFieldsPerObject ?? 12
3177
+ };
3178
+ }
3179
+ /**
3180
+ * Find object definitions whose name/label/fields match terms in the query.
3181
+ *
3182
+ * Returns matches sorted by score (descending) capped at `limit`. When
3183
+ * the query yields no matches, returns an empty array — callers may
3184
+ * fall back to a generic "describe what data exists" tool call.
3185
+ */
3186
+ async retrieve(query) {
3187
+ const terms = tokenise(query);
3188
+ if (terms.length === 0) return [];
3189
+ const objects = await this.metadata.listObjects();
3190
+ const hits = [];
3191
+ for (const raw of objects) {
3192
+ const obj = raw;
3193
+ if (!obj?.name) continue;
3194
+ const score = scoreObject(obj, terms);
3195
+ if (score >= this.options.minScore) {
3196
+ hits.push({ object: obj, score });
3197
+ }
3198
+ }
3199
+ hits.sort((a, b) => b.score - a.score);
3200
+ return hits.slice(0, this.options.limit);
3201
+ }
3202
+ /**
3203
+ * Render hits as a compact Markdown schema snippet.
3204
+ *
3205
+ * Designed to be appended to the system message — every line carries
3206
+ * exactly the information a model needs to choose object/field names
3207
+ * for query construction.
3208
+ */
3209
+ static renderSnippet(hits, maxFieldsPerObject = 12) {
3210
+ if (hits.length === 0) return "";
3211
+ const lines = ["## Schema context (auto-injected)"];
3212
+ for (const hit of hits) {
3213
+ const obj = hit.object;
3214
+ const parts = [];
3215
+ if (obj.label) parts.push(obj.label);
3216
+ if (obj.pluralLabel && obj.pluralLabel !== obj.label) parts.push(`(${obj.pluralLabel})`);
3217
+ const header = parts.length > 0 ? ` \u2014 ${parts.join(" ")}` : "";
3218
+ lines.push(`### ${obj.name}${header}`);
3219
+ const fields = Object.entries(obj.fields ?? {}).slice(0, maxFieldsPerObject);
3220
+ for (const [name, field] of fields) {
3221
+ lines.push(` - ${name}: ${describeField(field)}`);
3222
+ }
3223
+ const total = Object.keys(obj.fields ?? {}).length;
3224
+ if (total > fields.length) {
3225
+ lines.push(` - \u2026${total - fields.length} more field(s)`);
3226
+ }
3227
+ }
3228
+ return lines.join("\n");
3229
+ }
3230
+ };
3231
+ function tokenise(query) {
3232
+ const raw = query.toLowerCase().match(/[a-z0-9]+/g) ?? [];
3233
+ return raw.filter((t) => t.length >= 2 && !STOPWORDS.has(t));
3234
+ }
3235
+ var STOPWORDS = /* @__PURE__ */ new Set([
3236
+ "the",
3237
+ "and",
3238
+ "for",
3239
+ "with",
3240
+ "from",
3241
+ "are",
3242
+ "has",
3243
+ "have",
3244
+ "had",
3245
+ "was",
3246
+ "were",
3247
+ "this",
3248
+ "that",
3249
+ "these",
3250
+ "those",
3251
+ "all",
3252
+ "any",
3253
+ "how",
3254
+ "what",
3255
+ "when",
3256
+ "where",
3257
+ "who",
3258
+ "why",
3259
+ "which",
3260
+ "show",
3261
+ "list",
3262
+ "find",
3263
+ "get",
3264
+ "count",
3265
+ "of",
3266
+ "in",
3267
+ "on",
3268
+ "at",
3269
+ "to",
3270
+ "as",
3271
+ "by",
3272
+ "is",
3273
+ "it",
3274
+ "an",
3275
+ "or",
3276
+ "be",
3277
+ "me"
3278
+ ]);
3279
+ function scoreObject(obj, terms) {
3280
+ let score = 0;
3281
+ const nameTokens = splitSnake(obj.name);
3282
+ const labelTokens = obj.label ? tokenise(obj.label) : [];
3283
+ const pluralTokens = obj.pluralLabel ? tokenise(obj.pluralLabel) : [];
3284
+ const descTokens = obj.description ? tokenise(obj.description) : [];
3285
+ for (const term of terms) {
3286
+ if (nameTokens.includes(term)) score += 3;
3287
+ else if (labelTokens.includes(term) || pluralTokens.includes(term)) score += 2;
3288
+ else if (descTokens.includes(term)) score += 1;
3289
+ }
3290
+ for (const [fieldName, field] of Object.entries(obj.fields ?? {})) {
3291
+ const fnTokens = splitSnake(fieldName);
3292
+ const flTokens = field.label ? tokenise(field.label) : [];
3293
+ for (const term of terms) {
3294
+ if (fnTokens.includes(term)) score += 2;
3295
+ else if (flTokens.includes(term)) score += 1;
3296
+ }
3297
+ }
3298
+ return score;
3299
+ }
3300
+ function splitSnake(name) {
3301
+ return name.toLowerCase().split("_").filter(Boolean);
3302
+ }
3303
+ function describeField(field) {
3304
+ const t = field.type ?? "unknown";
3305
+ if (t === "lookup" && field.reference) return `lookup \u2192 ${field.reference}`;
3306
+ if (t === "select" && Array.isArray(field.options)) {
3307
+ const values = field.options.map(
3308
+ (o) => typeof o === "string" ? o : o.value
3309
+ ).filter(Boolean).slice(0, 6);
3310
+ return `select(${values.join("|")})`;
3311
+ }
3312
+ return t;
3313
+ }
3314
+
3315
+ // src/tools/query-data.tool.ts
3316
+ var QueryPlanSchema = import_zod.z.object({
3317
+ objectName: import_zod.z.string().min(1).describe('The snake_case object name to query (e.g. "task", "account").'),
3318
+ where: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional().describe(
3319
+ 'Filter conditions as key-value pairs. Use MongoDB-style operators for ranges, e.g. {"amount": {"$gt": 100}}.'
3320
+ ),
3321
+ fields: import_zod.z.array(import_zod.z.string()).optional().describe("Field names to return. Omit to return all fields."),
3322
+ orderBy: import_zod.z.array(
3323
+ import_zod.z.object({
3324
+ field: import_zod.z.string(),
3325
+ order: import_zod.z.enum(["asc", "desc"])
3326
+ })
3327
+ ).optional().describe("Sort order. First entry is primary sort key."),
3328
+ limit: import_zod.z.number().int().min(1).max(200).optional().describe("Maximum number of records (default 20, max 200).")
3329
+ });
3330
+ var QUERY_DATA_TOOL = {
3331
+ name: "query_data",
3332
+ description: "Answer a natural-language question about the user's data. Internally retrieves the relevant object schema, generates an ObjectQL query, executes it, and returns the matching records. Prefer this tool over `query_records` / `aggregate_data` when the user's intent is expressed in plain language.",
3333
+ parameters: {
3334
+ type: "object",
3335
+ properties: {
3336
+ request: {
3337
+ type: "string",
3338
+ description: "The natural-language question to answer (paraphrase the user's request if needed for clarity)."
3339
+ },
3340
+ model: {
3341
+ type: "string",
3342
+ description: "Optional model id to use for query planning. Defaults to the AI service's default model."
3343
+ }
3344
+ },
3345
+ required: ["request"],
3346
+ additionalProperties: false
3347
+ }
3348
+ };
3349
+ function createQueryDataHandler(ctx) {
3350
+ const retriever = new SchemaRetriever(ctx.metadata);
3351
+ const maxLimit = ctx.maxLimit ?? 100;
3352
+ return async (args) => {
3353
+ const { request, model } = args;
3354
+ if (!request || typeof request !== "string") {
3355
+ return JSON.stringify({ error: "query_data: `request` is required" });
3356
+ }
3357
+ if (!ctx.ai.generateObject) {
3358
+ return JSON.stringify({
3359
+ error: "query_data requires structured-output support. Configure a Vercel-AI-SDK-backed adapter (OpenAI, Anthropic, Google)."
3360
+ });
3361
+ }
3362
+ const hits = await retriever.retrieve(request);
3363
+ if (hits.length === 0) {
3364
+ return JSON.stringify({
3365
+ error: "No matching objects in metadata. Ask the user which object(s) to query, or list available objects via list_objects."
3366
+ });
3367
+ }
3368
+ const snippet = SchemaRetriever.renderSnippet(hits);
3369
+ const planMessages = [
3370
+ {
3371
+ role: "system",
3372
+ content: "You translate user data questions into a single ObjectQL query plan. Use ONLY the objects and fields listed in the schema context below. Never invent field names. If the question is ambiguous, pick the most likely interpretation and use a reasonable `limit`.\n\n" + snippet
3373
+ },
3374
+ { role: "user", content: request }
3375
+ ];
3376
+ let plan;
3377
+ try {
3378
+ const generated = await ctx.ai.generateObject(planMessages, QueryPlanSchema, {
3379
+ model,
3380
+ schemaName: "ObjectQLQueryPlan",
3381
+ schemaDescription: "A single ObjectQL find() query to answer the user request."
3382
+ });
3383
+ plan = generated.object;
3384
+ } catch (err) {
3385
+ return JSON.stringify({
3386
+ error: `Failed to plan query: ${err instanceof Error ? err.message : String(err)}`
3387
+ });
3388
+ }
3389
+ const matchedObject = hits.find((h) => h.object.name === plan.objectName)?.object ?? hits[0].object;
3390
+ if (matchedObject.name !== plan.objectName) {
3391
+ return JSON.stringify({
3392
+ error: `Planned object "${plan.objectName}" is not in the retrieved schema. Available: ${hits.map((h) => h.object.name).join(", ")}`
3393
+ });
3394
+ }
3395
+ const limit = Math.min(plan.limit ?? 20, maxLimit);
3396
+ try {
3397
+ const records = await ctx.dataEngine.find(plan.objectName, {
3398
+ where: plan.where,
3399
+ fields: plan.fields,
3400
+ orderBy: plan.orderBy,
3401
+ limit
3402
+ });
3403
+ return JSON.stringify({
3404
+ plan,
3405
+ count: records.length,
3406
+ records
3407
+ });
3408
+ } catch (err) {
3409
+ return JSON.stringify({
3410
+ plan,
3411
+ error: `Query execution failed: ${err instanceof Error ? err.message : String(err)}`
3412
+ });
3413
+ }
3414
+ };
3415
+ }
3416
+ function registerQueryDataTool(registry, context) {
3417
+ registry.register(QUERY_DATA_TOOL, createQueryDataHandler(context));
3418
+ }
3419
+
3420
+ // src/tools/action-tools.ts
3421
+ function actionSkipReason(action) {
3422
+ if (action.aiExposed === false) {
3423
+ return "opted-out via aiExposed:false";
3424
+ }
3425
+ if (action.type !== "script") return `type='${action.type}' not yet supported`;
3426
+ if (!action.target && !action.body) return "no target or body";
3427
+ if (action.confirmText) return "requires confirmation (confirmText set)";
3428
+ if (action.mode === "delete") return "mode='delete' \u2014 destructive";
3429
+ if (action.variant === "danger") return "variant='danger' \u2014 destructive";
3430
+ return null;
3431
+ }
3432
+ function fieldTypeToJsonType(t) {
3433
+ switch (t) {
3434
+ case "number":
3435
+ case "currency":
3436
+ case "percent":
3437
+ case "rating":
3438
+ case "slider":
3439
+ case "autonumber":
3440
+ return "number";
3441
+ case "boolean":
3442
+ case "toggle":
3443
+ return "boolean";
3444
+ case "multiselect":
3445
+ case "checkboxes":
3446
+ case "tags":
3447
+ return "array";
3448
+ default:
3449
+ return "string";
3450
+ }
3451
+ }
3452
+ function resolveParam(param, ownerObject, allObjects) {
3453
+ const fieldRef = param.field;
3454
+ const owner = param.objectOverride && allObjects.get(param.objectOverride) ? allObjects.get(param.objectOverride) : ownerObject;
3455
+ const field = fieldRef ? owner?.fields?.[fieldRef] : void 0;
3456
+ const name = param.name ?? fieldRef;
3457
+ if (!name) return null;
3458
+ const type = param.type ?? field?.type;
3459
+ const jsonType = fieldTypeToJsonType(type);
3460
+ const schema = { type: jsonType };
3461
+ const label = typeof param.label === "string" ? param.label : field?.label;
3462
+ const help = param.helpText ?? field?.description;
3463
+ const description = [label, help].filter(Boolean).join(" \u2014 ") || void 0;
3464
+ if (description) schema.description = description;
3465
+ const optionSource = param.options ?? field?.options;
3466
+ if (Array.isArray(optionSource) && optionSource.length > 0) {
3467
+ const values = optionSource.map((o) => typeof o === "string" ? o : o.value).filter((v) => typeof v === "string");
3468
+ if (values.length > 0) {
3469
+ schema.enum = jsonType === "array" ? void 0 : values;
3470
+ if (jsonType === "array") {
3471
+ schema.items = { type: "string", enum: values };
3472
+ }
3473
+ }
3474
+ } else if (jsonType === "array") {
3475
+ schema.items = { type: "string" };
3476
+ }
3477
+ if (param.defaultValue !== void 0) {
3478
+ schema.default = param.defaultValue;
3479
+ }
3480
+ const required = Boolean(param.required ?? field?.required ?? false);
3481
+ return { name, schema, required };
3482
+ }
3483
+ function buildParametersSchema(action, ownerObject, allObjects) {
3484
+ const properties = {};
3485
+ const required = [];
3486
+ const isRowContext = Array.isArray(action.locations) && action.locations.some((l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related");
3487
+ if (action.objectName && isRowContext) {
3488
+ properties.recordId = {
3489
+ type: "string",
3490
+ description: `The ${action.objectName} record id to act on.`
3491
+ };
3492
+ if (action.recordIdParam || action.recordIdField) {
3493
+ required.push("recordId");
3494
+ }
3495
+ }
3496
+ for (const param of action.params ?? []) {
3497
+ const resolved = resolveParam(param, ownerObject, allObjects);
3498
+ if (!resolved) continue;
3499
+ properties[resolved.name] = resolved.schema;
3500
+ if (resolved.required) required.push(resolved.name);
3501
+ }
3502
+ return {
3503
+ type: "object",
3504
+ properties,
3505
+ ...required.length > 0 ? { required } : {},
3506
+ additionalProperties: false
3507
+ };
3508
+ }
3509
+ function actionToolName(action, prefix = "action_") {
3510
+ return `${prefix}${action.name}`;
3511
+ }
3512
+ function describeAction(action, ownerObject) {
3513
+ const label = typeof action.label === "string" ? action.label : action.name.replace(/_/g, " ");
3514
+ const target = action.objectName ?? ownerObject?.name;
3515
+ const targetLabel = ownerObject?.label ?? target;
3516
+ const parts = [];
3517
+ parts.push(`${label}${targetLabel ? ` \u2014 operates on ${targetLabel}` : ""}.`);
3518
+ if (action.successMessage && typeof action.successMessage === "string") {
3519
+ parts.push(`On success: ${action.successMessage}`);
3520
+ }
3521
+ if (action.mode) parts.push(`Mode: ${action.mode}.`);
3522
+ parts.push(
3523
+ "Use this when the user asks to perform this operation in natural language."
3524
+ );
3525
+ return parts.join(" ");
3526
+ }
3527
+ function actionToToolDefinition(action, ownerObject, allObjects, toolPrefix = "action_") {
3528
+ if (actionSkipReason(action) !== null) return null;
3529
+ return {
3530
+ name: actionToolName(action, toolPrefix),
3531
+ description: describeAction(action, ownerObject),
3532
+ parameters: buildParametersSchema(action, ownerObject, allObjects)
3533
+ };
3534
+ }
3535
+ function buildHandlerEngineAdapter(engine) {
3536
+ return {
3537
+ update: (object, id, data) => engine.update(object, { ...data, id }, { where: { id } }),
3538
+ insert: (object, data) => engine.insert(object, data),
3539
+ find: (object, where) => engine.find(object, { where }),
3540
+ delete: (object, ids) => engine.delete(object, { where: { id: ids.length === 1 ? ids[0] : { $in: ids } } })
3541
+ };
3542
+ }
3543
+ function createActionToolHandler(action, ctx) {
3544
+ const principal = ctx.principal ?? { id: "ai_agent", name: "AI Assistant" };
3545
+ const engineAdapter = buildHandlerEngineAdapter(ctx.dataEngine);
3546
+ const requiresRecord = Array.isArray(action.locations) && action.locations.some(
3547
+ (l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related"
3548
+ );
3549
+ return async (args) => {
3550
+ const objectName = action.objectName;
3551
+ const target = action.target;
3552
+ const result = {
3553
+ ok: false,
3554
+ action: action.name,
3555
+ objectName
3556
+ };
3557
+ if (!objectName) {
3558
+ result.error = "Action has no objectName; cannot dispatch.";
3559
+ return JSON.stringify(result);
3560
+ }
3561
+ if (!target) {
3562
+ result.error = "Action has no target handler.";
3563
+ return JSON.stringify(result);
3564
+ }
3565
+ const recordId = typeof args.recordId === "string" && args.recordId.length > 0 ? args.recordId : void 0;
3566
+ let record;
3567
+ if (requiresRecord) {
3568
+ if (!recordId) {
3569
+ result.error = `recordId is required for this action \u2014 supply the id of the ${objectName} record to act on (use query_data first if you don't have it).`;
3570
+ return JSON.stringify(result);
3571
+ }
3572
+ try {
3573
+ const found = await ctx.dataEngine.find(objectName, {
3574
+ where: { id: recordId },
3575
+ limit: 1
3576
+ });
3577
+ record = found[0];
3578
+ if (!record) {
3579
+ result.error = `Record ${recordId} not found in ${objectName}.`;
3580
+ return JSON.stringify(result);
3581
+ }
3582
+ result.recordId = recordId;
3583
+ } catch (err) {
3584
+ result.error = `Failed to load record: ${err instanceof Error ? err.message : String(err)}`;
3585
+ return JSON.stringify(result);
3586
+ }
3587
+ }
3588
+ const { recordId: _omit, ...userParams } = args;
3589
+ try {
3590
+ const handlerCtx = {
3591
+ record,
3592
+ user: principal,
3593
+ engine: engineAdapter,
3594
+ params: userParams
3595
+ };
3596
+ const out = await ctx.dataEngine.executeAction?.(objectName, target, handlerCtx);
3597
+ result.ok = true;
3598
+ result.result = out ?? null;
3599
+ const successMsg = typeof action.successMessage === "string" ? action.successMessage : void 0;
3600
+ result.message = successMsg ?? `Action '${action.name}' executed successfully.`;
3601
+ return JSON.stringify(result);
3602
+ } catch (err) {
3603
+ result.error = err instanceof Error ? err.message : String(err);
3604
+ return JSON.stringify(result);
3605
+ }
3606
+ };
3607
+ }
3608
+ async function registerActionsAsTools(registry, context) {
3609
+ const objects = await context.metadata.listObjects();
3610
+ const objMap = new Map(
3611
+ objects.filter((o) => Boolean(o?.name)).map((o) => [o.name, o])
3612
+ );
3613
+ const registered = [];
3614
+ const skipped = [];
3615
+ const prefix = context.toolPrefix ?? "action_";
3616
+ for (const obj of objects) {
3617
+ if (!obj?.actions || !Array.isArray(obj.actions)) continue;
3618
+ for (const action of obj.actions) {
3619
+ if (!action || typeof action.name !== "string") continue;
3620
+ const normalized = {
3621
+ ...action,
3622
+ objectName: action.objectName ?? obj.name
3623
+ };
3624
+ const reason = actionSkipReason(normalized);
3625
+ if (reason !== null) {
3626
+ skipped.push({ action: normalized.name, reason });
3627
+ continue;
3628
+ }
3629
+ const definition = actionToToolDefinition(normalized, obj, objMap, prefix);
3630
+ if (!definition) continue;
3631
+ if (registry.has(definition.name)) {
3632
+ skipped.push({ action: normalized.name, reason: "tool name already registered" });
3633
+ continue;
3634
+ }
3635
+ registry.register(definition, createActionToolHandler(normalized, context));
3636
+ registered.push(definition.name);
3637
+ }
3638
+ }
3639
+ return { registered, skipped };
3640
+ }
3641
+
2554
3642
  // src/agent-runtime.ts
2555
3643
  var import_ai7 = require("@objectstack/spec/ai");
2556
3644
  var AgentRuntime = class {
@@ -2853,6 +3941,16 @@ var SkillRegistry = class {
2853
3941
  const resolved = [];
2854
3942
  for (const skill of skills) {
2855
3943
  for (const toolName of skill.tools) {
3944
+ if (toolName.endsWith("*")) {
3945
+ const prefix = toolName.slice(0, -1);
3946
+ for (const def2 of availableTools) {
3947
+ if (!def2.name.startsWith(prefix)) continue;
3948
+ if (seen.has(def2.name)) continue;
3949
+ resolved.push(def2);
3950
+ seen.add(def2.name);
3951
+ }
3952
+ continue;
3953
+ }
2856
3954
  if (seen.has(toolName)) continue;
2857
3955
  const def = toolMap.get(toolName);
2858
3956
  if (def) {
@@ -2916,7 +4014,8 @@ Always answer in the same language the user is using. Detailed tool-usage guidan
2916
4014
  maxTokens: 4096
2917
4015
  },
2918
4016
  // Capability bundle lives on the skill; the agent only references it.
2919
- skills: ["data_explorer"],
4017
+ // `data_explorer` = read side, `actions_executor` = write side.
4018
+ skills: ["data_explorer", "actions_executor"],
2920
4019
  active: true,
2921
4020
  visibility: "global",
2922
4021
  guardrails: {
@@ -2996,6 +4095,7 @@ Guidelines:
2996
4095
  7. Never expose internal IDs unless the user explicitly asks for them.
2997
4096
  8. Always answer in the same language the user is using.`,
2998
4097
  tools: [
4098
+ "query_data",
2999
4099
  "list_objects",
3000
4100
  "describe_object",
3001
4101
  "query_records",
@@ -3065,6 +4165,44 @@ Guidelines:
3065
4165
  active: true
3066
4166
  };
3067
4167
 
4168
+ // src/skills/actions-executor-skill.ts
4169
+ var ACTIONS_EXECUTOR_SKILL = {
4170
+ name: "actions_executor",
4171
+ label: "Action Executor",
4172
+ description: "Perform business operations on the user's data \u2014 invoke actions like 'mark as complete', 'start task', 'clone record' through natural language.",
4173
+ instructions: `You can perform business operations by invoking the user's registered actions.
4174
+
4175
+ Capabilities:
4176
+ - Each tool whose name starts with \`action_\` is a business operation declared on an object.
4177
+ - Read the tool description carefully \u2014 it tells you what the action does and what record types it applies to.
4178
+ - Most actions need a \`recordId\` argument. If you don't already have one from a prior \`query_data\` call, run \`query_data\` first to find the right record, then invoke the action with its id.
4179
+
4180
+ Guidelines:
4181
+ 1. Confirm intent \u2014 when the user says "complete it" / "start that one", make sure you know *which* record they mean. Ask if ambiguous.
4182
+ 2. Use \`query_data\` to look up records by natural-language description ("the design review task", "tickets assigned to me").
4183
+ 3. After invoking an action, the tool returns \`{ ok, message, result }\`. Summarise success in plain language; surface errors verbatim.
4184
+ 4. Never invent recordIds. If \`query_data\` didn't return one, tell the user instead of guessing.
4185
+ 5. Action tools are pre-filtered for safety \u2014 destructive operations (\`mode: 'delete'\`, \`variant: 'danger'\`, anything with \`confirmText\`) are *not* exposed here and require explicit user confirmation in the UI.
4186
+ 6. Always answer in the same language the user is using.`,
4187
+ // Dynamically materialised: the runtime registers one tool per Action,
4188
+ // and the skill subscribes to the whole family via the `action_*`
4189
+ // glob (resolved by SkillRegistry.flattenToTools).
4190
+ tools: ["action_*"],
4191
+ triggerPhrases: [
4192
+ "complete",
4193
+ "mark as",
4194
+ "start",
4195
+ "finish",
4196
+ "clone",
4197
+ "duplicate",
4198
+ "do it",
4199
+ "run",
4200
+ "invoke",
4201
+ "execute"
4202
+ ],
4203
+ active: true
4204
+ };
4205
+
3068
4206
  // src/adapters/vercel-adapter.ts
3069
4207
  var import_ai9 = require("ai");
3070
4208
  function buildVercelOptions(options) {
@@ -3148,11 +4286,102 @@ var VercelLLMAdapter = class {
3148
4286
  "[VercelLLMAdapter] Embeddings require a dedicated EmbeddingModel. Configure an embedding adapter instead."
3149
4287
  );
3150
4288
  }
4289
+ async generateObject(messages, schema, options) {
4290
+ const { schemaName, schemaDescription, ...rest } = options ?? {};
4291
+ const result = await (0, import_ai9.generateObject)({
4292
+ model: this.model,
4293
+ messages,
4294
+ schema,
4295
+ schemaName,
4296
+ schemaDescription,
4297
+ ...buildVercelOptions(rest)
4298
+ });
4299
+ return {
4300
+ object: result.object,
4301
+ model: result.response?.modelId,
4302
+ usage: result.usage ? {
4303
+ promptTokens: result.usage.inputTokens ?? 0,
4304
+ completionTokens: result.usage.outputTokens ?? 0,
4305
+ totalTokens: result.usage.totalTokens ?? 0
4306
+ } : void 0
4307
+ };
4308
+ }
3151
4309
  async listModels() {
3152
4310
  return [];
3153
4311
  }
3154
4312
  };
3155
4313
 
4314
+ // src/model-registry.ts
4315
+ var ModelRegistry = class {
4316
+ constructor(config = {}) {
4317
+ this.models = /* @__PURE__ */ new Map();
4318
+ for (const model of config.models ?? []) {
4319
+ this.models.set(model.id, model);
4320
+ }
4321
+ this.defaultModelId = config.defaultModelId;
4322
+ }
4323
+ /** Register or replace a model. */
4324
+ register(model) {
4325
+ this.models.set(model.id, model);
4326
+ }
4327
+ /** Look up a model by id. */
4328
+ get(id) {
4329
+ return this.models.get(id);
4330
+ }
4331
+ /** Look up a model by id, throwing if missing. */
4332
+ getOrThrow(id) {
4333
+ const model = this.models.get(id);
4334
+ if (!model) {
4335
+ throw new Error(
4336
+ `[ModelRegistry] Unknown model "${id}". Registered: ${[...this.models.keys()].join(", ") || "(none)"}`
4337
+ );
4338
+ }
4339
+ return model;
4340
+ }
4341
+ /** Resolve the default model (explicit > first registered > undefined). */
4342
+ getDefault() {
4343
+ if (this.defaultModelId) {
4344
+ return this.models.get(this.defaultModelId);
4345
+ }
4346
+ return this.models.values().next().value;
4347
+ }
4348
+ /** Set the default model id (must already be registered). */
4349
+ setDefault(id) {
4350
+ this.getOrThrow(id);
4351
+ this.defaultModelId = id;
4352
+ }
4353
+ /** All registered models. */
4354
+ list() {
4355
+ return [...this.models.values()];
4356
+ }
4357
+ /** Number of registered models. */
4358
+ get size() {
4359
+ return this.models.size;
4360
+ }
4361
+ /**
4362
+ * Estimate cost in the model's currency (defaults to USD).
4363
+ *
4364
+ * Returns `undefined` when the model is unknown or has no pricing data.
4365
+ * Costs are computed as `(tokens / 1000) * pricePer1kTokens` for input and
4366
+ * output independently, then summed.
4367
+ */
4368
+ estimateCost(modelId, usage) {
4369
+ const model = this.models.get(modelId);
4370
+ if (!model?.pricing) return void 0;
4371
+ return computeCost(model.pricing, usage);
4372
+ }
4373
+ };
4374
+ function computeCost(pricing, usage) {
4375
+ const inputCost = pricing.inputCostPer1kTokens != null ? usage.promptTokens / 1e3 * pricing.inputCostPer1kTokens : 0;
4376
+ const outputCost = pricing.outputCostPer1kTokens != null ? usage.completionTokens / 1e3 * pricing.outputCostPer1kTokens : 0;
4377
+ return {
4378
+ inputCost,
4379
+ outputCost,
4380
+ totalCost: inputCost + outputCost,
4381
+ currency: pricing.currency ?? "USD"
4382
+ };
4383
+ }
4384
+
3156
4385
  // src/plugin.ts
3157
4386
  var AIServicePlugin = class {
3158
4387
  constructor(options = {}) {
@@ -3274,10 +4503,34 @@ var AIServicePlugin = class {
3274
4503
  adapterDescription = detected.description;
3275
4504
  }
3276
4505
  ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
4506
+ const modelRegistry = new ModelRegistry({
4507
+ models: this.options.models,
4508
+ defaultModelId: this.options.defaultModelId
4509
+ });
4510
+ if (modelRegistry.size > 0) {
4511
+ ctx.logger.info(`[AI] ModelRegistry initialised with ${modelRegistry.size} model(s)`);
4512
+ }
4513
+ let traceRecorder;
4514
+ if (this.options.traceRecorder === null) {
4515
+ ctx.logger.debug("[AI] Tracing disabled (traceRecorder=null)");
4516
+ } else if (this.options.traceRecorder) {
4517
+ traceRecorder = this.options.traceRecorder;
4518
+ } else {
4519
+ try {
4520
+ const engine = ctx.getService("data");
4521
+ if (engine && typeof engine.insert === "function") {
4522
+ traceRecorder = new ObjectQLTraceRecorder(engine, { logger: ctx.logger });
4523
+ ctx.logger.info("[AI] Using ObjectQLTraceRecorder (IDataEngine detected)");
4524
+ }
4525
+ } catch {
4526
+ }
4527
+ }
3277
4528
  const config = {
3278
4529
  adapter,
3279
4530
  logger: ctx.logger,
3280
- conversationService
4531
+ conversationService,
4532
+ modelRegistry,
4533
+ traceRecorder
3281
4534
  };
3282
4535
  this.service = new AIService(config);
3283
4536
  if (hasExisting) {
@@ -3292,7 +4545,8 @@ var AIServicePlugin = class {
3292
4545
  type: "plugin",
3293
4546
  scope: "project",
3294
4547
  namespace: "ai",
3295
- objects: [AiConversationObject, AiMessageObject]
4548
+ objects: [AiConversationObject, AiMessageObject, AiTraceObject],
4549
+ views: [AiTraceView]
3296
4550
  });
3297
4551
  if (this.options.debug) {
3298
4552
  ctx.hook("ai:beforeChat", async (messages) => {
@@ -3324,6 +4578,39 @@ var AIServicePlugin = class {
3324
4578
  if (dataEngine) {
3325
4579
  registerDataTools(this.service.toolRegistry, { dataEngine });
3326
4580
  ctx.logger.info("[AI] Built-in data tools registered");
4581
+ if (metadataService) {
4582
+ registerQueryDataTool(this.service.toolRegistry, {
4583
+ ai: this.service,
4584
+ metadata: metadataService,
4585
+ dataEngine
4586
+ });
4587
+ ctx.logger.info("[AI] query_data tool registered");
4588
+ try {
4589
+ const { registered, skipped } = await registerActionsAsTools(
4590
+ this.service.toolRegistry,
4591
+ {
4592
+ metadata: metadataService,
4593
+ dataEngine
4594
+ }
4595
+ );
4596
+ if (registered.length > 0) {
4597
+ ctx.logger.info(
4598
+ `[AI] ${registered.length} action tool(s) registered: ${registered.join(", ")}`
4599
+ );
4600
+ }
4601
+ if (skipped.length > 0) {
4602
+ ctx.logger.debug(
4603
+ `[AI] Skipped ${skipped.length} action(s) for AI exposure`,
4604
+ { skipped }
4605
+ );
4606
+ }
4607
+ } catch (err) {
4608
+ ctx.logger.warn(
4609
+ "[AI] Failed to register action tools",
4610
+ err instanceof Error ? { error: err.message } : { error: String(err) }
4611
+ );
4612
+ }
4613
+ }
3327
4614
  if (metadataService) {
3328
4615
  const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
3329
4616
  for (const toolDef of DATA_TOOL_DEFINITIONS2) {
@@ -3374,6 +4661,19 @@ var AIServicePlugin = class {
3374
4661
  } catch (err) {
3375
4662
  ctx.logger.warn("[AI] Failed to register data_explorer skill", err instanceof Error ? { error: err.message } : { error: String(err) });
3376
4663
  }
4664
+ try {
4665
+ const skillExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("skill", ACTIONS_EXECUTOR_SKILL.name)) : false;
4666
+ if (skillExists === null) {
4667
+ ctx.logger.warn("[AI] Metadata service timed out checking actions_executor skill, skipping");
4668
+ } else if (!skillExists) {
4669
+ await withTimeout(metadataService.register("skill", ACTIONS_EXECUTOR_SKILL.name, ACTIONS_EXECUTOR_SKILL));
4670
+ ctx.logger.info("[AI] actions_executor skill registered");
4671
+ } else {
4672
+ ctx.logger.debug("[AI] actions_executor skill already exists, skipping auto-registration");
4673
+ }
4674
+ } catch (err) {
4675
+ ctx.logger.warn("[AI] Failed to register actions_executor skill", err instanceof Error ? { error: err.message } : { error: String(err) });
4676
+ }
3377
4677
  }
3378
4678
  }
3379
4679
  } catch {
@@ -3834,29 +5134,45 @@ function registerPackageTools(registry, context) {
3834
5134
  }
3835
5135
  // Annotate the CommonJS export names for ESM import in node:
3836
5136
  0 && (module.exports = {
5137
+ ACTIONS_EXECUTOR_SKILL,
3837
5138
  AIService,
3838
5139
  AIServicePlugin,
3839
5140
  AgentRuntime,
3840
5141
  AiConversationObject,
3841
5142
  AiMessageObject,
5143
+ AiTraceObject,
5144
+ AiTraceView,
3842
5145
  DATA_CHAT_AGENT,
5146
+ DATA_EXPLORER_SKILL,
3843
5147
  DATA_TOOL_DEFINITIONS,
3844
5148
  InMemoryConversationService,
3845
5149
  METADATA_ASSISTANT_AGENT,
5150
+ METADATA_AUTHORING_SKILL,
3846
5151
  METADATA_TOOL_DEFINITIONS,
3847
5152
  MemoryLLMAdapter,
5153
+ ModelRegistry,
5154
+ NullTraceRecorder,
3848
5155
  ObjectQLConversationService,
5156
+ ObjectQLTraceRecorder,
3849
5157
  PACKAGE_TOOL_DEFINITIONS,
5158
+ QUERY_DATA_TOOL,
5159
+ SchemaRetriever,
3850
5160
  SkillRegistry,
3851
5161
  ToolRegistry,
3852
5162
  VercelLLMAdapter,
5163
+ actionSkipReason,
5164
+ actionToToolDefinition,
5165
+ actionToolName,
3853
5166
  addFieldTool,
3854
5167
  buildAIRoutes,
3855
5168
  buildAgentRoutes,
3856
5169
  buildAssistantRoutes,
3857
5170
  buildToolRoutes,
5171
+ buildTraceEvent,
5172
+ computeCost,
3858
5173
  createObjectTool,
3859
5174
  createPackageTool,
5175
+ createQueryDataHandler,
3860
5176
  deleteFieldTool,
3861
5177
  describeObjectTool,
3862
5178
  encodeStreamPart,
@@ -3866,9 +5182,11 @@ function registerPackageTools(registry, context) {
3866
5182
  listObjectsTool,
3867
5183
  listPackagesTool,
3868
5184
  modifyFieldTool,
5185
+ registerActionsAsTools,
3869
5186
  registerDataTools,
3870
5187
  registerMetadataTools,
3871
5188
  registerPackageTools,
5189
+ registerQueryDataTool,
3872
5190
  setActivePackageTool
3873
5191
  });
3874
5192
  //# sourceMappingURL=index.cjs.map