@niadra/sdk 0.1.0 → 0.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 (133) hide show
  1. package/CHANGELOG.md +161 -20
  2. package/README.md +484 -38
  3. package/dist/ai-sdk.cjs +1037 -0
  4. package/dist/ai-sdk.cjs.map +1 -0
  5. package/dist/ai-sdk.d.cts +118 -0
  6. package/dist/ai-sdk.d.ts +118 -0
  7. package/dist/ai-sdk.js +1030 -0
  8. package/dist/ai-sdk.js.map +1 -0
  9. package/dist/anthropic.cjs +528 -0
  10. package/dist/anthropic.cjs.map +1 -0
  11. package/dist/anthropic.d.cts +35 -0
  12. package/dist/anthropic.d.ts +35 -0
  13. package/dist/anthropic.js +523 -0
  14. package/dist/anthropic.js.map +1 -0
  15. package/dist/bedrock.cjs +392 -0
  16. package/dist/bedrock.cjs.map +1 -0
  17. package/dist/bedrock.d.cts +25 -0
  18. package/dist/bedrock.d.ts +25 -0
  19. package/dist/bedrock.js +388 -0
  20. package/dist/bedrock.js.map +1 -0
  21. package/dist/cli.js +11045 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/client-DsIxZxZk.d.cts +6905 -0
  24. package/dist/client-DsIxZxZk.d.ts +6905 -0
  25. package/dist/cloudflare-agents.cjs +626 -0
  26. package/dist/cloudflare-agents.cjs.map +1 -0
  27. package/dist/cloudflare-agents.d.cts +113 -0
  28. package/dist/cloudflare-agents.d.ts +113 -0
  29. package/dist/cloudflare-agents.js +622 -0
  30. package/dist/cloudflare-agents.js.map +1 -0
  31. package/dist/elevenlabs.cjs +738 -0
  32. package/dist/elevenlabs.cjs.map +1 -0
  33. package/dist/elevenlabs.d.cts +86 -0
  34. package/dist/elevenlabs.d.ts +86 -0
  35. package/dist/elevenlabs.js +733 -0
  36. package/dist/elevenlabs.js.map +1 -0
  37. package/dist/genkit.cjs +323 -0
  38. package/dist/genkit.cjs.map +1 -0
  39. package/dist/genkit.d.cts +67 -0
  40. package/dist/genkit.d.ts +67 -0
  41. package/dist/genkit.js +318 -0
  42. package/dist/genkit.js.map +1 -0
  43. package/dist/google-adk.cjs +705 -0
  44. package/dist/google-adk.cjs.map +1 -0
  45. package/dist/google-adk.d.cts +91 -0
  46. package/dist/google-adk.d.ts +91 -0
  47. package/dist/google-adk.js +701 -0
  48. package/dist/google-adk.js.map +1 -0
  49. package/dist/google-genai.cjs +422 -0
  50. package/dist/google-genai.cjs.map +1 -0
  51. package/dist/google-genai.d.cts +26 -0
  52. package/dist/google-genai.d.ts +26 -0
  53. package/dist/google-genai.js +418 -0
  54. package/dist/google-genai.js.map +1 -0
  55. package/dist/index.cjs +11449 -1796
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.d.cts +1006 -1247
  58. package/dist/index.d.ts +1006 -1247
  59. package/dist/index.js +11393 -1797
  60. package/dist/index.js.map +1 -1
  61. package/dist/intercept-0_lJE6k1.d.cts +20 -0
  62. package/dist/intercept-D7qFCaRb.d.ts +20 -0
  63. package/dist/langchain.cjs +1077 -0
  64. package/dist/langchain.cjs.map +1 -0
  65. package/dist/langchain.d.cts +102 -0
  66. package/dist/langchain.d.ts +102 -0
  67. package/dist/langchain.js +1068 -0
  68. package/dist/langchain.js.map +1 -0
  69. package/dist/livekit.cjs +404 -0
  70. package/dist/livekit.cjs.map +1 -0
  71. package/dist/livekit.d.cts +113 -0
  72. package/dist/livekit.d.ts +113 -0
  73. package/dist/livekit.js +398 -0
  74. package/dist/livekit.js.map +1 -0
  75. package/dist/llamaindex.cjs +349 -0
  76. package/dist/llamaindex.cjs.map +1 -0
  77. package/dist/llamaindex.d.cts +92 -0
  78. package/dist/llamaindex.d.ts +92 -0
  79. package/dist/llamaindex.js +344 -0
  80. package/dist/llamaindex.js.map +1 -0
  81. package/dist/mastra.cjs +1082 -0
  82. package/dist/mastra.cjs.map +1 -0
  83. package/dist/mastra.d.cts +86 -0
  84. package/dist/mastra.d.ts +86 -0
  85. package/dist/mastra.js +1075 -0
  86. package/dist/mastra.js.map +1 -0
  87. package/dist/openai-agents.cjs +465 -0
  88. package/dist/openai-agents.cjs.map +1 -0
  89. package/dist/openai-agents.d.cts +75 -0
  90. package/dist/openai-agents.d.ts +75 -0
  91. package/dist/openai-agents.js +459 -0
  92. package/dist/openai-agents.js.map +1 -0
  93. package/dist/retell.cjs +846 -0
  94. package/dist/retell.cjs.map +1 -0
  95. package/dist/retell.d.cts +161 -0
  96. package/dist/retell.d.ts +161 -0
  97. package/dist/retell.js +841 -0
  98. package/dist/retell.js.map +1 -0
  99. package/dist/shared-Bb5B59Bu.d.ts +39 -0
  100. package/dist/shared-DTPWVlWm.d.cts +39 -0
  101. package/dist/strands.cjs +352 -0
  102. package/dist/strands.cjs.map +1 -0
  103. package/dist/strands.d.cts +72 -0
  104. package/dist/strands.d.ts +72 -0
  105. package/dist/strands.js +347 -0
  106. package/dist/strands.js.map +1 -0
  107. package/dist/twilio.cjs +174 -0
  108. package/dist/twilio.cjs.map +1 -0
  109. package/dist/twilio.d.cts +64 -0
  110. package/dist/twilio.d.ts +64 -0
  111. package/dist/twilio.js +167 -0
  112. package/dist/twilio.js.map +1 -0
  113. package/dist/vapi.cjs +661 -0
  114. package/dist/vapi.cjs.map +1 -0
  115. package/dist/vapi.d.cts +78 -0
  116. package/dist/vapi.d.ts +78 -0
  117. package/dist/vapi.js +656 -0
  118. package/dist/vapi.js.map +1 -0
  119. package/dist/voltagent.cjs +509 -0
  120. package/dist/voltagent.cjs.map +1 -0
  121. package/dist/voltagent.d.cts +70 -0
  122. package/dist/voltagent.d.ts +70 -0
  123. package/dist/voltagent.js +503 -0
  124. package/dist/voltagent.js.map +1 -0
  125. package/dist/webhook-1ozGyksG.d.ts +37 -0
  126. package/dist/webhook-CG4om_DK.d.cts +37 -0
  127. package/dist/whatsapp.cjs +214 -0
  128. package/dist/whatsapp.cjs.map +1 -0
  129. package/dist/whatsapp.d.cts +81 -0
  130. package/dist/whatsapp.d.ts +81 -0
  131. package/dist/whatsapp.js +207 -0
  132. package/dist/whatsapp.js.map +1 -0
  133. package/package.json +355 -7
