@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.js CHANGED
@@ -844,8 +844,112 @@ var MemoryLLMAdapter = class {
844
844
  async chat(messages, options) {
845
845
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
846
846
  const userContent = lastUserMessage?.content;
847
- const text = typeof userContent === "string" ? userContent : "(complex content)";
848
- const content = lastUserMessage ? `[memory] ${text}` : "[memory] (no user message)";
847
+ const userText = typeof userContent === "string" ? userContent : "(complex content)";
848
+ const tools = options?.tools;
849
+ const hasQueryDataTool = Array.isArray(tools) && tools.some((t) => t?.name === "query_data");
850
+ const alreadyCalledQueryData = messages.some(
851
+ (m) => m.role === "tool" && Array.isArray(m.content) && m.content.some((c) => c?.toolName === "query_data")
852
+ );
853
+ const alreadyCalledAction = messages.some(
854
+ (m) => m.role === "tool" && Array.isArray(m.content) && m.content.some(
855
+ (c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")
856
+ )
857
+ );
858
+ if (Array.isArray(tools) && !alreadyCalledAction && lastUserMessage) {
859
+ const actionTools = tools.filter((t) => typeof t?.name === "string" && t.name.startsWith("action_"));
860
+ const chosen = pickActionTool(userText, actionTools);
861
+ if (chosen) {
862
+ const recordId = extractRecordIdFromMessages(messages, userText);
863
+ if (recordId) {
864
+ const toolCallId = `memory_tc_${Date.now().toString(36)}`;
865
+ return {
866
+ content: "",
867
+ model: options?.model ?? "memory",
868
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
869
+ toolCalls: [
870
+ {
871
+ type: "tool-call",
872
+ toolCallId,
873
+ toolName: chosen.name,
874
+ input: { recordId }
875
+ }
876
+ ]
877
+ };
878
+ }
879
+ }
880
+ }
881
+ if (hasQueryDataTool && !alreadyCalledQueryData && lastUserMessage) {
882
+ const toolCallId = `memory_tc_${Date.now().toString(36)}`;
883
+ return {
884
+ content: "",
885
+ model: options?.model ?? "memory",
886
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
887
+ toolCalls: [
888
+ {
889
+ type: "tool-call",
890
+ toolCallId,
891
+ toolName: "query_data",
892
+ input: { request: userText }
893
+ }
894
+ ]
895
+ };
896
+ }
897
+ if (alreadyCalledAction) {
898
+ const lastTool = [...messages].reverse().find((m) => m.role === "tool");
899
+ const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => typeof c?.toolName === "string" && c.toolName.startsWith("action_")) : void 0;
900
+ const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
901
+ let payload = {};
902
+ if (typeof raw === "string") {
903
+ try {
904
+ payload = JSON.parse(raw);
905
+ } catch {
906
+ }
907
+ } else if (raw && typeof raw === "object") {
908
+ payload = raw;
909
+ }
910
+ if (payload.error) {
911
+ return {
912
+ content: `[memory] action ${payload.action ?? ""} failed: ${payload.error}`,
913
+ model: options?.model ?? "memory",
914
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
915
+ };
916
+ }
917
+ return {
918
+ content: `[memory] ${payload.message ?? "Action executed."} (${payload.action ?? "action"})`,
919
+ model: options?.model ?? "memory",
920
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
921
+ };
922
+ }
923
+ if (alreadyCalledQueryData) {
924
+ const lastTool = [...messages].reverse().find((m) => m.role === "tool");
925
+ const part = Array.isArray(lastTool?.content) ? lastTool.content.find((c) => c?.toolName === "query_data") : void 0;
926
+ let payload = {};
927
+ const raw = part?.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part?.result;
928
+ if (typeof raw === "string") {
929
+ try {
930
+ payload = JSON.parse(raw);
931
+ } catch {
932
+ payload = {};
933
+ }
934
+ } else if (raw && typeof raw === "object") {
935
+ payload = raw;
936
+ }
937
+ if (payload.error) {
938
+ return {
939
+ content: `[memory] query_data failed: ${payload.error}`,
940
+ model: options?.model ?? "memory",
941
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
942
+ };
943
+ }
944
+ const records = payload.records ?? [];
945
+ const count = payload.count ?? records.length;
946
+ return {
947
+ content: `[memory] Found ${count} record${count === 1 ? "" : "s"} for "${userText}".`,
948
+ model: options?.model ?? "memory",
949
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
950
+ };
951
+ }
952
+ const content = lastUserMessage ? `[memory] ${userText}` : "[memory] (no user message)";
849
953
  return {
850
954
  content,
851
955
  model: options?.model ?? "memory",
@@ -897,23 +1001,42 @@ var MemoryLLMAdapter = class {
897
1001
  */
898
1002
  async generateObject(messages, schema, options) {
899
1003
  const sys = messages.filter((m) => m.role === "system").map((m) => typeof m.content === "string" ? m.content : "").join("\n");
900
- const headerRe = /^###\s+([a-z0-9_]+)\b/gim;
1004
+ const headerRe = /^###\s+([a-z0-9_]+)(?:\s+—\s+([^\n]+))?/gim;
901
1005
  const candidates = [];
902
1006
  for (const match of sys.matchAll(headerRe)) {
903
- if (match[1]) candidates.push(match[1]);
1007
+ const machineName = match[1];
1008
+ if (!machineName) continue;
1009
+ const aliasText = match[2] ?? "";
1010
+ const aliasTokens = /* @__PURE__ */ new Set();
1011
+ for (const t of machineName.split(/[^a-z0-9]+/)) {
1012
+ if (t) aliasTokens.add(t);
1013
+ }
1014
+ for (const t of aliasText.toLowerCase().split(/[^a-z0-9]+/)) {
1015
+ if (t) aliasTokens.add(t);
1016
+ }
1017
+ for (const t of [...aliasTokens]) {
1018
+ if (t.length > 3 && t.endsWith("s")) aliasTokens.add(t.slice(0, -1));
1019
+ }
1020
+ candidates.push({ name: machineName, aliasTokens });
904
1021
  }
905
1022
  const lastUser = [...messages].reverse().find((m) => m.role === "user");
906
1023
  const userText = typeof lastUser?.content === "string" ? lastUser.content.toLowerCase() : "";
907
1024
  const userTokens = new Set(
908
1025
  userText.split(/[^a-z0-9_]+/).filter((t) => t.length > 1)
909
1026
  );
910
- let chosen = candidates[0];
1027
+ for (const t of [...userTokens]) {
1028
+ if (t.length > 3 && t.endsWith("s")) userTokens.add(t.slice(0, -1));
1029
+ }
1030
+ let chosen = candidates[0]?.name;
911
1031
  let bestScore = -1;
912
- for (const name of candidates) {
913
- const score = name.split(/[^a-z0-9]+/).reduce((acc, tok) => acc + (tok && userTokens.has(tok) ? 1 : 0), 0);
1032
+ for (const cand of candidates) {
1033
+ let score = 0;
1034
+ for (const tok of cand.aliasTokens) {
1035
+ if (userTokens.has(tok)) score += 1;
1036
+ }
914
1037
  if (score > bestScore) {
915
1038
  bestScore = score;
916
- chosen = name;
1039
+ chosen = cand.name;
917
1040
  }
918
1041
  }
919
1042
  const attempts = [];
@@ -934,6 +1057,98 @@ var MemoryLLMAdapter = class {
934
1057
  );
935
1058
  }
936
1059
  };
1060
+ function pickActionTool(userText, actionTools) {
1061
+ if (actionTools.length === 0 || !userText) return null;
1062
+ const userTokens = new Set(
1063
+ userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2)
1064
+ );
1065
+ const ACTION_VERBS = /* @__PURE__ */ new Set([
1066
+ "complete",
1067
+ "finish",
1068
+ "done",
1069
+ "close",
1070
+ "start",
1071
+ "begin",
1072
+ "resume",
1073
+ "clone",
1074
+ "copy",
1075
+ "duplicate",
1076
+ "cancel",
1077
+ "abort",
1078
+ "archive",
1079
+ "restore",
1080
+ "approve",
1081
+ "reject",
1082
+ "assign",
1083
+ "unassign",
1084
+ "export",
1085
+ "import",
1086
+ "send",
1087
+ "notify",
1088
+ "publish",
1089
+ "unpublish",
1090
+ "mark"
1091
+ ]);
1092
+ const hasActionVerb = [...userTokens].some((t) => ACTION_VERBS.has(t));
1093
+ if (!hasActionVerb) return null;
1094
+ let best = null;
1095
+ let bestScore = 0;
1096
+ for (const tool of actionTools) {
1097
+ const nameTokens = tool.name.replace(/^action_/, "").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
1098
+ let score = 0;
1099
+ for (const tok of nameTokens) {
1100
+ if (!userTokens.has(tok)) continue;
1101
+ score += ACTION_VERBS.has(tok) ? 3 : 1;
1102
+ }
1103
+ if (score > bestScore) {
1104
+ bestScore = score;
1105
+ best = tool;
1106
+ }
1107
+ }
1108
+ return bestScore >= 3 ? best : null;
1109
+ }
1110
+ function extractRecordIdFromMessages(messages, userText) {
1111
+ const userTokens = userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
1112
+ for (let i = messages.length - 1; i >= 0; i--) {
1113
+ const m = messages[i];
1114
+ if (m.role !== "tool" || !Array.isArray(m.content)) continue;
1115
+ const parts = m.content;
1116
+ for (const part of parts) {
1117
+ if (part?.toolName !== "query_data") continue;
1118
+ const raw = part.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part.result;
1119
+ let payload = {};
1120
+ if (typeof raw === "string") {
1121
+ try {
1122
+ payload = JSON.parse(raw);
1123
+ } catch {
1124
+ }
1125
+ } else if (raw && typeof raw === "object") {
1126
+ payload = raw;
1127
+ }
1128
+ const records = payload.records ?? [];
1129
+ if (records.length === 0) continue;
1130
+ let bestId;
1131
+ let bestScore = -1;
1132
+ for (const rec of records) {
1133
+ if (!rec || typeof rec !== "object") continue;
1134
+ const id = rec.id;
1135
+ if (typeof id !== "string" && typeof id !== "number") continue;
1136
+ const hay = Object.values(rec).filter((v) => typeof v === "string").join(" ").toLowerCase();
1137
+ const hayTokens = hay.split(/[^a-z0-9]+/).filter(Boolean);
1138
+ let score = 0;
1139
+ for (const ut of userTokens) {
1140
+ if (hayTokens.includes(ut)) score += 1;
1141
+ }
1142
+ if (score > bestScore) {
1143
+ bestScore = score;
1144
+ bestId = String(id);
1145
+ }
1146
+ }
1147
+ return bestId ?? String(records[0].id);
1148
+ }
1149
+ }
1150
+ return void 0;
1151
+ }
937
1152
 
938
1153
  // src/tools/tool-registry.ts
939
1154
  var ToolRegistry = class {
@@ -1297,6 +1512,13 @@ var _AIService = class _AIService {
1297
1512
  * maximum number of iterations (`maxIterations`) is reached.
1298
1513
  */
1299
1514
  async chatWithTools(messages, options) {
1515
+ return this.instrument(
1516
+ "chat_with_tools",
1517
+ options,
1518
+ () => this.chatWithToolsImpl(messages, options)
1519
+ );
1520
+ }
1521
+ async chatWithToolsImpl(messages, options) {
1300
1522
  const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
1301
1523
  const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1302
1524
  const registeredTools = this.toolRegistry.getAll();
@@ -2808,6 +3030,63 @@ var AiTraceObject = ObjectSchema3.create({
2808
3030
  }
2809
3031
  });
2810
3032
 
3033
+ // src/views/ai-trace.view.ts
3034
+ import { defineView } from "@objectstack/spec";
3035
+ var AiTraceView = defineView({
3036
+ list: {
3037
+ type: "grid",
3038
+ data: { provider: "object", object: "ai_traces" },
3039
+ columns: [
3040
+ { field: "created_at", label: "Time" },
3041
+ { field: "operation" },
3042
+ { field: "model" },
3043
+ { field: "agent_id", label: "Agent" },
3044
+ { field: "latency_ms", label: "Latency (ms)" },
3045
+ { field: "total_tokens", label: "Tokens" },
3046
+ { field: "cost_total", label: "Cost" },
3047
+ { field: "status" }
3048
+ ],
3049
+ sort: [{ field: "created_at", order: "desc" }],
3050
+ pagination: { pageSize: 50 },
3051
+ searchableFields: ["conversation_id", "agent_id", "model", "error"],
3052
+ filterableFields: ["operation", "model", "status"]
3053
+ },
3054
+ listViews: {
3055
+ errors: {
3056
+ label: "Errors",
3057
+ type: "grid",
3058
+ data: { provider: "object", object: "ai_traces" },
3059
+ columns: [
3060
+ { field: "created_at", label: "Time" },
3061
+ { field: "operation" },
3062
+ { field: "model" },
3063
+ { field: "latency_ms", label: "Latency (ms)" },
3064
+ { field: "error" }
3065
+ ],
3066
+ filter: [
3067
+ { field: "status", operator: "=", value: "error" }
3068
+ ],
3069
+ sort: [{ field: "created_at", order: "desc" }]
3070
+ },
3071
+ by_model: {
3072
+ label: "By Model",
3073
+ type: "grid",
3074
+ data: { provider: "object", object: "ai_traces" },
3075
+ columns: [
3076
+ { field: "model" },
3077
+ { field: "operation" },
3078
+ { field: "latency_ms", label: "Latency (ms)" },
3079
+ { field: "total_tokens", label: "Tokens" },
3080
+ { field: "cost_total", label: "Cost" },
3081
+ { field: "status" },
3082
+ { field: "created_at", label: "Time" }
3083
+ ],
3084
+ grouping: { fields: [{ field: "model" }] },
3085
+ sort: [{ field: "created_at", order: "desc" }]
3086
+ }
3087
+ }
3088
+ });
3089
+
2811
3090
  // src/plugin.ts
2812
3091
  init_data_tools();
2813
3092
  init_metadata_tools();
@@ -2860,8 +3139,11 @@ var SchemaRetriever = class {
2860
3139
  const lines = ["## Schema context (auto-injected)"];
2861
3140
  for (const hit of hits) {
2862
3141
  const obj = hit.object;
2863
- const label = obj.label ? ` \u2014 ${obj.label}` : "";
2864
- lines.push(`### ${obj.name}${label}`);
3142
+ const parts = [];
3143
+ if (obj.label) parts.push(obj.label);
3144
+ if (obj.pluralLabel && obj.pluralLabel !== obj.label) parts.push(`(${obj.pluralLabel})`);
3145
+ const header = parts.length > 0 ? ` \u2014 ${parts.join(" ")}` : "";
3146
+ lines.push(`### ${obj.name}${header}`);
2865
3147
  const fields = Object.entries(obj.fields ?? {}).slice(0, maxFieldsPerObject);
2866
3148
  for (const [name, field] of fields) {
2867
3149
  lines.push(` - ${name}: ${describeField(field)}`);
@@ -3063,6 +3345,228 @@ function registerQueryDataTool(registry, context) {
3063
3345
  registry.register(QUERY_DATA_TOOL, createQueryDataHandler(context));
3064
3346
  }
3065
3347
 
3348
+ // src/tools/action-tools.ts
3349
+ function actionSkipReason(action) {
3350
+ if (action.aiExposed === false) {
3351
+ return "opted-out via aiExposed:false";
3352
+ }
3353
+ if (action.type !== "script") return `type='${action.type}' not yet supported`;
3354
+ if (!action.target && !action.body) return "no target or body";
3355
+ if (action.confirmText) return "requires confirmation (confirmText set)";
3356
+ if (action.mode === "delete") return "mode='delete' \u2014 destructive";
3357
+ if (action.variant === "danger") return "variant='danger' \u2014 destructive";
3358
+ return null;
3359
+ }
3360
+ function fieldTypeToJsonType(t) {
3361
+ switch (t) {
3362
+ case "number":
3363
+ case "currency":
3364
+ case "percent":
3365
+ case "rating":
3366
+ case "slider":
3367
+ case "autonumber":
3368
+ return "number";
3369
+ case "boolean":
3370
+ case "toggle":
3371
+ return "boolean";
3372
+ case "multiselect":
3373
+ case "checkboxes":
3374
+ case "tags":
3375
+ return "array";
3376
+ default:
3377
+ return "string";
3378
+ }
3379
+ }
3380
+ function resolveParam(param, ownerObject, allObjects) {
3381
+ const fieldRef = param.field;
3382
+ const owner = param.objectOverride && allObjects.get(param.objectOverride) ? allObjects.get(param.objectOverride) : ownerObject;
3383
+ const field = fieldRef ? owner?.fields?.[fieldRef] : void 0;
3384
+ const name = param.name ?? fieldRef;
3385
+ if (!name) return null;
3386
+ const type = param.type ?? field?.type;
3387
+ const jsonType = fieldTypeToJsonType(type);
3388
+ const schema = { type: jsonType };
3389
+ const label = typeof param.label === "string" ? param.label : field?.label;
3390
+ const help = param.helpText ?? field?.description;
3391
+ const description = [label, help].filter(Boolean).join(" \u2014 ") || void 0;
3392
+ if (description) schema.description = description;
3393
+ const optionSource = param.options ?? field?.options;
3394
+ if (Array.isArray(optionSource) && optionSource.length > 0) {
3395
+ const values = optionSource.map((o) => typeof o === "string" ? o : o.value).filter((v) => typeof v === "string");
3396
+ if (values.length > 0) {
3397
+ schema.enum = jsonType === "array" ? void 0 : values;
3398
+ if (jsonType === "array") {
3399
+ schema.items = { type: "string", enum: values };
3400
+ }
3401
+ }
3402
+ } else if (jsonType === "array") {
3403
+ schema.items = { type: "string" };
3404
+ }
3405
+ if (param.defaultValue !== void 0) {
3406
+ schema.default = param.defaultValue;
3407
+ }
3408
+ const required = Boolean(param.required ?? field?.required ?? false);
3409
+ return { name, schema, required };
3410
+ }
3411
+ function buildParametersSchema(action, ownerObject, allObjects) {
3412
+ const properties = {};
3413
+ const required = [];
3414
+ const isRowContext = Array.isArray(action.locations) && action.locations.some((l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related");
3415
+ if (action.objectName && isRowContext) {
3416
+ properties.recordId = {
3417
+ type: "string",
3418
+ description: `The ${action.objectName} record id to act on.`
3419
+ };
3420
+ if (action.recordIdParam || action.recordIdField) {
3421
+ required.push("recordId");
3422
+ }
3423
+ }
3424
+ for (const param of action.params ?? []) {
3425
+ const resolved = resolveParam(param, ownerObject, allObjects);
3426
+ if (!resolved) continue;
3427
+ properties[resolved.name] = resolved.schema;
3428
+ if (resolved.required) required.push(resolved.name);
3429
+ }
3430
+ return {
3431
+ type: "object",
3432
+ properties,
3433
+ ...required.length > 0 ? { required } : {},
3434
+ additionalProperties: false
3435
+ };
3436
+ }
3437
+ function actionToolName(action, prefix = "action_") {
3438
+ return `${prefix}${action.name}`;
3439
+ }
3440
+ function describeAction(action, ownerObject) {
3441
+ const label = typeof action.label === "string" ? action.label : action.name.replace(/_/g, " ");
3442
+ const target = action.objectName ?? ownerObject?.name;
3443
+ const targetLabel = ownerObject?.label ?? target;
3444
+ const parts = [];
3445
+ parts.push(`${label}${targetLabel ? ` \u2014 operates on ${targetLabel}` : ""}.`);
3446
+ if (action.successMessage && typeof action.successMessage === "string") {
3447
+ parts.push(`On success: ${action.successMessage}`);
3448
+ }
3449
+ if (action.mode) parts.push(`Mode: ${action.mode}.`);
3450
+ parts.push(
3451
+ "Use this when the user asks to perform this operation in natural language."
3452
+ );
3453
+ return parts.join(" ");
3454
+ }
3455
+ function actionToToolDefinition(action, ownerObject, allObjects, toolPrefix = "action_") {
3456
+ if (actionSkipReason(action) !== null) return null;
3457
+ return {
3458
+ name: actionToolName(action, toolPrefix),
3459
+ description: describeAction(action, ownerObject),
3460
+ parameters: buildParametersSchema(action, ownerObject, allObjects)
3461
+ };
3462
+ }
3463
+ function buildHandlerEngineAdapter(engine) {
3464
+ return {
3465
+ update: (object, id, data) => engine.update(object, { ...data, id }, { where: { id } }),
3466
+ insert: (object, data) => engine.insert(object, data),
3467
+ find: (object, where) => engine.find(object, { where }),
3468
+ delete: (object, ids) => engine.delete(object, { where: { id: ids.length === 1 ? ids[0] : { $in: ids } } })
3469
+ };
3470
+ }
3471
+ function createActionToolHandler(action, ctx) {
3472
+ const principal = ctx.principal ?? { id: "ai_agent", name: "AI Assistant" };
3473
+ const engineAdapter = buildHandlerEngineAdapter(ctx.dataEngine);
3474
+ const requiresRecord = Array.isArray(action.locations) && action.locations.some(
3475
+ (l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related"
3476
+ );
3477
+ return async (args) => {
3478
+ const objectName = action.objectName;
3479
+ const target = action.target;
3480
+ const result = {
3481
+ ok: false,
3482
+ action: action.name,
3483
+ objectName
3484
+ };
3485
+ if (!objectName) {
3486
+ result.error = "Action has no objectName; cannot dispatch.";
3487
+ return JSON.stringify(result);
3488
+ }
3489
+ if (!target) {
3490
+ result.error = "Action has no target handler.";
3491
+ return JSON.stringify(result);
3492
+ }
3493
+ const recordId = typeof args.recordId === "string" && args.recordId.length > 0 ? args.recordId : void 0;
3494
+ let record;
3495
+ if (requiresRecord) {
3496
+ if (!recordId) {
3497
+ 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).`;
3498
+ return JSON.stringify(result);
3499
+ }
3500
+ try {
3501
+ const found = await ctx.dataEngine.find(objectName, {
3502
+ where: { id: recordId },
3503
+ limit: 1
3504
+ });
3505
+ record = found[0];
3506
+ if (!record) {
3507
+ result.error = `Record ${recordId} not found in ${objectName}.`;
3508
+ return JSON.stringify(result);
3509
+ }
3510
+ result.recordId = recordId;
3511
+ } catch (err) {
3512
+ result.error = `Failed to load record: ${err instanceof Error ? err.message : String(err)}`;
3513
+ return JSON.stringify(result);
3514
+ }
3515
+ }
3516
+ const { recordId: _omit, ...userParams } = args;
3517
+ try {
3518
+ const handlerCtx = {
3519
+ record,
3520
+ user: principal,
3521
+ engine: engineAdapter,
3522
+ params: userParams
3523
+ };
3524
+ const out = await ctx.dataEngine.executeAction?.(objectName, target, handlerCtx);
3525
+ result.ok = true;
3526
+ result.result = out ?? null;
3527
+ const successMsg = typeof action.successMessage === "string" ? action.successMessage : void 0;
3528
+ result.message = successMsg ?? `Action '${action.name}' executed successfully.`;
3529
+ return JSON.stringify(result);
3530
+ } catch (err) {
3531
+ result.error = err instanceof Error ? err.message : String(err);
3532
+ return JSON.stringify(result);
3533
+ }
3534
+ };
3535
+ }
3536
+ async function registerActionsAsTools(registry, context) {
3537
+ const objects = await context.metadata.listObjects();
3538
+ const objMap = new Map(
3539
+ objects.filter((o) => Boolean(o?.name)).map((o) => [o.name, o])
3540
+ );
3541
+ const registered = [];
3542
+ const skipped = [];
3543
+ const prefix = context.toolPrefix ?? "action_";
3544
+ for (const obj of objects) {
3545
+ if (!obj?.actions || !Array.isArray(obj.actions)) continue;
3546
+ for (const action of obj.actions) {
3547
+ if (!action || typeof action.name !== "string") continue;
3548
+ const normalized = {
3549
+ ...action,
3550
+ objectName: action.objectName ?? obj.name
3551
+ };
3552
+ const reason = actionSkipReason(normalized);
3553
+ if (reason !== null) {
3554
+ skipped.push({ action: normalized.name, reason });
3555
+ continue;
3556
+ }
3557
+ const definition = actionToToolDefinition(normalized, obj, objMap, prefix);
3558
+ if (!definition) continue;
3559
+ if (registry.has(definition.name)) {
3560
+ skipped.push({ action: normalized.name, reason: "tool name already registered" });
3561
+ continue;
3562
+ }
3563
+ registry.register(definition, createActionToolHandler(normalized, context));
3564
+ registered.push(definition.name);
3565
+ }
3566
+ }
3567
+ return { registered, skipped };
3568
+ }
3569
+
3066
3570
  // src/agent-runtime.ts
3067
3571
  import { AgentSchema } from "@objectstack/spec/ai";
3068
3572
  var AgentRuntime = class {
@@ -3365,6 +3869,16 @@ var SkillRegistry = class {
3365
3869
  const resolved = [];
3366
3870
  for (const skill of skills) {
3367
3871
  for (const toolName of skill.tools) {
3872
+ if (toolName.endsWith("*")) {
3873
+ const prefix = toolName.slice(0, -1);
3874
+ for (const def2 of availableTools) {
3875
+ if (!def2.name.startsWith(prefix)) continue;
3876
+ if (seen.has(def2.name)) continue;
3877
+ resolved.push(def2);
3878
+ seen.add(def2.name);
3879
+ }
3880
+ continue;
3881
+ }
3368
3882
  if (seen.has(toolName)) continue;
3369
3883
  const def = toolMap.get(toolName);
3370
3884
  if (def) {
@@ -3428,7 +3942,8 @@ Always answer in the same language the user is using. Detailed tool-usage guidan
3428
3942
  maxTokens: 4096
3429
3943
  },
3430
3944
  // Capability bundle lives on the skill; the agent only references it.
3431
- skills: ["data_explorer"],
3945
+ // `data_explorer` = read side, `actions_executor` = write side.
3946
+ skills: ["data_explorer", "actions_executor"],
3432
3947
  active: true,
3433
3948
  visibility: "global",
3434
3949
  guardrails: {
@@ -3508,6 +4023,7 @@ Guidelines:
3508
4023
  7. Never expose internal IDs unless the user explicitly asks for them.
3509
4024
  8. Always answer in the same language the user is using.`,
3510
4025
  tools: [
4026
+ "query_data",
3511
4027
  "list_objects",
3512
4028
  "describe_object",
3513
4029
  "query_records",
@@ -3577,6 +4093,44 @@ Guidelines:
3577
4093
  active: true
3578
4094
  };
3579
4095
 
4096
+ // src/skills/actions-executor-skill.ts
4097
+ var ACTIONS_EXECUTOR_SKILL = {
4098
+ name: "actions_executor",
4099
+ label: "Action Executor",
4100
+ description: "Perform business operations on the user's data \u2014 invoke actions like 'mark as complete', 'start task', 'clone record' through natural language.",
4101
+ instructions: `You can perform business operations by invoking the user's registered actions.
4102
+
4103
+ Capabilities:
4104
+ - Each tool whose name starts with \`action_\` is a business operation declared on an object.
4105
+ - Read the tool description carefully \u2014 it tells you what the action does and what record types it applies to.
4106
+ - 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.
4107
+
4108
+ Guidelines:
4109
+ 1. Confirm intent \u2014 when the user says "complete it" / "start that one", make sure you know *which* record they mean. Ask if ambiguous.
4110
+ 2. Use \`query_data\` to look up records by natural-language description ("the design review task", "tickets assigned to me").
4111
+ 3. After invoking an action, the tool returns \`{ ok, message, result }\`. Summarise success in plain language; surface errors verbatim.
4112
+ 4. Never invent recordIds. If \`query_data\` didn't return one, tell the user instead of guessing.
4113
+ 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.
4114
+ 6. Always answer in the same language the user is using.`,
4115
+ // Dynamically materialised: the runtime registers one tool per Action,
4116
+ // and the skill subscribes to the whole family via the `action_*`
4117
+ // glob (resolved by SkillRegistry.flattenToTools).
4118
+ tools: ["action_*"],
4119
+ triggerPhrases: [
4120
+ "complete",
4121
+ "mark as",
4122
+ "start",
4123
+ "finish",
4124
+ "clone",
4125
+ "duplicate",
4126
+ "do it",
4127
+ "run",
4128
+ "invoke",
4129
+ "execute"
4130
+ ],
4131
+ active: true
4132
+ };
4133
+
3580
4134
  // src/adapters/vercel-adapter.ts
3581
4135
  import { generateText, streamText, generateObject, tool as vercelTool, jsonSchema } from "ai";
3582
4136
  function buildVercelOptions(options) {
@@ -3919,7 +4473,8 @@ var AIServicePlugin = class {
3919
4473
  type: "plugin",
3920
4474
  scope: "project",
3921
4475
  namespace: "ai",
3922
- objects: [AiConversationObject, AiMessageObject, AiTraceObject]
4476
+ objects: [AiConversationObject, AiMessageObject, AiTraceObject],
4477
+ views: [AiTraceView]
3923
4478
  });
3924
4479
  if (this.options.debug) {
3925
4480
  ctx.hook("ai:beforeChat", async (messages) => {
@@ -3958,6 +4513,31 @@ var AIServicePlugin = class {
3958
4513
  dataEngine
3959
4514
  });
3960
4515
  ctx.logger.info("[AI] query_data tool registered");
4516
+ try {
4517
+ const { registered, skipped } = await registerActionsAsTools(
4518
+ this.service.toolRegistry,
4519
+ {
4520
+ metadata: metadataService,
4521
+ dataEngine
4522
+ }
4523
+ );
4524
+ if (registered.length > 0) {
4525
+ ctx.logger.info(
4526
+ `[AI] ${registered.length} action tool(s) registered: ${registered.join(", ")}`
4527
+ );
4528
+ }
4529
+ if (skipped.length > 0) {
4530
+ ctx.logger.debug(
4531
+ `[AI] Skipped ${skipped.length} action(s) for AI exposure`,
4532
+ { skipped }
4533
+ );
4534
+ }
4535
+ } catch (err) {
4536
+ ctx.logger.warn(
4537
+ "[AI] Failed to register action tools",
4538
+ err instanceof Error ? { error: err.message } : { error: String(err) }
4539
+ );
4540
+ }
3961
4541
  }
3962
4542
  if (metadataService) {
3963
4543
  const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
@@ -4009,6 +4589,19 @@ var AIServicePlugin = class {
4009
4589
  } catch (err) {
4010
4590
  ctx.logger.warn("[AI] Failed to register data_explorer skill", err instanceof Error ? { error: err.message } : { error: String(err) });
4011
4591
  }
4592
+ try {
4593
+ const skillExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("skill", ACTIONS_EXECUTOR_SKILL.name)) : false;
4594
+ if (skillExists === null) {
4595
+ ctx.logger.warn("[AI] Metadata service timed out checking actions_executor skill, skipping");
4596
+ } else if (!skillExists) {
4597
+ await withTimeout(metadataService.register("skill", ACTIONS_EXECUTOR_SKILL.name, ACTIONS_EXECUTOR_SKILL));
4598
+ ctx.logger.info("[AI] actions_executor skill registered");
4599
+ } else {
4600
+ ctx.logger.debug("[AI] actions_executor skill already exists, skipping auto-registration");
4601
+ }
4602
+ } catch (err) {
4603
+ ctx.logger.warn("[AI] Failed to register actions_executor skill", err instanceof Error ? { error: err.message } : { error: String(err) });
4604
+ }
4012
4605
  }
4013
4606
  }
4014
4607
  } catch {
@@ -4468,16 +5061,20 @@ function registerPackageTools(registry, context) {
4468
5061
  registry.register(setActivePackageTool, createSetActivePackageHandler(context));
4469
5062
  }
4470
5063
  export {
5064
+ ACTIONS_EXECUTOR_SKILL,
4471
5065
  AIService,
4472
5066
  AIServicePlugin,
4473
5067
  AgentRuntime,
4474
5068
  AiConversationObject,
4475
5069
  AiMessageObject,
4476
5070
  AiTraceObject,
5071
+ AiTraceView,
4477
5072
  DATA_CHAT_AGENT,
5073
+ DATA_EXPLORER_SKILL,
4478
5074
  DATA_TOOL_DEFINITIONS,
4479
5075
  InMemoryConversationService,
4480
5076
  METADATA_ASSISTANT_AGENT,
5077
+ METADATA_AUTHORING_SKILL,
4481
5078
  METADATA_TOOL_DEFINITIONS,
4482
5079
  MemoryLLMAdapter,
4483
5080
  ModelRegistry,
@@ -4490,6 +5087,9 @@ export {
4490
5087
  SkillRegistry,
4491
5088
  ToolRegistry,
4492
5089
  VercelLLMAdapter,
5090
+ actionSkipReason,
5091
+ actionToToolDefinition,
5092
+ actionToolName,
4493
5093
  addFieldTool,
4494
5094
  buildAIRoutes,
4495
5095
  buildAgentRoutes,
@@ -4509,6 +5109,7 @@ export {
4509
5109
  listObjectsTool,
4510
5110
  listPackagesTool,
4511
5111
  modifyFieldTool,
5112
+ registerActionsAsTools,
4512
5113
  registerDataTools,
4513
5114
  registerMetadataTools,
4514
5115
  registerPackageTools,