@niadra/sdk 0.1.1 → 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 +160 -68
  2. package/README.md +451 -22
  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 +11421 -1895
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.d.cts +959 -1294
  58. package/dist/index.d.ts +959 -1294
  59. package/dist/index.js +11368 -1896
  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 +351 -6
package/README.md CHANGED
@@ -3,9 +3,11 @@
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
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
9
11
  Node 20+, Deno, Bun, Cloudflare Workers and the Vercel Edge Runtime: it needs only `fetch` and
10
12
  Web Crypto, and CI runs the build on each of them.
11
13
 
@@ -19,21 +21,26 @@ npm install @niadra/sdk
19
21
  ## The problem it solves
20
22
 
21
23
  A customer tells your WhatsApp agent that order 4471 arrived with a broken lid and that she needs a
22
- replacement by Friday. An hour later she calls. Without shared memory, the voice agent asks her to
23
- explain everything again, and nobody remembers the Friday promise. With Niadra, the voice agent
24
- starts the call knowing about the open replacement and its deadline, and when the billing agent
25
- 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.
26
29
 
27
30
  Niadra does the remembering for you:
28
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;
29
37
  - it turns conversations and system events into facts, open items and promises, each with the
30
38
  turns that prove it;
31
- - it ties them to the right person across phone numbers, e-mails, WhatsApp ids and CRM ids, and to
32
- the companies and partners that person acts for;
33
39
  - it compiles a short context for each agent, holding back what the customer's verification level
34
40
  does not allow, and records a receipt of every read.
35
41
 
36
- 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.
37
44
 
38
45
  ## Quickstart
39
46
 
@@ -82,8 +89,9 @@ resolves identity across channels and systems, closes items when a system of rec
82
89
  action, and filters what each agent may read by the verification level of the conversation.
83
90
 
84
91
  **What happens if Niadra is slow or down?** The agent keeps answering without the memory. Every
85
- call has its own time budget (150 ms for voice context, 300 ms otherwise) and resolves with an
86
- 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.
87
95
 
88
96
  **What about privacy and LGPD or GDPR?** Items carry a verification level and a purpose, and the
89
97
  policy decides what each agent sees. Every read leaves a receipt, and a person can be erased or
@@ -131,10 +139,13 @@ const messages = [...history, { role: "user", content: `${ctx.suffix}\n\n${userT
131
139
  ```
132
140
 
133
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.
134
- - `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.
135
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.
136
146
  - `source` says where the result came from (`network`, `cache`, `stale`, `fallback` or `none`), and `error` says what went wrong when something did.
137
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.
138
149
 
139
150
  ### Conversations
140
151
 
@@ -154,7 +165,13 @@ await convo.handoff({ target: "human", reason: "asked for a person" });
154
165
  await convo.end();
155
166
  ```
156
167
 
157
- 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.
158
175
 
159
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.
160
177
 
@@ -209,7 +226,7 @@ await task.end();
209
226
  | `handoff({ conversation_id, target })` | A transfer to a human or another agent | Sent at once |
210
227
  | `feedback({ subject, action, ... })` | A correction of what Niadra derived: `retract_fact`, `correct_fact`, `resolve_open_item`, `conversation_outcome` | Sent at once |
211
228
 
212
- 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 within 200 ms (`queue.turnFlushIntervalMs`), taking whatever else is waiting along.
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.
213
230
 
214
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.
215
232
 
@@ -231,12 +248,14 @@ if (first) {
231
248
 
232
249
  `search()` also reports recurrence: how many times the same kind of issue came back, and how it was last resolved.
233
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
+
234
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.
235
254
 
236
255
  ### Business objects
237
256
 
238
257
  ```ts
239
- 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
240
259
  const { data: page } = await niadra.objectTimeline("invoice:erp:0823", { limit: 20 });
241
260
  ```
242
261
 
@@ -268,7 +287,22 @@ for (const call of response.choices[0].message.tool_calls ?? []) {
268
287
  }
269
288
  ```
270
289
 
271
- 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.
272
306
 
273
307
  ### Subject tokens for MCP
274
308
 
@@ -278,6 +312,395 @@ The definitions use the `{ type: "function", function: { name, description, para
278
312
  const { data } = await niadra.subjectToken({ subject: marina, conversation_id: "wa-8812", verification: "V1" });
279
313
  ```
280
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
+
281
704
  ## Failure behavior
282
705
 
283
706
  Memory should make an agent better, never make it fail. By default:
@@ -299,7 +722,8 @@ Every call you wait for has its own time budget for the whole call, retries and
299
722
 
300
723
  | Call | Default |
301
724
  | --- | --- |
302
- | `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 |
303
727
  | `search()`, `timeline()`, `open()`, `objectState()`, `objectTimeline()` | 600 ms, 300 ms through voice conversations and voice-bound tools |
304
728
  | `subjectToken()` | 2 s |
305
729
  | `identify()`, `verify()`, `handoff()`, `feedback()` and the reservation in `uploadMedia()` | 5 s |
@@ -341,9 +765,10 @@ Create one client per process and share it: it owns the queue and the cache.
341
765
  new Niadra({
342
766
  apiKey: "nia_sk_live_...", // default: NIADRA_API_KEY
343
767
  baseURL: "http://localhost:4010", // default: NIADRA_BASE_URL, then derived from the key
344
- 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 },
345
770
  cache: { ttlMs: 10_000, staleWhileRevalidateMs: 600_000, maxStaleMs: 1_800_000, maxEntries: 1000 },
346
- queue: { flushAt: 15, flushIntervalMs: 1000, turnFlushIntervalMs: 200, maxBatchSize: 100, maxQueueSize: 10_000, maxAttempts: 3 },
771
+ queue: { flushAt: 15, flushIntervalMs: 1000, turnFlushIntervalMs: 0, maxBatchSize: 100, maxQueueSize: 10_000, maxAttempts: 3 },
347
772
  strict: false,
348
773
  flushOnExit: true,
349
774
  logger: console, // anything with debug, warn and error
@@ -364,10 +789,14 @@ Include `requestId` when you contact support.
364
789
 
365
790
  ```sh
366
791
  pnpm install
367
- pnpm check # typecheck, lint, tests
368
- 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
369
796
  ```
370
797
 
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
+
371
800
  ## Documentation in Portuguese
372
801
 
373
802
  The documentation is also available in Portuguese at [docs.niadra.com](https://docs.niadra.com),