@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
@@ -1,11 +1,13 @@
1
1
  import * as react from 'react';
2
2
  import { AlquimiaSDK } from '../sdk/index.js';
3
3
  import { AlquimiaAdapter } from '../adapters/index.js';
4
- import { W as WhisperProvider, S as StableDiffusionProvider, C as CharacterizationProvider, R as RatingsProvider, L as LoggerProvider } from '../providers-mx21gNgZ.js';
5
- import { G as GenuiSurfaceItem, e as AlquimiaMessage, F as ToolEvent, a as AIMessageChunk, R as RatingData } from '../type-D-JaSMkV.js';
6
- import { T as ToolExecutionResponse } from '../types-CMVbyEv8.js';
7
- import { a as WorklogState } from '../types-lOliroPw.js';
4
+ import { W as WhisperProvider, S as StableDiffusionProvider, C as CharacterizationProvider, R as RatingsProvider, L as LoggerProvider } from '../providers-J_Z9rQ3Q.js';
5
+ import { G as GenuiSurfaceItem, a as AlquimiaMessage, b as ToolEvent, c as AIMessageChunk, R as RatingData } from '../type-BFtGEEb4.js';
6
+ import { ToolApprovalResult } from '../types/index.js';
8
7
  import { createMessageId } from '../utils/index.js';
8
+ import { a as WorklogState } from '../types-lOliroPw.js';
9
+ import { A as ApprovalRequest } from '../approvals-CcnmqUkQ.js';
10
+ import { T as ToolExecutionResponse } from '../types-CMVbyEv8.js';
9
11
  import { C as CatalogManifest } from '../registry-Bcu_ZYL1.js';
10
12
  import { U as UIAction } from '../actions-CP1d4Ye1.js';
11
13
  import '@elastic/apm-rum';
@@ -40,11 +42,40 @@ interface UseAlquimiaConfig {
40
42
  source?: 'client' | 'agent';
41
43
  };
42
44
  }
