@alquimia-ai/tools 2.4.0 → 2.7.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 (77) hide show
  1. package/README.md +529 -19
  2. package/dist/actions/index.d.mts +2 -2
  3. package/dist/actions/index.d.ts +2 -2
  4. package/dist/actions/index.js +1 -1
  5. package/dist/actions/index.js.map +1 -1
  6. package/dist/actions/index.mjs +1 -1
  7. package/dist/actions/index.mjs.map +1 -1
  8. package/dist/{actions-CZU_GRy5.d.mts → actions-BjDPJdyA.d.mts} +1 -1
  9. package/dist/{actions-B0hfoYRd.d.ts → actions-CP1d4Ye1.d.ts} +1 -1
  10. package/dist/adapters/index.d.mts +2 -2
  11. package/dist/adapters/index.d.ts +2 -2
  12. package/dist/adapters/index.js.map +1 -1
  13. package/dist/genui/index.d.mts +3 -3
  14. package/dist/genui/index.d.ts +3 -3
  15. package/dist/genui/index.js +9 -1
  16. package/dist/genui/index.js.map +1 -1
  17. package/dist/genui/index.mjs +9 -1
  18. package/dist/genui/index.mjs.map +1 -1
  19. package/dist/hooks/index.d.mts +10 -10
  20. package/dist/hooks/index.d.ts +10 -10
  21. package/dist/hooks/index.js +88 -12
  22. package/dist/hooks/index.js.map +1 -1
  23. package/dist/hooks/index.mjs +88 -12
  24. package/dist/hooks/index.mjs.map +1 -1
  25. package/dist/next/index.js +23 -6
  26. package/dist/next/index.js.map +1 -1
  27. package/dist/next/index.mjs +23 -6
  28. package/dist/next/index.mjs.map +1 -1
  29. package/dist/providers/index.d.mts +4 -4
  30. package/dist/providers/index.d.ts +4 -4
  31. package/dist/providers/index.js +111 -110
  32. package/dist/providers/index.js.map +1 -1
  33. package/dist/providers/index.mjs +111 -110
  34. package/dist/providers/index.mjs.map +1 -1
  35. package/dist/{providers-BJTXCtI3.d.ts → providers-B4IOiGfr.d.mts} +8 -8
  36. package/dist/{providers-0Gi78uNQ.d.mts → providers-mx21gNgZ.d.ts} +8 -8
  37. package/dist/proxy.d.mts +4 -4
  38. package/dist/proxy.d.ts +4 -4
  39. package/dist/proxy.js +23 -6
  40. package/dist/proxy.js.map +1 -1
  41. package/dist/proxy.mjs +23 -6
  42. package/dist/proxy.mjs.map +1 -1
  43. package/dist/sdk/index.d.mts +39 -5
  44. package/dist/sdk/index.d.ts +39 -5
  45. package/dist/sdk/index.js +49 -6
  46. package/dist/sdk/index.js.map +1 -1
  47. package/dist/sdk/index.mjs +49 -6
  48. package/dist/sdk/index.mjs.map +1 -1
  49. package/dist/services/index.d.mts +3 -3
  50. package/dist/services/index.d.ts +3 -3
  51. package/dist/services/index.js.map +1 -1
  52. package/dist/services/index.mjs.map +1 -1
  53. package/dist/type-61gaZga-.d.mts +356 -0
  54. package/dist/type-D-JaSMkV.d.ts +356 -0
  55. package/dist/types/index.d.mts +80 -304
  56. package/dist/types/index.d.ts +80 -304
  57. package/dist/types/index.js.map +1 -1
  58. package/dist/types/index.mjs.map +1 -1
  59. package/dist/{types-DnfWUE9V.d.mts → types-CMVbyEv8.d.mts} +3 -2
  60. package/dist/{types-DnfWUE9V.d.ts → types-CMVbyEv8.d.ts} +3 -2
  61. package/dist/types-lOliroPw.d.mts +111 -0
  62. package/dist/types-lOliroPw.d.ts +111 -0
  63. package/dist/utils/index.d.mts +2 -2
  64. package/dist/utils/index.d.ts +2 -2
  65. package/dist/utils/index.js +1 -1
  66. package/dist/utils/index.js.map +1 -1
  67. package/dist/utils/index.mjs +1 -1
  68. package/dist/utils/index.mjs.map +1 -1
  69. package/dist/worklog/index.d.mts +5 -2
  70. package/dist/worklog/index.d.ts +5 -2
  71. package/dist/worklog/index.js +22 -1
  72. package/dist/worklog/index.js.map +1 -1
  73. package/dist/worklog/index.mjs +22 -1
  74. package/dist/worklog/index.mjs.map +1 -1
  75. package/package.json +1 -1
  76. package/dist/types-Dvu9y4WM.d.mts +0 -53
  77. package/dist/types-Dvu9y4WM.d.ts +0 -53