package/README.md CHANGED
@@ -3,12 +3,15 @@
3
3
  [![npm](https://img.shields.io/npm/v/@niadra/sdk)](https://www.npmjs.com/package/@niadra/sdk)
4
4
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
5
5
 
6
- **Niadra is the shared customer memory for every AI agent in a company.** The WhatsApp agent, the
7
- voice agent, the billing agent and the human team read the same memory before they act and write
8
- back what they said and did. This package connects a TypeScript or JavaScript agent to it, on Node
9
- 18+ and on edge runtimes (Vercel, Cloudflare Workers, Deno).
10
-
11
- [Website](https://niadra.com/en) · [Documentation](https://niadra.com/en/docs) ·
6
+ **Niadra is the omnichannel memory layer for a company's AI agents.** It takes the events from
7
+ every channel, platform and system, builds one memory of each customer and delivers it to any AI
8
+ agent, from any vendor, under governance. The WhatsApp agent, the voice agent, the billing agent
9
+ inside the ERP and the human team read the same memory before they act and write back what they
10
+ said and did. This package connects a TypeScript or JavaScript agent to it, on
11
+ Node 20+, Deno, Bun, Cloudflare Workers and the Vercel Edge Runtime: it needs only `fetch` and
12
+ Web Crypto, and CI runs the build on each of them.
13
+
14
+ [Website](https://niadra.com/en) · [Documentation](https://docs.niadra.com/en) ·
12
15
  [Talk to us](https://niadra.com/en/enterprise) · [Python SDK](https://github.com/ainiadra/niadra-sdk-python)
13
16
 
14
17
  ```sh
@@ -18,21 +21,26 @@ npm install @niadra/sdk
18
21
  ## The problem it solves
19
22
 
20
23
  A customer tells your WhatsApp agent that order 4471 arrived with a broken lid and that she needs a
21
- replacement by Friday. An hour later she calls. Without shared memory, the voice agent asks her to
22
- explain everything again, and nobody remembers the Friday promise. With Niadra, the voice agent
23
- starts the call knowing about the open replacement and its deadline, and when the billing agent
24
- credits her invoice, the other agents see it within seconds.
24
+ replacement by Friday. An hour later she calls, and the voice agent runs on another vendor's
25
+ platform. Without a memory that spans both, the voice agent asks her to explain everything again,
26
+ and nobody remembers the Friday promise. With Niadra, the voice agent starts the call knowing about
27
+ the open replacement and its deadline, and when the billing agent credits her invoice in the ERP,
28
+ the other agents see it within seconds.
25
29
 
26
30
  Niadra does the remembering for you:
27
31
 
32
+ - it takes in every event, whatever its source: what the customer said on any channel, what the
33
+ CRM, the ERP or the help desk recorded, what an agent did, through the SDK, a webhook, a batch or
34
+ a file, and through the ready-made adapters below;
35
+ - it ties every event to the right person across phone numbers, e-mails, WhatsApp ids and CRM ids,
36
+ and to the companies and partners that person acts for;
28
37
  - it turns conversations and system events into facts, open items and promises, each with the
29
38
  turns that prove it;
30
- - it ties them to the right person across phone numbers, e-mails, WhatsApp ids and CRM ids, and to
31
- the companies and partners that person acts for;
32
39
  - it compiles a short context for each agent, holding back what the customer's verification level
33
40
  does not allow, and records a receipt of every read.
34
41
 
35
- Your agents keep their own models, prompts and vendors. Niadra is the memory layer they share.
42
+ Your agents keep their own models, prompts and vendors. Niadra is the memory layer they share, and
43
+ it stays neutral: no vendor reads another vendor's memory, and no framework owns it.
36
44
 
37
45
  ## Quickstart
38
46
 
@@ -81,8 +89,9 @@ resolves identity across channels and systems, closes items when a system of rec
81
89
  action, and filters what each agent may read by the verification level of the conversation.
82
90
 
83
91
  **What happens if Niadra is slow or down?** The agent keeps answering without the memory. Every
84
- call has its own time budget (150 ms for voice context, 300 ms otherwise) and resolves with an
85
- error value instead of throwing, unless you ask for strict mode.
92
+ call has its own time budget (300 ms for context; in a voice call the pack is served from memory
93
+ and a turn waits at most 200 ms for its slots) and resolves with an error value instead of
94
+ throwing, unless you ask for strict mode.
86
95
 
87
96
  **What about privacy and LGPD or GDPR?** Items carry a verification level and a purpose, and the
88
97
  policy decides what each agent sees. Every read leaves a receipt, and a person can be erased or
@@ -90,7 +99,8 @@ exported on request. The SDK never logs handles or message text.
90
99
 
91
100
  **Which models and frameworks does it work with?** Any. The context is text you place in your
92
101
  prompt, the tools follow the common function-calling format, and `wrap()` covers
93
- OpenAI-compatible clients.
102
+ OpenAI-compatible clients; `agent(text, { usage })` takes the usage of an OpenAI or Anthropic
103
+ response.
94
104
 
95
105
  ## Concepts
96
106
 
@@ -129,10 +139,13 @@ const messages = [...history, { role: "user", content: `${ctx.suffix}\n\n${userT
129
139
  ```
130
140
 
131
141
  - `text` is the pack. Inside a conversation the server pins it: the same bytes on every turn, so your model provider's prompt cache keeps hitting.
132
- - `suffix` holds what changes turn by turn: the delta and the live turns from other channels that the pack has not absorbed yet. It belongs at the end of the prompt, after the conversation.
142
+ - `suffix` holds what changes turn by turn: the live turns from other channels that the pack has not absorbed yet, this turn's slots and the delta, in that order. It belongs at the end of the prompt, after the conversation.
143
+ - `turn` is the customer's last turn; a conversation sends it for you. It goes as `query`, and the answer adds `response.slots`: what that turn needs from memory that the pinned pack left out (the protocol number the customer asks for, the earlier conversations on the same topic with their dates, or a line saying memory has no record of it), in `suffix` after the live turns and before the delta. The pack itself stays the pinned one.
144
+ - `niadra.prefetch({ subject, conversation_id, text })` sends a partial transcript while the customer is still speaking, so the read that answers the turn finds their memory warm. It runs in the background, one at a time per conversation (the newest text waits), and never rejects.
133
145
  - `delta: true` asks for what changed since this agent last read the subject. The server sends each change once, so the SDK hands each delta out once too, even one fetched by a background refresh; `conversation()` keeps them for you.
134
146
  - `source` says where the result came from (`network`, `cache`, `stale`, `fallback` or `none`), and `error` says what went wrong when something did.
135
147
  - Pass `object: "invoice:erp:0823"` instead of `subject` when the task is about a business object, and `about` for the account a person acts for.
148
+ - `format: "json"` also returns `pack`: the same pack as typed sections (`context-pack.v1`: `preamble`, `sections` with a stable `name`, a `layer` and their `lines`, `variables`, the `stamp` and this turn's `slots`, each with its `section`, `derived` kind, `channels` and `text`), for programs that build their own prompt. `convo.context({ format: "json" })` works the same way.
136
149
 
137
150
  ### Conversations
138
151
 
@@ -152,7 +165,13 @@ await convo.handoff({ target: "human", reason: "asked for a person" });
152
165
  await convo.end();
153
166
  ```
154
167
 
155
- After the first pack, each read also asks for the delta, and the conversation keeps every delta it receives, in order, in `suffix`, ahead of the live turns. When the server pins a new pack, after `verify()` for instance, the kept deltas are dropped: the new pack already has them. A read with `query` is a one-off and leaves them alone.
168
+ After the first pack, each read also asks for the delta, and the conversation keeps every delta it receives, in order, in `suffix`, after the live turns. When the server pins a new pack, after `verify()` for instance, the kept deltas are dropped: the new pack already has them. `query` picks a read's slots by other words than the turn; the pack is the pinned one all the same.
169
+
170
+ Every read sends the customer's last turn, the text of the last `customer()`, and the answer carries what it needs from memory as slots in `suffix`. Pass `turn` when the platform has the turn before `customer()` recorded it, or `turn: null` to read without one. In a voice call, `convo.prefetch(partialTranscript)` sends the turn so far while the customer speaks, and `convo.begin()` at call start (ringing, the inbound webhook) starts the first read so it runs while the call is set up; `await convo.ready()` waits for it there. Ending the conversation drops the pack and everything else the SDK kept in memory for it.
171
+
172
+ ### Voice
173
+
174
+ In the `voice` view with a conversation id, no turn waits on a round trip to the region for what can be known in advance. The first read starts at call start (`begin()`) and is awaited there (`ready()`, within `timeouts.contextVoiceStart`, 1.5 s). After that the pinned pack, which the server keeps byte-stable for the whole conversation, comes from memory at once and is revalidated by ETag in the background. While the customer speaks, `prefetch()` warms the server and, once the partial transcript has stayed the same for 200 ms, reads the turn with it; the final turn takes that read's slots and delta when its words start with the partial's and the partial carries at least three quarters of them. A turn waits at most `timeouts.contextVoice` (200 ms) for such a read still on its way; past that it gets the pack without slots, and the read, which goes on, leaves its delta for the next turn. The first voice read of a client measures the round trip to the region once (`GET /healthz`, `niadra.rtt`) and logs a warning when the budgets cannot hold it. `voice: false` sends every voice turn to the API as the other views do.
156
175
 
157
176
  Call `markInjected()` each time you put the pack in a prompt. The agent's turns and actions that follow carry it as `context_stamp`, with the pack's etag, which is how Niadra tells a context that arrived after the agent spoke from one the agent had and did not use. `timings` keeps the first injection and the first agent turn, for your own checks.
158
177
 
@@ -168,6 +187,22 @@ const completion = await openai.chat.completions.create({ model: "gpt-4.1", mess
168
187
 
169
188
  Every `chat.completions.create` and `chat.completions.parse` call through the wrapper, streaming or not, gets the pack after your leading system messages and the suffix at the end. The injection is stamped, and the model's answer (its first choice) is recorded as the agent's turn: at once, or when a stream ends or you stop reading it. `.withResponse()` keeps working and records too; `.asResponse()` returns the raw HTTP response, so nothing is recorded then. Pass a function instead of a conversation to pick one per call; when it returns `null`, the call passes through untouched. Nothing the wrapper does can fail your call: a context it cannot fetch is left out, and a failure to record the answer is logged, without content.
170
189
 
190
+ #### The provider's prompt cache
191
+
192
+ The agent's turn also carries the usage the provider reported for the call: every input token (`usage.prompt_tokens`), the ones read from its prompt cache (`usage.prompt_tokens_details.cached_tokens`) and, through gateways that pass Anthropic's fields along, the ones written to it. A stream reports usage only when you ask for it with `stream_options: { include_usage: true }`; the wrapper never changes your request to get it. Niadra sums the usage per agent, vendor and model, and the Console shows the cache's hit rate and the estimated savings next to the rest of the space's usage.
193
+
194
+ Without `wrap()`, pass the provider's response with the turn. An OpenAI response and an Anthropic one are both understood (Anthropic's `usage.input_tokens`, `cache_read_input_tokens` and `cache_creation_input_tokens`):
195
+
196
+ ```ts
197
+ const message = await anthropic.messages.create({ model: "claude-sonnet-4-5", max_tokens: 1024, system, messages });
198
+ convo.agent(textOf(message), { usage: message });
199
+
200
+ // or build it yourself
201
+ convo.agent(reply, { usage: { provider: "openai", model: "gpt-4.1", prompt_tokens: 3000, cached_tokens: 2048 } });
202
+ ```
203
+
204
+ `modelUsage(response)` reads one yourself. A response without usage is left out, and the turn is recorded either way.
205
+
171
206
  ### Tasks
172
207
 
173
208
  Internal agents (billing, collections, triage) work in tasks rather than conversations. A task centers its pack on its object, keeps deltas and stamps like a conversation, scopes the cache, groups events and ends with `task.ended`.
@@ -191,6 +226,8 @@ await task.end();
191
226
  | `handoff({ conversation_id, target })` | A transfer to a human or another agent | Sent at once |
192
227
  | `feedback({ subject, action, ... })` | A correction of what Niadra derived: `retract_fact`, `correct_fact`, `resolve_open_item`, `conversation_outcome` | Sent at once |
193
228
 
229
+ Queued items leave in batches when 15 are waiting or a second after the first one, whichever comes first. A message with a `conversation_id` is a turn the other agents read in `live`, so it leaves at once (`queue.turnFlushIntervalMs`, 0 by default), taking whatever else is waiting along. One batch is in flight per client, so turns queued while it is answered leave together in the next one: a burst of turns costs one request per round trip, not one per turn.
230
+
194
231
  Every item carries an idempotency key: the provider's message id when you pass one, a UUIDv7 otherwise. Retrying the same event is harmless.
195
232
 
196
233
  `identify()`, `verify()`, `handoff()` and `feedback()` resolve to `{ ok, idempotency_key, error }` once the server has answered. A correction is recorded as an event, so it is audited like any other. A `context()` call made after `identify()` or `verify()` resolves already reflects it.
@@ -211,10 +248,14 @@ if (first) {
211
248
 
212
249
  `search()` also reports recurrence: how many times the same kind of issue came back, and how it was last resolved.
213
250
 
251
+ `filters.when` takes the period in the customer's own words, in Portuguese, English or Spanish (`"semana passada"`, `"last week"`, `"en marzo"`); the answer says in `window` how the server read it, and lists in `ignored` a filter it could not read. Items whose validity ended (an event recorded with `valid_until`, such as an offer valid until Friday) leave reads unless you pass `show_expired: true`. An opened item carries its `versions`, oldest first.
252
+
253
+ The handle, the search and the conversation id go in request bodies, never in a URL: a conversation id may be a phone number or an e-mail. `open()` sends `POST /v1/history/open`, and the tool kit adds the bound customer to it, so the server opens only that customer's items.
254
+
214
255
  ### Business objects
215
256
 
216
257
  ```ts
217
- const { data: invoice } = await niadra.objectState("invoice:erp:0823"); // state, as_of, open items
258
+ const { data: invoice } = await niadra.objectState("invoice:erp:0823"); // each field with its logical value and freshness
218
259
  const { data: page } = await niadra.objectTimeline("invoice:erp:0823", { limit: 20 });
219
260
  ```
220
261
 
@@ -229,7 +270,7 @@ if (upload) {
229
270
  }
230
271
  ```
231
272
 
232
- Media never travels inside an event. `uploadMedia()` takes a `Uint8Array`, `ArrayBuffer` or `Blob`, reserves an upload, sends the bytes straight to storage over a signed URL (HTTPS only, with exactly the headers the signature covers and nothing else, so never your key or default headers; storage checks the body against the declared size and digest), and resolves with the reference and digest for the event. With `subject`, the file is stored under that person, so erasing them erases it even if no event ever references it. Hashing uses Web Crypto, which Node 18 only exposes behind a flag.
273
+ Media never travels inside an event. `uploadMedia()` takes a `Uint8Array`, `ArrayBuffer` or `Blob`, reserves an upload, sends the bytes straight to storage over a signed URL (HTTPS only, with exactly the headers the signature covers and nothing else, so never your key or default headers; storage checks the body against the declared size and digest), and resolves with the reference and digest for the event. With `subject`, the file is stored under that person, so erasing them erases it even if no event ever references it. Hashing uses Web Crypto.
233
274
 
234
275
  ### Tools for any model
235
276
 
@@ -246,7 +287,22 @@ for (const call of response.choices[0].message.tool_calls ?? []) {
246
287
  }
247
288
  ```
248
289
 
249
- The definitions use the `{ type: "function", function: { name, description, parameters } }` shape. For APIs that expect `{ name, description, input_schema }`, map `function.parameters` to `input_schema`.
290
+ The definitions use the `{ type: "function", function: { name, description, parameters } }` shape. For APIs that expect `{ name, description, input_schema }`, map `function.parameters` to `input_schema`. They are, word for word, the definitions the server publishes (`GET /v1/history/tools`) and the Python SDK ships, so a model sees one toolset whatever language the agent is written in; a test checks it byte for byte. The kit still reads the flat filter fields the 0.1 definitions offered.
291
+
292
+ ### The agent's own memory
293
+
294
+ Besides the customer's memory, an agent can keep working notes about its job: a procedure that worked, how a tool or a process of the company behaves, a pitfall to avoid. Never anything about a customer: the server refuses a note with personal data (422 `personal_data_in_agent_memory`) instead of masking it. The space turns it on; reading needs the `agent_memory` (or `context`) scope and writing `agent_memory:write`.
295
+
296
+ ```ts
297
+ const notes = await convo.agentMemory({ max_tokens: 300 }); // or niadra.agentMemory({ view, tags })
298
+ const system = [instructions, notes.text, ctx.text].filter(Boolean).join("\n\n");
299
+
300
+ const kit = convo.tools({ agentMemory: true, writeAgentMemory: true }); // + search_agent_memory and remember
301
+ await niadra.remember({ kind: "tool_note", title: "Dates need a time zone", body: "The scheduling API refuses dates without one.", tags: ["scheduling"] });
302
+ const { data: found } = await niadra.searchAgentMemory("credit on an invoice", { tags: ["erp"] });
303
+ ```
304
+
305
+ The block goes after your instructions and before the customer's context: it is the same for every customer, so it stays in the cacheable prefix. It is served from an ETag cache like the context, and is empty (never an error) when the space has it off. `remember()` waits for the server and resolves with the note, or with a `proposal_id` when the space wants a person to approve notes; through the `remember` tool, a refusal for personal data reaches the model as a request to rewrite the note. Every integration takes `agentMemory: true` (or `{ write: true, max_tokens, tags }`) to do all of this for you.
250
306
 
251
307
  ### Subject tokens for MCP
252
308
 
@@ -256,6 +312,395 @@ The definitions use the `{ type: "function", function: { name, description, para
256
312
  const { data } = await niadra.subjectToken({ subject: marina, conversation_id: "wa-8812", verification: "V1" });
257
313
  ```
258
314
 
315
+ ## Integrations
316
+
317
+ Each integration is a subpath of this package, with its framework as an optional peer dependency: `@niadra/sdk` itself loads no framework, and you install only the one you use. Every adapter wires the same five things into the framework's own lifecycle:
318
+
319
+ 1. **Context before the model call**: the pack after your instructions, the suffix (live turns, the turn's slots and deltas) at the end, within the read budget (in a voice call the pack comes from memory and a turn waits at most 200 ms for its slots).
320
+ 2. **Turns**: what the customer said and what the agent answered, with the provider's usage when the framework exposes it, and the end of the conversation.
321
+ 3. **Tools**: `search_customer_history`, `get_customer_timeline` and `open_history_item` in the framework's tool format, bound to the customer outside the model's reach. No tool has a parameter that names a customer.
322
+ 4. **Verification**: what the framework or the carrier proved, recorded with `verify()` before the first context read.
323
+ 5. **Handoff**: a transfer to another agent or to a person, recorded with `handoff()`.
324
+
325
+ All of it is fail-open: when Niadra is slow or down, the agent answers without memory, and nothing throws into the framework. Runnable examples are in [`examples/`](examples).
326
+
327
+ | Import | For | Tested with |
328
+ | --- | --- | --- |
329
+ | `@niadra/sdk/livekit` | LiveKit Agents (Node) | `@livekit/agents` 1.9.0 |
330
+ | `@niadra/sdk/elevenlabs` | ElevenLabs Agents Platform (webhooks and server tools) | recorded payloads; signatures checked against `@elevenlabs/elevenlabs-js` 2.69.0 |
331
+ | `@niadra/sdk/vapi` | Vapi (server URL) | recorded payloads typed with `@vapi-ai/server-sdk` 2.0.1 |
332
+ | `@niadra/sdk/whatsapp` | WhatsApp Cloud API (Meta webhooks) | recorded payloads, signatures computed in the test |
333
+ | `@niadra/sdk/twilio` | Twilio Voice, Messaging and Conversations webhooks | recorded payloads; signatures checked against `twilio` 6.1.1 |
334
+ | `@niadra/sdk/ai-sdk` | Vercel AI SDK 5, 6 and 7 | `ai` 7.0.114 with its mock models (v4 and v3 specifications) |
335
+ | `@niadra/sdk/mastra` | Mastra | `@mastra/core` 1.71.0, a real `Agent` over a mock model |
336
+ | `@niadra/sdk/langchain` | LangChain.js and LangGraph.js | `@langchain/core` 1.2.12, `@langchain/langgraph` 1.4.17, a real graph with `ToolNode` |
337
+ | `@niadra/sdk/openai-agents` | OpenAI Agents SDK (JavaScript) | `@openai/agents` 0.18.0, a real `Runner` over a scripted model |
338
+ | `@niadra/sdk/anthropic` | Anthropic SDK (`messages.create`, streaming or not) | `@anthropic-ai/sdk` 0.128.0 over recorded API answers |
339
+ | `@niadra/sdk/google-genai` | Google Gen AI SDK (`generateContent`, `generateContentStream`) | `@google/genai` 2.24.0 over recorded API answers |
340
+ | `@niadra/sdk/bedrock` | Amazon Bedrock Converse (`ConverseCommand`, `ConverseStreamCommand`) | `@aws-sdk/client-bedrock-runtime` 3.1140.0 with a recorded service answer |
341
+ | `@niadra/sdk/retell` | Retell AI (inbound and agent webhooks, custom functions, custom LLM websocket) | recorded payloads; signatures made by `retell-sdk` 6.0.1 and tool configurations typed with it |
342
+ | `@niadra/sdk/llamaindex` | LlamaIndex.TS (agents, multi-agent workflows, chat engines) | `@llamaindex/core` 0.6.23 and `@llamaindex/workflow` 1.1.25, real agents over a scripted LLM |
343
+ | `@niadra/sdk/genkit` | Genkit (Firebase Genkit for JavaScript) | `genkit` 1.42.0 with its own mock model, tool loop included |
344
+ | `@niadra/sdk/voltagent` | VoltAgent | `@voltagent/core` 2.10.0 on AI SDK 6.0.291 (its peer range), a real `Agent` over a mock model |
345
+ | `@niadra/sdk/google-adk` | Agent Development Kit for TypeScript (Google ADK) | `@google/adk` 2.1.0, a real `InMemoryRunner` with sub-agents over a scripted model |
346
+ | `@niadra/sdk/strands` | Strands Agents for TypeScript (AWS), Node 22+ | `@strands-agents/sdk` 1.19.0, a real `Agent` through its AI SDK model adapter |
347
+ | `@niadra/sdk/cloudflare-agents` | Cloudflare Agents SDK (`Agent`, `AIChatAgent`, voice agents, the Workers AI binding) | `agents` 0.24.0 types; run inside workerd by `pnpm runtimes` |
348
+
349
+ Pipecat has no subpath here: its pipeline, where the model call happens, runs in Python (the Python SDK has `niadra[pipecat]`), and its JavaScript packages are browser clients and transports, where a Niadra key must never go. Daily's JavaScript SDK is a browser call client too. LangGraph.js is covered by `@niadra/sdk/langchain`: `withNiadraContext()` inside the model node gives the context without writing it into the graph's checkpointed state.
350
+
351
+ Two more live in [`packages/`](packages), each with its own `package.json`, tests and README, apart from `@niadra/sdk`: [`n8n-nodes-niadra`](packages/n8n-nodes-niadra) (an n8n community node: Get Context, Track Turn, Search History, Verify, Handoff, End) and [`flowise-nodes-niadra`](packages/flowise-nodes-niadra) (a Flowise memory node that puts the context before every model call and records the turns, and a tool node with the kit).
352
+
353
+ ### LiveKit Agents
354
+
355
+ ```ts
356
+ import { NiadraAgent, NiadraMemory, attestationProof, sipConversationId, sipSubject } from "@niadra/sdk/livekit";
357
+
358
+ const caller = await ctx.waitForParticipant();
359
+ const conversation = niadra.conversation({
360
+ subject: sipSubject(caller), // sip.phoneNumber, else the identity
361
+ channel: "voice",
362
+ conversation_id: sipConversationId(caller, ctx.room.name), // sip.callID, else the room
363
+ });
364
+ const memory = new NiadraMemory({ conversation, verify: attestationProof(caller.attributes["sip.h.x-stir-verstat"]) });
365
+ memory.attach(session); // answers, handoffs, end of call
366
+ await session.start({ agent: new NiadraAgent({ instructions, memory }), room: ctx.room });
367
+ ```
368
+
369
+ `NiadraAgent` is a LiveKit `Agent` whose `onUserTurnCompleted` records the final transcript (with its STT confidence), then puts the pack in the turn's chat context right after the instructions and the suffix after the new message. LiveKit builds that context for one reply only, so nothing piles up in the agent's history and the prompt prefix stays the same turn after turn. The navigation kit joins the agent's own tools as the `niadra` toolset. With your own `Agent` subclass, call `memory.onUserTurnCompleted(turnCtx, newMessage)` from your hook and add `memory.toolset()` to its tools. `NiadraMemory` starts the caller's first read when it is built, and the first reply waits for it (see [Voice](#voice)). While the caller is still speaking, `attach(session)` also listens to `user_input_transcribed` (interim and final) and sends the turn so far with `prefetch()`, in the background, so the turn's slots are read before LiveKit ends the turn; a prefetch never holds or fails a turn.
370
+
371
+ `attach(session)` records the agent's answers from `conversation_item_added` (with the LLM usage LiveKit measured), a handoff for each `AgentHandoffItem` (`session.updateAgent()` or a tool that returns another agent), and the end of the conversation on `close`. Call `memory.handoffToHuman(reason)` right before a SIP transfer to a person. LiveKit's SIP attributes carry no STIR/SHAKEN attestation: map the carrier's header to a participant attribute in the trunk settings and pass it to `attestationProof()` (`A` proves V2, `B` and `C` prove V1).
372
+
373
+ ### ElevenLabs Agents Platform
374
+
375
+ For calls that reach ElevenLabs by phone, the integration lives on your server, in three webhooks. No ElevenLabs package is needed, and the handlers run on Node, Deno, Bun, Workers and the Edge Runtime.
376
+
377
+ ```ts
378
+ import { elevenLabs } from "@niadra/sdk/elevenlabs";
379
+
380
+ const handlers = elevenLabs({ niadra, secret: process.env.NIADRA_ELEVENLABS_SECRET, webhookSecret: process.env.ELEVENLABS_WEBHOOK_SECRET });
381
+ const respond = (c, { status, body }) => c.json(body, status); // Hono here; any framework works
382
+
383
+ app.post("/elevenlabs/initiation", async (c) => respond(c, await handlers.initiation(await c.req.json(), c.req.raw.headers)));
384
+ app.post("/elevenlabs/tools", async (c) => respond(c, await handlers.tool(await c.req.json(), c.req.raw.headers)));
385
+ app.post("/elevenlabs/post-call", async (c) => respond(c, await handlers.postCall(await c.req.text(), c.req.raw.headers)));
386
+ ```
387
+
388
+ - `initiation` answers the conversation initiation webhook: it opens the conversation by ElevenLabs' `conversation_id`, records what the call proved (`verify: (call) => attestationProof(...)`), reads the voice context and returns it as the dynamic variables `niadra_context` and `niadra_turn`. Put `{{niadra_context}}` in the agent's system prompt. When Niadra is slow or down, the call goes on with empty variables.
389
+ - `tool` serves the navigation kit as server tools. `handlers.toolConfigs({ url, secretId })` writes their configuration with the same descriptions as every other SDK; the conversation id and the caller come from ElevenLabs' system variables (`system__conversation_id`, `system__caller_id`), which the model never fills. The caller and level the initiation webhook saw are kept in a `CallStore`, in memory by default; pass one backed by your key-value store on serverless.
390
+ - `postCall` checks `ElevenLabs-Signature` (HMAC-SHA256 over `timestamp.body`, 30-minute window), records every turn of the transcript with its time in the call and the LLM usage ElevenLabs reports, records `transfer_to_agent` and `transfer_to_number` as handoffs, and ends the conversation.
391
+
392
+ The initiation and tool endpoints return customer context, so both require `secret` in the `x-niadra-secret` header: keep it as an ElevenLabs workspace secret and reference it in the webhook's and the tools' request headers. The full server is in [`examples/elevenlabs-hono.ts`](examples/elevenlabs-hono.ts).
393
+
394
+ ### Vapi
395
+
396
+ One handler for the assistant's server URL takes every server message and answers the ones that matter. It checks the server secret (`x-vapi-secret`, or `Authorization: Bearer`).
397
+
398
+ ```ts
399
+ import { vapi, vapiTools } from "@niadra/sdk/vapi";
400
+
401
+ const handle = vapi({ niadra, secret: process.env.VAPI_SERVER_SECRET, assistant: "YOUR_ASSISTANT_ID" });
402
+ app.post("/vapi", async (c) => {
403
+ const { status, body } = await handle(await c.req.json(), c.req.raw.headers);
404
+ return c.json(body, status);
405
+ });
406
+ ```
407
+
408
+ - `assistant-request` opens the conversation by Vapi's call id with the customer's number as the subject, records what the call proved (`verify`), reads the voice context and answers with your assistant: a saved one gets the context in its variables (`{{niadra_context}}`, `{{niadra_turn}}`); a transient one also gets the pack as a system message right after its own. `assistant` can be a function of the call and its context.
409
+ - `tool-calls` runs the navigation kit for the caller of that call. Add the tools to the assistant with `vapiTools({ url, secret })`, which carries the SDK's descriptions; tool calls that are not Niadra's go to `otherTool(name, args, call)`.
410
+ - `transfer-destination-request` and `transfer-update` record the transfer to a person, once per call, and answer with your `transfer(call)` destination.
411
+ - `end-of-call-report` records every spoken turn with its time and ends the conversation.
412
+
413
+ The full server is in [`examples/vapi-hono.ts`](examples/vapi-hono.ts).
414
+
415
+ ### Retell AI
416
+
417
+ The same design as ElevenLabs and Vapi, on your server. Every request is checked against `X-Retell-Signature` (HMAC-SHA256 of the raw body and its timestamp, keyed with the Retell API key that signs webhooks, five-minute window), so the handlers take the raw body.
418
+
419
+ ```ts
420
+ import { retell } from "@niadra/sdk/retell";
421
+
422
+ const handlers = retell({ niadra, apiKey: process.env.RETELL_API_KEY });
423
+ const respond = (c, { status, body }) => c.json(body, status);
424
+
425
+ app.post("/retell/inbound", async (c) => respond(c, await handlers.inbound(await c.req.text(), c.req.raw.headers)));
426
+ app.post("/retell/webhook", async (c) => respond(c, await handlers.webhook(await c.req.text(), c.req.raw.headers)));
427
+ app.post("/retell/tools", async (c) => respond(c, await handlers.tool(await c.req.text(), c.req.raw.headers)));
428
+ ```
429
+
430
+ - `inbound` answers the phone number's inbound webhook: it opens the conversation by `call_inbound.call_id`, records what the call proved (`verify`), reads the voice context and returns it as the dynamic variables `niadra_context` and `niadra_turn` (plus your `inboundFields(call)`, such as `override_agent_id`). Put `{{niadra_context}}` in the agent's prompt.
431
+ - `tool` serves the navigation kit as custom functions; `handlers.toolConfigs({ url })` writes them for the LLM's `general_tools`. The customer comes from the call Retell sends with each function call, never from the arguments; other functions go to `otherTool(name, args, call)`.
432
+ - `webhook` takes the agent webhook: `call_started` keeps the customer of outbound and web calls (the callee on outbound calls), `transfer_started` records the transfer to a person once, and `call_ended` records every utterance of `transcript_object` with its time in the call and ends the conversation.
433
+ - `llm(callId, { send, instructions })` serves a custom LLM websocket: `open()` asks for the call details (and speaks your greeting), `receive(event)` answers pings, records the utterances once a response is required and resolves to a turn whose `messages` carry your instructions, the pack and the call so far with the suffix at the end; `turn.respond(text)` sends the response (streamed with `{ complete: false }`, with `endCall` or `transferNumber`). Utterances carry the same idempotency key on the websocket and in `call_ended`, so recording both ways stores them once. Each `update_only` event whose transcript ends with the caller speaking sends that utterance so far with `prefetch()`, in the background.
434
+
435
+ ### WhatsApp Cloud API
436
+
437
+ Translation only: it never sends a message.
438
+
439
+ ```ts
440
+ import { readWhatsApp, recordInbound, recordOutbound, whatsAppChallenge } from "@niadra/sdk/whatsapp";
441
+
442
+ app.get("/whatsapp", (c) => c.text(whatsAppChallenge(new URL(c.req.url).searchParams, VERIFY_TOKEN).body));
443
+ app.post("/whatsapp", async (c) => {
444
+ const { status, messages } = await readWhatsApp(await c.req.text(), c.req.raw.headers, { appSecret: META_APP_SECRET });
445
+ for (const inbound of messages) {
446
+ const convo = niadra.conversation({ subject: inbound.subject, channel: "whatsapp", conversation_id: threadIdFor(inbound) });
447
+ recordInbound(convo, inbound); // keyed by the wamid: redeliveries are harmless
448
+ const ctx = await convo.context();
449
+ // ... answer with your model, send through the Graph API ...
450
+ recordOutbound(convo, reply, await sendResponse.json()); // keyed by the wamid Meta returned
451
+ }
452
+ return c.body(null, status);
453
+ });
454
+ ```
455
+
456
+ `readWhatsApp` checks `X-Hub-Signature-256` (HMAC-SHA256 of the raw body with the app secret) and reads each message with the customer as `wa_id`, the profile name, the business number, the text (or caption, or the title of a button or list reply), the media reference and the message it replies to. For media, download it with the Graph API, pass it to `niadra.uploadMedia()` and hand the result to `recordInbound(convo, inbound, upload)`. See [`examples/whatsapp-cloud.ts`](examples/whatsapp-cloud.ts).
457
+
458
+ ### Twilio
459
+
460
+ ```ts
461
+ import { readTwilio, recordTwilioInbound, verifyTwilio } from "@niadra/sdk/twilio";
462
+
463
+ const { status, request } = await readTwilio(`${PUBLIC_URL}/twilio/voice`, await c.req.text(), c.req.raw.headers, { authToken });
464
+ const convo = niadra.conversation({ subject: request.subject, channel: request.channel, conversation_id: request.conversationId });
465
+ await verifyTwilio(convo, request); // StirVerstat: TN-Validation-Passed-A proves V2, B and C prove V1
466
+ recordTwilioInbound(convo, request); // Body, or SpeechResult with its Confidence
467
+ ```
468
+
469
+ `readTwilio` checks `X-Twilio-Signature` against the exact public URL Twilio called and reads the request: WhatsApp by `WaId`, voice and SMS by the number on the customer's side (`To` on outbound calls), Conversations by `Author`; `CallSid` or `ConversationSid` as the conversation id; `MessageSid` as the idempotency key. Record the attestation once, on the call's first webhook, and open later ones at `request.proof.level`, as [`examples/twilio-voice.ts`](examples/twilio-voice.ts) does.
470
+
471
+ ### Vercel AI SDK
472
+
473
+ A language model middleware, so it works with every provider, and the navigation kit as AI SDK tools:
474
+
475
+ ```ts
476
+ import { streamText, wrapLanguageModel } from "ai";
477
+ import { niadraMiddleware, niadraTools } from "@niadra/sdk/ai-sdk";
478
+
479
+ const convo = niadra.conversation({ subject: handles.appUserId(session.userId), channel: "web_chat", conversation_id: chatId });
480
+ const result = streamText({
481
+ model: wrapLanguageModel({ model: openai("gpt-4.1"), middleware: niadraMiddleware(convo, { verify: { method: "login", level: "V2" } }) }),
482
+ system: "You are Acme's support agent.",
483
+ messages,
484
+ tools: { ...niadraTools(convo), ...yourTools },
485
+ });
486
+ ```
487
+
488
+ On every model call, `transformParams` records the newest user message as the customer's turn (once, however many steps a tool loop takes), puts the pack as a system message right after your system prompt, and adds the suffix as a text part at the end of the last user message, where every provider accepts it. `wrapGenerate` and `wrapStream` record the model's text as the agent's turn with the usage the provider reported, including prompt cache reads and writes; steps that only call tools record nothing. Pass a function instead of a conversation to wrap the model once and pick the conversation per call. The same middleware works with AI SDK 5, 6 and 7. See [`examples/ai-sdk.ts`](examples/ai-sdk.ts).
489
+
490
+ ### Mastra
491
+
492
+ A processor that goes in both of the agent's processor lists, and the navigation kit as Mastra tools. Mastra's own `Memory` stays as it is; Niadra is the customer's memory shared with the company's other agents.
493
+
494
+ ```ts
495
+ import { niadraProcessor, niadraTools } from "@niadra/sdk/mastra";
496
+
497
+ const niadraContext = niadraProcessor();
498
+ const support = new Agent({
499
+ id: "support", name: "Acme support", model: openai("gpt-4.1"),
500
+ instructions: "You are Acme's support agent.",
501
+ tools: ({ requestContext }) => niadraTools(requestContext.get("niadra")),
502
+ inputProcessors: [niadraContext],
503
+ outputProcessors: [niadraContext],
504
+ });
505
+
506
+ await support.generate(text, { requestContext: new RequestContext([["niadra", convo]]) });
507
+ ```
508
+
509
+ `processLLMRequest` rewrites only the prompt sent to the model, not the message list, so the pack and the suffix never land in Mastra's memory: the pack goes after the system messages, the suffix at the end of the last user message, and the customer's newest message is recorded once per turn. `processOutputResult` records the final answer with the usage Mastra summed for the run. The conversation comes from the request context under `niadra` (or pass `niadraProcessor({ session })`). For an agent that cannot take processors, `niadraInstructions(base)` returns dynamic instructions with the context, without recording turns. See [`examples/mastra.ts`](examples/mastra.ts).
510
+
511
+ ### LangChain.js and LangGraph.js
512
+
513
+ ```ts
514
+ import { NiadraCallbackHandler, niadraContext, niadraTools, withNiadraContext } from "@niadra/sdk/langchain";
515
+
516
+ // LCEL: a runnable before the model
517
+ const chain = niadraContext(convo).pipe(model);
518
+ await chain.invoke(messages, { callbacks: [new NiadraCallbackHandler(convo)] });
519
+
520
+ // LangGraph: inside the model node, so the context never lands in the graph's state
521
+ const tools = niadraTools(convo);
522
+ graph.addNode("agent", async (state) => ({ messages: [await model.bindTools(tools).invoke(await withNiadraContext(convo, state.messages))] }));
523
+ graph.addNode("tools", new ToolNode(tools));
524
+ ```
525
+
526
+ `niadraContext` and `withNiadraContext` record the newest human message once and return the messages with the pack as a system message after the leading ones and the suffix at the end of the last human message. `NiadraCallbackHandler` records each answer with the usage LangChain standardizes in `usage_metadata` (prompt cache reads and writes included); answers that only call tools record nothing. `niadraTools` returns `DynamicStructuredTool`s bound to the customer. See [`examples/langgraph.ts`](examples/langgraph.ts).
527
+
528
+ ### OpenAI Agents SDK
529
+
530
+ ```ts
531
+ import { NiadraSession, niadraInstructions, niadraRunHooks, niadraTools } from "@niadra/sdk/openai-agents";
532
+
533
+ const agent = new Agent({
534
+ name: "Support",
535
+ instructions: niadraInstructions("You are Acme's support agent.", convo),
536
+ tools: niadraTools(convo),
537
+ });
538
+ niadraRunHooks(runner, convo); // handoffs between agents
539
+ await runner.run(agent, text, { session: new NiadraSession(convo) });
540
+ ```
541
+
542
+ `niadraInstructions` makes the instructions dynamic: your text, then the pack, then the suffix (the SDK builds the system prompt from the instructions alone). `NiadraSession` is a `Session` that keeps the run's items in another session (`MemorySession` by default, or yours as `inner`) and records the customer's messages and the agent's answers. `niadraTools` returns non-strict function tools bound to the customer, since the canonical schemas have optional fields. `niadraRunHooks` records each `agent_handoff`. See [`examples/openai-agents.ts`](examples/openai-agents.ts).
543
+
544
+ ### LlamaIndex.TS
545
+
546
+ ```ts
547
+ import { agent } from "@llamaindex/workflow";
548
+ import { NiadraMemory, niadraTools } from "@niadra/sdk/llamaindex";
549
+
550
+ const support = agent({ llm, systemPrompt: "You are Acme's support agent.", tools: niadraTools(convo), memory: new NiadraMemory(convo) });
551
+ await support.run(text);
552
+ ```
553
+
554
+ `NiadraMemory` is a LlamaIndex `Memory` (it takes the same messages and options as `createMemory()`), so it serves agents, multi-agent workflows and chat engines alike. In `getLLM()`, which every model call goes through, it records the customer's newest message once and returns a copy of the messages with the pack after the leading system messages and the suffix at the end of the last user message; the stored history keeps only what was said. `add()` records the final answer and a `handOff` between agents. For a memory you build yourself, `NiadraMemoryBlock` gives the same context as a fixed block (priority 0). `niadraTools` returns `FunctionTool`s with the canonical JSON Schemas.
555
+
556
+ ### Genkit
557
+
558
+ ```ts
559
+ import { niadraMiddleware, niadraTools } from "@niadra/sdk/genkit";
560
+
561
+ const { text } = await ai.generate({
562
+ model: googleAI.model("gemini-2.5-flash"),
563
+ system: "You are Acme's support agent.",
564
+ prompt: text,
565
+ tools: niadraTools(convo),
566
+ use: [niadraMiddleware(convo, { model: "googleai/gemini-2.5-flash" })],
567
+ });
568
+ ```
569
+
570
+ The middleware runs around every model call of the request, tool loop included: the pack joins your system message as a text part (several providers read only one system message), the suffix joins the last user message, the customer's newest message is recorded once and the answer with Genkit's usage (`inputTokens`, `cachedContentTokens`) when you name the model, which a model middleware does not see. The tools are unregistered Genkit tools, so each request carries the ones bound to its own customer.
571
+
572
+ ### VoltAgent
573
+
574
+ ```ts
575
+ import { niadraHooks, niadraTools } from "@niadra/sdk/voltagent";
576
+
577
+ const support = new Agent({ name: "support", instructions: "You are Acme's support agent.", model: openai("gpt-4.1"), hooks: niadraHooks() });
578
+ await support.generateText(text, { context: { niadra: convo }, tools: niadraTools(convo) });
579
+ ```
580
+
581
+ `onPrepareModelMessages` puts the pack after your instructions and the suffix at the end of the last user message only in what goes to the model, so VoltAgent's own memory keeps what was said; `onEnd` records the answer with the operation's usage; `onHandoff` records delegations to sub-agents when the hooks are built for one conversation. The conversation comes from the operation context under `niadra`. VoltAgent 2.x runs on AI SDK 6.
582
+
583
+ ### Google ADK
584
+
585
+ ```ts
586
+ import { niadraAdk } from "@niadra/sdk/google-adk";
587
+
588
+ const memory = niadraAdk({
589
+ session: (context) => niadra.conversation({ subject: handles.appUserId(context.userId), channel: "web_chat", conversation_id: context.sessionId }),
590
+ });
591
+ const support = new LlmAgent({ name: "support", model: "gemini-2.5-flash", instruction: "You are Acme's support agent.", ...memory });
592
+ ```
593
+
594
+ One agent definition serves every ADK session: `session` is called once per ADK session id. `beforeModelCallback` records the user's message of each invocation once and adds the pack after your instruction in `systemInstruction` and the suffix to the last user content; ADK rebuilds the request from the session's events on every call, so nothing lands in the session. `afterModelCallback` records the final answer with `usageMetadata` and `transfer_to_agent` as a handoff. The tools declare the canonical JSON Schemas (`parametersJsonSchema`) and find the customer from the tool's context. Give the same `...memory` to sub-agents.
595
+
596
+ ### Strands Agents
597
+
598
+ ```ts
599
+ import { NiadraPlugin } from "@niadra/sdk/strands";
600
+
601
+ const support = new Agent({ model, systemPrompt: "You are Acme's support agent.", plugins: [new NiadraPlugin(convo)] });
602
+ await support.invoke(text);
603
+ ```
604
+
605
+ A Strands plugin: an input middleware of `InvokeModelStage` adds the pack after your system prompt and folds the suffix into the last user message (keeping the cache point before it, as Strands' own context injector does) without touching `agent.messages`; an output middleware records the answer with the model's usage (Bedrock and Anthropic count cached tokens apart, the others inside); `getTools()` adds the kit. Strands for TypeScript needs Node 22 or later.
606
+
607
+ ### Cloudflare Agents SDK
608
+
609
+ ```ts
610
+ import { niadraAgent } from "@niadra/sdk/cloudflare-agents";
611
+
612
+ export class Support extends AIChatAgent<Env> {
613
+ async onChatMessage() {
614
+ niadra ??= new Niadra({ apiKey: this.env.NIADRA_API_KEY });
615
+ const memory = niadraAgent(this, { niadra, subject: handles.appUserId(this.name) });
616
+ const result = streamText({
617
+ model: wrapLanguageModel({ model: workersAI("@cf/openai/gpt-oss-120b"), middleware: memory.middleware }),
618
+ system: "You are Acme's support agent.",
619
+ messages: await convertToModelMessages(this.messages),
620
+ tools: { ...memory.tools(), ...yourTools },
621
+ });
622
+ return result.toUIMessageStreamResponse();
623
+ }
624
+ }
625
+ ```
626
+
627
+ `niadraAgent(this, ...)` returns one helper per agent instance (a Durable Object), bound to a conversation whose id is the agent's name unless you give another. `middleware` is the AI SDK middleware with one addition: after each answer it hands the queued writes to `ctx.waitUntil()`. For a model called without the AI SDK (the Workers AI binding, a voice agent's `onTurn()`), `prepare(messages)` returns the messages with the context in place and `record(text, { usage: workersAiUsage(answer, model) })` records the answer. Only web APIs and the AI SDK: `pnpm runtimes` bundles it and runs it inside workerd.
628
+
629
+ ### Anthropic, Google Gen AI and Amazon Bedrock
630
+
631
+ The same idea as `wrap()` for OpenAI, one wrapper per SDK. The client itself is never modified.
632
+
633
+ ```ts
634
+ import { wrapAnthropic } from "@niadra/sdk/anthropic";
635
+ import { wrapGoogleGenAI } from "@niadra/sdk/google-genai";
636
+ import { wrapBedrock } from "@niadra/sdk/bedrock";
637
+
638
+ const claude = wrapAnthropic(new Anthropic(), convo); // messages.create, streaming or not
639
+ const gemini = wrapGoogleGenAI(new GoogleGenAI({}), convo); // models.generateContent and generateContentStream
640
+ const bedrock = wrapBedrock(new BedrockRuntimeClient({}), convo); // ConverseCommand and ConverseStreamCommand
641
+ ```
642
+
643
+ Each call gets the pack after your system text (Anthropic's `system`, Gemini's `config.systemInstruction`, one more Converse `system` block) and the suffix at the end of the last user message; the newest user text is recorded as the customer's turn and the answer as the agent's, with the provider's usage and prompt cache counts: Anthropic's `input_tokens`, `cache_read_input_tokens` and `cache_creation_input_tokens`, Gemini's `promptTokenCount` and `cachedContentTokenCount`, Bedrock's `inputTokens`, `cacheReadInputTokens` and `cacheWriteInputTokens`. On Anthropic, the pack's system block gets a cache breakpoint only when you already use prompt caching and one of the four is left. `.withResponse()` keeps working. For Anthropic's `messages.stream()` helper, prepare the body with `anthropicParams(convo, body)` and pass the final message to `recordAnthropic(convo, message)`. Azure OpenAI needs nothing new: `AzureOpenAI` has the OpenAI client's shape, so `wrap()` covers it. See [`examples/anthropic.ts`](examples/anthropic.ts), [`examples/google-genai.ts`](examples/google-genai.ts) and [`examples/bedrock.ts`](examples/bedrock.ts).
644
+
645
+ ## The agent core
646
+
647
+ What an agent does in a turn, recorded and checked in its own process, with Niadra never on the agent's
648
+ path. Every feature below is off until the space turns it on; a space that did not ask sees no change.
649
+
650
+ ```ts
651
+ import { Niadra, handles } from "@niadra/sdk";
652
+
653
+ const niadra = new Niadra({ apiKey: process.env.NIADRA_API_KEY });
654
+ const checkPrice = niadra.tool("check_price", (sku: string) => catalog[sku], {
655
+ // the objects a result showed, for the claim contract
656
+ provenance: (p) => [{ ref: `product:store:${p.sku}`, fields: { price_sale: p.price_sale } }],
657
+ });
658
+
659
+ const convo = niadra.conversation({
660
+ subject: handles.phone("+5511912345678"),
661
+ channel: "whatsapp",
662
+ conversation_id: "thread-82",
663
+ agent_id: "store",
664
+ });
665
+ convo.customer("Quanto está o vestido PX?");
666
+ await convo.turn({ build: Niadra.build({ prompts: { store: "v16" }, model: "gpt-4.1-mini" }) }, async () => {
667
+ const ctx = await convo.context({ include: ["state", "constraints"] });
668
+ checkPrice("PX-4471");
669
+ const guarded = await convo.claims.guardText(draft); // the claim contract acts before the text goes
670
+ convo.agent(guarded.text);
671
+ });
672
+ ```
673
+
674
+ | Concept | In the SDK | Example |
675
+ | --- | --- | --- |
676
+ | Turn records | `conversation.turn()`, `niadra.tool()`, the adapters' `turns: true` | `examples/claim-guard.ts` |
677
+ | Claims | `conversation.claims.guard()`, `guardText()`, `check()`; `niadra.internalText` | `examples/claim-guard.ts` |
678
+ | Coordination | `conversation.check()`, `declare`, `claim()`; `niadra.mayContact()`; `niadra.contactGateway()` | `examples/coordination.ts` |
679
+ | Typed state | `context({ include: ["state", "constraints", "coordination", "budget"] })`, `niadra.verifyClaim()`, `niadra.resolvers` | `examples/object-state.ts` |
680
+ | Working state | `conversation.agentState.get()` and `put()` | `examples/working-state.ts` |
681
+ | Field access | `niadra.tool(name, fn, { maskOutput: true })` | `examples/masked-tool.ts` |
682
+ | Replay | `Replayer`, `npx niadra replay` | |
683
+ | Tool counterfactual | `Counterfactual`, `npx niadra counterfactual` | `examples/tool-counterfactual.ts` |
684
+ | Type derivation and the claim contract in CI | `npx niadra types derive --check`, `npx niadra contract test` | `examples/ci/niadra-checks.yml` |
685
+
686
+ - **Turn records** follow the async context, so parallel sub-agents keep their own turns, and leave from a
687
+ bounded queue in the background, in the content mode the space names. Closing a turn never waits for the
688
+ network.
689
+ - **Include blocks** come in the same read as the pack. The state view's lines and the constraints go in
690
+ `suffix` after the slots, inside one `<niadra>` section, byte for byte what the Python SDK writes; a read
691
+ without `include` keeps its suffix. With Niadra down, the last good blocks serve.
692
+ - **Coordination** decides by each purpose's direction when Niadra does not answer within 200 ms: a
693
+ customer's message and service go, marketing, retention, collection and an effect with a key wait, and
694
+ the local copy of the opt-out list always holds.
695
+ - **Tool bindings** the space declares come in the SDK profile: a tool without `binding` in code measures the
696
+ constraints block through the one served for its name, and its `capabilities.mask_output` decides the
697
+ masking when the code leaves `maskOutput` unset.
698
+ - **The `niadra` command** (Node) runs `replay`, `counterfactual`, `resolver-worker` (the space's refresh
699
+ requests, read with your resolvers inside your boundary; a watch fires only on a value the worker
700
+ confirmed, and a resolver returns `NOT_FOUND` when the source no longer has the object), `types derive` and
701
+ `contract test`, with the Python command's arguments and exit codes.
702
+ - `niadra.api` has one typed method per route of these features; unlike the rest of the client, it rejects.
703
+
259
704
  ## Failure behavior
260
705
 
261
706
  Memory should make an agent better, never make it fail. By default:
@@ -273,17 +718,18 @@ Memory should make an agent better, never make it fail. By default:
273
718
  | Queue full (10,000 items) | New events are dropped and logged. |
274
719
  | Server rejects one item of a batch (207) | Only that item fails; the rest are stored. |
275
720
 
276
- Every read has its own time budget, independent of your platform's:
721
+ Every call you wait for has its own time budget for the whole call, retries and waits included, independent of your platform's:
277
722
 
278
723
  | Call | Default |
279
724
  | --- | --- |
280
- | `context()` | 300 ms, 150 ms with `view: "voice"` |
725
+ | `context()` | 300 ms, 200 ms with `view: "voice"` (in a voice conversation, only the wait for a turn's slots: the pack comes from memory) |
726
+ | The first read of a voice call (`begin()`, `ready()`) | 1.5 s, spent while the phone rings or the inbound webhook runs |
281
727
  | `search()`, `timeline()`, `open()`, `objectState()`, `objectTimeline()` | 600 ms, 300 ms through voice conversations and voice-bound tools |
282
728
  | `subjectToken()` | 2 s |
283
- | Each attempt of a batch, `feedback()` or an upload reservation | 5 s |
284
- | Each attempt of an `uploadMedia()` transfer | 60 s |
729
+ | `identify()`, `verify()`, `handoff()`, `feedback()` and the reservation in `uploadMedia()` | 5 s |
730
+ | The transfer in `uploadMedia()` | 60 s |
285
731
 
286
- Override them with `timeouts`, or per call with `{ timeout }`. Pass `{ signal }` to cancel a call.
732
+ An `identify()`, `verify()` or `handoff()` that runs out of time resolves with a `NiadraTimeoutError` and stays in the queue, which keeps sending it. `track()` never waits; each attempt of a background batch has 5 s. Override them with `timeouts`, or per call with `{ timeout }`. Pass `{ signal }` to cancel a call.
287
733
 
288
734
  ### The context cache
289
735
 
@@ -310,7 +756,6 @@ Configure it with `cache: { ttlMs, staleWhileRevalidateMs, maxStaleMs, maxEntrie
310
756
 
311
757
  - **Long-running Node services:** queued events are flushed when the event loop runs out of work (`beforeExit`). That event does not fire on signals or `process.exit()`, so call `await niadra.shutdown()` in your SIGTERM handler.
312
758
  - **Serverless functions:** `await niadra.flush()` before returning.
313
- - **Edge runtimes:** pass the flush to the platform, for example `ctx.waitUntil(niadra.flush())`.
314
759
 
315
760
  Create one client per process and share it: it owns the queue and the cache.
316
761
 
@@ -320,9 +765,10 @@ Create one client per process and share it: it owns the queue and the cache.
320
765
  new Niadra({
321
766
  apiKey: "nia_sk_live_...", // default: NIADRA_API_KEY
322
767
  baseURL: "http://localhost:4010", // default: NIADRA_BASE_URL, then derived from the key
323
- timeouts: { context: 300, contextVoice: 150, navigation: 600, navigationVoice: 300, write: 5000, token: 2000, upload: 60_000 },
768
+ timeouts: { context: 300, contextVoice: 200, contextVoiceStart: 1500, navigation: 600, navigationVoice: 300, write: 5000, token: 2000, upload: 60_000 },
769
+ voice: { enabled: true, settleMs: 200, minCoverage: 0.75, probe: true },
324
770
  cache: { ttlMs: 10_000, staleWhileRevalidateMs: 600_000, maxStaleMs: 1_800_000, maxEntries: 1000 },
325
- queue: { flushAt: 15, flushIntervalMs: 1000, maxBatchSize: 100, maxQueueSize: 10_000, maxAttempts: 3 },
771
+ queue: { flushAt: 15, flushIntervalMs: 1000, turnFlushIntervalMs: 0, maxBatchSize: 100, maxQueueSize: 10_000, maxAttempts: 3 },
326
772
  strict: false,
327
773
  flushOnExit: true,
328
774
  logger: console, // anything with debug, warn and error
@@ -343,18 +789,18 @@ Include `requestId` when you contact support.
343
789
 
344
790
  ```sh
345
791
  pnpm install
346
- pnpm check # typecheck, lint, tests
347
- pnpm build # ESM and CommonJS into dist/
792
+ pnpm check # typecheck, lint, tests (the integrations with their frameworks' real types)
793
+ pnpm build # ESM and CommonJS into dist/, one entry per integration
794
+ pnpm runtimes # the build on Deno, Bun, workerd and the Edge Runtime
795
+ pnpm --filter "./packages/*" check # the n8n and Flowise nodes
348
796
  ```
349
797
 
350
- ## Em português
798
+ VoltAgent 2.x runs on AI SDK 6 while every other test runs on AI SDK 7; `.pnpmfile.cjs` gives VoltAgent its own copy at install. The Strands tests run on Node 22 and later and skip on Node 20.
799
+
800
+ ## Documentation in Portuguese
351
801
 
352
- A Niadra é a memória de clientes compartilhada por todos os agentes de IA de uma empresa: o agente
353
- do WhatsApp, o de voz, o de cobrança e o time humano leem a mesma memória antes de agir e registram
354
- o que disseram e fizeram. Este pacote conecta um agente em TypeScript ou JavaScript a essa memória:
355
- `context()` antes de chamar o modelo, `track()` depois, e `action()` quando o agente faz algo num
356
- sistema. Documentação em [niadra.com/docs](https://niadra.com/docs) e contato em
357
- [niadra.com/enterprise](https://niadra.com/enterprise).
802
+ The documentation is also available in Portuguese at [docs.niadra.com](https://docs.niadra.com),
803
+ and the contact page in Portuguese at [niadra.com/enterprise](https://niadra.com/enterprise).
358
804
 
359
805
  ## License
360
806