@alquimia-ai/tools 2.7.0 → 2.8.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.
Files changed (63) hide show
  1. package/README.md +88 -12
  2. package/dist/actions/index.d.mts +1 -1
  3. package/dist/actions/index.d.ts +1 -1
  4. package/dist/adapters/fetch.d.mts +1 -0
  5. package/dist/adapters/fetch.d.ts +1 -0
  6. package/dist/adapters/fetch.js +4 -0
  7. package/dist/adapters/fetch.js.map +1 -1
  8. package/dist/adapters/fetch.mjs +4 -0
  9. package/dist/adapters/fetch.mjs.map +1 -1
  10. package/dist/adapters/index.d.mts +1 -0
  11. package/dist/adapters/index.d.ts +1 -0
  12. package/dist/adapters/index.js.map +1 -1
  13. package/dist/adapters/next.d.mts +1 -0
  14. package/dist/adapters/next.d.ts +1 -0
  15. package/dist/adapters/next.js +4 -0
  16. package/dist/adapters/next.js.map +1 -1
  17. package/dist/adapters/next.mjs +4 -0
  18. package/dist/adapters/next.mjs.map +1 -1
  19. package/dist/approvals-CcnmqUkQ.d.ts +59 -0
  20. package/dist/approvals-dn4sd_Ot.d.mts +59 -0
  21. package/dist/hooks/index.d.mts +36 -5
  22. package/dist/hooks/index.d.ts +36 -5
  23. package/dist/hooks/index.js +208 -12
  24. package/dist/hooks/index.js.map +1 -1
  25. package/dist/hooks/index.mjs +208 -12
  26. package/dist/hooks/index.mjs.map +1 -1
  27. package/dist/next/index.d.mts +7 -0
  28. package/dist/next/index.d.ts +7 -0
  29. package/dist/next/index.js +56 -40
  30. package/dist/next/index.js.map +1 -1
  31. package/dist/next/index.mjs +56 -40
  32. package/dist/next/index.mjs.map +1 -1
  33. package/dist/providers/index.d.mts +3 -3
  34. package/dist/providers/index.d.ts +3 -3
  35. package/dist/{providers-mx21gNgZ.d.ts → providers-J_Z9rQ3Q.d.ts} +1 -1
  36. package/dist/{providers-B4IOiGfr.d.mts → providers-YKiEag2L.d.mts} +1 -1
  37. package/dist/proxy.d.mts +2 -0
  38. package/dist/proxy.d.ts +2 -0
  39. package/dist/proxy.js +47 -40
  40. package/dist/proxy.js.map +1 -1
  41. package/dist/proxy.mjs +47 -40
  42. package/dist/proxy.mjs.map +1 -1
  43. package/dist/sdk/index.d.mts +25 -3
  44. package/dist/sdk/index.d.ts +25 -3
  45. package/dist/sdk/index.js +73 -10
  46. package/dist/sdk/index.js.map +1 -1
  47. package/dist/sdk/index.mjs +73 -10
  48. package/dist/sdk/index.mjs.map +1 -1
  49. package/dist/services/index.d.mts +1 -1
  50. package/dist/services/index.d.ts +1 -1
  51. package/dist/{type-D-JaSMkV.d.ts → type-BFtGEEb4.d.ts} +1 -1
  52. package/dist/{type-61gaZga-.d.mts → type-C_sKpIM1.d.mts} +1 -1
  53. package/dist/types/index.d.mts +23 -2
  54. package/dist/types/index.d.ts +23 -2
  55. package/dist/utils/index.d.mts +1 -1
  56. package/dist/utils/index.d.ts +1 -1
  57. package/dist/worklog/index.d.mts +1 -0
  58. package/dist/worklog/index.d.ts +1 -0
  59. package/dist/worklog/index.js +75 -2
  60. package/dist/worklog/index.js.map +1 -1
  61. package/dist/worklog/index.mjs +75 -2
  62. package/dist/worklog/index.mjs.map +1 -1
  63. package/package.json +1 -1
