@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.
- package/README.md +603 -23
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.d.ts +1 -1
- package/dist/adapters/fetch.d.mts +1 -0
- package/dist/adapters/fetch.d.ts +1 -0
- package/dist/adapters/fetch.js +4 -0
- package/dist/adapters/fetch.js.map +1 -1
- package/dist/adapters/fetch.mjs +4 -0
- package/dist/adapters/fetch.mjs.map +1 -1
- package/dist/adapters/index.d.mts +1 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/next.d.mts +1 -0
- package/dist/adapters/next.d.ts +1 -0
- package/dist/adapters/next.js +4 -0
- package/dist/adapters/next.js.map +1 -1
- package/dist/adapters/next.mjs +4 -0
- package/dist/adapters/next.mjs.map +1 -1
- package/dist/approvals-CcnmqUkQ.d.ts +59 -0
- package/dist/approvals-dn4sd_Ot.d.mts +59 -0
- package/dist/hooks/index.d.mts +36 -5
- package/dist/hooks/index.d.ts +36 -5
- package/dist/hooks/index.js +269 -17
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +269 -17
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/next/index.d.mts +7 -0
- package/dist/next/index.d.ts +7 -0
- package/dist/next/index.js +56 -37
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +56 -37
- package/dist/next/index.mjs.map +1 -1
- package/dist/providers/index.d.mts +3 -3
- package/dist/providers/index.d.ts +3 -3
- package/dist/{providers-b5_yMyMo.d.ts → providers-J_Z9rQ3Q.d.ts} +1 -1
- package/dist/{providers-DiR9cgyo.d.mts → providers-YKiEag2L.d.mts} +1 -1
- package/dist/proxy.d.mts +2 -0
- package/dist/proxy.d.ts +2 -0
- package/dist/proxy.js +47 -37
- package/dist/proxy.js.map +1 -1
- package/dist/proxy.mjs +47 -37
- package/dist/proxy.mjs.map +1 -1
- package/dist/sdk/index.d.mts +60 -5
- package/dist/sdk/index.d.ts +60 -5
- package/dist/sdk/index.js +115 -15
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/index.mjs +115 -15
- package/dist/sdk/index.mjs.map +1 -1
- package/dist/services/index.d.mts +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/{type-xksRnBUW.d.ts → type-BFtGEEb4.d.ts} +45 -1
- package/dist/{type-DqX3-9QS.d.mts → type-C_sKpIM1.d.mts} +45 -1
- package/dist/types/index.d.mts +28 -5
- package/dist/types/index.d.ts +28 -5
- package/dist/types/index.js.map +1 -1
- package/dist/types/index.mjs.map +1 -1
- package/dist/{types-C-6fgK1_.d.mts → types-lOliroPw.d.mts} +1 -1
- package/dist/{types-C-6fgK1_.d.ts → types-lOliroPw.d.ts} +1 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/worklog/index.d.mts +3 -2
- package/dist/worklog/index.d.ts +3 -2
- package/dist/worklog/index.js +94 -2
- package/dist/worklog/index.js.map +1 -1
- package/dist/worklog/index.mjs +94 -2
- package/dist/worklog/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/hooks/index.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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:
|
|
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] = {
|
|
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,
|