@alquimia-ai/tools 2.5.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 (67) hide show
  1. package/README.md +603 -23
  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 +269 -17
  24. package/dist/hooks/index.js.map +1 -1
  25. package/dist/hooks/index.mjs +269 -17
  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 -37
  30. package/dist/next/index.js.map +1 -1
  31. package/dist/next/index.mjs +56 -37
  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-b5_yMyMo.d.ts → providers-J_Z9rQ3Q.d.ts} +1 -1
  36. package/dist/{providers-DiR9cgyo.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 -37
  40. package/dist/proxy.js.map +1 -1
  41. package/dist/proxy.mjs +47 -37
  42. package/dist/proxy.mjs.map +1 -1
  43. package/dist/sdk/index.d.mts +60 -5
  44. package/dist/sdk/index.d.ts +60 -5
  45. package/dist/sdk/index.js +115 -15
  46. package/dist/sdk/index.js.map +1 -1
  47. package/dist/sdk/index.mjs +115 -15
  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-xksRnBUW.d.ts → type-BFtGEEb4.d.ts} +45 -1
  52. package/dist/{type-DqX3-9QS.d.mts → type-C_sKpIM1.d.mts} +45 -1
  53. package/dist/types/index.d.mts +28 -5
  54. package/dist/types/index.d.ts +28 -5
  55. package/dist/types/index.js.map +1 -1
  56. package/dist/types/index.mjs.map +1 -1
  57. package/dist/{types-C-6fgK1_.d.mts → types-lOliroPw.d.mts} +1 -1
  58. package/dist/{types-C-6fgK1_.d.ts → types-lOliroPw.d.ts} +1 -1
  59. package/dist/utils/index.d.mts +1 -1
  60. package/dist/utils/index.d.ts +1 -1
  61. package/dist/worklog/index.d.mts +3 -2
  62. package/dist/worklog/index.d.ts +3 -2
  63. package/dist/worklog/index.js +94 -2
  64. package/dist/worklog/index.js.map +1 -1
  65. package/dist/worklog/index.mjs +94 -2
  66. package/dist/worklog/index.mjs.map +1 -1
  67. package/package.json +1 -1
@@ -48,6 +48,12 @@ var AlquimiaSDK = class {
48
48
  this.streamId = null;
49
49
  // W3C traceparent for the current turn, so infer and stream share one trace.
50
50
  this.traceParent = null;
51
+ // Registry version tag resolved by the runtime for this task (v0.5.1), so
52
+ // the tool-completion half of the turn lands on the same agentspace version.
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;
51
57
  this.tools = [];
52
58
  this.extraInstructions = null;
53
59
  this.assistantConfig = null;
@@ -128,6 +134,19 @@ var AlquimiaSDK = class {
128
134
  this.userId = userId;
129
135
  return this;
130
136
  }
137
+ /**
138
+ * Pin inference to a cached registry version. Sent as the `version_tag`
139
+ * query parameter, which `POST /event/infer/{assistant_id}` accepts from
140
+ * **runtime v0.5.2** onward; older runtimes ignore it.
141
+ *
142
+ * Works on every version: pass a versioned assistant ref
143
+ * (`<agentspace>/<assistant>[:<tag>]`) as the `assistantId` instead — the
144
+ * runtime parses the tag out of the path itself.
145
+ */
146
+ withVersionTag(versionTag) {
147
+ this.versionTag = versionTag;
148
+ return this;
149
+ }
131
150
  getEnforceCharacterization() {
132
151
  return this.enforceCharacterization ?? true;
133
152
  }
@@ -143,19 +162,29 @@ var AlquimiaSDK = class {
143
162
  }
144
163
  return this.whisperProvider.speechToText(audio);
145
164
  }