@@ -14,6 +14,9 @@ var AlquimiaSDK = class {
14
14
  // Registry version tag resolved by the runtime for this task (v0.5.1), so
15
15
  // the tool-completion half of the turn lands on the same agentspace version.
16
16
  this.versionTag = null;
17
+ // Agentspace the runtime resolved for this task. Tool completion and
18
+ // approval authorize a non-admin caller against it, so it is echoed back.
19
+ this.agentspaceId = null;
17
20
  this.tools = [];
18
21
  this.extraInstructions = null;
19
22
  this.assistantConfig = null;
@@ -165,6 +168,7 @@ var AlquimiaSDK = class {
165
168
  this.traceParent = opts.traceParent ?? null;
166
169
  this.streamId = result.taskid ?? null;
167
170
  this.versionTag = result.versiontag ?? this.versionTag;
171
+ this.agentspaceId = result.agentspaceid ?? null;
168
172
  this.attachmentResponses = result.attachments ?? [];
169
173
  this.attachments = [];
170
174
  return this;
@@ -184,17 +188,37 @@ var AlquimiaSDK = class {
184
188
  "AlquimiaSDK: adapter does not support tool completion (resolveToolCompletionUrl missing)"
185
189
  );
186
190
  }
187
- const adapterHeaders = this.adapter.getHeaders?.() ?? {};
188
- const headers = {
189
- "Content-Type": "application/json",
190
- assistantid: this.assistantId,
191
- ...this.streamId && { taskid: this.streamId },
192
- ...this.conversationId && { sessionid: this.conversationId },
193
- ...this.userId && { userid: this.userId },
194
- ...this.versionTag && { versiontag: this.versionTag },
195
- ...adapterHeaders
191
+ await this.axiosInstance.post(url, response, { headers: this.buildDecisionHeaders() });
192
+ }
193
+ /**
194
+ * Answer a `HumanApprovalRequired` gate: POSTs a {@link ToolApprovalEvent} to
195
+ * the runtime's `/event/tool-approval`. `controlId` is the gate's own control
196
+ * id (the `HumanApprovalRequired` record's), not the parked tool call's.
197
+ *
198
+ * Never throws for a runtime refusal — each 409 means something different to
199
+ * the UI, so the answer comes back classified as a {@link ToolApprovalResult}.
200
+ * It does throw when the adapter has no approval route, which is a wiring bug
201
+ * rather than a runtime answer.
202
+ */
203
+ async submitToolApproval(controlId, approved, message) {
204
+ const url = this.adapter.resolveToolApprovalUrl?.();
205
+ if (!url) {
206
+ throw new Error(
207
+ "AlquimiaSDK: adapter does not support tool approval (resolveToolApprovalUrl missing)"
208
+ );
209
+ }
210
+ const event = {
211
+ event_class: "ToolApprovalEvent",
212
+ control_id: controlId,
213
+ result: approved,
214
+ ...message && { message }
196
215
  };
197
- await this.axiosInstance.post(url, response, { headers });
216
+ try {
217
+ await this.axiosInstance.post(url, event, { headers: this.buildDecisionHeaders() });
218
+ return { outcome: "accepted" };
219
+ } catch (error) {
220
+ return classifyApprovalError(error);
221
+ }
198
222
  }
199
223
  async generateImage(query) {
200
224
  if (!this.stableDiffusionProvider) {
@@ -265,6 +289,23 @@ var AlquimiaSDK = class {
265
289
  if (this.streamId) headers["task-id"] = this.streamId;
266
290
  return headers;
267
291
  }
292
+ /**
293
+ * Correlation headers for tool-completion and tool-approval: the runtime's
294
+ * `CommonAttributes` header aliases, with no separator. Not interchangeable
295
+ * with the kebab-case set in {@link buildIdentityHeaders}.
296
+ */
297
+ buildDecisionHeaders() {
298
+ return {
299
+ "Content-Type": "application/json",
300
+ assistantid: this.assistantId,
301
+ ...this.streamId && { taskid: this.streamId },
302
+ ...this.conversationId && { sessionid: this.conversationId },
303
+ ...this.userId && { userid: this.userId },
304
+ ...this.agentspaceId && { agentspaceid: this.agentspaceId },
305
+ ...this.versionTag && { versiontag: this.versionTag },
306
+ ...this.adapter.getHeaders?.() ?? {}
307
+ };
308
+ }
268
309
  getUrlStream() {
269
310
  if (!this.streamId) {
270
311
  throw new Error("Stream ID not initialized. Call sendMessage() first");
@@ -285,7 +326,29 @@ var AlquimiaSDK = class {
285
326
  getVersionTag() {
286
327
  return this.versionTag;
287
328
  }
329
+ /** Agentspace the runtime resolved for the current task, if any. */
330
+ getAgentspaceId() {
331
+ return this.agentspaceId;
332
+ }
288
333
  };
334
+ function classifyApprovalError(error) {
335
+ const response = error?.response;
336
+ const status = response?.status;
337
+ const data = response?.data;
338
+ const rawDetail = typeof data === "string" ? data : data?.detail ?? data?.error ?? error?.message;
339
+ const detail = typeof rawDetail === "string" ? rawDetail : JSON.stringify(rawDetail ?? "");
340
+ let outcome = "error";
341
+ if (status === 409) {
342
+ if (/original channel/i.test(detail)) outcome = "wrong-channel";
343
+ else if (/already been recorded/i.test(detail)) outcome = "already-decided";
344
+ else if (/no pending approval/i.test(detail)) outcome = "not-pending";
345
+ } else if (status === 401 || status === 403) {
346
+ outcome = "forbidden";
347
+ } else if (status === 404) {
348
+ outcome = "not-found";
349
+ }
350
+ return { outcome, ...status !== void 0 && { status }, detail };
351
+ }
289
352
  var alquimia_sdk_default = AlquimiaSDK;
290
353
 
291
354
  // src/utils/utils.ts
@@ -389,6 +452,10 @@ var agentTitle = (r) => {
389
452
  return typeof name === "string" && name.length > 0 ? `Agent: ${name}` : "Agent-to-agent";
390
453
  };
391
454
  var constTitle = (label) => () => label;
455
+ var approvalTitle = (r) => {
456
+ const name = asRecord(asRecord(r.data).parameters).name;
457
+ return typeof name === "string" && name.length > 0 ? `Approval required: ${name}` : "Human approval required";
458
+ };
392
459
  var shieldBlockTitle = (r) => {
393
460
  const id = asRecord(r.data).shield_id;
394
461
  return typeof id === "string" && id.length > 0 ? `Blocked by ${id}` : "Blocked by safeguard";
@@ -440,7 +507,7 @@ var EVENT_REGISTRY = {
440
507
  HumanApprovalRequired: {
441
508
  kind: "tool",
442
509
  role: "open",
443
- title: constTitle("Human approval required"),
510
+ title: approvalTitle,
444
511
  pending: true
445
512
  },
446
513
  HumanApprovalRequiredResponse: { kind: "tool", role: "close", title: constTitle("Human approval") }
@@ -529,7 +596,12 @@ function reduceWorklog(state, record) {
529
596
  const pos = Object.prototype.hasOwnProperty.call(next.index, key) ? next.index[key] : void 0;
530
597
  if (pos != null) {
531
598
  const existing = next.nodes[pos];
532
- next.nodes[pos] = { ...existing, raw: [...existing.raw, record] };
599
+ next.nodes[pos] = {
600
+ ...existing,
601
+ command: existing.command ?? record.data,
602
+ title: existing.command === void 0 ? interp.title(record) : existing.title,
603
+ raw: [...existing.raw, record]
604
+ };
533
605
  } else {
534
606
  next.index[key] = next.nodes.length;
535
607
  next.nodes.push(makeOpenNode(record, interp, key));
@@ -554,6 +626,59 @@ function frameToRecord(frame) {
554
626
  };
555
627
  }
556
628
 
629
+ // src/worklog/approvals.ts
630
+ function initialApprovalsState() {
631
+ return { order: [], byId: {} };
632
+ }
633
+ var asObject = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
634
+ var MCP_APPROVAL_EXTENSION = "mcp_approval";
635
+ function describeRequest(record) {
636
+ const execution = asObject(asObject(record.data)?.parameters) ?? {};
637
+ const name = execution.name;
638
+ const callId = execution.control_id;
639
+ const args = execution.tool_call_parameters !== void 0 && execution.tool_call_parameters !== null ? execution.tool_call_parameters : execution.parameters;
640
+ const extensions = asObject(execution.parameters)?.runtime_extensions;
641
+ return {
642
+ toolName: typeof name === "string" && name.length > 0 ? name : null,
643
+ toolCallId: typeof callId === "string" ? callId : null,
644
+ arguments: args ?? null,
645
+ channelOwned: Array.isArray(extensions) && extensions.includes(MCP_APPROVAL_EXTENSION)
646
+ };
647
+ }
648
+ function describeDecision(record) {
649
+ const approved = record.data === true;
650
+ return {
651
+ approved,
652
+ reason: !approved && typeof record.data === "string" && record.data.length > 0 ? record.data : null,
653
+ decidedAt: record.date_submitted
654
+ };
655
+ }
656
+ var blank = (controlId) => ({
657
+ controlId,
658
+ toolName: null,
659
+ toolCallId: null,
660
+ arguments: null,
661
+ channelOwned: false,
662
+ requestedAt: null,
663
+ decision: null
664
+ });
665
+ function reduceApprovals(state, record) {
666
+ const isRequest = record.event_class === "HumanApprovalRequired";
667
+ const isResponse = record.event_class === "HumanApprovalRequiredResponse";
668
+ if (!isRequest && !isResponse || !record.control_id) return state;
669
+ const id = record.control_id;
670
+ const known = state.byId[id];
671
+ const current = known ?? blank(id);
672
+ const next = isRequest ? { ...current, ...describeRequest(record), requestedAt: record.date_submitted } : { ...current, decision: current.decision ?? describeDecision(record) };
673
+ return {
674
+ order: known ? state.order : [...state.order, id],
675
+ byId: { ...state.byId, [id]: next }
676
+ };
677
+ }
678
+ function listApprovals(state) {
679
+ return state.order.map((id) => state.byId[id]);
680
+ }
681
+
557
682
  // src/hooks/pending-client-tool.ts
558
683
  function extractPendingClientTool(frame) {
559
684
  if (!frame || typeof frame !== "object") return null;
@@ -916,6 +1041,7 @@ function buildGenuiClause(manifest, allowlist) {
916
1041
  import { zodToJsonSchema } from "zod-to-json-schema";
917
1042
 
918
1043
  // src/hooks/alquimia.hook.tsx
1044
+ var TERMINAL_REFUSALS = /* @__PURE__ */ new Set(["already-decided", "wrong-channel", "not-pending", "not-found", "forbidden"]);
919
1045
  function useAlquimia(config) {
920
1046
  const sdk = useMemo(() => {
921
1047
  const instance = new alquimia_sdk_default(config.assistantId, config.adapter, {
@@ -960,6 +1086,12 @@ function useAlquimia(config) {
960
1086
  const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
961
1087
  const [pendingClientTool, setPendingClientTool] = useState(null);
962
1088
  const completedToolsRef = useRef(/* @__PURE__ */ new Set());
1089
+ const [approvalsState, setApprovalsState] = useState(initialApprovalsState);
1090
+ const approvalsStateRef = useRef(approvalsState);
1091
+ approvalsStateRef.current = approvalsState;
1092
+ const [approvalSubmissions, setApprovalSubmissions] = useState({});
1093
+ const approvalCallsRef = useRef(/* @__PURE__ */ new Map());
1094
+ const approvalAnchorsRef = useRef({});
963
1095
  const genuiCatalog = config.genui?.catalog ?? coreCatalog;
964
1096
  const [genuiSurfaces, setGenuiSurfaces] = useState([]);
965
1097
  const messagesLenRef = useRef(0);
@@ -1242,6 +1374,14 @@ function useAlquimia(config) {
1242
1374
  if (worklogEnabled) {
1243
1375
  setWorklog((prev) => reduceWorklog(prev ?? initialWorklogState(), frameToRecord(data)));
1244
1376
  }
1377
+ if ((data.event_class === "HumanApprovalRequired" || data.event_class === "HumanApprovalRequiredResponse") && data.control_id) {
1378
+ if (!(data.control_id in approvalAnchorsRef.current)) {
1379
+ approvalAnchorsRef.current[data.control_id] = genuiAnchorRef.current;
1380
+ }
1381
+ const record = frameToRecord(data);
1382
+ setApprovalsState((prev) => reduceApprovals(prev, record));
1383
+ return;
1384
+ }
1245
1385
  const pending = extractPendingClientTool(data);
1246
1386
  if (pending) {
1247
1387
  setPendingClientTool(pending);
@@ -1290,6 +1430,59 @@ function useAlquimia(config) {
1290
1430
  },
1291
1431
  [sdk]
1292
1432
  );
1433
+ const approveTool = useCallback2(
1434
+ (controlId, approved, message) => {
1435
+ const existing = approvalCallsRef.current.get(controlId);
1436
+ if (existing) return existing;
1437
+ if (approvalsStateRef.current.byId[controlId]?.decision) {
1438
+ return Promise.resolve({ outcome: "already-decided" });
1439
+ }
1440
+ const reason = message?.trim() || null;
1441
+ setApprovalSubmissions((prev) => ({
1442
+ ...prev,
1443
+ [controlId]: { inFlight: true, approved, reason, result: prev[controlId]?.result ?? null }
1444
+ }));
1445
+ const call = sdk.submitToolApproval(controlId, approved, reason ?? void 0).catch((error) => ({
1446
+ outcome: "error",
1447
+ detail: error instanceof Error ? error.message : String(error)
1448
+ })).then((result) => {
1449
+ if (result.outcome !== "accepted") approvalCallsRef.current.delete(controlId);
1450
+ setApprovalSubmissions((prev) => ({
1451
+ ...prev,
1452
+ [controlId]: { inFlight: false, approved, reason, result }
1453
+ }));
1454
+ return result;
1455
+ });
1456
+ approvalCallsRef.current.set(controlId, call);
1457
+ return call;
1458
+ },
1459
+ [sdk]
1460
+ );
1461
+ const toolApprovals = useMemo(
1462
+ () => listApprovals(approvalsState).map((gate) => {
1463
+ const sub = approvalSubmissions[gate.controlId];
1464
+ const acceptedHere = sub?.result?.outcome === "accepted";
1465
+ const refused = !!sub?.result && TERMINAL_REFUSALS.has(sub.result.outcome);
1466
+ let status = "pending";
1467
+ if (gate.decision) status = gate.decision.approved ? "approved" : "rejected";
1468
+ else if (sub?.inFlight) status = "submitting";
1469
+ else if (acceptedHere) status = sub.approved ? "approved" : "rejected";
1470
+ return {
1471
+ ...gate,
1472
+ status,
1473
+ reason: gate.decision?.reason ?? (acceptedHere && !sub.approved ? sub.reason : null),
1474
+ decidedHere: acceptedHere && (gate.decision ? gate.decision.approved === sub.approved : true),
1475
+ canAnswer: status === "pending" && !gate.channelOwned && !refused,
1476
+ lastResult: sub?.result ?? null,
1477
+ afterCount: approvalAnchorsRef.current[gate.controlId] ?? 0
1478
+ };
1479
+ }),
1480
+ [approvalsState, approvalSubmissions]
1481
+ );
1482
+ const pendingApprovals2 = useMemo(
1483
+ () => toolApprovals.filter((a) => a.status === "pending" || a.status === "submitting"),
1484
+ [toolApprovals]
1485
+ );
1293
1486
  useEffect(() => {
1294
1487
  if (!config.genui) return;
1295
1488
  if (!pendingClientTool || pendingClientTool.toolName !== "render_ui") return;
@@ -1375,6 +1568,9 @@ function useAlquimia(config) {
1375
1568
  activeTool,
1376
1569
  pendingClientTool,
1377
1570
  completeToolExecution,
1571
+ toolApprovals,
1572
+ pendingApprovals: pendingApprovals2,
1573
+ approveTool,
1378
1574
  genui,
1379
1575
  cleanMessages,
1380
1576
  createMessageId,