@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
@@ -11,6 +11,12 @@ var AlquimiaSDK = class {
11
11
  this.streamId = null;
12
12
  // W3C traceparent for the current turn, so infer and stream share one trace.
13
13
  this.traceParent = null;
14
+ // Registry version tag resolved by the runtime for this task (v0.5.1), so
15
+ // the tool-completion half of the turn lands on the same agentspace version.
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;
14
20
  this.tools = [];
15
21
  this.extraInstructions = null;
16
22
  this.assistantConfig = null;
@@ -91,6 +97,19 @@ var AlquimiaSDK = class {
91
97
  this.userId = userId;
92
98
  return this;
93
99
  }
100
+ /**
101
+ * Pin inference to a cached registry version. Sent as the `version_tag`
102
+ * query parameter, which `POST /event/infer/{assistant_id}` accepts from
103
+ * **runtime v0.5.2** onward; older runtimes ignore it.
104
+ *
105
+ * Works on every version: pass a versioned assistant ref
106
+ * (`<agentspace>/<assistant>[:<tag>]`) as the `assistantId` instead — the
107
+ * runtime parses the tag out of the path itself.
108
+ */
109
+ withVersionTag(versionTag) {
110
+ this.versionTag = versionTag;
111
+ return this;
112
+ }
94
113
  getEnforceCharacterization() {
95
114
  return this.enforceCharacterization ?? true;
96
115
  }
@@ -106,19 +125,29 @@ var AlquimiaSDK = class {
106
125
  }
107
126
  return this.whisperProvider.speechToText(audio);
108
127
  }
