@objectstack/service-ai 6.0.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
@@ -848,16 +848,20 @@ var init_metadata_tools = __esm({
848
848
  // src/index.ts
849
849
  var index_exports = {};
850
850
  __export(index_exports, {
851
+ ACTIONS_EXECUTOR_SKILL: () => ACTIONS_EXECUTOR_SKILL,
851
852
  AIService: () => AIService,
852
853
  AIServicePlugin: () => AIServicePlugin,
853
854
  AgentRuntime: () => AgentRuntime,
854
855
  AiConversationObject: () => AiConversationObject,
855
856
  AiMessageObject: () => AiMessageObject,
856
857
  AiTraceObject: () => AiTraceObject,
858
+ AiTraceView: () => AiTraceView,
857
859
  DATA_CHAT_AGENT: () => DATA_CHAT_AGENT,
860
+ DATA_EXPLORER_SKILL: () => DATA_EXPLORER_SKILL,
858
861
  DATA_TOOL_DEFINITIONS: () => DATA_TOOL_DEFINITIONS,
859
862
  InMemoryConversationService: () => InMemoryConversationService,
860
863
  METADATA_ASSISTANT_AGENT: () => METADATA_ASSISTANT_AGENT,
864
+ METADATA_AUTHORING_SKILL: () => METADATA_AUTHORING_SKILL,
861
865
  METADATA_TOOL_DEFINITIONS: () => METADATA_TOOL_DEFINITIONS,
862
866
  MemoryLLMAdapter: () => MemoryLLMAdapter,
863
867
  ModelRegistry: () => ModelRegistry,
@@ -870,6 +874,9 @@ __export(index_exports, {
870
874
  SkillRegistry: () => SkillRegistry,
871
875
  ToolRegistry: () => ToolRegistry,
872
876
  VercelLLMAdapter: () => VercelLLMAdapter,
877
+ actionSkipReason: () => actionSkipReason,
878
+ actionToToolDefinition: () => actionToToolDefinition,
879
+ actionToolName: () => actionToolName,
873
880
  addFieldTool: () => addFieldTool,
874
881
  buildAIRoutes: () => buildAIRoutes,
875
882
  buildAgentRoutes: () => buildAgentRoutes,
@@ -889,6 +896,7 @@ __export(index_exports, {
889
896
  listObjectsTool: () => listObjectsTool,
890
897
  listPackagesTool: () => listPackagesTool,
891
898
  modifyFieldTool: () => modifyFieldTool,
899
+ registerActionsAsTools: () => registerActionsAsTools,
892
900
  registerDataTools: () => registerDataTools,
893
901
  registerMetadataTools: () => registerMetadataTools,
894
902
  registerPackageTools: () => registerPackageTools,
@@ -908,8 +916,112 @@ var MemoryLLMAdapter = class {
908
916
  async chat(messages, options) {
909
917
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
910
918
  const userContent = lastUserMessage?.content;
911
- const text = typeof userContent === "string" ? userContent : "(complex content)";
912
- 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)";
913
1025
  return {
914
1026
  content,
915
1027
  model: options?.model ?? "memory",
@@ -961,23 +1073,42 @@ var MemoryLLMAdapter = class {
961
1073
  */
962
1074
  async generateObject(messages, schema, options) {
963
1075
  const sys = messages.filter((m) => m.role === "system").map((m) => typeof m.content === "string" ? m.content : "").join("\n");
964
- const headerRe = /^###\s+([a-z0-9_]+)\b/gim;
1076
+ const headerRe = /^###\s+([a-z0-9_]+)(?:\s+—\s+([^\n]+))?/gim;
965
1077
  const candidates = [];
966
1078
  for (const match of sys.matchAll(headerRe)) {
967
- if (match[1]) candidates.push(match[1]);
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 });
968
1093
  }
969
1094
  const lastUser = [...messages].reverse().find((m) => m.role === "user");
970
1095
  const userText = typeof lastUser?.content === "string" ? lastUser.content.toLowerCase() : "";
971
1096
  const userTokens = new Set(
972
1097
  userText.split(/[^a-z0-9_]+/).filter((t) => t.length > 1)
973
1098
  );
974
- let chosen = candidates[0];
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;
975
1103
  let bestScore = -1;
976
- for (const name of candidates) {
977
- const score = name.split(/[^a-z0-9]+/).reduce((acc, tok) => acc + (tok && userTokens.has(tok) ? 1 : 0), 0);
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
+ }
978
1109
  if (score > bestScore) {
979
1110
  bestScore = score;
980
- chosen = name;
1111
+ chosen = cand.name;
981
1112
  }
982
1113
  }
983
1114
  const attempts = [];
@@ -998,6 +1129,98 @@ var MemoryLLMAdapter = class {
998
1129
  );
999
1130
  }
1000
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
+ }
1001
1224
 
1002
1225
  // src/tools/tool-registry.ts
1003
1226
  var ToolRegistry = class {
@@ -1361,6 +1584,13 @@ var _AIService = class _AIService {
1361
1584
  * maximum number of iterations (`maxIterations`) is reached.
1362
1585
  */
1363
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) {
1364
1594
  const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
1365
1595
  const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1366
1596
  const registeredTools = this.toolRegistry.getAll();
@@ -2872,6 +3102,63 @@ var AiTraceObject = import_data3.ObjectSchema.create({
2872
3102
  }
2873
3103
  });
2874
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
+
2875
3162
  // src/plugin.ts
2876
3163
  init_data_tools();
2877
3164
  init_metadata_tools();
@@ -2924,8 +3211,11 @@ var SchemaRetriever = class {
2924
3211
  const lines = ["## Schema context (auto-injected)"];
2925
3212
  for (const hit of hits) {
2926
3213
  const obj = hit.object;
2927
- const label = obj.label ? ` \u2014 ${obj.label}` : "";
2928
- lines.push(`### ${obj.name}${label}`);
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}`);
2929
3219
  const fields = Object.entries(obj.fields ?? {}).slice(0, maxFieldsPerObject);
2930
3220
  for (const [name, field] of fields) {
2931
3221
  lines.push(` - ${name}: ${describeField(field)}`);
@@ -3127,6 +3417,228 @@ function registerQueryDataTool(registry, context) {
3127
3417
  registry.register(QUERY_DATA_TOOL, createQueryDataHandler(context));
3128
3418
  }
3129
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
+
3130
3642
  // src/agent-runtime.ts
3131
3643
  var import_ai7 = require("@objectstack/spec/ai");
3132
3644
  var AgentRuntime = class {
@@ -3429,6 +3941,16 @@ var SkillRegistry = class {
3429
3941
  const resolved = [];
3430
3942
  for (const skill of skills) {
3431
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
+ }
3432
3954
  if (seen.has(toolName)) continue;
3433
3955
  const def = toolMap.get(toolName);
3434
3956
  if (def) {
@@ -3492,7 +4014,8 @@ Always answer in the same language the user is using. Detailed tool-usage guidan
3492
4014
  maxTokens: 4096
3493
4015
  },
3494
4016
  // Capability bundle lives on the skill; the agent only references it.
3495
- skills: ["data_explorer"],
4017
+ // `data_explorer` = read side, `actions_executor` = write side.
4018
+ skills: ["data_explorer", "actions_executor"],
3496
4019
  active: true,
3497
4020
  visibility: "global",
3498
4021
  guardrails: {
@@ -3572,6 +4095,7 @@ Guidelines:
3572
4095
  7. Never expose internal IDs unless the user explicitly asks for them.
3573
4096
  8. Always answer in the same language the user is using.`,
3574
4097
  tools: [
4098
+ "query_data",
3575
4099
  "list_objects",
3576
4100
  "describe_object",
3577
4101
  "query_records",
@@ -3641,6 +4165,44 @@ Guidelines:
3641
4165
  active: true
3642
4166
  };
3643
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
+
3644
4206
  // src/adapters/vercel-adapter.ts
3645
4207
  var import_ai9 = require("ai");
3646
4208
  function buildVercelOptions(options) {
@@ -3983,7 +4545,8 @@ var AIServicePlugin = class {
3983
4545
  type: "plugin",
3984
4546
  scope: "project",
3985
4547
  namespace: "ai",
3986
- objects: [AiConversationObject, AiMessageObject, AiTraceObject]
4548
+ objects: [AiConversationObject, AiMessageObject, AiTraceObject],
4549
+ views: [AiTraceView]
3987
4550
  });
3988
4551
  if (this.options.debug) {
3989
4552
  ctx.hook("ai:beforeChat", async (messages) => {
@@ -4022,6 +4585,31 @@ var AIServicePlugin = class {
4022
4585
  dataEngine
4023
4586
  });
4024
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
+ }
4025
4613
  }
4026
4614
  if (metadataService) {
4027
4615
  const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
@@ -4073,6 +4661,19 @@ var AIServicePlugin = class {
4073
4661
  } catch (err) {
4074
4662
  ctx.logger.warn("[AI] Failed to register data_explorer skill", err instanceof Error ? { error: err.message } : { error: String(err) });
4075
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
+ }
4076
4677
  }
4077
4678
  }
4078
4679
  } catch {
@@ -4533,16 +5134,20 @@ function registerPackageTools(registry, context) {
4533
5134
  }
4534
5135
  // Annotate the CommonJS export names for ESM import in node:
4535
5136
  0 && (module.exports = {
5137
+ ACTIONS_EXECUTOR_SKILL,
4536
5138
  AIService,
4537
5139
  AIServicePlugin,
4538
5140
  AgentRuntime,
4539
5141
  AiConversationObject,
4540
5142
  AiMessageObject,
4541
5143
  AiTraceObject,
5144
+ AiTraceView,
4542
5145
  DATA_CHAT_AGENT,
5146
+ DATA_EXPLORER_SKILL,
4543
5147
  DATA_TOOL_DEFINITIONS,
4544
5148
  InMemoryConversationService,
4545
5149
  METADATA_ASSISTANT_AGENT,
5150
+ METADATA_AUTHORING_SKILL,
4546
5151
  METADATA_TOOL_DEFINITIONS,
4547
5152
  MemoryLLMAdapter,
4548
5153
  ModelRegistry,
@@ -4555,6 +5160,9 @@ function registerPackageTools(registry, context) {
4555
5160
  SkillRegistry,
4556
5161
  ToolRegistry,
4557
5162
  VercelLLMAdapter,
5163
+ actionSkipReason,
5164
+ actionToToolDefinition,
5165
+ actionToolName,
4558
5166
  addFieldTool,
4559
5167
  buildAIRoutes,
4560
5168
  buildAgentRoutes,
@@ -4574,6 +5182,7 @@ function registerPackageTools(registry, context) {
4574
5182
  listObjectsTool,
4575
5183
  listPackagesTool,
4576
5184
  modifyFieldTool,
5185
+ registerActionsAsTools,
4577
5186
  registerDataTools,
4578
5187
  registerMetadataTools,
4579
5188
  registerPackageTools,