@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.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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:
|
|
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] = {
|
|
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,
|