109
- async sendMessage(query, traceParent) {
128
+ async sendMessage(query, options = {}) {
110
129
  if (!this.conversationId) {
111
130
  throw new Error(
112
131
  "Conversation not initialized. Call withConversationId() before sendMessage()"
113
132
  );
114
133
  }
115
- const inferUrl = this.adapter.resolveInferUrl(this.assistantId);
134
+ const opts = typeof options === "string" ? { traceParent: options } : options;
135
+ if (query === void 0 && !opts.inputAudio) {
136
+ throw new Error(
137
+ "AlquimiaSDK: sendMessage needs a query, an inputAudio blob, or both"
138
+ );
139
+ }
140
+ const baseInferUrl = this.adapter.resolveInferUrl(this.assistantId);
141
+ const inferUrl = this.versionTag ? `${baseInferUrl}${baseInferUrl.includes("?") ? "&" : "?"}version_tag=${encodeURIComponent(this.versionTag)}` : baseInferUrl;
116
142
  const adapterHeaders = this.adapter.getHeaders?.() ?? {};
117
143
  const hasClientTools = Array.isArray(this.tools) && this.tools.length > 0;
118
144
  const initMessage = {
119
- query,
145
+ ...query !== void 0 && { query },
120
146
  session_id: this.conversationId,
121
147
  user_id: this.userId,
148
+ ...opts.inputAudio && { input_audio: opts.inputAudio },
149
+ // Only sent when set: the runtime defaults `output_audio` to true.
150
+ ...opts.outputAudio !== void 0 && { output_audio: opts.outputAudio },
122
151
  ...this.extraInstructions && { extra_instructions: this.extraInstructions },
123
152
  ...this.assistantConfig && { config: this.assistantConfig },
124
153
  ...hasClientTools && {
@@ -131,13 +160,15 @@ var AlquimiaSDK = class {
131
160
  const result = (await this.axiosInstance.post(inferUrl, initMessage, {
132
161
  headers: {
133
162
  "Content-Type": "application/json",
134
- ...traceParent && { "x-trace-parent": traceParent },
163
+ ...opts.traceParent && { "x-trace-parent": opts.traceParent },
135
164
  ...adapterHeaders,
136
165
  ...this.buildIdentityHeaders()
137
166
  }
138
167
  })).data;
139
- this.traceParent = traceParent ?? null;
168
+ this.traceParent = opts.traceParent ?? null;
140
169
  this.streamId = result.taskid ?? null;
170
+ this.versionTag = result.versiontag ?? this.versionTag;
171
+ this.agentspaceId = result.agentspaceid ?? null;
141
172
  this.attachmentResponses = result.attachments ?? [];
142
173
  this.attachments = [];
143
174
  return this;
@@ -157,16 +188,37 @@ var AlquimiaSDK = class {
157
188
  "AlquimiaSDK: adapter does not support tool completion (resolveToolCompletionUrl missing)"
158
189
  );
159
190
  }
160
- const adapterHeaders = this.adapter.getHeaders?.() ?? {};
161
- const headers = {
162
- "Content-Type": "application/json",
163
- assistantid: this.assistantId,
164
- ...this.streamId && { taskid: this.streamId },
165
- ...this.conversationId && { sessionid: this.conversationId },
166
- ...this.userId && { userid: this.userId },
167
- ...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 }
168
215
  };
169
- 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
+ }
170
222
  }
171
223
  async generateImage(query) {
172
224
  if (!this.stableDiffusionProvider) {
@@ -206,18 +258,23 @@ var AlquimiaSDK = class {
206
258
  getAttachmentResponses() {
207
259
  return this.attachmentResponses;
208
260
  }
261
+ /**
262
+ * Upload a file into the session context. Returns the runtime {@link RuntimeBlob},
263
+ * which is what `sendMessage`'s `inputAudio` expects for audio inference.
264
+ */
209
265
  async uploadAttachment(file, _attachmentId) {
210
266
  const url = this.adapter.resolveBlobUploadUrl();
211
267
  const adapterHeaders = this.adapter.getHeaders?.() ?? {};
212
268
  const formData = new FormData();
213
269
  formData.append("file", file);
214
- await this.axiosInstance.post(url, formData, {
270
+ const response = await this.axiosInstance.post(url, formData, {
215
271
  headers: {
216
272
  "Content-Type": "multipart/form-data",
217
273
  ...adapterHeaders,
218
274
  ...this.buildIdentityHeaders()
219
275
  }
220
276
  });
277
+ return response.data;
221
278
  }
222
279
  /**
223
280
  * Identity headers (kebab-case) required by `/context/blob/upload` and
@@ -232,6 +289,23 @@ var AlquimiaSDK = class {
232
289
  if (this.streamId) headers["task-id"] = this.streamId;
233
290
  return headers;
234
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
+ }
235
309
  getUrlStream() {
236
310
  if (!this.streamId) {
237
311
  throw new Error("Stream ID not initialized. Call sendMessage() first");
@@ -248,7 +322,33 @@ var AlquimiaSDK = class {
248
322
  getTaskId() {
249
323
  return this.streamId;
250
324
  }
325
+ /** Registry version tag resolved by the runtime for the current task, if any. */
326
+ getVersionTag() {
327
+ return this.versionTag;
328
+ }
329
+ /** Agentspace the runtime resolved for the current task, if any. */
330
+ getAgentspaceId() {
331
+ return this.agentspaceId;
332
+ }
251
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
+ }
252
352
  var alquimia_sdk_default = AlquimiaSDK;
253
353
 
254
354
  // src/utils/utils.ts
@@ -352,6 +452,18 @@ var agentTitle = (r) => {
352
452
  return typeof name === "string" && name.length > 0 ? `Agent: ${name}` : "Agent-to-agent";
353
453
  };
354
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
+ };
459
+ var shieldBlockTitle = (r) => {
460
+ const id = asRecord(r.data).shield_id;
461
+ return typeof id === "string" && id.length > 0 ? `Blocked by ${id}` : "Blocked by safeguard";
462
+ };
463
+ var knowledgeTitle = (r) => {
464
+ const query = asRecord(r.data).query;
465
+ return typeof query === "string" && query.length > 0 ? `Knowledge: ${query}` : "Knowledge retrieval";
466
+ };
355
467
  var EVENT_REGISTRY = {
356
468
  // Run envelope
357
469
  AssistantInference: { kind: "answer", role: "run-open", title: constTitle("Inference started") },
@@ -359,6 +471,9 @@ var EVENT_REGISTRY = {
359
471
  // Safeguard
360
472
  ShieldInference: { kind: "safeguard", role: "open", title: constTitle("Safeguard") },
361
473
  ShieldInferenceResponse: { kind: "safeguard", role: "close", title: constTitle("Safeguard") },
474
+ // Emitted when a shield enforcement decision blocks the request (core 0.5.x).
475
+ // Response-only: the reducer opens it already closed when no command matched.
476
+ ShieldBlockedResponse: { kind: "safeguard", role: "close", title: shieldBlockTitle },
362
477
  // Reasoning
363
478
  ResponseInference: { kind: "reasoning", role: "open", title: constTitle("Reasoning") },
364
479
  ResponseInferenceResponse: { kind: "reasoning", role: "close", title: constTitle("Reasoning") },
@@ -376,6 +491,14 @@ var EVENT_REGISTRY = {
376
491
  A2AInference: { kind: "a2a", role: "open", title: agentTitle },
377
492
  AgentDiscovery: { kind: "a2a", role: "open", title: constTitle("Agent discovery") },
378
493
  AgentDiscoveryResponse: { kind: "a2a", role: "close", title: constTitle("Agent discovery") },
494
+ // Knowledge retrieval (RAG) — core 0.5.x
495
+ KnowledgeRetrieval: { kind: "knowledge", role: "open", title: knowledgeTitle },
496
+ KnowledgeRetrievalResponse: { kind: "knowledge", role: "close", title: knowledgeTitle },
497
+ // Speech pipeline — core 0.5.x
498
+ SpeechTranscription: { kind: "speech", role: "open", title: constTitle("Speech-to-text") },
499
+ SpeechTranscriptionResponse: { kind: "speech", role: "close", title: constTitle("Speech-to-text") },
500
+ SpeechSynthesis: { kind: "speech", role: "open", title: constTitle("Text-to-speech") },
501
+ SpeechSynthesisResponse: { kind: "speech", role: "close", title: constTitle("Text-to-speech") },
379
502
  // Memory / persistence
380
503
  ContextPersistence: { kind: "memory", role: "open", title: constTitle("Memory persistence") },
381
504
  ContextFlush: { kind: "memory", role: "open", title: constTitle("Memory flush") },
@@ -384,7 +507,7 @@ var EVENT_REGISTRY = {
384
507
  HumanApprovalRequired: {
385
508
  kind: "tool",
386
509
  role: "open",
387
- title: constTitle("Human approval required"),
510
+ title: approvalTitle,
388
511
  pending: true
389
512
  },
390
513
  HumanApprovalRequiredResponse: { kind: "tool", role: "close", title: constTitle("Human approval") }
@@ -473,7 +596,12 @@ function reduceWorklog(state, record) {
473
596
  const pos = Object.prototype.hasOwnProperty.call(next.index, key) ? next.index[key] : void 0;
474
597
  if (pos != null) {
475
598
  const existing = next.nodes[pos];
476
- 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
+ };
477
605
  } else {
478
606
  next.index[key] = next.nodes.length;
479
607
  next.nodes.push(makeOpenNode(record, interp, key));
@@ -498,6 +626,59 @@ function frameToRecord(frame) {
498
626
  };
499
627
  }
500
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
+
501
682
  // src/hooks/pending-client-tool.ts
502
683
  function extractPendingClientTool(frame) {
503
684
  if (!frame || typeof frame !== "object") return null;
@@ -860,6 +1041,7 @@ function buildGenuiClause(manifest, allowlist) {
860
1041
  import { zodToJsonSchema } from "zod-to-json-schema";
861
1042
 
862
1043
  // src/hooks/alquimia.hook.tsx
1044
+ var TERMINAL_REFUSALS = /* @__PURE__ */ new Set(["already-decided", "wrong-channel", "not-pending", "not-found", "forbidden"]);
863
1045
  function useAlquimia(config) {
864
1046
  const sdk = useMemo(() => {
865
1047
  const instance = new alquimia_sdk_default(config.assistantId, config.adapter, {
@@ -904,6 +1086,12 @@ function useAlquimia(config) {
904
1086
  const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
905
1087
  const [pendingClientTool, setPendingClientTool] = useState(null);
906
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({});
907
1095
  const genuiCatalog = config.genui?.catalog ?? coreCatalog;
908
1096
  const [genuiSurfaces, setGenuiSurfaces] = useState([]);
909
1097
  const messagesLenRef = useRef(0);
@@ -1186,6 +1374,14 @@ function useAlquimia(config) {
1186
1374
  if (worklogEnabled) {
1187
1375
  setWorklog((prev) => reduceWorklog(prev ?? initialWorklogState(), frameToRecord(data)));
1188
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
+ }
1189
1385
  const pending = extractPendingClientTool(data);
1190
1386
  if (pending) {
1191
1387
  setPendingClientTool(pending);
@@ -1234,6 +1430,59 @@ function useAlquimia(config) {
1234
1430
  },
1235
1431
  [sdk]
1236
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
+ );
1237
1486
  useEffect(() => {
1238
1487
  if (!config.genui) return;
1239
1488
  if (!pendingClientTool || pendingClientTool.toolName !== "render_ui") return;
@@ -1319,6 +1568,9 @@ function useAlquimia(config) {
1319
1568
  activeTool,
1320
1569
  pendingClientTool,
1321
1570
  completeToolExecution,
1571
+ toolApprovals,
1572
+ pendingApprovals: pendingApprovals2,
1573
+ approveTool,
1322
1574
  genui,
1323
1575
  cleanMessages,
1324
1576
  createMessageId,