45
+ /**
46
+ * A human-approval gate as the hook exposes it: what the worklog says, overlaid
47
+ * with this client's own submission.
48
+ */
49
+ interface ToolApproval extends ApprovalRequest {
50
+ /**
51
+ * `pending` until someone decides; `submitting` while this client's call is
52
+ * in flight; `approved`/`rejected` once the runtime accepted a decision —
53
+ * this client's (optimistically, before the stream confirms) or anyone's.
54
+ */
55
+ status: 'pending' | 'submitting' | 'approved' | 'rejected';
56
+ /** Rejection reason, from the worklog or from this client's submission. */
57
+ reason: string | null;
58
+ /** `true` when the accepted decision was made from this client. */
59
+ decidedHere: boolean;
60
+ /**
61
+ * Whether offering Approve / Reject here can still work. `false` for a
62
+ * channel-owned gate, and after the runtime refused this client with a 409,
63
+ * 403 or 404 — retrying those can only fail the same way.
64
+ */
65
+ canAnswer: boolean;
66
+ /** The runtime's answer to this client's last submission, if any. */
67
+ lastResult: ToolApprovalResult | null;
68
+ /** Message count the gate was raised after, for anchoring it in the chat. */
69
+ afterCount: number;
70
+ }
43
71
  declare function useAlquimia(config: UseAlquimiaConfig): {
44
72
  sdk: AlquimiaSDK;
45
73
  activeTool: any;
46
74
  pendingClientTool: PendingClientTool | null;
47
75
  completeToolExecution: (controlId: string, result: ToolExecutionResponse) => Promise<void>;
76
+ toolApprovals: ToolApproval[];
77
+ pendingApprovals: ToolApproval[];
78
+ approveTool: (controlId: string, approved: boolean, message?: string) => Promise<ToolApprovalResult>;
48
79
  genui: {
49
80
  surfaces: GenuiSurfaceItem[];
50
81
  onAgentAction: (controlId: string) => (action: UIAction) => void;
@@ -105,4 +136,4 @@ declare function useRatings({ assistantId, sendRating, topicId, onError }: UseRa
105
136
  hasReviewed: boolean;
106
137
  };
107
138
 
108
- export { useAlquimia, useRatings };
139
+ export { type ToolApproval, type UseAlquimiaConfig, useAlquimia, useRatings };
@@ -51,6 +51,9 @@ var AlquimiaSDK = class {
51
51
  // Registry version tag resolved by the runtime for this task (v0.5.1), so
52
52
  // the tool-completion half of the turn lands on the same agentspace version.
53
53
  this.versionTag = null;
54
+ // Agentspace the runtime resolved for this task. Tool completion and
55
+ // approval authorize a non-admin caller against it, so it is echoed back.
56
+ this.agentspaceId = null;
54
57
  this.tools = [];
55
58
  this.extraInstructions = null;
56
59
  this.assistantConfig = null;
@@ -202,6 +205,7 @@ var AlquimiaSDK = class {
202
205
  this.traceParent = opts.traceParent ?? null;
203
206
  this.streamId = result.taskid ?? null;
204
207
  this.versionTag = result.versiontag ?? this.versionTag;
208
+ this.agentspaceId = result.agentspaceid ?? null;
205
209
  this.attachmentResponses = result.attachments ?? [];
206
210
  this.attachments = [];
207
211
  return this;
@@ -221,17 +225,37 @@ var AlquimiaSDK = class {
221
225
  "AlquimiaSDK: adapter does not support tool completion (resolveToolCompletionUrl missing)"
222
226
  );
223
227
  }
224
- const adapterHeaders = this.adapter.getHeaders?.() ?? {};
225
- const headers = {
226
- "Content-Type": "application/json",
227
- assistantid: this.assistantId,
228
- ...this.streamId && { taskid: this.streamId },
229
- ...this.conversationId && { sessionid: this.conversationId },
230
- ...this.userId && { userid: this.userId },
231
- ...this.versionTag && { versiontag: this.versionTag },
232
- ...adapterHeaders
228
+ await this.axiosInstance.post(url, response, { headers: this.buildDecisionHeaders() });
229
+ }
230
+ /**
231
+ * Answer a `HumanApprovalRequired` gate: POSTs a {@link ToolApprovalEvent} to
232
+ * the runtime's `/event/tool-approval`. `controlId` is the gate's own control
233
+ * id (the `HumanApprovalRequired` record's), not the parked tool call's.
234
+ *
235
+ * Never throws for a runtime refusal — each 409 means something different to
236
+ * the UI, so the answer comes back classified as a {@link ToolApprovalResult}.
237
+ * It does throw when the adapter has no approval route, which is a wiring bug
238
+ * rather than a runtime answer.
239
+ */
240
+ async submitToolApproval(controlId, approved, message) {
241
+ const url = this.adapter.resolveToolApprovalUrl?.();
242
+ if (!url) {
243
+ throw new Error(
244
+ "AlquimiaSDK: adapter does not support tool approval (resolveToolApprovalUrl missing)"
245
+ );
246
+ }
247
+ const event = {
248
+ event_class: "ToolApprovalEvent",
249
+ control_id: controlId,
250
+ result: approved,
251
+ ...message && { message }
233
252
  };
234
- await this.axiosInstance.post(url, response, { headers });
253
+ try {
254
+ await this.axiosInstance.post(url, event, { headers: this.buildDecisionHeaders() });
255
+ return { outcome: "accepted" };
256
+ } catch (error) {
257
+ return classifyApprovalError(error);
258
+ }
235
259
  }
236
260
  async generateImage(query) {
237
261
  if (!this.stableDiffusionProvider) {
@@ -302,6 +326,23 @@ var AlquimiaSDK = class {
302
326
  if (this.streamId) headers["task-id"] = this.streamId;
303
327
  return headers;
304
328
  }
329
+ /**
330
+ * Correlation headers for tool-completion and tool-approval: the runtime's
331
+ * `CommonAttributes` header aliases, with no separator. Not interchangeable
332
+ * with the kebab-case set in {@link buildIdentityHeaders}.
333
+ */
334
+ buildDecisionHeaders() {
335
+ return {
336
+ "Content-Type": "application/json",
337
+ assistantid: this.assistantId,
338
+ ...this.streamId && { taskid: this.streamId },
339
+ ...this.conversationId && { sessionid: this.conversationId },
340
+ ...this.userId && { userid: this.userId },
341
+ ...this.agentspaceId && { agentspaceid: this.agentspaceId },
342
+ ...this.versionTag && { versiontag: this.versionTag },
343
+ ...this.adapter.getHeaders?.() ?? {}
344
+ };
345
+ }
305
346
  getUrlStream() {
306
347
  if (!this.streamId) {
307
348
  throw new Error("Stream ID not initialized. Call sendMessage() first");
@@ -322,7 +363,29 @@ var AlquimiaSDK = class {
322
363
  getVersionTag() {
323
364
  return this.versionTag;
324
365
  }
366
+ /** Agentspace the runtime resolved for the current task, if any. */
367
+ getAgentspaceId() {
368
+ return this.agentspaceId;
369
+ }
325
370
  };
371
+ function classifyApprovalError(error) {
372
+ const response = error?.response;
373
+ const status = response?.status;
374
+ const data = response?.data;
375
+ const rawDetail = typeof data === "string" ? data : data?.detail ?? data?.error ?? error?.message;
376
+ const detail = typeof rawDetail === "string" ? rawDetail : JSON.stringify(rawDetail ?? "");
377
+ let outcome = "error";
378
+ if (status === 409) {
379
+ if (/original channel/i.test(detail)) outcome = "wrong-channel";
380
+ else if (/already been recorded/i.test(detail)) outcome = "already-decided";
381
+ else if (/no pending approval/i.test(detail)) outcome = "not-pending";
382
+ } else if (status === 401 || status === 403) {
383
+ outcome = "forbidden";
384
+ } else if (status === 404) {
385
+ outcome = "not-found";
386
+ }
387
+ return { outcome, ...status !== void 0 && { status }, detail };
388
+ }
326
389
  var alquimia_sdk_default = AlquimiaSDK;
327
390
 
328
391
  // src/utils/utils.ts
@@ -426,6 +489,10 @@ var agentTitle = (r) => {
426
489
  return typeof name === "string" && name.length > 0 ? `Agent: ${name}` : "Agent-to-agent";
427
490
  };
428
491
  var constTitle = (label) => () => label;
492
+ var approvalTitle = (r) => {
493
+ const name = asRecord(asRecord(r.data).parameters).name;
494
+ return typeof name === "string" && name.length > 0 ? `Approval required: ${name}` : "Human approval required";
495
+ };
429
496
  var shieldBlockTitle = (r) => {
430
497
  const id = asRecord(r.data).shield_id;
431
498
  return typeof id === "string" && id.length > 0 ? `Blocked by ${id}` : "Blocked by safeguard";
@@ -477,7 +544,7 @@ var EVENT_REGISTRY = {
477
544
  HumanApprovalRequired: {
478
545
  kind: "tool",
479
546
  role: "open",
480
- title: constTitle("Human approval required"),
547
+ title: approvalTitle,
481
548
  pending: true
482
549
  },
483
550
  HumanApprovalRequiredResponse: { kind: "tool", role: "close", title: constTitle("Human approval") }
@@ -566,7 +633,12 @@ function reduceWorklog(state, record) {
566
633
  const pos = Object.prototype.hasOwnProperty.call(next.index, key) ? next.index[key] : void 0;
567
634
  if (pos != null) {
568
635
  const existing = next.nodes[pos];
569
- next.nodes[pos] = { ...existing, raw: [...existing.raw, record] };
636
+ next.nodes[pos] = {
637
+ ...existing,
638
+ command: existing.command ?? record.data,
639
+ title: existing.command === void 0 ? interp.title(record) : existing.title,
640
+ raw: [...existing.raw, record]
641
+ };
570
642
  } else {
571
643
  next.index[key] = next.nodes.length;
572
644
  next.nodes.push(makeOpenNode(record, interp, key));
@@ -591,6 +663,59 @@ function frameToRecord(frame) {
591
663
  };
592
664
  }
593
665
 
666
+ // src/worklog/approvals.ts
667
+ function initialApprovalsState() {
668
+ return { order: [], byId: {} };
669
+ }
670
+ var asObject = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
671
+ var MCP_APPROVAL_EXTENSION = "mcp_approval";
672
+ function describeRequest(record) {
673
+ const execution = asObject(asObject(record.data)?.parameters) ?? {};
674
+ const name = execution.name;
675
+ const callId = execution.control_id;
676
+ const args = execution.tool_call_parameters !== void 0 && execution.tool_call_parameters !== null ? execution.tool_call_parameters : execution.parameters;
677
+ const extensions = asObject(execution.parameters)?.runtime_extensions;
678
+ return {
679
+ toolName: typeof name === "string" && name.length > 0 ? name : null,
680
+ toolCallId: typeof callId === "string" ? callId : null,
681
+ arguments: args ?? null,
682
+ channelOwned: Array.isArray(extensions) && extensions.includes(MCP_APPROVAL_EXTENSION)
683
+ };
684
+ }
685
+ function describeDecision(record) {
686
+ const approved = record.data === true;
687
+ return {
688
+ approved,
689
+ reason: !approved && typeof record.data === "string" && record.data.length > 0 ? record.data : null,
690
+ decidedAt: record.date_submitted
691
+ };
692
+ }
693
+ var blank = (controlId) => ({
694
+ controlId,
695
+ toolName: null,
696
+ toolCallId: null,
697
+ arguments: null,
698
+ channelOwned: false,
699
+ requestedAt: null,
700
+ decision: null
701
+ });
702
+ function reduceApprovals(state, record) {
703
+ const isRequest = record.event_class === "HumanApprovalRequired";
704
+ const isResponse = record.event_class === "HumanApprovalRequiredResponse";
705
+ if (!isRequest && !isResponse || !record.control_id) return state;
706
+ const id = record.control_id;
707
+ const known = state.byId[id];
708
+ const current = known ?? blank(id);
709
+ const next = isRequest ? { ...current, ...describeRequest(record), requestedAt: record.date_submitted } : { ...current, decision: current.decision ?? describeDecision(record) };
710
+ return {
711
+ order: known ? state.order : [...state.order, id],
712
+ byId: { ...state.byId, [id]: next }
713
+ };
714
+ }
715
+ function listApprovals(state) {
716
+ return state.order.map((id) => state.byId[id]);
717
+ }
718
+
594
719
  // src/hooks/pending-client-tool.ts
595
720
  function extractPendingClientTool(frame) {
596
721
  if (!frame || typeof frame !== "object") return null;
@@ -953,6 +1078,7 @@ function buildGenuiClause(manifest, allowlist) {
953
1078
  var import_zod_to_json_schema = require("zod-to-json-schema");
954
1079
 
955
1080
  // src/hooks/alquimia.hook.tsx
1081
+ var TERMINAL_REFUSALS = /* @__PURE__ */ new Set(["already-decided", "wrong-channel", "not-pending", "not-found", "forbidden"]);
956
1082
  function useAlquimia(config) {
957
1083
  const sdk = (0, import_react2.useMemo)(() => {
958
1084
  const instance = new alquimia_sdk_default(config.assistantId, config.adapter, {
@@ -997,6 +1123,12 @@ function useAlquimia(config) {
997
1123
  const [isUploadingAttachments, setIsUploadingAttachments] = (0, import_react2.useState)(false);
998
1124
  const [pendingClientTool, setPendingClientTool] = (0, import_react2.useState)(null);
999
1125
  const completedToolsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
1126
+ const [approvalsState, setApprovalsState] = (0, import_react2.useState)(initialApprovalsState);
1127
+ const approvalsStateRef = (0, import_react2.useRef)(approvalsState);
1128
+ approvalsStateRef.current = approvalsState;
1129
+ const [approvalSubmissions, setApprovalSubmissions] = (0, import_react2.useState)({});
1130
+ const approvalCallsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1131
+ const approvalAnchorsRef = (0, import_react2.useRef)({});
1000
1132
  const genuiCatalog = config.genui?.catalog ?? coreCatalog;
1001
1133
  const [genuiSurfaces, setGenuiSurfaces] = (0, import_react2.useState)([]);
1002
1134
  const messagesLenRef = (0, import_react2.useRef)(0);
@@ -1279,6 +1411,14 @@ function useAlquimia(config) {
1279
1411
  if (worklogEnabled) {
1280
1412
  setWorklog((prev) => reduceWorklog(prev ?? initialWorklogState(), frameToRecord(data)));
1281
1413
  }
1414
+ if ((data.event_class === "HumanApprovalRequired" || data.event_class === "HumanApprovalRequiredResponse") && data.control_id) {
1415
+ if (!(data.control_id in approvalAnchorsRef.current)) {
1416
+ approvalAnchorsRef.current[data.control_id] = genuiAnchorRef.current;
1417
+ }
1418
+ const record = frameToRecord(data);
1419
+ setApprovalsState((prev) => reduceApprovals(prev, record));
1420
+ return;
1421
+ }
1282
1422
  const pending = extractPendingClientTool(data);
1283
1423
  if (pending) {
1284
1424
  setPendingClientTool(pending);
@@ -1327,6 +1467,59 @@ function useAlquimia(config) {
1327
1467
  },
1328
1468
  [sdk]
1329
1469
  );
1470
+ const approveTool = (0, import_react2.useCallback)(
1471
+ (controlId, approved, message) => {
1472
+ const existing = approvalCallsRef.current.get(controlId);
1473
+ if (existing) return existing;
1474
+ if (approvalsStateRef.current.byId[controlId]?.decision) {
1475
+ return Promise.resolve({ outcome: "already-decided" });
1476
+ }
1477
+ const reason = message?.trim() || null;
1478
+ setApprovalSubmissions((prev) => ({
1479
+ ...prev,
1480
+ [controlId]: { inFlight: true, approved, reason, result: prev[controlId]?.result ?? null }
1481
+ }));
1482
+ const call = sdk.submitToolApproval(controlId, approved, reason ?? void 0).catch((error) => ({
1483
+ outcome: "error",
1484
+ detail: error instanceof Error ? error.message : String(error)
1485
+ })).then((result) => {
1486
+ if (result.outcome !== "accepted") approvalCallsRef.current.delete(controlId);
1487
+ setApprovalSubmissions((prev) => ({
1488
+ ...prev,
1489
+ [controlId]: { inFlight: false, approved, reason, result }
1490
+ }));
1491
+ return result;
1492
+ });
1493
+ approvalCallsRef.current.set(controlId, call);
1494
+ return call;
1495
+ },
1496
+ [sdk]
1497
+ );
1498
+ const toolApprovals = (0, import_react2.useMemo)(
1499
+ () => listApprovals(approvalsState).map((gate) => {
1500
+ const sub = approvalSubmissions[gate.controlId];
1501
+ const acceptedHere = sub?.result?.outcome === "accepted";
1502
+ const refused = !!sub?.result && TERMINAL_REFUSALS.has(sub.result.outcome);
1503
+ let status = "pending";
1504
+ if (gate.decision) status = gate.decision.approved ? "approved" : "rejected";
1505
+ else if (sub?.inFlight) status = "submitting";
1506
+ else if (acceptedHere) status = sub.approved ? "approved" : "rejected";
1507
+ return {
1508
+ ...gate,
1509
+ status,
1510
+ reason: gate.decision?.reason ?? (acceptedHere && !sub.approved ? sub.reason : null),
1511
+ decidedHere: acceptedHere && (gate.decision ? gate.decision.approved === sub.approved : true),
1512
+ canAnswer: status === "pending" && !gate.channelOwned && !refused,
1513
+ lastResult: sub?.result ?? null,
1514
+ afterCount: approvalAnchorsRef.current[gate.controlId] ?? 0
1515
+ };
1516
+ }),
1517
+ [approvalsState, approvalSubmissions]
1518
+ );
1519
+ const pendingApprovals2 = (0, import_react2.useMemo)(
1520
+ () => toolApprovals.filter((a) => a.status === "pending" || a.status === "submitting"),
1521
+ [toolApprovals]
1522
+ );
1330
1523
  (0, import_react2.useEffect)(() => {
1331
1524
  if (!config.genui) return;
1332
1525
  if (!pendingClientTool || pendingClientTool.toolName !== "render_ui") return;
@@ -1412,6 +1605,9 @@ function useAlquimia(config) {
1412
1605
  activeTool,
1413
1606
  pendingClientTool,
1414
1607
  completeToolExecution,
1608
+ toolApprovals,
1609
+ pendingApprovals: pendingApprovals2,
1610
+ approveTool,
1415
1611
  genui,
1416
1612
  cleanMessages,
1417
1613
  createMessageId,