package/README.md CHANGED
@@ -1,38 +1,548 @@
1
1
  # @alquimia-ai/tools
2
2
 
3
- Tools for Alquimia SDK.
3
+ The runtime half of the Alquimia frontend SDK. It turns the Alquimia Runtime's HTTP + SSE API into React state: a chat hook, a builder-style SDK class, URL adapters, a server-side proxy, a generative-UI protocol, and a normalized execution trace.
4
4
 
5
- ## Table of Contents
5
+ It ships no UI. Pair it with [`@alquimia-ai/ui`](../ui) for the component library, or wire its return values to your own components.
6
6
 
7
- - [Installation](#installation)
8
- - [Usage](#usage)
9
- - [Available Components](#available-components)
7
+ - **Runtime compatibility:** Alquimia Runtime **v0.5.2** / alquimia-core **v0.5.3**
8
+ - **Peer deps:** React ≥ 18. `next` ≥ 14 only if you use the Next.js helpers.
9
+
10
+ ---
11
+
12
+ ## Table of contents
13
+
14
+ - [Install](#install)
15
+ - [The mental model](#the-mental-model)
16
+ - [Quick start](#quick-start)
17
+ - [`useAlquimia`](#usealquimia)
18
+ - [`AlquimiaSDK`](#alquimiasdk)
19
+ - [Anatomy of a turn](#anatomy-of-a-turn)
20
+ - [Version-pinned agents](#version-pinned-agents)
21
+ - [Multimodal and audio input](#multimodal-and-audio-input)
22
+ - [Attachments and blobs](#attachments-and-blobs)
23
+ - [Client tools and GenUI](#client-tools-and-genui)
24
+ - [Worklog](#worklog)
25
+ - [Providers](#providers)
26
+ - [Server proxy](#server-proxy)
27
+ - [Distributed tracing](#distributed-tracing)
28
+ - [Types](#types)
29
+ - [Export map](#export-map)
10
30
  - [Development](#development)
11
- - [Contributing](#contributing)
12
- - [License](#license)
13
31
 
14
- ## Installation
32
+ ---
15
33
 
16
- To install the `@alquimia-ai/tools` package, use the following command:
34
+ ## Install
17
35
 
18
36
  ```sh
19
- yarn add @alquimia-ai/tools
37
+ npm install @alquimia-ai/tools
20
38
  ```
21
39
 
22
- Or with npm:
40
+ Environment variables live on the **server**, never in the browser:
23
41
 
24
- ```sh
25
- npm install @alquimia-ai/tools
42
+ ```bash
43
+ ASSISTANT_BASEURL=https://your-runtime.example.com
44
+ ALQUIMIA_ASSISTANT_API_KEY=your-secret-key
45
+ ```
46
+
47
+ ---
48
+
49
+ ## The mental model
50
+
51
+ Two pieces decide where every request goes and who holds the credentials.
52
+
53
+ **The adapter** resolves URLs on the client. It never adds an `Authorization` header.
54
+
55
+ **The proxy** runs on your server, injects the API key, and forwards to the runtime.
56
+
57
+ ```
58
+ browser your server alquimia runtime
59
+ ─────── ─────────── ────────────────
60
+ useAlquimia / AlquimiaSDK
61
+ └─ adapter.resolveInferUrl() ──→ handleInfer ────────────────→ POST /event/infer/{assistantId}
62
+ └─ adapter.resolveStreamUrl() ─→ handleStream ───────────────→ GET /event/stream/{taskId}
63
+ └─ adapter.resolveBlobUploadUrl() → handleBlobUpload ────────→ POST /context/blob/upload
64
+ └─ adapter.resolveToolCompletionUrl() → handleToolCompletion → POST /event/tool-completion
65
+ ↑
66
+ Authorization: Bearer <apiKey>
67
+ ```
68
+
69
+ The proxy exists for two reasons. The obvious one is that the API key must not reach the browser. The load-bearing one is that the stream leg uses `EventSource`, and **`EventSource` cannot set request headers** — so `getHeaders()` is honoured on infer, blob upload and tool-completion (all axios POSTs) and silently ignored on the stream. If your runtime or its gateway requires auth on every request, a server hop on the stream is not optional.
70
+
71
+ Three supported topologies:
72
+
73
+ | Topology | Adapter | Server side |
74
+ |---|---|---|
75
+ | Next.js App Router | `createNextJsAdapter()` | `createNextJsRouteHandlers()` |
76
+ | SPA + your own server (Express/Hono/Fastify/Bun/Deno) | `createFetchAdapter({ baseUrl })` | `createAlquimiaProxyHandler()` |
77
+ | SPA, no server | custom `AlquimiaAdapter` | a ~30-line proxy for the stream leg only |
78
+
79
+ ---
80
+
81
+ ## Quick start
82
+
83
+ ### Next.js App Router
84
+
85
+ Four route files, five lines each:
86
+
87
+ ```ts
88
+ // app/api/chat/[...path]/route.ts
89
+ import { createNextJsRouteHandlers } from '@alquimia-ai/tools/next';
90
+
91
+ const handlers = createNextJsRouteHandlers({
92
+ assistantBaseUrl: process.env.ASSISTANT_BASEURL!,
93
+ apiKey: process.env.ALQUIMIA_ASSISTANT_API_KEY!,
94
+ });
95
+
96
+ export const POST = handlers.handleInfer;
97
+ ```
98
+
99
+ ```ts
100
+ // app/api/stream/[...path]/route.ts → export const GET = handlers.handleStream;
101
+ // app/api/blob/upload/route.ts → export const POST = handlers.handleBlobUpload;
102
+ // app/api/tool-completion/route.ts → export const POST = handlers.handleToolCompletion;
103
+ ```
104
+
105
+ The last two take no `[...path]` segment: identity travels in headers, and the pending tool call is correlated by `control_id` in the body.
106
+
107
+ ```tsx
108
+ 'use client';
109
+ import { useAlquimia } from '@alquimia-ai/tools/hooks';
110
+ import { createNextJsAdapter } from '@alquimia-ai/tools/adapters/next';
111
+
112
+ const adapter = createNextJsAdapter();
113
+
114
+ export function Chat({ assistantId }: { assistantId: string }) {
115
+ const alquimia = useAlquimia({ assistantId, adapter });
116
+
117
+ return (
118
+ <form onSubmit={(e) => alquimia.handleSubmit(e)}>
119
+ {alquimia.messages.map((m) => <p key={m.id}>{m.content}</p>)}
120
+ <input value={alquimia.input} onChange={alquimia.handleInputChange} />
121
+ </form>
122
+ );
123
+ }
124
+ ```
125
+
126
+ ### Any server
127
+
128
+ ```ts
129
+ import { createAlquimiaProxyHandler } from '@alquimia-ai/tools/proxy';
130
+
131
+ const handler = createAlquimiaProxyHandler({
132
+ assistantBaseUrl: process.env.ASSISTANT_BASEURL!,
133
+ apiKey: process.env.ALQUIMIA_ASSISTANT_API_KEY!,
134
+ });
135
+
136
+ // Hono
137
+ app.post('/chat/:path{.+}', (c) => handler.handleInfer(c.req.raw, c.req.param('path')));
138
+ app.get('/stream/:path{.+}', (c) => handler.handleStream(c.req.raw, c.req.param('path')));
139
+ app.post('/blob/upload', (c) => handler.handleBlobUpload(c.req.raw));
140
+ app.post('/tool-completion', (c) => handler.handleToolCompletion(c.req.raw));
141
+ ```
142
+
143
+ The handlers speak the Web Fetch API (`Request` → `Response`), so any modern runtime works.
144
+
145
+ ```ts
146
+ import { createFetchAdapter } from '@alquimia-ai/tools/adapters/fetch';
147
+ const adapter = createFetchAdapter({ baseUrl: 'https://my-api.example.com' });
148
+ ```
149
+
150
+ ### Custom adapter
151
+
152
+ Implement the interface directly to split legs across hosts:
153
+
154
+ ```ts
155
+ import type { AlquimiaAdapter } from '@alquimia-ai/tools/adapters';
156
+
157
+ const adapter: AlquimiaAdapter = {
158
+ resolveInferUrl: (assistantId) => `${BACKEND}/event/infer/${assistantId}`,
159
+ resolveStreamUrl: (taskId) => `${STREAM_PROXY}/event/stream/${taskId}`,
160
+ resolveBlobUploadUrl: () => `${BACKEND}/context/blob/upload`,
161
+ resolveToolCompletionUrl: () => `${BACKEND}/event/tool-completion`,
162
+ getHeaders: () => ({ Authorization: `Bearer ${API_KEY}` }),
163
+ };
164
+ ```
165
+
166
+ `busMode: true` on either built-in adapter appends `?bus_mode=true` to the stream URL, which makes the runtime emit granular worklog frames instead of only assistant deltas.
167
+
168
+ ---
169
+
170
+ ## `useAlquimia`
171
+
172
+ One hook owns the whole chat: message list, input, streaming flags, attachments, audio state, worklog, and the GenUI loop.
173
+
174
+ ```ts
175
+ const alquimia = useAlquimia({
176
+ assistantId: 'support-agent', // required
177
+ adapter, // required
178
+ providers: { // all optional
179
+ whisper, stableDiffusion, characterization, ratings, logger,
180
+ },
181
+ options: {
182
+ enforceCharacterization: false,
183
+ userId: user?.email,
184
+ extraInstructions: { tone: 'formal' }, // injected as runtime prompt clauses
185
+ worklog: true, // build the execution trace
186
+ },
187
+ genui: {
188
+ catalog: coreCatalog, // default: the built-in ~40-component catalog
189
+ allow: 'all', // or an explicit component allowlist
190
+ source: 'agent', // 'agent' | 'client'
191
+ },
192
+ });
193
+ ```
194
+
195
+ ### What it returns
196
+
197
+ **Messages and input**
198
+
199
+ | Key | Type | Notes |
200
+ |---|---|---|
201
+ | `messages` | `AlquimiaMessage[]` | full conversation |
202
+ | `cleanMessages` | `AlquimiaMessage[]` | without system/internal entries |
203
+ | `input` | `string` | controlled input value |
204
+ | `handleInputChange` | `(e) => void` | textarea change handler |
205
+ | `handleReplaceInput` | `(text: string) => void` | used by speech-to-text |
206
+ | `populateMessages` | `(messages) => void` | hydrate from history |
207
+ | `createMessageId` | `() => string` | id helper |
208
+
209
+ **Sending**
210
+
211
+ | Key | Notes |
212
+ |---|---|
213
+ | `handleSubmit(event, traceParentId?, sessionId?, additionalInfo?)` | the normal path |
214
+ | `sendMessage` | lower-level send |
215
+ | `handleSystemMessage(text, opts)` | inject a system turn |
216
+ | `handleLoadingCancel()` | abort an in-flight turn and close the stream |
217
+ | `lastRequest` / `setLastRequest` | for retry UX |
218
+
219
+ **Status flags**
220
+
221
+ `isLoading`, `isMessageLoading`, `isMessageStreaming`, `isStreamingLoading`, `streamingMessageId`, `hasThinkings`, `isUploadingAttachments`.
222
+
223
+ **Attachments**
224
+
225
+ `attachments`, `addAttachment`, `addAttachments`, `removeAttachment`, `clearAttachments`.
226
+
227
+ **Audio**
228
+
229
+ `isAudioRecording`, `setIsAudioRecording`.
230
+
231
+ **Tools and GenUI**
232
+
233
+ `activeTool`, `setActiveTool`, `pendingClientTool`, `completeToolExecution`, `genui`.
234
+
235
+ **Trace**
236
+
237
+ `worklog` — a `WorklogState`, or `undefined` unless `options.worklog` is on.
238
+
239
+ **Escape hatch**
240
+
241
+ `sdk` — the underlying `AlquimiaSDK` instance.
242
+
243
+ ---
244
+
245
+ ## `AlquimiaSDK`
246
+
247
+ A builder. Every `with*` returns `this`, so configuration chains; the getters read back what the runtime resolved.
248
+
249
+ ```ts
250
+ import { AlquimiaSDK } from '@alquimia-ai/tools/sdk';
251
+
252
+ const sdk = new AlquimiaSDK('support-agent', adapter)
253
+ .withConversationId(conversationId)
254
+ .withUserId('user-42')
255
+ .withExtraInstructions({ tone: 'formal' });
256
+
257
+ await sdk.sendMessage('hello');
258
+ const streamUrl = sdk.getUrlStream();
259
+ ```
260
+
261
+ ### Configuration
262
+
263
+ | Method | Purpose |
264
+ |---|---|
265
+ | `withConversationId(id)` | **required before `sendMessage`** — the runtime `session_id` |
266
+ | `withUserId(id)` | `user_id` on the payload and the `user-id` header |
267
+ | `withExtraInstructions(record)` | `extra_instructions` — prompt clauses merged by the runtime |
268
+ | `withAssistantConfig(config)` | full `AssistantConfig` override for the turn |
269
+ | `withTools(schemas)` | client-side tool schemas — sent as a `native` evaluation strategy |
270
+ | `withAttachments(payloads)` | attachment metadata for the next turn |
271
+ | `withVersionTag(tag)` | pin a registry version (see below) |
272
+ | `withWhisperProvider` / `withStableDiffusionProvider` / `withAnalyzeCharacterizationProvider` / `withRatingsProvider` / `withLoggerProvider` | inject optional providers |
273
+
274
+ ### Turn
275
+
276
+ | Method | Purpose |
277
+ |---|---|
278
+ | `sendMessage(query?, options?)` | POST infer. `options` is `{ traceParent?, inputAudio?, outputAudio? }`, or a bare traceparent string for backwards compatibility |
279
+ | `getUrlStream()` | SSE URL for the task, with the traceparent appended as a query param |
280
+ | `getStreamId()` / `getTaskId()` | the runtime `taskid` |
281
+ | `getVersionTag()` | the registry version the runtime resolved |
282
+ | `submitToolResult(response)` | POST tool-completion for a pending client tool |
283
+
284
+ ### Side channels
285
+
286
+ | Method | Requires |
287
+ |---|---|
288
+ | `uploadAttachment(file)` → `RuntimeBlob` | — |
289
+ | `getAttachmentResponses()` | — |
290
+ | `textToSpeech(text)` / `speechToText(audio)` | a whisper provider |
291
+ | `generateImage(query)` | a stable-diffusion provider |
292
+ | `analyzeCharacterization(text)` | a characterization provider |
293
+ | `rate(data)` | a ratings provider |
294
+ | `logInfo` / `logError` | a logger provider |
295
+
296
+ ---
297
+
298
+ ## Anatomy of a turn
299
+
300
+ ```
301
+ 1. POST /event/infer/{assistantId}
302
+ body { query, session_id, user_id, ... }
303
+ headers assistant-id, session-id, user-id, task-id, x-trace-parent
304
+ → CommonAttributes { taskid, versiontag, attachments }
305
+
306
+ 2. GET /event/stream/{taskid} (EventSource)
307
+ ← assistant deltas, thinkings, tool events
308
+ ← granular worklog frames when bus_mode=true
309
+
310
+ 3. POST /event/tool-completion (only if the agent called a client tool)
311
+ body ToolExecutionResponse { control_id, tool_name, status, result }
312
+ headers taskid, sessionid, userid, assistantid, versiontag
313
+ → the run resumes on the same stream
314
+ ```
315
+
316
+ Step 3 is what makes client tools and GenUI work: the runtime parks the task, the browser answers, the run continues. The correlation headers are not decoration — the runtime looks the parked task up by them, and rejects a completion whose `control_id` it is not awaiting with a `409`.
317
+
318
+ ---
319
+
320
+ ## Version-pinned agents
321
+
322
+ The runtime keeps cached registry versions of an agentspace. Two ways to target one:
323
+
324
+ **A versioned assistant ref**, which works on every runtime version:
325
+
326
+ ```ts
327
+ const sdk = new AlquimiaSDK('prod/support-agent:v1.2', adapter);
328
+ ```
329
+
330
+ The runtime parses `<agentspace>/<assistant>[:<tag>]` out of the path itself.
331
+
332
+ **`withVersionTag()`**, which sends `?version_tag=` on infer and needs **runtime ≥ 0.5.2** — older runtimes ignore the parameter:
333
+
334
+ ```ts
335
+ sdk.withVersionTag('v1.2');
336
+ ```
337
+
338
+ Either way, the runtime answers with the version it actually resolved in `CommonAttributes.versiontag`. The SDK stores it and echoes it as the `versiontag` header on tool-completion, so both halves of a turn land on the same snapshot even when "latest" moves underneath a live conversation. Read it with `getVersionTag()`.
339
+
340
+ The tag is a header **only** on tool-completion and tool-approval, where the runtime binds it through a `CommonAttributes` header model. On infer it is a query parameter. Sending it as a header there does nothing.
341
+
342
+ ---
343
+
344
+ ## Multimodal and audio input
345
+
346
+ `query` accepts the runtime's full `Content` shape — plain text, or an OpenAI-style list of content parts:
347
+
348
+ ```ts
349
+ await sdk.sendMessage([
350
+ { type: 'text', text: 'what is in this picture?' },
351
+ { type: 'image_url', image_url: { url: imageUrl, detail: 'low' } },
352
+ ]);
353
+ ```
354
+
355
+ Audio input is a two-step flow, because `input_audio` is a **blob reference**, not raw bytes:
356
+
357
+ ```ts
358
+ const blob = await sdk.uploadAttachment(audioFile); // POST /context/blob/upload → RuntimeBlob
359
+ await sdk.sendMessage(undefined, { inputAudio: blob }); // audio-only turn
26
360
  ```
27
361
 
28
- ## Available Features
362
+ `query` is optional when audio is supplied, and both can be sent together. A turn with neither throws before any request is made.
363
+
364
+ Audio **output** is on by default server-side for an agent with a TTS adapter. To force a text-only reply:
365
+
366
+ ```ts
367
+ await sdk.sendMessage('hello', { outputAudio: false });
368
+ ```
369
+
370
+ The field is only sent when you set it, so leaving it alone preserves the runtime's own default.
371
+
372
+ ---
29
373
 
30
- - **Context**: Session and user context.
31
- - **Hooks**: Main hook that applies the SDK and most important chat behaviour.
32
- - **SDK Class and providers**: Main SDK class with its providers.
374
+ ## Attachments and blobs
375
+
376
+ ```ts
377
+ const blob = await sdk.uploadAttachment(file);
378
+ // { blob_id, filename, content_size, content_type, checksum, ... }
379
+ ```
380
+
381
+ Identity travels in kebab-case headers (`assistant-id`, `session-id`, `user-id`, `task-id`), which the proxy forwards; the path carries nothing. The infer response echoes the blob ids the runtime associated with the turn — read them with `getAttachmentResponses()`.
382
+
383
+ Through the hook, `addAttachments(files)` queues them and they upload automatically after the turn's infer call resolves.
384
+
385
+ ---
386
+
387
+ ## Client tools and GenUI
388
+
389
+ A client tool is a tool the **browser** executes. The agent calls it, the runtime parks the task, the SDK surfaces it as `pendingClientTool`, and `completeToolExecution` posts the result back.
390
+
391
+ GenUI is that mechanism with one specific tool, `render_ui`, whose arguments are a declarative component tree. The agent never emits markup — only component names and props from a catalog you authorize.
392
+
393
+ ```ts
394
+ const alquimia = useAlquimia({
395
+ assistantId, adapter,
396
+ genui: { allow: ['Stack', 'TextField', 'Button'] },
397
+ });
398
+ ```
33
399
 
400
+ The `/genui` module holds the protocol, independent of any renderer:
34
401
 
35
- ## License
402
+ | Export | Purpose |
403
+ |---|---|
404
+ | `coreCatalog`, `CORE_CATALOG_ID` | the default ~40-component catalog with Zod prop schemas |
405
+ | `coreCatalogJson` | the published A2UI catalog artifact, as a value |
406
+ | `buildRenderUiSchema(manifest, allow)` | the JSON-schema the agent sees |
407
+ | `buildGenuiClause(manifest, allow)` | the prompt clause describing the catalog |
408
+ | `validateSurface(input, manifest)` | validates an agent-authored tree before render |
409
+ | `validateProps(node, def)` | per-component prop validation |
410
+ | `resolveDynamic`, `getPointer`, `setPointer`, `isPathRef` | data-binding against the surface's data model |
411
+ | `classifyAction`, `assertSafeActionName` | agent-bound vs local action handling |
412
+ | `buildSurfaceResult`, `buildDismissResult` | the payloads posted back on submit or dismiss |
413
+ | `emitA2uiCatalog` | export a manifest as an A2UI catalog artifact |
36
414
 
37
- This project is licensed under the MIT License. See the LICENSE file for more details.
415
+ Components come from `@alquimia-ai/ui/components/genui` (`A2uiRenderer`, `coreUiRegistry`, `AssistantChat`), or from your own registry — pass your catalog to the hook and your components to the renderer, and the same agent composes your design system.
416
+
417
+ ---
418
+
419
+ ## Worklog
420
+
421
+ The worklog is the agent's execution trace, normalized into a tree you can render as "show your work".
422
+
423
+ ```ts
424
+ const alquimia = useAlquimia({ assistantId, adapter, options: { worklog: true } });
425
+ alquimia.worklog?.nodes; // WorklogNode[]
426
+ alquimia.worklog?.status; // 'idle' | 'running' | 'success' | 'error'
427
+ alquimia.worklog?.answer;
428
+ ```
429
+
430
+ Each runtime event class maps to an interpreter that says which bucket it belongs to (`NodeKind`) and whether it opens or closes a node. Commands and responses are merged by `control_id`.
431
+
432
+ | Bucket | Event classes |
433
+ |---|---|
434
+ | `answer` | `AssistantInference(+Response)` — the run envelope |
435
+ | `safeguard` | `ShieldInference(+Response)`, `ShieldBlockedResponse` |
436
+ | `reasoning` | `ResponseInference(+Response)` |
437
+ | `tool` | `ServerToolExecution`, `BuiltinToolExecution`, `ClientToolExecution`, `UnknownToolExecution`, `ToolSchema`, `HumanApprovalRequired`, and their responses |
438
+ | `a2a` | `A2AInference`, `AgentDiscovery(+Response)` |
439
+ | `memory` | `ContextPersistence`, `ContextFlush(+Response)` |
440
+ | `knowledge` | `KnowledgeRetrieval(+Response)` |
441
+ | `speech` | `SpeechTranscription(+Response)`, `SpeechSynthesis(+Response)` |
442
+ | `unknown` | anything unregistered — rendered by class name rather than dropped |
443
+
444
+ Standalone API, usable without the chat hook:
445
+
446
+ ```ts
447
+ import { foldWorklog, reduceWorklog, frameToRecord, useWorklog } from '@alquimia-ai/tools/worklog';
448
+
449
+ const state = foldWorklog(records); // fold a full history
450
+ const next = reduceWorklog(state, record); // fold one frame at a time
451
+ const record = frameToRecord(sseFrame); // normalize a raw SSE frame
452
+ ```
453
+
454
+ The module also types the runtime's worklog REST API — `WorklogSummary`, `WorklogDetail`, `WorklogEventDetail`, `WorklogPage`, `WorklogEventPage`, `WorklogVerificationResult` — for reading `/worklog/*` from a backend. Those endpoints are audit surface; this package does not call them.
455
+
456
+ ---
457
+
458
+ ## Providers
459
+
460
+ Providers are abstract classes with swappable implementations, so the app picks its vendor without the SDK depending on one.
461
+
462
+ | Abstract | Implementations |
463
+ |---|---|
464
+ | `WhisperProvider` (TTS + STT) | `AlquimiaWhisperProvider`, `OpenAIWhisperProvider`, `ElevenLabsWhisperProvider`, `OrpheusWhisperProvider` |
465
+ | `StableDiffusionProvider` | `OpenAIStableDiffusionProvider`, `StabilityProvider` |
466
+ | `CharacterizationProvider` | `OpenAIAnalyzeCharProvider` |
467
+ | `RatingsProvider` | `AlquimiaRatingsProvider` |
468
+ | `LoggerProvider` | `ElasticLoggerProvider` (`@alquimia-ai/tools/providers/elastic`) |
469
+
470
+ These are **client-side integrations**, separate from the runtime's own audio pipeline. An agent configured with a runtime STT/TTS adapter transcribes and synthesizes server-side — that path is `input_audio` / `output_audio` on infer, not a provider.
471
+
472
+ ---
473
+
474
+ ## Server proxy
475
+
476
+ ```ts
477
+ createAlquimiaProxyHandler({
478
+ assistantBaseUrl, // required
479
+ apiKey, // required
480
+ inferRoute: 'event/infer', // defaults shown
481
+ streamRoute: 'event/stream',
482
+ blobUploadRoute: 'context/blob/upload',
483
+ toolCompletionRoute: 'event/tool-completion',
484
+ })
485
+ ```
486
+
487
+ Returns `handleInfer`, `handleStream`, `handleBlobUpload`, `handleToolCompletion`.
488
+
489
+ What it does beyond adding the bearer token:
490
+
491
+ - Forwards identity headers on infer/stream/blob upload: `session-id`, `user-id`, `assistant-id`, `task-id`, `parent-task-id`, `agentspace-id`, `channel-id`, `depth`.
492
+ - Forwards correlation headers on tool-completion: `taskid`, `sessionid`, `userid`, `assistantid`, `parenttaskid`, `agentspaceid`, `channelid`, `depth`, `versiontag`.
493
+ - Preserves the infer query string, so `?version_tag=` and `?agentspace_id=` survive the hop.
494
+ - Streams the SSE body through untouched.
495
+ - Turns the stream's `?trace_parent=` query param back into an `x-trace-parent` header, and does not pass the param upstream.
496
+
497
+ ---
498
+
499
+ ## Distributed tracing
500
+
501
+ Infer and stream are two HTTP calls that must land in **one** trace, and `EventSource` cannot set headers. So the SDK sends `x-trace-parent` on infer, remembers the traceparent for the turn, and appends it to the stream URL as `?trace_parent=`; the proxy converts it back to a header and strips the param. When a turn has no trace context, the header is omitted entirely rather than sent empty.
502
+
503
+ `@alquimia-ai/tools/services` exposes the Elastic APM RUM integration, and `handleApmRequest` in `/actions` proxies the APM ingest endpoint.
504
+
505
+ ---
506
+
507
+ ## Types
508
+
509
+ `@alquimia-ai/tools/types` covers both the wire and the view model.
510
+
511
+ **Wire:** `CommonAttributes` (with `versiontag`), `InferInitResponse`, `Content` / `ContentPart` / `TextContentPart` / `ImageContentPart` / `NonStandardContentPart`, `RuntimeBlob`, `AttachmentPayload`, `NativeToolsEvaluationStrategy`, `RuntimeEvent` and the `/event/*` payload union, `TTSResult`, `RatingData`.
512
+
513
+ **View:** `AlquimiaMessage`, `AlquimiaEventData`, `AIMessageChunk`, `AssistantInferenceResponse`, `ToolEvent`, `Tooler`, `ThinkingsInferenceResponse`, `GenuiSurfaceItem`, `ActionResponse`.
514
+
515
+ ---
516
+
517
+ ## Export map
518
+
519
+ | Entry | Contents |
520
+ |---|---|
521
+ | `/adapters` | `AlquimiaAdapter`, `AlquimiaSDKOptions` |
522
+ | `/adapters/next` | `createNextJsAdapter` |
523
+ | `/adapters/fetch` | `createFetchAdapter` |
524
+ | `/sdk` | `AlquimiaSDK` |
525
+ | `/hooks` | `useAlquimia`, `useRatings` |
526
+ | `/proxy` | `createAlquimiaProxyHandler`, `ProxyConfig` |
527
+ | `/next` | `createNextJsRouteHandlers`, `handleChatRequest`, `handleStreamRequest`, `initConversation` |
528
+ | `/providers` | provider base classes and implementations |
529
+ | `/providers/elastic` | `ElasticLoggerProvider` |
530
+ | `/genui` | the GenUI protocol |
531
+ | `/worklog` | reducer, registry, types, `useWorklog` |
532
+ | `/actions` | `initConversation`, CRUD helpers, `handleApmRequest` |
533
+ | `/services` | Elastic APM RUM |
534
+ | `/types` | wire + view types |
535
+ | `/utils` | header/cookie/formatting helpers |
536
+
537
+ ---
538
+
539
+ ## Development
540
+
541
+ ```sh
542
+ yarn test # vitest
543
+ yarn lint # eslint --max-warnings 0
544
+ yarn build # tsup
545
+ yarn genui:catalog # regenerate the published A2UI catalog artifact
546
+ ```
38
547
 
548
+ Changes to published behaviour need a changeset — see [`docs/how-to-use-changeset.md`](../../docs/how-to-use-changeset.md).
@@ -1,8 +1,8 @@
1
- import { BaseAPIConfig } from '../types/index.mjs';
1
+ import { B as BaseAPIConfig } from '../type-61gaZga-.mjs';
2
2
  export { i as initConversation } from '../session.action-DirvOWt0.mjs';
3
3
  import '@elastic/apm-rum';
4
4
  import 'ai';
5
- import '../types-DnfWUE9V.mjs';
5
+ import '../types-CMVbyEv8.mjs';
6
6
 
7
7
  interface ActionResponse<T = void> {
8
8
  success: boolean;
@@ -1,8 +1,8 @@
1
- import { BaseAPIConfig } from '../types/index.js';
1
+ import { B as BaseAPIConfig } from '../type-D-JaSMkV.js';
2
2
  export { i as initConversation } from '../session.action-DirvOWt0.js';
3
3
  import '@elastic/apm-rum';
4
4
  import 'ai';
5
- import '../types-DnfWUE9V.js';
5
+ import '../types-CMVbyEv8.js';
6
6
 
7
7
  interface ActionResponse<T = void> {
8
8
  success: boolean;
@@ -48,7 +48,7 @@ function isTextContent(buffer) {
48
48
  try {
49
49
  const text = new TextDecoder().decode(buffer);
50
50
  const trimmedText = text.trim();
51
- return /^[\w\#\-\*\|\[\]\-\+\=\s\n\r\t\!]/.test(trimmedText) || trimmedText.startsWith("#") || trimmedText.startsWith("![") || trimmedText.startsWith("```") || trimmedText.startsWith("---") || /^[a-zA-Z0-9\s\n\r\t]/.test(trimmedText) || /^\!\[.*\]\(.*\)/.test(trimmedText);
51
+ return /^[\w#\-*|[\]+=\s\n\r\t!]/.test(trimmedText) || trimmedText.startsWith("#") || trimmedText.startsWith("![") || trimmedText.startsWith("```") || trimmedText.startsWith("---") || /^[a-zA-Z0-9\s\n\r\t]/.test(trimmedText) || /^!\[.*\]\(.*\)/.test(trimmedText);
52
52
  } catch {
53
53
  return false;
54
54
  }