146
- async sendMessage(query, traceParent) {
165
+ async sendMessage(query, options = {}) {
147
166
  if (!this.conversationId) {
148
167
  throw new Error(
149
168
  "Conversation not initialized. Call withConversationId() before sendMessage()"
150
169
  );
151
170
  }
152
- const inferUrl = this.adapter.resolveInferUrl(this.assistantId);
171
+ const opts = typeof options === "string" ? { traceParent: options } : options;
172
+ if (query === void 0 && !opts.inputAudio) {
173
+ throw new Error(
174
+ "AlquimiaSDK: sendMessage needs a query, an inputAudio blob, or both"
175
+ );
176
+ }
177
+ const baseInferUrl = this.adapter.resolveInferUrl(this.assistantId);
178
+ const inferUrl = this.versionTag ? `${baseInferUrl}${baseInferUrl.includes("?") ? "&" : "?"}version_tag=${encodeURIComponent(this.versionTag)}` : baseInferUrl;
153
179
  const adapterHeaders = this.adapter.getHeaders?.() ?? {};
154
180
  const hasClientTools = Array.isArray(this.tools) && this.tools.length > 0;
155
181
  const initMessage = {
156
- query,
182
+ ...query !== void 0 && { query },
157
183
  session_id: this.conversationId,
158
184
  user_id: this.userId,
185
+ ...opts.inputAudio && { input_audio: opts.inputAudio },
186
+ // Only sent when set: the runtime defaults `output_audio` to true.
187
+ ...opts.outputAudio !== void 0 && { output_audio: opts.outputAudio },
159
188
  ...this.extraInstructions && { extra_instructions: this.extraInstructions },
160
189
  ...this.assistantConfig && { config: this.assistantConfig },
161
190
  ...hasClientTools && {
@@ -168,13 +197,15 @@ var AlquimiaSDK = class {
168
197
  const result = (await this.axiosInstance.post(inferUrl, initMessage, {
169
198
  headers: {
170
199
  "Content-Type": "application/json",
171
- ...traceParent && { "x-trace-parent": traceParent },
200
+ ...opts.traceParent && { "x-trace-parent": opts.traceParent },
172
201
  ...adapterHeaders,
173
202
  ...this.buildIdentityHeaders()
174
203
  }
175
204
  })).data;
176
- this.traceParent = traceParent ?? null;
205
+ this.traceParent = opts.traceParent ?? null;
177
206
  this.streamId = result.taskid ?? null;
207
+ this.versionTag = result.versiontag ?? this.versionTag;
208
+ this.agentspaceId = result.agentspaceid ?? null;
178
209
  this.attachmentResponses = result.attachments ?? [];
179
210
  this.attachments = [];
180
211
  return this;
@@ -194,16 +225,37 @@ var AlquimiaSDK = class {
194
225
  "AlquimiaSDK: adapter does not support tool completion (resolveToolCompletionUrl missing)"
195
226
  );
196
227
  }
197
- const adapterHeaders = this.adapter.getHeaders?.() ?? {};
198
- const headers = {
199
- "Content-Type": "application/json",
200
- assistantid: this.assistantId,
201
- ...this.streamId && { taskid: this.streamId },
202
- ...this.conversationId && { sessionid: this.conversationId },
203
- ...this.userId && { userid: this.userId },
204
- ...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 }
205
252
  };
206
- 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
+ }
207
259
  }
208
260
  async generateImage(query) {
209
261
  if (!this.stableDiffusionProvider) {
@@ -243,18 +295,23 @@ var AlquimiaSDK = class {
243
295
  getAttachmentResponses() {
244
296
  return this.attachmentResponses;
245
297
  }
298
+ /**
299
+ * Upload a file into the session context. Returns the runtime {@link RuntimeBlob},
300
+ * which is what `sendMessage`'s `inputAudio` expects for audio inference.
301
+ */
246
302
  async uploadAttachment(file, _attachmentId) {
247
303
  const url = this.adapter.resolveBlobUploadUrl();
248
304
  const adapterHeaders = this.adapter.getHeaders?.() ?? {};
249
305
  const formData = new FormData();
250
306
  formData.append("file", file);
251
- await this.axiosInstance.post(url, formData, {
307
+ const response = await this.axiosInstance.post(url, formData, {
252
308
  headers: {
253
309
  "Content-Type": "multipart/form-data",
254
310
  ...adapterHeaders,
255
311
  ...this.buildIdentityHeaders()
256
312
  }
257
313
  });
314
+ return response.data;
258
315
  }
259
316
  /**
260
317
  * Identity headers (kebab-case) required by `/context/blob/upload` and
@@ -269,6 +326,23 @@ var AlquimiaSDK = class {
269
326
  if (this.streamId) headers["task-id"] = this.streamId;
270
327
  return headers;
271
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
+ }
272
346
  getUrlStream() {
273
347
  if (!this.streamId) {
274
348
  throw new Error("Stream ID not initialized. Call sendMessage() first");
@@ -285,7 +359,33 @@ var AlquimiaSDK = class {
285
359
  getTaskId() {
286
360
  return this.streamId;
287
361
  }
362
+ /** Registry version tag resolved by the runtime for the current task, if any. */
363
+ getVersionTag() {
364
+ return this.versionTag;
365
+ }
366
+ /** Agentspace the runtime resolved for the current task, if any. */
367
+ getAgentspaceId() {
368
+ return this.agentspaceId;
369
+ }
288
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
+ }
289
389
  var alquimia_sdk_default = AlquimiaSDK;
290
390
 
291
391
  // src/utils/utils.ts
@@ -389,6 +489,18 @@ var agentTitle = (r) => {
389
489
  return typeof name === "string" && name.length > 0 ? `Agent: ${name}` : "Agent-to-agent";
390
490
  };
391
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
+ };
496
+ var shieldBlockTitle = (r) => {
497
+ const id = asRecord(r.data).shield_id;
498
+ return typeof id === "string" && id.length > 0 ? `Blocked by ${id}` : "Blocked by safeguard";
499
+ };
500
+ var knowledgeTitle = (r) => {
501
+ const query = asRecord(r.data).query;
502
+ return typeof query === "string" && query.length > 0 ? `Knowledge: ${query}` : "Knowledge retrieval";
503
+ };
392
504
  var EVENT_REGISTRY = {
393
505
  // Run envelope
394
506
  AssistantInference: { kind: "answer", role: "run-open", title: constTitle("Inference started") },
@@ -396,6 +508,9 @@ var EVENT_REGISTRY = {
396
508
  // Safeguard
397
509
  ShieldInference: { kind: "safeguard", role: "open", title: constTitle("Safeguard") },
398
510
  ShieldInferenceResponse: { kind: "safeguard", role: "close", title: constTitle("Safeguard") },
511
+ // Emitted when a shield enforcement decision blocks the request (core 0.5.x).
512
+ // Response-only: the reducer opens it already closed when no command matched.
513
+ ShieldBlockedResponse: { kind: "safeguard", role: "close", title: shieldBlockTitle },
399
514
  // Reasoning
400
515
  ResponseInference: { kind: "reasoning", role: "open", title: constTitle("Reasoning") },
401
516
  ResponseInferenceResponse: { kind: "reasoning", role: "close", title: constTitle("Reasoning") },
@@ -413,6 +528,14 @@ var EVENT_REGISTRY = {
413
528
  A2AInference: { kind: "a2a", role: "open", title: agentTitle },
414
529
  AgentDiscovery: { kind: "a2a", role: "open", title: constTitle("Agent discovery") },
415
530
  AgentDiscoveryResponse: { kind: "a2a", role: "close", title: constTitle("Agent discovery") },
531
+ // Knowledge retrieval (RAG) — core 0.5.x
532
+ KnowledgeRetrieval: { kind: "knowledge", role: "open", title: knowledgeTitle },
533
+ KnowledgeRetrievalResponse: { kind: "knowledge", role: "close", title: knowledgeTitle },
534
+ // Speech pipeline — core 0.5.x
535
+ SpeechTranscription: { kind: "speech", role: "open", title: constTitle("Speech-to-text") },
536
+ SpeechTranscriptionResponse: { kind: "speech", role: "close", title: constTitle("Speech-to-text") },
537
+ SpeechSynthesis: { kind: "speech", role: "open", title: constTitle("Text-to-speech") },
538
+ SpeechSynthesisResponse: { kind: "speech", role: "close", title: constTitle("Text-to-speech") },
416
539
  // Memory / persistence
417
540
  ContextPersistence: { kind: "memory", role: "open", title: constTitle("Memory persistence") },
418
541
  ContextFlush: { kind: "memory", role: "open", title: constTitle("Memory flush") },
@@ -421,7 +544,7 @@ var EVENT_REGISTRY = {
421
544
  HumanApprovalRequired: {
422
545
  kind: "tool",
423
546
  role: "open",
424
- title: constTitle("Human approval required"),
547
+ title: approvalTitle,
425
548
  pending: true
426
549
  },
427
550
  HumanApprovalRequiredResponse: { kind: "tool", role: "close", title: constTitle("Human approval") }
@@ -510,7 +633,12 @@ function reduceWorklog(state, record) {
510
633
  const pos = Object.prototype.hasOwnProperty.call(next.index, key) ? next.index[key] : void 0;
511
634
  if (pos != null) {
512
635
  const existing = next.nodes[pos];
513
- 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
+ };
514
642
  } else {
515
643
  next.index[key] = next.nodes.length;
516
644
  next.nodes.push(makeOpenNode(record, interp, key));
@@ -535,6 +663,59 @@ function frameToRecord(frame) {
535
663
  };
536
664
  }
537
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
+
538
719
  // src/hooks/pending-client-tool.ts
539
720
  function extractPendingClientTool(frame) {
540
721
  if (!frame || typeof frame !== "object") return null;
@@ -897,6 +1078,7 @@ function buildGenuiClause(manifest, allowlist) {
897
1078
  var import_zod_to_json_schema = require("zod-to-json-schema");
898
1079
 
899
1080
  // src/hooks/alquimia.hook.tsx
1081
+ var TERMINAL_REFUSALS = /* @__PURE__ */ new Set(["already-decided", "wrong-channel", "not-pending", "not-found", "forbidden"]);
900
1082
  function useAlquimia(config) {
901
1083
  const sdk = (0, import_react2.useMemo)(() => {
902
1084
  const instance = new alquimia_sdk_default(config.assistantId, config.adapter, {
@@ -941,6 +1123,12 @@ function useAlquimia(config) {
941
1123
  const [isUploadingAttachments, setIsUploadingAttachments] = (0, import_react2.useState)(false);
942
1124
  const [pendingClientTool, setPendingClientTool] = (0, import_react2.useState)(null);
943
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)({});
944
1132
  const genuiCatalog = config.genui?.catalog ?? coreCatalog;
945
1133
  const [genuiSurfaces, setGenuiSurfaces] = (0, import_react2.useState)([]);
946
1134
  const messagesLenRef = (0, import_react2.useRef)(0);
@@ -1223,6 +1411,14 @@ function useAlquimia(config) {
1223
1411
  if (worklogEnabled) {
1224
1412
  setWorklog((prev) => reduceWorklog(prev ?? initialWorklogState(), frameToRecord(data)));
1225
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
+ }
1226
1422
  const pending = extractPendingClientTool(data);
1227
1423
  if (pending) {
1228
1424
  setPendingClientTool(pending);
@@ -1271,6 +1467,59 @@ function useAlquimia(config) {
1271
1467
  },
1272
1468
  [sdk]
1273
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
+ );
1274
1523
  (0, import_react2.useEffect)(() => {
1275
1524
  if (!config.genui) return;
1276
1525
  if (!pendingClientTool || pendingClientTool.toolName !== "render_ui") return;
@@ -1356,6 +1605,9 @@ function useAlquimia(config) {
1356
1605
  activeTool,
1357
1606
  pendingClientTool,
1358
1607
  completeToolExecution,
1608
+ toolApprovals,
1609
+ pendingApprovals: pendingApprovals2,
1610
+ approveTool,
1359
1611
  genui,
1360
1612
  cleanMessages,
1361
1613
  createMessageId,