@objectstack/service-ai 6.0.0 → 6.2.0
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 +1501 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1147 -117
- package/dist/index.d.ts +1147 -117
- package/dist/index.js +1493 -38
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
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
|
|
912
|
-
const
|
|
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_]+)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
977
|
-
|
|
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,103 @@ 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
|
+
"delete",
|
|
1164
|
+
"remove",
|
|
1165
|
+
"purge",
|
|
1166
|
+
"destroy",
|
|
1167
|
+
"erase"
|
|
1168
|
+
]);
|
|
1169
|
+
const hasActionVerb = [...userTokens].some((t) => ACTION_VERBS.has(t));
|
|
1170
|
+
if (!hasActionVerb) return null;
|
|
1171
|
+
let best = null;
|
|
1172
|
+
let bestScore = 0;
|
|
1173
|
+
for (const tool of actionTools) {
|
|
1174
|
+
const nameTokens = tool.name.replace(/^action_/, "").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
|
|
1175
|
+
let score = 0;
|
|
1176
|
+
for (const tok of nameTokens) {
|
|
1177
|
+
if (!userTokens.has(tok)) continue;
|
|
1178
|
+
score += ACTION_VERBS.has(tok) ? 3 : 1;
|
|
1179
|
+
}
|
|
1180
|
+
if (score > bestScore) {
|
|
1181
|
+
bestScore = score;
|
|
1182
|
+
best = tool;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return bestScore >= 3 ? best : null;
|
|
1186
|
+
}
|
|
1187
|
+
function extractRecordIdFromMessages(messages, userText) {
|
|
1188
|
+
const userTokens = userText.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
|
|
1189
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1190
|
+
const m = messages[i];
|
|
1191
|
+
if (m.role !== "tool" || !Array.isArray(m.content)) continue;
|
|
1192
|
+
const parts = m.content;
|
|
1193
|
+
for (const part of parts) {
|
|
1194
|
+
if (part?.toolName !== "query_data") continue;
|
|
1195
|
+
const raw = part.output && typeof part.output === "object" && "value" in part.output ? part.output.value : part.result;
|
|
1196
|
+
let payload = {};
|
|
1197
|
+
if (typeof raw === "string") {
|
|
1198
|
+
try {
|
|
1199
|
+
payload = JSON.parse(raw);
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
} else if (raw && typeof raw === "object") {
|
|
1203
|
+
payload = raw;
|
|
1204
|
+
}
|
|
1205
|
+
const records = payload.records ?? [];
|
|
1206
|
+
if (records.length === 0) continue;
|
|
1207
|
+
let bestId;
|
|
1208
|
+
let bestScore = -1;
|
|
1209
|
+
for (const rec of records) {
|
|
1210
|
+
if (!rec || typeof rec !== "object") continue;
|
|
1211
|
+
const id = rec.id;
|
|
1212
|
+
if (typeof id !== "string" && typeof id !== "number") continue;
|
|
1213
|
+
const hay = Object.values(rec).filter((v) => typeof v === "string").join(" ").toLowerCase();
|
|
1214
|
+
const hayTokens = hay.split(/[^a-z0-9]+/).filter(Boolean);
|
|
1215
|
+
let score = 0;
|
|
1216
|
+
for (const ut of userTokens) {
|
|
1217
|
+
if (hayTokens.includes(ut)) score += 1;
|
|
1218
|
+
}
|
|
1219
|
+
if (score > bestScore) {
|
|
1220
|
+
bestScore = score;
|
|
1221
|
+
bestId = String(id);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return bestId ?? String(records[0].id);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return void 0;
|
|
1228
|
+
}
|
|
1001
1229
|
|
|
1002
1230
|
// src/tools/tool-registry.ts
|
|
1003
1231
|
var ToolRegistry = class {
|
|
@@ -1241,12 +1469,20 @@ function finishPart(result) {
|
|
|
1241
1469
|
}
|
|
1242
1470
|
var _AIService = class _AIService {
|
|
1243
1471
|
constructor(config = {}) {
|
|
1472
|
+
/**
|
|
1473
|
+
* Map of tool-name → dispatcher used to re-run an approved pending
|
|
1474
|
+
* action. Populated by `registerActionsAsTools()` when action
|
|
1475
|
+
* approval is enabled. Kept private because callers should go
|
|
1476
|
+
* through `approvePendingAction()`.
|
|
1477
|
+
*/
|
|
1478
|
+
this.pendingDispatchers = /* @__PURE__ */ new Map();
|
|
1244
1479
|
this.adapter = config.adapter ?? new MemoryLLMAdapter();
|
|
1245
1480
|
this.logger = config.logger ?? (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
1246
1481
|
this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
|
|
1247
1482
|
this.conversationService = config.conversationService ?? new InMemoryConversationService();
|
|
1248
1483
|
this.modelRegistry = config.modelRegistry;
|
|
1249
1484
|
this.traceRecorder = config.traceRecorder ?? new NullTraceRecorder();
|
|
1485
|
+
this.dataEngine = config.dataEngine;
|
|
1250
1486
|
this.logger.info(
|
|
1251
1487
|
`[AI] Service initialized with adapter="${this.adapter.name}", tools=${this.toolRegistry.size}, models=${this.modelRegistry?.size ?? 0}`
|
|
1252
1488
|
);
|
|
@@ -1361,6 +1597,13 @@ var _AIService = class _AIService {
|
|
|
1361
1597
|
* maximum number of iterations (`maxIterations`) is reached.
|
|
1362
1598
|
*/
|
|
1363
1599
|
async chatWithTools(messages, options) {
|
|
1600
|
+
return this.instrument(
|
|
1601
|
+
"chat_with_tools",
|
|
1602
|
+
options,
|
|
1603
|
+
() => this.chatWithToolsImpl(messages, options)
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
async chatWithToolsImpl(messages, options) {
|
|
1364
1607
|
const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
|
|
1365
1608
|
const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
|
|
1366
1609
|
const registeredTools = this.toolRegistry.getAll();
|
|
@@ -1513,11 +1756,144 @@ var _AIService = class _AIService {
|
|
|
1513
1756
|
yield textDeltaPart("stream", result.content);
|
|
1514
1757
|
yield finishPart(result);
|
|
1515
1758
|
}
|
|
1759
|
+
// ── HITL: pending-action queue ─────────────────────────────────
|
|
1760
|
+
/**
|
|
1761
|
+
* Register a dispatcher callback for a tool. Called by
|
|
1762
|
+
* `registerActionsAsTools()` when action approval is enabled so the
|
|
1763
|
+
* approval handler can re-run the exact same code path the LLM
|
|
1764
|
+
* would have triggered.
|
|
1765
|
+
*/
|
|
1766
|
+
registerPendingActionDispatcher(toolName, dispatch) {
|
|
1767
|
+
this.pendingDispatchers.set(toolName, dispatch);
|
|
1768
|
+
}
|
|
1769
|
+
async proposePendingAction(input) {
|
|
1770
|
+
if (!this.dataEngine) {
|
|
1771
|
+
throw new Error("proposePendingAction requires a dataEngine \u2014 wire it via AIServiceConfig.");
|
|
1772
|
+
}
|
|
1773
|
+
const id = `pa_${cryptoRandomId()}`;
|
|
1774
|
+
const row = {
|
|
1775
|
+
id,
|
|
1776
|
+
conversation_id: input.conversationId ?? null,
|
|
1777
|
+
message_id: input.messageId ?? null,
|
|
1778
|
+
object_name: input.objectName,
|
|
1779
|
+
action_name: input.actionName,
|
|
1780
|
+
tool_name: input.toolName,
|
|
1781
|
+
tool_input: JSON.stringify(input.toolInput ?? {}),
|
|
1782
|
+
status: "pending",
|
|
1783
|
+
proposed_by: input.proposedBy ?? "ai_agent",
|
|
1784
|
+
proposed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1785
|
+
};
|
|
1786
|
+
await this.dataEngine.insert("ai_pending_actions", row);
|
|
1787
|
+
this.logger.info(
|
|
1788
|
+
`[AI] pending action proposed: ${id} (${input.toolName} on ${input.objectName})`
|
|
1789
|
+
);
|
|
1790
|
+
return { id };
|
|
1791
|
+
}
|
|
1792
|
+
async approvePendingAction(id, actorId) {
|
|
1793
|
+
if (!this.dataEngine) {
|
|
1794
|
+
throw new Error("approvePendingAction requires a dataEngine.");
|
|
1795
|
+
}
|
|
1796
|
+
const row = await this.loadPendingRow(id);
|
|
1797
|
+
if (row.status !== "pending") {
|
|
1798
|
+
throw new Error(`pending action ${id} is already ${row.status}`);
|
|
1799
|
+
}
|
|
1800
|
+
const dispatch = this.pendingDispatchers.get(row.tool_name);
|
|
1801
|
+
if (!dispatch) {
|
|
1802
|
+
throw new Error(
|
|
1803
|
+
`no dispatcher registered for tool '${row.tool_name}' \u2014 was the AI plugin restarted without re-registering actions?`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
await this.dataEngine.update(
|
|
1807
|
+
"ai_pending_actions",
|
|
1808
|
+
{
|
|
1809
|
+
id,
|
|
1810
|
+
status: "approved",
|
|
1811
|
+
decided_by: actorId,
|
|
1812
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1813
|
+
},
|
|
1814
|
+
{ where: { id } }
|
|
1815
|
+
);
|
|
1816
|
+
let parsed = {};
|
|
1817
|
+
try {
|
|
1818
|
+
parsed = row.tool_input ? JSON.parse(row.tool_input) : {};
|
|
1819
|
+
} catch {
|
|
1820
|
+
parsed = {};
|
|
1821
|
+
}
|
|
1822
|
+
try {
|
|
1823
|
+
const out = await dispatch(parsed);
|
|
1824
|
+
await this.dataEngine.update(
|
|
1825
|
+
"ai_pending_actions",
|
|
1826
|
+
{ id, status: "executed", result: JSON.stringify(out ?? null) },
|
|
1827
|
+
{ where: { id } }
|
|
1828
|
+
);
|
|
1829
|
+
this.logger.info(`[AI] pending action ${id} executed by ${actorId}`);
|
|
1830
|
+
return { status: "executed", result: out };
|
|
1831
|
+
} catch (err) {
|
|
1832
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1833
|
+
await this.dataEngine.update(
|
|
1834
|
+
"ai_pending_actions",
|
|
1835
|
+
{ id, status: "failed", error: msg },
|
|
1836
|
+
{ where: { id } }
|
|
1837
|
+
);
|
|
1838
|
+
this.logger.warn(`[AI] pending action ${id} failed after approval: ${msg}`);
|
|
1839
|
+
return { status: "failed", error: msg };
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
async rejectPendingAction(id, actorId, reason) {
|
|
1843
|
+
if (!this.dataEngine) {
|
|
1844
|
+
throw new Error("rejectPendingAction requires a dataEngine.");
|
|
1845
|
+
}
|
|
1846
|
+
const row = await this.loadPendingRow(id);
|
|
1847
|
+
if (row.status !== "pending") {
|
|
1848
|
+
throw new Error(`pending action ${id} is already ${row.status}`);
|
|
1849
|
+
}
|
|
1850
|
+
await this.dataEngine.update(
|
|
1851
|
+
"ai_pending_actions",
|
|
1852
|
+
{
|
|
1853
|
+
id,
|
|
1854
|
+
status: "rejected",
|
|
1855
|
+
decided_by: actorId,
|
|
1856
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1857
|
+
rejection_reason: reason ?? null
|
|
1858
|
+
},
|
|
1859
|
+
{ where: { id } }
|
|
1860
|
+
);
|
|
1861
|
+
this.logger.info(`[AI] pending action ${id} rejected by ${actorId}`);
|
|
1862
|
+
}
|
|
1863
|
+
async listPendingActions(filter) {
|
|
1864
|
+
if (!this.dataEngine) return [];
|
|
1865
|
+
const where = {};
|
|
1866
|
+
if (filter?.status) {
|
|
1867
|
+
where.status = Array.isArray(filter.status) ? { in: filter.status } : filter.status;
|
|
1868
|
+
}
|
|
1869
|
+
if (filter?.conversationId) where.conversation_id = filter.conversationId;
|
|
1870
|
+
if (filter?.objectName) where.object_name = filter.objectName;
|
|
1871
|
+
const rows = await this.dataEngine.find("ai_pending_actions", {
|
|
1872
|
+
where,
|
|
1873
|
+
limit: filter?.limit ?? 100,
|
|
1874
|
+
orderBy: [{ field: "proposed_at", order: "desc" }]
|
|
1875
|
+
});
|
|
1876
|
+
return rows;
|
|
1877
|
+
}
|
|
1878
|
+
async loadPendingRow(id) {
|
|
1879
|
+
const rows = await this.dataEngine.find("ai_pending_actions", {
|
|
1880
|
+
where: { id },
|
|
1881
|
+
limit: 1
|
|
1882
|
+
});
|
|
1883
|
+
const row = rows[0];
|
|
1884
|
+
if (!row) throw new Error(`pending action ${id} not found`);
|
|
1885
|
+
return row;
|
|
1886
|
+
}
|
|
1516
1887
|
};
|
|
1517
1888
|
// ── Tool Call Loop ────────────────────────────────────────────
|
|
1518
1889
|
/** Default maximum iterations for the tool call loop. */
|
|
1519
1890
|
_AIService.DEFAULT_MAX_ITERATIONS = 10;
|
|
1520
1891
|
var AIService = _AIService;
|
|
1892
|
+
function cryptoRandomId() {
|
|
1893
|
+
const g = globalThis;
|
|
1894
|
+
if (g.crypto?.randomUUID) return g.crypto.randomUUID();
|
|
1895
|
+
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1896
|
+
}
|
|
1521
1897
|
|
|
1522
1898
|
// src/stream/vercel-stream-encoder.ts
|
|
1523
1899
|
function sse(data) {
|
|
@@ -2402,6 +2778,132 @@ function buildToolRoutes(aiService, logger) {
|
|
|
2402
2778
|
];
|
|
2403
2779
|
}
|
|
2404
2780
|
|
|
2781
|
+
// src/routes/pending-action-routes.ts
|
|
2782
|
+
function buildPendingActionRoutes(aiService, logger) {
|
|
2783
|
+
const supported = typeof aiService.listPendingActions === "function" && typeof aiService.approvePendingAction === "function" && typeof aiService.rejectPendingAction === "function";
|
|
2784
|
+
if (!supported) {
|
|
2785
|
+
logger.warn(
|
|
2786
|
+
"[AI] HITL pending-action methods not implemented on AI service \u2014 routes return 501."
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
const notImpl = () => ({
|
|
2790
|
+
status: 501,
|
|
2791
|
+
body: { error: "Pending-action queue not available (dataEngine not wired)" }
|
|
2792
|
+
});
|
|
2793
|
+
return [
|
|
2794
|
+
// ── List pending actions ───────────────────────────────────────
|
|
2795
|
+
{
|
|
2796
|
+
method: "GET",
|
|
2797
|
+
path: "/api/v1/ai/pending-actions",
|
|
2798
|
+
description: "List pending actions in the HITL approval queue",
|
|
2799
|
+
auth: true,
|
|
2800
|
+
permissions: ["ai:read"],
|
|
2801
|
+
handler: async (req) => {
|
|
2802
|
+
if (!supported) return notImpl();
|
|
2803
|
+
try {
|
|
2804
|
+
const query = req.query ?? {};
|
|
2805
|
+
const status = typeof query.status === "string" ? query.status : void 0;
|
|
2806
|
+
const conversationId = typeof query.conversationId === "string" ? query.conversationId : void 0;
|
|
2807
|
+
const limitRaw = query.limit;
|
|
2808
|
+
const limit = typeof limitRaw === "string" ? Number(limitRaw) : void 0;
|
|
2809
|
+
const rows = await aiService.listPendingActions({
|
|
2810
|
+
status,
|
|
2811
|
+
conversationId,
|
|
2812
|
+
limit: Number.isFinite(limit) ? limit : void 0
|
|
2813
|
+
});
|
|
2814
|
+
return { status: 200, body: { items: rows, total: rows.length } };
|
|
2815
|
+
} catch (err) {
|
|
2816
|
+
logger.error(
|
|
2817
|
+
"[AI Route] /pending-actions list error",
|
|
2818
|
+
err instanceof Error ? err : void 0
|
|
2819
|
+
);
|
|
2820
|
+
return { status: 500, body: { error: "Failed to list pending actions" } };
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
},
|
|
2824
|
+
// ── Get a single pending action ────────────────────────────────
|
|
2825
|
+
{
|
|
2826
|
+
method: "GET",
|
|
2827
|
+
path: "/api/v1/ai/pending-actions/:id",
|
|
2828
|
+
description: "Get a single pending action by id",
|
|
2829
|
+
auth: true,
|
|
2830
|
+
permissions: ["ai:read"],
|
|
2831
|
+
handler: async (req) => {
|
|
2832
|
+
if (!supported) return notImpl();
|
|
2833
|
+
const id = req.params?.id;
|
|
2834
|
+
if (!id) return { status: 400, body: { error: "id is required" } };
|
|
2835
|
+
try {
|
|
2836
|
+
const rows = await aiService.listPendingActions({});
|
|
2837
|
+
const found = rows.find((r) => r.id === id);
|
|
2838
|
+
if (!found) return { status: 404, body: { error: `Pending action ${id} not found` } };
|
|
2839
|
+
return { status: 200, body: found };
|
|
2840
|
+
} catch (err) {
|
|
2841
|
+
logger.error(
|
|
2842
|
+
"[AI Route] /pending-actions/:id error",
|
|
2843
|
+
err instanceof Error ? err : void 0
|
|
2844
|
+
);
|
|
2845
|
+
return { status: 500, body: { error: "Failed to load pending action" } };
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
},
|
|
2849
|
+
// ── Approve & execute ──────────────────────────────────────────
|
|
2850
|
+
{
|
|
2851
|
+
method: "POST",
|
|
2852
|
+
path: "/api/v1/ai/pending-actions/:id/approve",
|
|
2853
|
+
description: "Approve a pending action and execute it immediately",
|
|
2854
|
+
auth: true,
|
|
2855
|
+
permissions: ["ai:approve"],
|
|
2856
|
+
handler: async (req) => {
|
|
2857
|
+
if (!supported) return notImpl();
|
|
2858
|
+
const id = req.params?.id;
|
|
2859
|
+
if (!id) return { status: 400, body: { error: "id is required" } };
|
|
2860
|
+
const actorId = req.user?.id ?? "system";
|
|
2861
|
+
try {
|
|
2862
|
+
const outcome = await aiService.approvePendingAction(id, actorId);
|
|
2863
|
+
const httpStatus = outcome.status === "executed" ? 200 : 500;
|
|
2864
|
+
return { status: httpStatus, body: outcome };
|
|
2865
|
+
} catch (err) {
|
|
2866
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2867
|
+
logger.error("[AI Route] /pending-actions/:id/approve error", err instanceof Error ? err : void 0);
|
|
2868
|
+
if (/not found/i.test(msg)) return { status: 404, body: { error: msg } };
|
|
2869
|
+
if (/already|not pending|no dispatcher/i.test(msg)) {
|
|
2870
|
+
return { status: 409, body: { error: msg } };
|
|
2871
|
+
}
|
|
2872
|
+
return { status: 500, body: { error: msg } };
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
},
|
|
2876
|
+
// ── Reject ─────────────────────────────────────────────────────
|
|
2877
|
+
{
|
|
2878
|
+
method: "POST",
|
|
2879
|
+
path: "/api/v1/ai/pending-actions/:id/reject",
|
|
2880
|
+
description: "Reject a pending action (will not be executed)",
|
|
2881
|
+
auth: true,
|
|
2882
|
+
permissions: ["ai:approve"],
|
|
2883
|
+
handler: async (req) => {
|
|
2884
|
+
if (!supported) return notImpl();
|
|
2885
|
+
const id = req.params?.id;
|
|
2886
|
+
if (!id) return { status: 400, body: { error: "id is required" } };
|
|
2887
|
+
const actorId = req.user?.id ?? "system";
|
|
2888
|
+
const body = req.body ?? {};
|
|
2889
|
+
const reason = typeof body.reason === "string" ? body.reason : void 0;
|
|
2890
|
+
try {
|
|
2891
|
+
await aiService.rejectPendingAction(id, actorId, reason);
|
|
2892
|
+
return { status: 200, body: { status: "rejected", id } };
|
|
2893
|
+
} catch (err) {
|
|
2894
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2895
|
+
logger.error("[AI Route] /pending-actions/:id/reject error", err instanceof Error ? err : void 0);
|
|
2896
|
+
if (/not found/i.test(msg)) return { status: 404, body: { error: msg } };
|
|
2897
|
+
if (/already|not pending/i.test(msg)) {
|
|
2898
|
+
return { status: 409, body: { error: msg } };
|
|
2899
|
+
}
|
|
2900
|
+
return { status: 500, body: { error: msg } };
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
];
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2405
2907
|
// src/conversation/objectql-conversation-service.ts
|
|
2406
2908
|
var import_node_crypto2 = require("crypto");
|
|
2407
2909
|
var CONVERSATIONS_OBJECT = "ai_conversations";
|
|
@@ -2872,6 +3374,438 @@ var AiTraceObject = import_data3.ObjectSchema.create({
|
|
|
2872
3374
|
}
|
|
2873
3375
|
});
|
|
2874
3376
|
|
|
3377
|
+
// src/objects/ai-pending-action.object.ts
|
|
3378
|
+
var import_data4 = require("@objectstack/spec/data");
|
|
3379
|
+
var AiPendingActionObject = import_data4.ObjectSchema.create({
|
|
3380
|
+
name: "ai_pending_actions",
|
|
3381
|
+
label: "AI Pending Action",
|
|
3382
|
+
pluralLabel: "AI Pending Actions",
|
|
3383
|
+
icon: "shield-check",
|
|
3384
|
+
isSystem: true,
|
|
3385
|
+
description: "Queue of AI-proposed action invocations awaiting human approval",
|
|
3386
|
+
fields: {
|
|
3387
|
+
id: import_data4.Field.text({
|
|
3388
|
+
label: "Request ID",
|
|
3389
|
+
required: true,
|
|
3390
|
+
readonly: true
|
|
3391
|
+
}),
|
|
3392
|
+
conversation_id: import_data4.Field.lookup("ai_conversations", {
|
|
3393
|
+
label: "Conversation",
|
|
3394
|
+
required: false,
|
|
3395
|
+
description: "Conversation that produced this proposal, if any"
|
|
3396
|
+
}),
|
|
3397
|
+
message_id: import_data4.Field.lookup("ai_messages", {
|
|
3398
|
+
label: "Message",
|
|
3399
|
+
required: false,
|
|
3400
|
+
description: "Assistant message containing the proposed tool call"
|
|
3401
|
+
}),
|
|
3402
|
+
object_name: import_data4.Field.text({
|
|
3403
|
+
label: "Object",
|
|
3404
|
+
required: true,
|
|
3405
|
+
maxLength: 128,
|
|
3406
|
+
description: 'Target object name (e.g. "task")'
|
|
3407
|
+
}),
|
|
3408
|
+
action_name: import_data4.Field.text({
|
|
3409
|
+
label: "Action",
|
|
3410
|
+
required: true,
|
|
3411
|
+
maxLength: 128,
|
|
3412
|
+
description: 'Declarative action name (e.g. "delete_task")'
|
|
3413
|
+
}),
|
|
3414
|
+
tool_name: import_data4.Field.text({
|
|
3415
|
+
label: "Tool",
|
|
3416
|
+
required: true,
|
|
3417
|
+
maxLength: 128,
|
|
3418
|
+
description: 'AI tool name exposed to the LLM (e.g. "action_delete_task")'
|
|
3419
|
+
}),
|
|
3420
|
+
tool_input: import_data4.Field.textarea({
|
|
3421
|
+
label: "Tool Input",
|
|
3422
|
+
required: true,
|
|
3423
|
+
description: "JSON-serialised tool arguments the LLM passed"
|
|
3424
|
+
}),
|
|
3425
|
+
status: import_data4.Field.select({
|
|
3426
|
+
label: "Status",
|
|
3427
|
+
required: true,
|
|
3428
|
+
defaultValue: "pending",
|
|
3429
|
+
options: [
|
|
3430
|
+
{ label: "Pending Approval", value: "pending" },
|
|
3431
|
+
{ label: "Approved (queued)", value: "approved" },
|
|
3432
|
+
{ label: "Executed", value: "executed" },
|
|
3433
|
+
{ label: "Failed", value: "failed" },
|
|
3434
|
+
{ label: "Rejected", value: "rejected" }
|
|
3435
|
+
]
|
|
3436
|
+
}),
|
|
3437
|
+
result: import_data4.Field.textarea({
|
|
3438
|
+
label: "Execution Result",
|
|
3439
|
+
required: false,
|
|
3440
|
+
description: "JSON-serialised result from the action when executed"
|
|
3441
|
+
}),
|
|
3442
|
+
error: import_data4.Field.textarea({
|
|
3443
|
+
label: "Error",
|
|
3444
|
+
required: false,
|
|
3445
|
+
description: "Error message when status=failed"
|
|
3446
|
+
}),
|
|
3447
|
+
rejection_reason: import_data4.Field.textarea({
|
|
3448
|
+
label: "Rejection Reason",
|
|
3449
|
+
required: false,
|
|
3450
|
+
description: "Why the reviewer rejected (shown back to the LLM)"
|
|
3451
|
+
}),
|
|
3452
|
+
proposed_by: import_data4.Field.text({
|
|
3453
|
+
label: "Proposed By",
|
|
3454
|
+
required: false,
|
|
3455
|
+
maxLength: 128,
|
|
3456
|
+
description: "Principal id of the AI agent that proposed the action"
|
|
3457
|
+
}),
|
|
3458
|
+
decided_by: import_data4.Field.text({
|
|
3459
|
+
label: "Decided By",
|
|
3460
|
+
required: false,
|
|
3461
|
+
maxLength: 128,
|
|
3462
|
+
description: "User id of the human who approved/rejected"
|
|
3463
|
+
}),
|
|
3464
|
+
proposed_at: import_data4.Field.datetime({
|
|
3465
|
+
label: "Proposed At",
|
|
3466
|
+
required: true,
|
|
3467
|
+
defaultValue: "NOW()",
|
|
3468
|
+
readonly: true
|
|
3469
|
+
}),
|
|
3470
|
+
decided_at: import_data4.Field.datetime({
|
|
3471
|
+
label: "Decided At",
|
|
3472
|
+
required: false,
|
|
3473
|
+
description: "When approve/reject happened"
|
|
3474
|
+
})
|
|
3475
|
+
},
|
|
3476
|
+
indexes: [
|
|
3477
|
+
{ fields: ["status"] },
|
|
3478
|
+
{ fields: ["conversation_id"] },
|
|
3479
|
+
{ fields: ["object_name"] },
|
|
3480
|
+
{ fields: ["proposed_at"] }
|
|
3481
|
+
],
|
|
3482
|
+
actions: [
|
|
3483
|
+
{
|
|
3484
|
+
name: "approve_pending_action",
|
|
3485
|
+
label: "Approve",
|
|
3486
|
+
type: "api",
|
|
3487
|
+
target: "/api/v1/ai/pending-actions/{recordId}/approve",
|
|
3488
|
+
method: "POST",
|
|
3489
|
+
locations: ["list_item", "record_header"],
|
|
3490
|
+
variant: "primary",
|
|
3491
|
+
confirmText: "Approve and execute this action now?",
|
|
3492
|
+
successMessage: "Action approved and executed.",
|
|
3493
|
+
// The approval click is the operator's authorisation gesture —
|
|
3494
|
+
// the LLM must not be allowed to bypass HITL by approving itself.
|
|
3495
|
+
aiExposed: false
|
|
3496
|
+
},
|
|
3497
|
+
{
|
|
3498
|
+
name: "reject_pending_action",
|
|
3499
|
+
label: "Reject",
|
|
3500
|
+
type: "api",
|
|
3501
|
+
target: "/api/v1/ai/pending-actions/{recordId}/reject",
|
|
3502
|
+
method: "POST",
|
|
3503
|
+
locations: ["list_item", "record_header"],
|
|
3504
|
+
variant: "danger",
|
|
3505
|
+
confirmText: "Reject this pending action? It will not be executed.",
|
|
3506
|
+
successMessage: "Action rejected.",
|
|
3507
|
+
aiExposed: false
|
|
3508
|
+
}
|
|
3509
|
+
],
|
|
3510
|
+
enable: {
|
|
3511
|
+
trackHistory: false,
|
|
3512
|
+
searchable: false,
|
|
3513
|
+
apiEnabled: true,
|
|
3514
|
+
apiMethods: ["get", "list"],
|
|
3515
|
+
trash: false,
|
|
3516
|
+
mru: false
|
|
3517
|
+
}
|
|
3518
|
+
});
|
|
3519
|
+
|
|
3520
|
+
// src/views/ai-trace.view.ts
|
|
3521
|
+
var import_spec = require("@objectstack/spec");
|
|
3522
|
+
var AiTraceView = (0, import_spec.defineView)({
|
|
3523
|
+
list: {
|
|
3524
|
+
type: "grid",
|
|
3525
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3526
|
+
columns: [
|
|
3527
|
+
{ field: "created_at", label: "Time" },
|
|
3528
|
+
{ field: "operation" },
|
|
3529
|
+
{ field: "model" },
|
|
3530
|
+
{ field: "agent_id", label: "Agent" },
|
|
3531
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3532
|
+
{ field: "total_tokens", label: "Tokens" },
|
|
3533
|
+
{ field: "cost_total", label: "Cost" },
|
|
3534
|
+
{ field: "status" }
|
|
3535
|
+
],
|
|
3536
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
3537
|
+
pagination: { pageSize: 50 },
|
|
3538
|
+
searchableFields: ["conversation_id", "agent_id", "model", "error"],
|
|
3539
|
+
filterableFields: ["operation", "model", "status"]
|
|
3540
|
+
},
|
|
3541
|
+
listViews: {
|
|
3542
|
+
errors: {
|
|
3543
|
+
label: "Errors",
|
|
3544
|
+
type: "grid",
|
|
3545
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3546
|
+
columns: [
|
|
3547
|
+
{ field: "created_at", label: "Time" },
|
|
3548
|
+
{ field: "operation" },
|
|
3549
|
+
{ field: "model" },
|
|
3550
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3551
|
+
{ field: "error" }
|
|
3552
|
+
],
|
|
3553
|
+
filter: [
|
|
3554
|
+
{ field: "status", operator: "=", value: "error" }
|
|
3555
|
+
],
|
|
3556
|
+
sort: [{ field: "created_at", order: "desc" }]
|
|
3557
|
+
},
|
|
3558
|
+
by_model: {
|
|
3559
|
+
label: "By Model",
|
|
3560
|
+
type: "grid",
|
|
3561
|
+
data: { provider: "object", object: "ai_traces" },
|
|
3562
|
+
columns: [
|
|
3563
|
+
{ field: "model" },
|
|
3564
|
+
{ field: "operation" },
|
|
3565
|
+
{ field: "latency_ms", label: "Latency (ms)" },
|
|
3566
|
+
{ field: "total_tokens", label: "Tokens" },
|
|
3567
|
+
{ field: "cost_total", label: "Cost" },
|
|
3568
|
+
{ field: "status" },
|
|
3569
|
+
{ field: "created_at", label: "Time" }
|
|
3570
|
+
],
|
|
3571
|
+
grouping: { fields: [{ field: "model" }] },
|
|
3572
|
+
sort: [{ field: "created_at", order: "desc" }]
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
|
|
3577
|
+
// src/views/ai-pending-action.view.ts
|
|
3578
|
+
var import_spec2 = require("@objectstack/spec");
|
|
3579
|
+
var AiPendingActionView = (0, import_spec2.defineView)({
|
|
3580
|
+
list: {
|
|
3581
|
+
type: "grid",
|
|
3582
|
+
data: { provider: "object", object: "ai_pending_actions" },
|
|
3583
|
+
columns: [
|
|
3584
|
+
{ field: "proposed_at", label: "Proposed", type: "datetime-relative", width: 140 },
|
|
3585
|
+
{ field: "status", width: 130 },
|
|
3586
|
+
{ field: "object_name", label: "Object", width: 140 },
|
|
3587
|
+
{ field: "action_name", label: "Action", width: 180 },
|
|
3588
|
+
{ field: "proposed_by", label: "Proposed by", width: 160 },
|
|
3589
|
+
{ field: "decided_by", label: "Decided by", width: 160 },
|
|
3590
|
+
{ field: "decided_at", label: "Decided", type: "datetime-relative", width: 140 }
|
|
3591
|
+
],
|
|
3592
|
+
sort: [{ field: "proposed_at", order: "desc" }],
|
|
3593
|
+
pagination: { pageSize: 50 },
|
|
3594
|
+
searchableFields: ["action_name", "object_name", "tool_name", "proposed_by"],
|
|
3595
|
+
filterableFields: ["status", "object_name", "action_name"],
|
|
3596
|
+
rowActions: ["approve_pending_action", "reject_pending_action"],
|
|
3597
|
+
// Click a row → open the detail drawer instead of navigating to a page.
|
|
3598
|
+
navigation: { mode: "drawer", view: "detail", width: "640px" },
|
|
3599
|
+
rowColor: {
|
|
3600
|
+
field: "status",
|
|
3601
|
+
mapping: {
|
|
3602
|
+
pending: "amber",
|
|
3603
|
+
approved: "blue",
|
|
3604
|
+
executed: "green",
|
|
3605
|
+
failed: "red",
|
|
3606
|
+
rejected: "gray"
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
},
|
|
3610
|
+
form: {
|
|
3611
|
+
type: "drawer",
|
|
3612
|
+
data: { provider: "object", object: "ai_pending_actions" },
|
|
3613
|
+
sections: [
|
|
3614
|
+
{
|
|
3615
|
+
label: "Proposal",
|
|
3616
|
+
columns: 2,
|
|
3617
|
+
fields: [
|
|
3618
|
+
{ field: "status", readonly: true },
|
|
3619
|
+
{ field: "proposed_at", readonly: true, widget: "datetime-relative" },
|
|
3620
|
+
{ field: "object_name", label: "Target object", readonly: true },
|
|
3621
|
+
{ field: "action_name", label: "Action", readonly: true },
|
|
3622
|
+
{ field: "tool_name", label: "Tool exposed to LLM", readonly: true, colSpan: 2 },
|
|
3623
|
+
{ field: "proposed_by", label: "Proposed by (AI agent)", readonly: true, colSpan: 2 }
|
|
3624
|
+
]
|
|
3625
|
+
},
|
|
3626
|
+
{
|
|
3627
|
+
label: "Tool input",
|
|
3628
|
+
collapsible: true,
|
|
3629
|
+
columns: 1,
|
|
3630
|
+
fields: [
|
|
3631
|
+
{
|
|
3632
|
+
field: "tool_input",
|
|
3633
|
+
label: "Arguments the LLM sent",
|
|
3634
|
+
readonly: true,
|
|
3635
|
+
widget: "json",
|
|
3636
|
+
colSpan: 1,
|
|
3637
|
+
helpText: "Pretty-printed JSON. Review carefully before approving \u2014 this is the exact payload that will be re-played against the handler."
|
|
3638
|
+
}
|
|
3639
|
+
]
|
|
3640
|
+
},
|
|
3641
|
+
{
|
|
3642
|
+
label: "Conversation context",
|
|
3643
|
+
collapsible: true,
|
|
3644
|
+
collapsed: true,
|
|
3645
|
+
columns: 2,
|
|
3646
|
+
fields: [
|
|
3647
|
+
// Both are lookups — Studio renders them as links to the related
|
|
3648
|
+
// ai_conversations / ai_messages record so operators can jump to
|
|
3649
|
+
// the full transcript for context.
|
|
3650
|
+
{ field: "conversation_id", label: "Conversation", readonly: true },
|
|
3651
|
+
{ field: "message_id", label: "Assistant message", readonly: true }
|
|
3652
|
+
]
|
|
3653
|
+
},
|
|
3654
|
+
{
|
|
3655
|
+
label: "Decision",
|
|
3656
|
+
collapsible: true,
|
|
3657
|
+
// Only meaningful once the row has been actioned; left collapsed
|
|
3658
|
+
// by default for pending rows so the eye lands on the proposal.
|
|
3659
|
+
collapsed: true,
|
|
3660
|
+
columns: 2,
|
|
3661
|
+
fields: [
|
|
3662
|
+
{ field: "decided_by", label: "Decided by", readonly: true },
|
|
3663
|
+
{ field: "decided_at", label: "Decided", readonly: true, widget: "datetime-relative" },
|
|
3664
|
+
{
|
|
3665
|
+
field: "rejection_reason",
|
|
3666
|
+
label: "Rejection reason",
|
|
3667
|
+
readonly: true,
|
|
3668
|
+
colSpan: 2,
|
|
3669
|
+
visibleOn: 'record.status == "rejected"'
|
|
3670
|
+
},
|
|
3671
|
+
{
|
|
3672
|
+
field: "result",
|
|
3673
|
+
label: "Execution result",
|
|
3674
|
+
readonly: true,
|
|
3675
|
+
widget: "json",
|
|
3676
|
+
colSpan: 2,
|
|
3677
|
+
visibleOn: 'record.status == "executed"'
|
|
3678
|
+
},
|
|
3679
|
+
{
|
|
3680
|
+
field: "error",
|
|
3681
|
+
label: "Error",
|
|
3682
|
+
readonly: true,
|
|
3683
|
+
colSpan: 2,
|
|
3684
|
+
visibleOn: 'record.status == "failed"'
|
|
3685
|
+
}
|
|
3686
|
+
]
|
|
3687
|
+
}
|
|
3688
|
+
]
|
|
3689
|
+
},
|
|
3690
|
+
formViews: {
|
|
3691
|
+
detail: {
|
|
3692
|
+
type: "drawer",
|
|
3693
|
+
data: { provider: "object", object: "ai_pending_actions" },
|
|
3694
|
+
// Mirror of the default form. Named separately so the list's
|
|
3695
|
+
// `navigation.view: 'detail'` resolves explicitly — Studio falls back
|
|
3696
|
+
// to `form` if a named view isn't registered, but being explicit
|
|
3697
|
+
// makes the wiring legible to readers of the metadata.
|
|
3698
|
+
sections: [
|
|
3699
|
+
{
|
|
3700
|
+
label: "Proposal",
|
|
3701
|
+
columns: 2,
|
|
3702
|
+
fields: [
|
|
3703
|
+
{ field: "status", readonly: true },
|
|
3704
|
+
{ field: "proposed_at", readonly: true, widget: "datetime-relative" },
|
|
3705
|
+
{ field: "object_name", label: "Target object", readonly: true },
|
|
3706
|
+
{ field: "action_name", label: "Action", readonly: true },
|
|
3707
|
+
{ field: "tool_name", label: "Tool exposed to LLM", readonly: true, colSpan: 2 },
|
|
3708
|
+
{ field: "proposed_by", label: "Proposed by (AI agent)", readonly: true, colSpan: 2 }
|
|
3709
|
+
]
|
|
3710
|
+
},
|
|
3711
|
+
{
|
|
3712
|
+
label: "Tool input",
|
|
3713
|
+
collapsible: true,
|
|
3714
|
+
columns: 1,
|
|
3715
|
+
fields: [
|
|
3716
|
+
{ field: "tool_input", label: "Arguments the LLM sent", readonly: true, widget: "json" }
|
|
3717
|
+
]
|
|
3718
|
+
},
|
|
3719
|
+
{
|
|
3720
|
+
label: "Conversation context",
|
|
3721
|
+
collapsible: true,
|
|
3722
|
+
collapsed: true,
|
|
3723
|
+
columns: 2,
|
|
3724
|
+
fields: [
|
|
3725
|
+
{ field: "conversation_id", label: "Conversation", readonly: true },
|
|
3726
|
+
{ field: "message_id", label: "Assistant message", readonly: true }
|
|
3727
|
+
]
|
|
3728
|
+
},
|
|
3729
|
+
{
|
|
3730
|
+
label: "Decision",
|
|
3731
|
+
collapsible: true,
|
|
3732
|
+
collapsed: true,
|
|
3733
|
+
columns: 2,
|
|
3734
|
+
fields: [
|
|
3735
|
+
{ field: "decided_by", label: "Decided by", readonly: true },
|
|
3736
|
+
{ field: "decided_at", label: "Decided", readonly: true, widget: "datetime-relative" },
|
|
3737
|
+
{ field: "rejection_reason", label: "Rejection reason", readonly: true, colSpan: 2, visibleOn: 'record.status == "rejected"' },
|
|
3738
|
+
{ field: "result", label: "Execution result", readonly: true, widget: "json", colSpan: 2, visibleOn: 'record.status == "executed"' },
|
|
3739
|
+
{ field: "error", label: "Error", readonly: true, colSpan: 2, visibleOn: 'record.status == "failed"' }
|
|
3740
|
+
]
|
|
3741
|
+
}
|
|
3742
|
+
]
|
|
3743
|
+
}
|
|
3744
|
+
},
|
|
3745
|
+
listViews: {
|
|
3746
|
+
pending: {
|
|
3747
|
+
label: "Pending",
|
|
3748
|
+
type: "grid",
|
|
3749
|
+
data: { provider: "object", object: "ai_pending_actions" },
|
|
3750
|
+
columns: [
|
|
3751
|
+
{ field: "proposed_at", label: "Proposed", type: "datetime-relative", width: 140 },
|
|
3752
|
+
{ field: "object_name", label: "Object", width: 140 },
|
|
3753
|
+
{ field: "action_name", label: "Action", width: 180 },
|
|
3754
|
+
{ field: "proposed_by", label: "Proposed by", width: 160 },
|
|
3755
|
+
{ field: "tool_name", label: "Tool", width: 200 }
|
|
3756
|
+
],
|
|
3757
|
+
filter: [{ field: "status", operator: "=", value: "pending" }],
|
|
3758
|
+
sort: [{ field: "proposed_at", order: "desc" }],
|
|
3759
|
+
rowActions: ["approve_pending_action", "reject_pending_action"],
|
|
3760
|
+
navigation: { mode: "drawer", view: "detail", width: "640px" }
|
|
3761
|
+
},
|
|
3762
|
+
executed: {
|
|
3763
|
+
label: "Executed",
|
|
3764
|
+
type: "grid",
|
|
3765
|
+
data: { provider: "object", object: "ai_pending_actions" },
|
|
3766
|
+
columns: [
|
|
3767
|
+
{ field: "decided_at", label: "Approved", type: "datetime-relative", width: 140 },
|
|
3768
|
+
{ field: "object_name", label: "Object", width: 140 },
|
|
3769
|
+
{ field: "action_name", label: "Action", width: 180 },
|
|
3770
|
+
{ field: "decided_by", label: "Approved by", width: 160 },
|
|
3771
|
+
{ field: "proposed_by", label: "Proposed by", width: 160 }
|
|
3772
|
+
],
|
|
3773
|
+
filter: [{ field: "status", operator: "=", value: "executed" }],
|
|
3774
|
+
sort: [{ field: "decided_at", order: "desc" }],
|
|
3775
|
+
navigation: { mode: "drawer", view: "detail", width: "640px" }
|
|
3776
|
+
},
|
|
3777
|
+
rejected: {
|
|
3778
|
+
label: "Rejected",
|
|
3779
|
+
type: "grid",
|
|
3780
|
+
data: { provider: "object", object: "ai_pending_actions" },
|
|
3781
|
+
columns: [
|
|
3782
|
+
{ field: "decided_at", label: "Rejected", type: "datetime-relative", width: 140 },
|
|
3783
|
+
{ field: "object_name", label: "Object", width: 140 },
|
|
3784
|
+
{ field: "action_name", label: "Action", width: 180 },
|
|
3785
|
+
{ field: "decided_by", label: "Rejected by", width: 160 },
|
|
3786
|
+
{ field: "rejection_reason", label: "Reason", wrap: true }
|
|
3787
|
+
],
|
|
3788
|
+
filter: [{ field: "status", operator: "=", value: "rejected" }],
|
|
3789
|
+
sort: [{ field: "decided_at", order: "desc" }],
|
|
3790
|
+
navigation: { mode: "drawer", view: "detail", width: "640px" }
|
|
3791
|
+
},
|
|
3792
|
+
failed: {
|
|
3793
|
+
label: "Failed",
|
|
3794
|
+
type: "grid",
|
|
3795
|
+
data: { provider: "object", object: "ai_pending_actions" },
|
|
3796
|
+
columns: [
|
|
3797
|
+
{ field: "decided_at", label: "When", type: "datetime-relative", width: 140 },
|
|
3798
|
+
{ field: "object_name", label: "Object", width: 140 },
|
|
3799
|
+
{ field: "action_name", label: "Action", width: 180 },
|
|
3800
|
+
{ field: "error", wrap: true }
|
|
3801
|
+
],
|
|
3802
|
+
filter: [{ field: "status", operator: "=", value: "failed" }],
|
|
3803
|
+
sort: [{ field: "decided_at", order: "desc" }],
|
|
3804
|
+
navigation: { mode: "drawer", view: "detail", width: "640px" }
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
});
|
|
3808
|
+
|
|
2875
3809
|
// src/plugin.ts
|
|
2876
3810
|
init_data_tools();
|
|
2877
3811
|
init_metadata_tools();
|
|
@@ -2924,8 +3858,11 @@ var SchemaRetriever = class {
|
|
|
2924
3858
|
const lines = ["## Schema context (auto-injected)"];
|
|
2925
3859
|
for (const hit of hits) {
|
|
2926
3860
|
const obj = hit.object;
|
|
2927
|
-
const
|
|
2928
|
-
|
|
3861
|
+
const parts = [];
|
|
3862
|
+
if (obj.label) parts.push(obj.label);
|
|
3863
|
+
if (obj.pluralLabel && obj.pluralLabel !== obj.label) parts.push(`(${obj.pluralLabel})`);
|
|
3864
|
+
const header = parts.length > 0 ? ` \u2014 ${parts.join(" ")}` : "";
|
|
3865
|
+
lines.push(`### ${obj.name}${header}`);
|
|
2929
3866
|
const fields = Object.entries(obj.fields ?? {}).slice(0, maxFieldsPerObject);
|
|
2930
3867
|
for (const [name, field] of fields) {
|
|
2931
3868
|
lines.push(` - ${name}: ${describeField(field)}`);
|
|
@@ -3025,17 +3962,17 @@ function describeField(field) {
|
|
|
3025
3962
|
// src/tools/query-data.tool.ts
|
|
3026
3963
|
var QueryPlanSchema = import_zod.z.object({
|
|
3027
3964
|
objectName: import_zod.z.string().min(1).describe('The snake_case object name to query (e.g. "task", "account").'),
|
|
3028
|
-
|
|
3029
|
-
'Filter conditions as
|
|
3965
|
+
whereJson: import_zod.z.string().nullable().describe(
|
|
3966
|
+
'Filter conditions encoded as a JSON object string. Examples: `{"status":"completed"}`, `{"subject":{"$contains":"Build"}}`, `{"amount":{"$gt":100}}`. Pass null to match all records.'
|
|
3030
3967
|
),
|
|
3031
|
-
fields: import_zod.z.array(import_zod.z.string()).
|
|
3968
|
+
fields: import_zod.z.array(import_zod.z.string()).nullable().describe("Field names to return. Pass null to return all fields."),
|
|
3032
3969
|
orderBy: import_zod.z.array(
|
|
3033
3970
|
import_zod.z.object({
|
|
3034
3971
|
field: import_zod.z.string(),
|
|
3035
3972
|
order: import_zod.z.enum(["asc", "desc"])
|
|
3036
3973
|
})
|
|
3037
|
-
).
|
|
3038
|
-
limit: import_zod.z.number().int().min(1).max(200).
|
|
3974
|
+
).nullable().describe("Sort order. First entry is primary sort key. Pass null for no sort."),
|
|
3975
|
+
limit: import_zod.z.number().int().min(1).max(200).nullable().describe("Maximum number of records (default 20, max 200). Pass null for default.")
|
|
3039
3976
|
});
|
|
3040
3977
|
var QUERY_DATA_TOOL = {
|
|
3041
3978
|
name: "query_data",
|
|
@@ -3046,10 +3983,6 @@ var QUERY_DATA_TOOL = {
|
|
|
3046
3983
|
request: {
|
|
3047
3984
|
type: "string",
|
|
3048
3985
|
description: "The natural-language question to answer (paraphrase the user's request if needed for clarity)."
|
|
3049
|
-
},
|
|
3050
|
-
model: {
|
|
3051
|
-
type: "string",
|
|
3052
|
-
description: "Optional model id to use for query planning. Defaults to the AI service's default model."
|
|
3053
3986
|
}
|
|
3054
3987
|
},
|
|
3055
3988
|
required: ["request"],
|
|
@@ -3060,7 +3993,7 @@ function createQueryDataHandler(ctx) {
|
|
|
3060
3993
|
const retriever = new SchemaRetriever(ctx.metadata);
|
|
3061
3994
|
const maxLimit = ctx.maxLimit ?? 100;
|
|
3062
3995
|
return async (args) => {
|
|
3063
|
-
const { request
|
|
3996
|
+
const { request } = args;
|
|
3064
3997
|
if (!request || typeof request !== "string") {
|
|
3065
3998
|
return JSON.stringify({ error: "query_data: `request` is required" });
|
|
3066
3999
|
}
|
|
@@ -3079,14 +4012,13 @@ function createQueryDataHandler(ctx) {
|
|
|
3079
4012
|
const planMessages = [
|
|
3080
4013
|
{
|
|
3081
4014
|
role: "system",
|
|
3082
|
-
content:
|
|
4015
|
+
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\nFilter operator hints:\n \u2022 For partial string matches (e.g. "task named Foo", "find X"), use case-insensitive substring matching with `$contains`: `{"subject": {"$contains": "Foo"}}`. Do NOT use equality unless the user clearly supplied the exact full value.\n \u2022 For numeric/date ranges use `$gt` / `$gte` / `$lt` / `$lte`.\n \u2022 For "is one of" use `$in: [...]`.\n \u2022 For exact equality just write the value: `{"status": "completed"}`.\n\n' + snippet
|
|
3083
4016
|
},
|
|
3084
4017
|
{ role: "user", content: request }
|
|
3085
4018
|
];
|
|
3086
4019
|
let plan;
|
|
3087
4020
|
try {
|
|
3088
4021
|
const generated = await ctx.ai.generateObject(planMessages, QueryPlanSchema, {
|
|
3089
|
-
model,
|
|
3090
4022
|
schemaName: "ObjectQLQueryPlan",
|
|
3091
4023
|
schemaDescription: "A single ObjectQL find() query to answer the user request."
|
|
3092
4024
|
});
|
|
@@ -3103,15 +4035,34 @@ function createQueryDataHandler(ctx) {
|
|
|
3103
4035
|
});
|
|
3104
4036
|
}
|
|
3105
4037
|
const limit = Math.min(plan.limit ?? 20, maxLimit);
|
|
4038
|
+
let where;
|
|
4039
|
+
if (plan.whereJson) {
|
|
4040
|
+
try {
|
|
4041
|
+
const parsed = JSON.parse(plan.whereJson);
|
|
4042
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
4043
|
+
where = parsed;
|
|
4044
|
+
} else {
|
|
4045
|
+
return JSON.stringify({
|
|
4046
|
+
plan,
|
|
4047
|
+
error: `whereJson must encode a JSON object, got: ${plan.whereJson}`
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
4050
|
+
} catch (err) {
|
|
4051
|
+
return JSON.stringify({
|
|
4052
|
+
plan,
|
|
4053
|
+
error: `whereJson is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
4054
|
+
});
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
3106
4057
|
try {
|
|
3107
4058
|
const records = await ctx.dataEngine.find(plan.objectName, {
|
|
3108
|
-
where
|
|
3109
|
-
fields: plan.fields,
|
|
3110
|
-
orderBy: plan.orderBy,
|
|
4059
|
+
where,
|
|
4060
|
+
fields: plan.fields ?? void 0,
|
|
4061
|
+
orderBy: plan.orderBy ?? void 0,
|
|
3111
4062
|
limit
|
|
3112
4063
|
});
|
|
3113
4064
|
return JSON.stringify({
|
|
3114
|
-
plan,
|
|
4065
|
+
plan: { ...plan, where },
|
|
3115
4066
|
count: records.length,
|
|
3116
4067
|
records
|
|
3117
4068
|
});
|
|
@@ -3127,6 +4078,402 @@ function registerQueryDataTool(registry, context) {
|
|
|
3127
4078
|
registry.register(QUERY_DATA_TOOL, createQueryDataHandler(context));
|
|
3128
4079
|
}
|
|
3129
4080
|
|
|
4081
|
+
// src/tools/action-tools.ts
|
|
4082
|
+
function actionRequiresApproval(action) {
|
|
4083
|
+
return Boolean(
|
|
4084
|
+
action.confirmText || action.mode === "delete" || action.variant === "danger"
|
|
4085
|
+
);
|
|
4086
|
+
}
|
|
4087
|
+
function actionSkipReason(action, ctx) {
|
|
4088
|
+
if (action.aiExposed === false) {
|
|
4089
|
+
return "opted-out via aiExposed:false";
|
|
4090
|
+
}
|
|
4091
|
+
if (action.type === "url" || action.type === "modal" || action.type === "form") {
|
|
4092
|
+
return `type='${action.type}' is UI-only`;
|
|
4093
|
+
}
|
|
4094
|
+
if (action.type !== "script" && action.type !== "api" && action.type !== "flow") {
|
|
4095
|
+
return `type='${action.type}' not supported`;
|
|
4096
|
+
}
|
|
4097
|
+
if (action.type === "script" && !action.target && !action.body) {
|
|
4098
|
+
return "no target or body";
|
|
4099
|
+
}
|
|
4100
|
+
if ((action.type === "api" || action.type === "flow") && !action.target) {
|
|
4101
|
+
return `type='${action.type}' requires a target`;
|
|
4102
|
+
}
|
|
4103
|
+
if (ctx) {
|
|
4104
|
+
if (action.type === "flow" && !ctx.automation) {
|
|
4105
|
+
return "no automation service available";
|
|
4106
|
+
}
|
|
4107
|
+
if (action.type === "api" && !ctx.apiClient && !ctx.apiBaseUrl) {
|
|
4108
|
+
return "no apiClient or apiBaseUrl configured";
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
if (actionRequiresApproval(action)) {
|
|
4112
|
+
const approvalReady = ctx?.enableActionApproval === true && Boolean(ctx?.aiService?.proposePendingAction);
|
|
4113
|
+
if (!approvalReady) {
|
|
4114
|
+
if (action.confirmText) return "requires confirmation (confirmText set)";
|
|
4115
|
+
if (action.mode === "delete") return "mode='delete' \u2014 destructive";
|
|
4116
|
+
if (action.variant === "danger") return "variant='danger' \u2014 destructive";
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
return null;
|
|
4120
|
+
}
|
|
4121
|
+
function fieldTypeToJsonType(t) {
|
|
4122
|
+
switch (t) {
|
|
4123
|
+
case "number":
|
|
4124
|
+
case "currency":
|
|
4125
|
+
case "percent":
|
|
4126
|
+
case "rating":
|
|
4127
|
+
case "slider":
|
|
4128
|
+
case "autonumber":
|
|
4129
|
+
return "number";
|
|
4130
|
+
case "boolean":
|
|
4131
|
+
case "toggle":
|
|
4132
|
+
return "boolean";
|
|
4133
|
+
case "multiselect":
|
|
4134
|
+
case "checkboxes":
|
|
4135
|
+
case "tags":
|
|
4136
|
+
return "array";
|
|
4137
|
+
default:
|
|
4138
|
+
return "string";
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
function resolveParam(param, ownerObject, allObjects) {
|
|
4142
|
+
const fieldRef = param.field;
|
|
4143
|
+
const owner = param.objectOverride && allObjects.get(param.objectOverride) ? allObjects.get(param.objectOverride) : ownerObject;
|
|
4144
|
+
const field = fieldRef ? owner?.fields?.[fieldRef] : void 0;
|
|
4145
|
+
const name = param.name ?? fieldRef;
|
|
4146
|
+
if (!name) return null;
|
|
4147
|
+
const type = param.type ?? field?.type;
|
|
4148
|
+
const jsonType = fieldTypeToJsonType(type);
|
|
4149
|
+
const schema = { type: jsonType };
|
|
4150
|
+
const label = typeof param.label === "string" ? param.label : field?.label;
|
|
4151
|
+
const help = param.helpText ?? field?.description;
|
|
4152
|
+
const description = [label, help].filter(Boolean).join(" \u2014 ") || void 0;
|
|
4153
|
+
if (description) schema.description = description;
|
|
4154
|
+
const optionSource = param.options ?? field?.options;
|
|
4155
|
+
if (Array.isArray(optionSource) && optionSource.length > 0) {
|
|
4156
|
+
const values = optionSource.map((o) => typeof o === "string" ? o : o.value).filter((v) => typeof v === "string");
|
|
4157
|
+
if (values.length > 0) {
|
|
4158
|
+
schema.enum = jsonType === "array" ? void 0 : values;
|
|
4159
|
+
if (jsonType === "array") {
|
|
4160
|
+
schema.items = { type: "string", enum: values };
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
} else if (jsonType === "array") {
|
|
4164
|
+
schema.items = { type: "string" };
|
|
4165
|
+
}
|
|
4166
|
+
if (param.defaultValue !== void 0) {
|
|
4167
|
+
schema.default = param.defaultValue;
|
|
4168
|
+
}
|
|
4169
|
+
const required = Boolean(param.required ?? field?.required ?? false);
|
|
4170
|
+
return { name, schema, required };
|
|
4171
|
+
}
|
|
4172
|
+
function buildParametersSchema(action, ownerObject, allObjects) {
|
|
4173
|
+
const properties = {};
|
|
4174
|
+
const required = [];
|
|
4175
|
+
const isRowContext = Array.isArray(action.locations) && action.locations.some((l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related");
|
|
4176
|
+
if (action.objectName && isRowContext) {
|
|
4177
|
+
properties.recordId = {
|
|
4178
|
+
type: "string",
|
|
4179
|
+
description: `The ${action.objectName} record id to act on.`
|
|
4180
|
+
};
|
|
4181
|
+
if (action.recordIdParam || action.recordIdField) {
|
|
4182
|
+
required.push("recordId");
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
for (const param of action.params ?? []) {
|
|
4186
|
+
const resolved = resolveParam(param, ownerObject, allObjects);
|
|
4187
|
+
if (!resolved) continue;
|
|
4188
|
+
properties[resolved.name] = resolved.schema;
|
|
4189
|
+
if (resolved.required) required.push(resolved.name);
|
|
4190
|
+
}
|
|
4191
|
+
return {
|
|
4192
|
+
type: "object",
|
|
4193
|
+
properties,
|
|
4194
|
+
...required.length > 0 ? { required } : {},
|
|
4195
|
+
additionalProperties: false
|
|
4196
|
+
};
|
|
4197
|
+
}
|
|
4198
|
+
function actionToolName(action, prefix = "action_") {
|
|
4199
|
+
return `${prefix}${action.name}`;
|
|
4200
|
+
}
|
|
4201
|
+
function describeAction(action, ownerObject) {
|
|
4202
|
+
const label = typeof action.label === "string" ? action.label : action.name.replace(/_/g, " ");
|
|
4203
|
+
const target = action.objectName ?? ownerObject?.name;
|
|
4204
|
+
const targetLabel = ownerObject?.label ?? target;
|
|
4205
|
+
const parts = [];
|
|
4206
|
+
parts.push(`${label}${targetLabel ? ` \u2014 operates on ${targetLabel}` : ""}.`);
|
|
4207
|
+
if (action.successMessage && typeof action.successMessage === "string") {
|
|
4208
|
+
parts.push(`On success: ${action.successMessage}`);
|
|
4209
|
+
}
|
|
4210
|
+
if (action.mode) parts.push(`Mode: ${action.mode}.`);
|
|
4211
|
+
parts.push(
|
|
4212
|
+
"Use this when the user asks to perform this operation in natural language."
|
|
4213
|
+
);
|
|
4214
|
+
return parts.join(" ");
|
|
4215
|
+
}
|
|
4216
|
+
function actionToToolDefinition(action, ownerObject, allObjects, toolPrefix = "action_") {
|
|
4217
|
+
if (action.aiExposed === false) return null;
|
|
4218
|
+
if (action.type === "url" || action.type === "modal" || action.type === "form") return null;
|
|
4219
|
+
return {
|
|
4220
|
+
name: actionToolName(action, toolPrefix),
|
|
4221
|
+
description: describeAction(action, ownerObject),
|
|
4222
|
+
parameters: buildParametersSchema(action, ownerObject, allObjects)
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
function buildHandlerEngineAdapter(engine) {
|
|
4226
|
+
return {
|
|
4227
|
+
update: (object, id, data) => engine.update(object, { ...data, id }, { where: { id } }),
|
|
4228
|
+
insert: (object, data) => engine.insert(object, data),
|
|
4229
|
+
find: (object, where) => engine.find(object, { where }),
|
|
4230
|
+
delete: async (object, ids) => {
|
|
4231
|
+
if (!Array.isArray(ids) || ids.length === 0) return 0;
|
|
4232
|
+
let count = 0;
|
|
4233
|
+
for (const id of ids) {
|
|
4234
|
+
await engine.delete(object, { where: { id } });
|
|
4235
|
+
count++;
|
|
4236
|
+
}
|
|
4237
|
+
return count;
|
|
4238
|
+
}
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
function createActionToolHandler(action, ctx) {
|
|
4242
|
+
const principal = ctx.principal ?? { id: "ai_agent", name: "AI Assistant" };
|
|
4243
|
+
const requiresRecord = Array.isArray(action.locations) && action.locations.some(
|
|
4244
|
+
(l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related"
|
|
4245
|
+
);
|
|
4246
|
+
return async (args) => {
|
|
4247
|
+
const objectName = action.objectName;
|
|
4248
|
+
const target = action.target;
|
|
4249
|
+
const result = {
|
|
4250
|
+
ok: false,
|
|
4251
|
+
action: action.name,
|
|
4252
|
+
objectName
|
|
4253
|
+
};
|
|
4254
|
+
if (!objectName) {
|
|
4255
|
+
result.error = "Action has no objectName; cannot dispatch.";
|
|
4256
|
+
return JSON.stringify(result);
|
|
4257
|
+
}
|
|
4258
|
+
if (!target && action.type !== "script") {
|
|
4259
|
+
result.error = "Action has no target.";
|
|
4260
|
+
return JSON.stringify(result);
|
|
4261
|
+
}
|
|
4262
|
+
const recordId = typeof args.recordId === "string" && args.recordId.length > 0 ? args.recordId : void 0;
|
|
4263
|
+
let record;
|
|
4264
|
+
if (requiresRecord) {
|
|
4265
|
+
if (!recordId) {
|
|
4266
|
+
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).`;
|
|
4267
|
+
return JSON.stringify(result);
|
|
4268
|
+
}
|
|
4269
|
+
try {
|
|
4270
|
+
const found = await ctx.dataEngine.find(objectName, {
|
|
4271
|
+
where: { id: recordId },
|
|
4272
|
+
limit: 1
|
|
4273
|
+
});
|
|
4274
|
+
record = found[0];
|
|
4275
|
+
if (!record) {
|
|
4276
|
+
result.error = `Record ${recordId} not found in ${objectName}.`;
|
|
4277
|
+
return JSON.stringify(result);
|
|
4278
|
+
}
|
|
4279
|
+
result.recordId = recordId;
|
|
4280
|
+
} catch (err) {
|
|
4281
|
+
result.error = `Failed to load record: ${err instanceof Error ? err.message : String(err)}`;
|
|
4282
|
+
return JSON.stringify(result);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
const { recordId: _omit, ...userParams } = args;
|
|
4286
|
+
if (ctx.enableActionApproval && actionRequiresApproval(action) && ctx.aiService?.proposePendingAction) {
|
|
4287
|
+
try {
|
|
4288
|
+
const toolName = `${ctx.toolPrefix ?? "action_"}${action.name}`;
|
|
4289
|
+
const { id } = await ctx.aiService.proposePendingAction({
|
|
4290
|
+
objectName,
|
|
4291
|
+
actionName: action.name,
|
|
4292
|
+
toolName,
|
|
4293
|
+
toolInput: args,
|
|
4294
|
+
proposedBy: principal.id
|
|
4295
|
+
});
|
|
4296
|
+
const pending = {
|
|
4297
|
+
ok: true,
|
|
4298
|
+
action: action.name,
|
|
4299
|
+
objectName,
|
|
4300
|
+
recordId,
|
|
4301
|
+
status: "pending_approval",
|
|
4302
|
+
pendingActionId: id,
|
|
4303
|
+
message: `Action '${action.name}' is destructive and requires human approval. Proposal queued as ${id}. An operator must approve via Studio's pending-actions inbox before it runs. Do NOT call this tool again for the same intent \u2014 wait for the operator.`
|
|
4304
|
+
};
|
|
4305
|
+
return JSON.stringify(pending);
|
|
4306
|
+
} catch (err) {
|
|
4307
|
+
result.error = `Failed to enqueue approval: ${err instanceof Error ? err.message : String(err)}`;
|
|
4308
|
+
return JSON.stringify(result);
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
try {
|
|
4312
|
+
let out;
|
|
4313
|
+
if (action.type === "api") {
|
|
4314
|
+
out = await dispatchApiAction(action, ctx, userParams, record, recordId);
|
|
4315
|
+
} else if (action.type === "flow") {
|
|
4316
|
+
out = await dispatchFlowAction(action, ctx, userParams, record, principal);
|
|
4317
|
+
} else {
|
|
4318
|
+
out = await dispatchScriptAction(action, ctx, userParams, record, principal);
|
|
4319
|
+
}
|
|
4320
|
+
result.ok = true;
|
|
4321
|
+
result.result = out ?? null;
|
|
4322
|
+
const successMsg = typeof action.successMessage === "string" ? action.successMessage : void 0;
|
|
4323
|
+
result.message = successMsg ?? `Action '${action.name}' executed successfully.`;
|
|
4324
|
+
return JSON.stringify(result);
|
|
4325
|
+
} catch (err) {
|
|
4326
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
4327
|
+
return JSON.stringify(result);
|
|
4328
|
+
}
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
async function dispatchScriptAction(action, ctx, params, record, principal) {
|
|
4332
|
+
const engineAdapter = buildHandlerEngineAdapter(ctx.dataEngine);
|
|
4333
|
+
const handlerCtx = { record, user: principal, engine: engineAdapter, params };
|
|
4334
|
+
return await ctx.dataEngine.executeAction?.(action.objectName, action.target, handlerCtx);
|
|
4335
|
+
}
|
|
4336
|
+
function buildApiRequestBody(action, args, record, recordId) {
|
|
4337
|
+
const shape = action.bodyShape;
|
|
4338
|
+
const wrapKey = shape && typeof shape === "object" && "wrap" in shape && typeof shape.wrap === "string" ? shape.wrap : void 0;
|
|
4339
|
+
const body = wrapKey ? { [wrapKey]: { ...args } } : { ...args };
|
|
4340
|
+
if (action.recordIdParam) {
|
|
4341
|
+
const idField = action.recordIdField ?? "id";
|
|
4342
|
+
const idValue = record ? record[idField] : recordId;
|
|
4343
|
+
if (idValue !== void 0) body[action.recordIdParam] = idValue;
|
|
4344
|
+
}
|
|
4345
|
+
if (action.bodyExtra && typeof action.bodyExtra === "object") {
|
|
4346
|
+
Object.assign(body, action.bodyExtra);
|
|
4347
|
+
}
|
|
4348
|
+
return body;
|
|
4349
|
+
}
|
|
4350
|
+
async function dispatchApiAction(action, ctx, params, record, recordId) {
|
|
4351
|
+
const client = ctx.apiClient ?? (ctx.apiBaseUrl ? createFetchApiClient({ baseUrl: ctx.apiBaseUrl, headers: ctx.apiHeaders }) : void 0);
|
|
4352
|
+
if (!client) {
|
|
4353
|
+
throw new Error('No apiClient configured for type:"api" action dispatch.');
|
|
4354
|
+
}
|
|
4355
|
+
const method = action.method ?? "POST";
|
|
4356
|
+
const body = buildApiRequestBody(action, params, record, recordId);
|
|
4357
|
+
return await client.request({
|
|
4358
|
+
url: action.target,
|
|
4359
|
+
method,
|
|
4360
|
+
body: method === "GET" || method === "DELETE" ? void 0 : body,
|
|
4361
|
+
headers: ctx.apiHeaders
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
async function dispatchFlowAction(action, ctx, params, record, principal) {
|
|
4365
|
+
if (!ctx.automation) {
|
|
4366
|
+
throw new Error('No automation service available for type:"flow" action dispatch.');
|
|
4367
|
+
}
|
|
4368
|
+
const result = await ctx.automation.execute(action.target, {
|
|
4369
|
+
triggerData: { record, params, user: principal, action: action.name }
|
|
4370
|
+
});
|
|
4371
|
+
if (result && typeof result === "object" && "success" in result && result.success === false) {
|
|
4372
|
+
throw new Error(
|
|
4373
|
+
`Flow '${action.target}' failed: ${result.error ?? "unknown error"}`
|
|
4374
|
+
);
|
|
4375
|
+
}
|
|
4376
|
+
return result;
|
|
4377
|
+
}
|
|
4378
|
+
function createFetchApiClient(options) {
|
|
4379
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4380
|
+
if (!fetchImpl) {
|
|
4381
|
+
throw new Error("createFetchApiClient: no global fetch available; pass options.fetch.");
|
|
4382
|
+
}
|
|
4383
|
+
return {
|
|
4384
|
+
async request({ url, method, body, headers }) {
|
|
4385
|
+
const absolute = /^https?:\/\//.test(url) ? url : `${(options.baseUrl ?? "").replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
4386
|
+
const res = await fetchImpl(absolute, {
|
|
4387
|
+
method,
|
|
4388
|
+
headers: {
|
|
4389
|
+
"Content-Type": "application/json",
|
|
4390
|
+
...options.headers ?? {},
|
|
4391
|
+
...headers ?? {}
|
|
4392
|
+
},
|
|
4393
|
+
body: body ? JSON.stringify(body) : void 0
|
|
4394
|
+
});
|
|
4395
|
+
const text = await res.text();
|
|
4396
|
+
const parsed = text ? safeJsonParse(text) : null;
|
|
4397
|
+
if (!res.ok) {
|
|
4398
|
+
const msg = parsed && typeof parsed === "object" && "error" in parsed ? parsed.error : text;
|
|
4399
|
+
throw new Error(`${method} ${absolute} \u2192 ${res.status}: ${typeof msg === "string" ? msg : JSON.stringify(msg)}`);
|
|
4400
|
+
}
|
|
4401
|
+
return parsed;
|
|
4402
|
+
}
|
|
4403
|
+
};
|
|
4404
|
+
}
|
|
4405
|
+
function safeJsonParse(s) {
|
|
4406
|
+
try {
|
|
4407
|
+
return JSON.parse(s);
|
|
4408
|
+
} catch {
|
|
4409
|
+
return s;
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
async function registerActionsAsTools(registry, context) {
|
|
4413
|
+
const objects = await context.metadata.listObjects();
|
|
4414
|
+
const objMap = new Map(
|
|
4415
|
+
objects.filter((o) => Boolean(o?.name)).map((o) => [o.name, o])
|
|
4416
|
+
);
|
|
4417
|
+
const registered = [];
|
|
4418
|
+
const skipped = [];
|
|
4419
|
+
const prefix = context.toolPrefix ?? "action_";
|
|
4420
|
+
for (const obj of objects) {
|
|
4421
|
+
if (!obj?.actions || !Array.isArray(obj.actions)) continue;
|
|
4422
|
+
for (const action of obj.actions) {
|
|
4423
|
+
if (!action || typeof action.name !== "string") continue;
|
|
4424
|
+
const normalized = {
|
|
4425
|
+
...action,
|
|
4426
|
+
objectName: action.objectName ?? obj.name
|
|
4427
|
+
};
|
|
4428
|
+
const reason = actionSkipReason(normalized, {
|
|
4429
|
+
automation: context.automation,
|
|
4430
|
+
apiClient: context.apiClient,
|
|
4431
|
+
apiBaseUrl: context.apiBaseUrl,
|
|
4432
|
+
enableActionApproval: context.enableActionApproval,
|
|
4433
|
+
aiService: context.aiService
|
|
4434
|
+
});
|
|
4435
|
+
if (reason !== null) {
|
|
4436
|
+
skipped.push({ action: normalized.name, reason });
|
|
4437
|
+
continue;
|
|
4438
|
+
}
|
|
4439
|
+
const definition = actionToToolDefinition(normalized, obj, objMap, prefix);
|
|
4440
|
+
if (!definition) continue;
|
|
4441
|
+
if (registry.has(definition.name)) {
|
|
4442
|
+
skipped.push({ action: normalized.name, reason: "tool name already registered" });
|
|
4443
|
+
continue;
|
|
4444
|
+
}
|
|
4445
|
+
const handler = createActionToolHandler(normalized, context);
|
|
4446
|
+
registry.register(definition, handler);
|
|
4447
|
+
registered.push(definition.name);
|
|
4448
|
+
if (context.enableActionApproval && actionRequiresApproval(normalized) && context.aiService?.registerPendingActionDispatcher) {
|
|
4449
|
+
const bypassCtx = {
|
|
4450
|
+
...context,
|
|
4451
|
+
enableActionApproval: false
|
|
4452
|
+
};
|
|
4453
|
+
const directHandler = createActionToolHandler(normalized, bypassCtx);
|
|
4454
|
+
context.aiService.registerPendingActionDispatcher(
|
|
4455
|
+
definition.name,
|
|
4456
|
+
async (input) => {
|
|
4457
|
+
const raw = await directHandler(input);
|
|
4458
|
+
let parsed = raw;
|
|
4459
|
+
try {
|
|
4460
|
+
parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
4461
|
+
} catch {
|
|
4462
|
+
parsed = raw;
|
|
4463
|
+
}
|
|
4464
|
+
if (parsed && typeof parsed === "object" && "ok" in parsed && parsed.ok === false) {
|
|
4465
|
+
const errMsg = parsed.error != null ? String(parsed.error) : "action handler reported failure";
|
|
4466
|
+
throw new Error(errMsg);
|
|
4467
|
+
}
|
|
4468
|
+
return parsed;
|
|
4469
|
+
}
|
|
4470
|
+
);
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
return { registered, skipped };
|
|
4475
|
+
}
|
|
4476
|
+
|
|
3130
4477
|
// src/agent-runtime.ts
|
|
3131
4478
|
var import_ai7 = require("@objectstack/spec/ai");
|
|
3132
4479
|
var AgentRuntime = class {
|
|
@@ -3429,6 +4776,16 @@ var SkillRegistry = class {
|
|
|
3429
4776
|
const resolved = [];
|
|
3430
4777
|
for (const skill of skills) {
|
|
3431
4778
|
for (const toolName of skill.tools) {
|
|
4779
|
+
if (toolName.endsWith("*")) {
|
|
4780
|
+
const prefix = toolName.slice(0, -1);
|
|
4781
|
+
for (const def2 of availableTools) {
|
|
4782
|
+
if (!def2.name.startsWith(prefix)) continue;
|
|
4783
|
+
if (seen.has(def2.name)) continue;
|
|
4784
|
+
resolved.push(def2);
|
|
4785
|
+
seen.add(def2.name);
|
|
4786
|
+
}
|
|
4787
|
+
continue;
|
|
4788
|
+
}
|
|
3432
4789
|
if (seen.has(toolName)) continue;
|
|
3433
4790
|
const def = toolMap.get(toolName);
|
|
3434
4791
|
if (def) {
|
|
@@ -3492,7 +4849,8 @@ Always answer in the same language the user is using. Detailed tool-usage guidan
|
|
|
3492
4849
|
maxTokens: 4096
|
|
3493
4850
|
},
|
|
3494
4851
|
// Capability bundle lives on the skill; the agent only references it.
|
|
3495
|
-
|
|
4852
|
+
// `data_explorer` = read side, `actions_executor` = write side.
|
|
4853
|
+
skills: ["data_explorer", "actions_executor"],
|
|
3496
4854
|
active: true,
|
|
3497
4855
|
visibility: "global",
|
|
3498
4856
|
guardrails: {
|
|
@@ -3572,6 +4930,7 @@ Guidelines:
|
|
|
3572
4930
|
7. Never expose internal IDs unless the user explicitly asks for them.
|
|
3573
4931
|
8. Always answer in the same language the user is using.`,
|
|
3574
4932
|
tools: [
|
|
4933
|
+
"query_data",
|
|
3575
4934
|
"list_objects",
|
|
3576
4935
|
"describe_object",
|
|
3577
4936
|
"query_records",
|
|
@@ -3641,6 +5000,44 @@ Guidelines:
|
|
|
3641
5000
|
active: true
|
|
3642
5001
|
};
|
|
3643
5002
|
|
|
5003
|
+
// src/skills/actions-executor-skill.ts
|
|
5004
|
+
var ACTIONS_EXECUTOR_SKILL = {
|
|
5005
|
+
name: "actions_executor",
|
|
5006
|
+
label: "Action Executor",
|
|
5007
|
+
description: "Perform business operations on the user's data \u2014 invoke actions like 'mark as complete', 'start task', 'clone record' through natural language.",
|
|
5008
|
+
instructions: `You can perform business operations by invoking the user's registered actions.
|
|
5009
|
+
|
|
5010
|
+
Capabilities:
|
|
5011
|
+
- Each tool whose name starts with \`action_\` is a business operation declared on an object.
|
|
5012
|
+
- Read the tool description carefully \u2014 it tells you what the action does and what record types it applies to.
|
|
5013
|
+
- 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.
|
|
5014
|
+
|
|
5015
|
+
Guidelines:
|
|
5016
|
+
1. Confirm intent \u2014 when the user says "complete it" / "start that one", make sure you know *which* record they mean. Ask if ambiguous.
|
|
5017
|
+
2. Use \`query_data\` to look up records by natural-language description ("the design review task", "tickets assigned to me").
|
|
5018
|
+
3. After invoking an action, the tool returns \`{ ok, message, result }\`. Summarise success in plain language; surface errors verbatim.
|
|
5019
|
+
4. Never invent recordIds. If \`query_data\` didn't return one, tell the user instead of guessing.
|
|
5020
|
+
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.
|
|
5021
|
+
6. Always answer in the same language the user is using.`,
|
|
5022
|
+
// Dynamically materialised: the runtime registers one tool per Action,
|
|
5023
|
+
// and the skill subscribes to the whole family via the `action_*`
|
|
5024
|
+
// glob (resolved by SkillRegistry.flattenToTools).
|
|
5025
|
+
tools: ["action_*"],
|
|
5026
|
+
triggerPhrases: [
|
|
5027
|
+
"complete",
|
|
5028
|
+
"mark as",
|
|
5029
|
+
"start",
|
|
5030
|
+
"finish",
|
|
5031
|
+
"clone",
|
|
5032
|
+
"duplicate",
|
|
5033
|
+
"do it",
|
|
5034
|
+
"run",
|
|
5035
|
+
"invoke",
|
|
5036
|
+
"execute"
|
|
5037
|
+
],
|
|
5038
|
+
active: true
|
|
5039
|
+
};
|
|
5040
|
+
|
|
3644
5041
|
// src/adapters/vercel-adapter.ts
|
|
3645
5042
|
var import_ai9 = require("ai");
|
|
3646
5043
|
function buildVercelOptions(options) {
|
|
@@ -3949,26 +5346,29 @@ var AIServicePlugin = class {
|
|
|
3949
5346
|
ctx.logger.info(`[AI] ModelRegistry initialised with ${modelRegistry.size} model(s)`);
|
|
3950
5347
|
}
|
|
3951
5348
|
let traceRecorder;
|
|
5349
|
+
let dataEngine;
|
|
5350
|
+
try {
|
|
5351
|
+
const engine = ctx.getService("data");
|
|
5352
|
+
if (engine && typeof engine.insert === "function") {
|
|
5353
|
+
dataEngine = engine;
|
|
5354
|
+
}
|
|
5355
|
+
} catch {
|
|
5356
|
+
}
|
|
3952
5357
|
if (this.options.traceRecorder === null) {
|
|
3953
5358
|
ctx.logger.debug("[AI] Tracing disabled (traceRecorder=null)");
|
|
3954
5359
|
} else if (this.options.traceRecorder) {
|
|
3955
5360
|
traceRecorder = this.options.traceRecorder;
|
|
3956
|
-
} else {
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
if (engine && typeof engine.insert === "function") {
|
|
3960
|
-
traceRecorder = new ObjectQLTraceRecorder(engine, { logger: ctx.logger });
|
|
3961
|
-
ctx.logger.info("[AI] Using ObjectQLTraceRecorder (IDataEngine detected)");
|
|
3962
|
-
}
|
|
3963
|
-
} catch {
|
|
3964
|
-
}
|
|
5361
|
+
} else if (dataEngine) {
|
|
5362
|
+
traceRecorder = new ObjectQLTraceRecorder(dataEngine, { logger: ctx.logger });
|
|
5363
|
+
ctx.logger.info("[AI] Using ObjectQLTraceRecorder (IDataEngine detected)");
|
|
3965
5364
|
}
|
|
3966
5365
|
const config = {
|
|
3967
5366
|
adapter,
|
|
3968
5367
|
logger: ctx.logger,
|
|
3969
5368
|
conversationService,
|
|
3970
5369
|
modelRegistry,
|
|
3971
|
-
traceRecorder
|
|
5370
|
+
traceRecorder,
|
|
5371
|
+
dataEngine
|
|
3972
5372
|
};
|
|
3973
5373
|
this.service = new AIService(config);
|
|
3974
5374
|
if (hasExisting) {
|
|
@@ -3983,7 +5383,8 @@ var AIServicePlugin = class {
|
|
|
3983
5383
|
type: "plugin",
|
|
3984
5384
|
scope: "project",
|
|
3985
5385
|
namespace: "ai",
|
|
3986
|
-
objects: [AiConversationObject, AiMessageObject, AiTraceObject]
|
|
5386
|
+
objects: [AiConversationObject, AiMessageObject, AiTraceObject, AiPendingActionObject],
|
|
5387
|
+
views: [AiTraceView, AiPendingActionView]
|
|
3987
5388
|
});
|
|
3988
5389
|
if (this.options.debug) {
|
|
3989
5390
|
ctx.hook("ai:beforeChat", async (messages) => {
|
|
@@ -4022,6 +5423,44 @@ var AIServicePlugin = class {
|
|
|
4022
5423
|
dataEngine
|
|
4023
5424
|
});
|
|
4024
5425
|
ctx.logger.info("[AI] query_data tool registered");
|
|
5426
|
+
try {
|
|
5427
|
+
let automation;
|
|
5428
|
+
try {
|
|
5429
|
+
automation = ctx.getService("automation");
|
|
5430
|
+
} catch {
|
|
5431
|
+
automation = void 0;
|
|
5432
|
+
}
|
|
5433
|
+
const apiBaseUrl = this.options.apiActionBaseUrl ?? process.env.OS_AI_ACTION_API_BASE_URL;
|
|
5434
|
+
const apiHeaders = this.options.apiActionHeaders;
|
|
5435
|
+
const { registered, skipped } = await registerActionsAsTools(
|
|
5436
|
+
this.service.toolRegistry,
|
|
5437
|
+
{
|
|
5438
|
+
metadata: metadataService,
|
|
5439
|
+
dataEngine,
|
|
5440
|
+
automation,
|
|
5441
|
+
apiBaseUrl,
|
|
5442
|
+
apiHeaders,
|
|
5443
|
+
enableActionApproval: this.options.enableActionApproval ?? false,
|
|
5444
|
+
aiService: this.service
|
|
5445
|
+
}
|
|
5446
|
+
);
|
|
5447
|
+
if (registered.length > 0) {
|
|
5448
|
+
ctx.logger.info(
|
|
5449
|
+
`[AI] ${registered.length} action tool(s) registered: ${registered.join(", ")}`
|
|
5450
|
+
);
|
|
5451
|
+
}
|
|
5452
|
+
if (skipped.length > 0) {
|
|
5453
|
+
ctx.logger.debug(
|
|
5454
|
+
`[AI] Skipped ${skipped.length} action(s) for AI exposure`,
|
|
5455
|
+
{ skipped }
|
|
5456
|
+
);
|
|
5457
|
+
}
|
|
5458
|
+
} catch (err) {
|
|
5459
|
+
ctx.logger.warn(
|
|
5460
|
+
"[AI] Failed to register action tools",
|
|
5461
|
+
err instanceof Error ? { error: err.message } : { error: String(err) }
|
|
5462
|
+
);
|
|
5463
|
+
}
|
|
4025
5464
|
}
|
|
4026
5465
|
if (metadataService) {
|
|
4027
5466
|
const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
|
|
@@ -4073,6 +5512,19 @@ var AIServicePlugin = class {
|
|
|
4073
5512
|
} catch (err) {
|
|
4074
5513
|
ctx.logger.warn("[AI] Failed to register data_explorer skill", err instanceof Error ? { error: err.message } : { error: String(err) });
|
|
4075
5514
|
}
|
|
5515
|
+
try {
|
|
5516
|
+
const skillExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("skill", ACTIONS_EXECUTOR_SKILL.name)) : false;
|
|
5517
|
+
if (skillExists === null) {
|
|
5518
|
+
ctx.logger.warn("[AI] Metadata service timed out checking actions_executor skill, skipping");
|
|
5519
|
+
} else if (!skillExists) {
|
|
5520
|
+
await withTimeout(metadataService.register("skill", ACTIONS_EXECUTOR_SKILL.name, ACTIONS_EXECUTOR_SKILL));
|
|
5521
|
+
ctx.logger.info("[AI] actions_executor skill registered");
|
|
5522
|
+
} else {
|
|
5523
|
+
ctx.logger.debug("[AI] actions_executor skill already exists, skipping auto-registration");
|
|
5524
|
+
}
|
|
5525
|
+
} catch (err) {
|
|
5526
|
+
ctx.logger.warn("[AI] Failed to register actions_executor skill", err instanceof Error ? { error: err.message } : { error: String(err) });
|
|
5527
|
+
}
|
|
4076
5528
|
}
|
|
4077
5529
|
}
|
|
4078
5530
|
} catch {
|
|
@@ -4170,6 +5622,9 @@ var AIServicePlugin = class {
|
|
|
4170
5622
|
const toolRoutes = buildToolRoutes(this.service, ctx.logger);
|
|
4171
5623
|
routes.push(...toolRoutes);
|
|
4172
5624
|
ctx.logger.info(`[AI] Tool routes registered (${toolRoutes.length} routes)`);
|
|
5625
|
+
const pendingRoutes = buildPendingActionRoutes(this.service, ctx.logger);
|
|
5626
|
+
routes.push(...pendingRoutes);
|
|
5627
|
+
ctx.logger.info(`[AI] Pending-action routes registered (${pendingRoutes.length} routes)`);
|
|
4173
5628
|
if (metadataService) {
|
|
4174
5629
|
const skillRegistry = new SkillRegistry(metadataService);
|
|
4175
5630
|
const agentRuntime = new AgentRuntime(metadataService, skillRegistry);
|
|
@@ -4533,16 +5988,20 @@ function registerPackageTools(registry, context) {
|
|
|
4533
5988
|
}
|
|
4534
5989
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4535
5990
|
0 && (module.exports = {
|
|
5991
|
+
ACTIONS_EXECUTOR_SKILL,
|
|
4536
5992
|
AIService,
|
|
4537
5993
|
AIServicePlugin,
|
|
4538
5994
|
AgentRuntime,
|
|
4539
5995
|
AiConversationObject,
|
|
4540
5996
|
AiMessageObject,
|
|
4541
5997
|
AiTraceObject,
|
|
5998
|
+
AiTraceView,
|
|
4542
5999
|
DATA_CHAT_AGENT,
|
|
6000
|
+
DATA_EXPLORER_SKILL,
|
|
4543
6001
|
DATA_TOOL_DEFINITIONS,
|
|
4544
6002
|
InMemoryConversationService,
|
|
4545
6003
|
METADATA_ASSISTANT_AGENT,
|
|
6004
|
+
METADATA_AUTHORING_SKILL,
|
|
4546
6005
|
METADATA_TOOL_DEFINITIONS,
|
|
4547
6006
|
MemoryLLMAdapter,
|
|
4548
6007
|
ModelRegistry,
|
|
@@ -4555,6 +6014,9 @@ function registerPackageTools(registry, context) {
|
|
|
4555
6014
|
SkillRegistry,
|
|
4556
6015
|
ToolRegistry,
|
|
4557
6016
|
VercelLLMAdapter,
|
|
6017
|
+
actionSkipReason,
|
|
6018
|
+
actionToToolDefinition,
|
|
6019
|
+
actionToolName,
|
|
4558
6020
|
addFieldTool,
|
|
4559
6021
|
buildAIRoutes,
|
|
4560
6022
|
buildAgentRoutes,
|
|
@@ -4574,6 +6036,7 @@ function registerPackageTools(registry, context) {
|
|
|
4574
6036
|
listObjectsTool,
|
|
4575
6037
|
listPackagesTool,
|
|
4576
6038
|
modifyFieldTool,
|
|
6039
|
+
registerActionsAsTools,
|
|
4577
6040
|
registerDataTools,
|
|
4578
6041
|
registerMetadataTools,
|
|
4579
6042
|
registerPackageTools,
|