@niadra/sdk 0.1.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +200 -62
- package/README.md +462 -24
- package/dist/ai-sdk.cjs +1036 -0
- package/dist/ai-sdk.cjs.map +1 -0
- package/dist/ai-sdk.d.cts +118 -0
- package/dist/ai-sdk.d.ts +118 -0
- package/dist/ai-sdk.js +1029 -0
- package/dist/ai-sdk.js.map +1 -0
- package/dist/anthropic.cjs +528 -0
- package/dist/anthropic.cjs.map +1 -0
- package/dist/anthropic.d.cts +35 -0
- package/dist/anthropic.d.ts +35 -0
- package/dist/anthropic.js +523 -0
- package/dist/anthropic.js.map +1 -0
- package/dist/bedrock.cjs +392 -0
- package/dist/bedrock.cjs.map +1 -0
- package/dist/bedrock.d.cts +25 -0
- package/dist/bedrock.d.ts +25 -0
- package/dist/bedrock.js +388 -0
- package/dist/bedrock.js.map +1 -0
- package/dist/cli.js +11101 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-B_ip8T2o.d.cts +6960 -0
- package/dist/client-B_ip8T2o.d.ts +6960 -0
- package/dist/cloudflare-agents.cjs +626 -0
- package/dist/cloudflare-agents.cjs.map +1 -0
- package/dist/cloudflare-agents.d.cts +113 -0
- package/dist/cloudflare-agents.d.ts +113 -0
- package/dist/cloudflare-agents.js +622 -0
- package/dist/cloudflare-agents.js.map +1 -0
- package/dist/elevenlabs.cjs +738 -0
- package/dist/elevenlabs.cjs.map +1 -0
- package/dist/elevenlabs.d.cts +86 -0
- package/dist/elevenlabs.d.ts +86 -0
- package/dist/elevenlabs.js +733 -0
- package/dist/elevenlabs.js.map +1 -0
- package/dist/genkit.cjs +323 -0
- package/dist/genkit.cjs.map +1 -0
- package/dist/genkit.d.cts +67 -0
- package/dist/genkit.d.ts +67 -0
- package/dist/genkit.js +318 -0
- package/dist/genkit.js.map +1 -0
- package/dist/google-adk.cjs +705 -0
- package/dist/google-adk.cjs.map +1 -0
- package/dist/google-adk.d.cts +91 -0
- package/dist/google-adk.d.ts +91 -0
- package/dist/google-adk.js +701 -0
- package/dist/google-adk.js.map +1 -0
- package/dist/google-genai.cjs +422 -0
- package/dist/google-genai.cjs.map +1 -0
- package/dist/google-genai.d.cts +26 -0
- package/dist/google-genai.d.ts +26 -0
- package/dist/google-genai.js +418 -0
- package/dist/google-genai.js.map +1 -0
- package/dist/index.cjs +11494 -1910
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +957 -1294
- package/dist/index.d.ts +957 -1294
- package/dist/index.js +11441 -1911
- package/dist/index.js.map +1 -1
- package/dist/intercept-CBaPK_-k.d.ts +20 -0
- package/dist/intercept-CxmBLfoj.d.cts +20 -0
- package/dist/langchain.cjs +1076 -0
- package/dist/langchain.cjs.map +1 -0
- package/dist/langchain.d.cts +102 -0
- package/dist/langchain.d.ts +102 -0
- package/dist/langchain.js +1067 -0
- package/dist/langchain.js.map +1 -0
- package/dist/livekit.cjs +404 -0
- package/dist/livekit.cjs.map +1 -0
- package/dist/livekit.d.cts +113 -0
- package/dist/livekit.d.ts +113 -0
- package/dist/livekit.js +398 -0
- package/dist/livekit.js.map +1 -0
- package/dist/llamaindex.cjs +349 -0
- package/dist/llamaindex.cjs.map +1 -0
- package/dist/llamaindex.d.cts +92 -0
- package/dist/llamaindex.d.ts +92 -0
- package/dist/llamaindex.js +344 -0
- package/dist/llamaindex.js.map +1 -0
- package/dist/mastra.cjs +1081 -0
- package/dist/mastra.cjs.map +1 -0
- package/dist/mastra.d.cts +86 -0
- package/dist/mastra.d.ts +86 -0
- package/dist/mastra.js +1074 -0
- package/dist/mastra.js.map +1 -0
- package/dist/openai-agents.cjs +465 -0
- package/dist/openai-agents.cjs.map +1 -0
- package/dist/openai-agents.d.cts +75 -0
- package/dist/openai-agents.d.ts +75 -0
- package/dist/openai-agents.js +459 -0
- package/dist/openai-agents.js.map +1 -0
- package/dist/retell.cjs +846 -0
- package/dist/retell.cjs.map +1 -0
- package/dist/retell.d.cts +161 -0
- package/dist/retell.d.ts +161 -0
- package/dist/retell.js +841 -0
- package/dist/retell.js.map +1 -0
- package/dist/shared-BLkAbvuf.d.ts +39 -0
- package/dist/shared-DEkF2Y_z.d.cts +39 -0
- package/dist/strands.cjs +352 -0
- package/dist/strands.cjs.map +1 -0
- package/dist/strands.d.cts +72 -0
- package/dist/strands.d.ts +72 -0
- package/dist/strands.js +347 -0
- package/dist/strands.js.map +1 -0
- package/dist/twilio.cjs +174 -0
- package/dist/twilio.cjs.map +1 -0
- package/dist/twilio.d.cts +64 -0
- package/dist/twilio.d.ts +64 -0
- package/dist/twilio.js +167 -0
- package/dist/twilio.js.map +1 -0
- package/dist/vapi.cjs +661 -0
- package/dist/vapi.cjs.map +1 -0
- package/dist/vapi.d.cts +78 -0
- package/dist/vapi.d.ts +78 -0
- package/dist/vapi.js +656 -0
- package/dist/vapi.js.map +1 -0
- package/dist/voltagent.cjs +509 -0
- package/dist/voltagent.cjs.map +1 -0
- package/dist/voltagent.d.cts +70 -0
- package/dist/voltagent.d.ts +70 -0
- package/dist/voltagent.js +503 -0
- package/dist/voltagent.js.map +1 -0
- package/dist/webhook-BaraTaA9.d.ts +37 -0
- package/dist/webhook-CuR0Wckd.d.cts +37 -0
- package/dist/whatsapp.cjs +214 -0
- package/dist/whatsapp.cjs.map +1 -0
- package/dist/whatsapp.d.cts +81 -0
- package/dist/whatsapp.d.ts +81 -0
- package/dist/whatsapp.js +207 -0
- package/dist/whatsapp.js.map +1 -0
- package/package.json +351 -6
package/README.md
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@niadra/sdk)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
-
**Niadra is the
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 (
|
|
86
|
-
|
|
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
|
|
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`,
|
|
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
|
|
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"); //
|
|
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,396 @@ 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. Every check about an outbound contact keeps that copy,
|
|
695
|
+
read again in the background once a minute.
|
|
696
|
+
- **Tool bindings** live in the space's `tool-bindings` document and come in the SDK profile, never in code: a
|
|
697
|
+
tool measures the constraints block through the one served for its name, the counterfactual runs through it,
|
|
698
|
+
and its `capabilities.mask_output` decides the masking when the code leaves `maskOutput` unset.
|
|
699
|
+
- **The `niadra` command** (Node) runs `replay`, `counterfactual`, `resolver-worker` (the space's refresh
|
|
700
|
+
requests, read with your resolvers inside your boundary; a watch fires only on a value the worker
|
|
701
|
+
confirmed, and a resolver returns `NOT_FOUND` when the source no longer has the object), `types derive` and
|
|
702
|
+
`contract test`, with the Python command's arguments and exit codes.
|
|
703
|
+
- `niadra.api` has one typed method per route of these features; unlike the rest of the client, it rejects.
|
|
704
|
+
|
|
281
705
|
## Failure behavior
|
|
282
706
|
|
|
283
707
|
Memory should make an agent better, never make it fail. By default:
|
|
@@ -291,21 +715,28 @@ Memory should make an agent better, never make it fail. By default:
|
|
|
291
715
|
| 401 or 403 | Not treated as an outage: the cached packs are dropped (all of them on 401, the one requested on 403) and `context()` returns empty. Revoking a key also stops what the process had cached. |
|
|
292
716
|
| 421 (the space moved to another cell) | Retried at once, up to three attempts. |
|
|
293
717
|
| 429 on a batch | Retried after `Retry-After`. |
|
|
294
|
-
| Batch failure | Retried with exponential backoff and jitter, three attempts. 4xx answers other than 408, 421 and 429 are never retried. A batch that still fails
|
|
718
|
+
| Batch failure | Retried with exponential backoff and jitter, three attempts. 4xx answers other than 408, 421 and 429 are never retried. A batch that still fails goes back to the front of the queue and leaves again after a pause that doubles, up to a minute, while Niadra stays down; each event keeps its idempotency key, so it is stored once. |
|
|
295
719
|
| Queue full (10,000 items) | New events are dropped and logged. |
|
|
296
720
|
| Server rejects one item of a batch (207) | Only that item fails; the rest are stored. |
|
|
297
721
|
|
|
722
|
+
With Niadra down, a read serves the conversation's last good pack (`source: "fallback"`) with `ageMs`
|
|
723
|
+
saying how old it is; the opt-out holds by the local copy of the suppression list; turn records, events
|
|
724
|
+
and declarations wait in their queues and leave once Niadra answers again, each stored once.
|
|
725
|
+
`test/chaos.test.ts` kills Niadra's process, silences its network, answers 503 and answers late in the
|
|
726
|
+
middle of a conversation, and checks all of it.
|
|
727
|
+
|
|
298
728
|
Every call you wait for has its own time budget for the whole call, retries and waits included, independent of your platform's:
|
|
299
729
|
|
|
300
730
|
| Call | Default |
|
|
301
731
|
| --- | --- |
|
|
302
|
-
| `context()` | 300 ms,
|
|
732
|
+
| `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) |
|
|
733
|
+
| The first read of a voice call (`begin()`, `ready()`) | 1.5 s, spent while the phone rings or the inbound webhook runs |
|
|
303
734
|
| `search()`, `timeline()`, `open()`, `objectState()`, `objectTimeline()` | 600 ms, 300 ms through voice conversations and voice-bound tools |
|
|
304
735
|
| `subjectToken()` | 2 s |
|
|
305
736
|
| `identify()`, `verify()`, `handoff()`, `feedback()` and the reservation in `uploadMedia()` | 5 s |
|
|
306
737
|
| The transfer in `uploadMedia()` | 60 s |
|
|
307
738
|
|
|
308
|
-
An `identify()`, `verify()` or `handoff()` that runs out of time resolves with
|
|
739
|
+
An `identify()`, `verify()` or `handoff()` that runs out of time, or that Niadra cannot take for now (a network error, a 5xx), resolves with that error 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.
|
|
309
740
|
|
|
310
741
|
### The context cache
|
|
311
742
|
|
|
@@ -316,6 +747,8 @@ Inside a conversation or task, packs are cached in memory:
|
|
|
316
747
|
- when a request fails: the last good pack, if it is less than 30 minutes old;
|
|
317
748
|
- at most 1,000 packs, the least recently used evicted first.
|
|
318
749
|
|
|
750
|
+
`source` says which of these served the read, and `ageMs` how long ago Niadra sent or confirmed that pack.
|
|
751
|
+
|
|
319
752
|
Refreshes send the cached ETag, so an unchanged pack costs a `not_modified` answer instead of the
|
|
320
753
|
full text, and only one background refresh per pack runs at a time. A 401 or 403 is not an
|
|
321
754
|
outage: the cached packs go (all of them on 401, the one requested on 403), so cutting a vendor's
|
|
@@ -341,9 +774,10 @@ Create one client per process and share it: it owns the queue and the cache.
|
|
|
341
774
|
new Niadra({
|
|
342
775
|
apiKey: "nia_sk_live_...", // default: NIADRA_API_KEY
|
|
343
776
|
baseURL: "http://localhost:4010", // default: NIADRA_BASE_URL, then derived from the key
|
|
344
|
-
timeouts: { context: 300, contextVoice:
|
|
777
|
+
timeouts: { context: 300, contextVoice: 200, contextVoiceStart: 1500, navigation: 600, navigationVoice: 300, write: 5000, token: 2000, upload: 60_000 },
|
|
778
|
+
voice: { enabled: true, settleMs: 200, minCoverage: 0.75, probe: true },
|
|
345
779
|
cache: { ttlMs: 10_000, staleWhileRevalidateMs: 600_000, maxStaleMs: 1_800_000, maxEntries: 1000 },
|
|
346
|
-
queue: { flushAt: 15, flushIntervalMs: 1000, turnFlushIntervalMs:
|
|
780
|
+
queue: { flushAt: 15, flushIntervalMs: 1000, turnFlushIntervalMs: 0, maxBatchSize: 100, maxQueueSize: 10_000, maxAttempts: 3 },
|
|
347
781
|
strict: false,
|
|
348
782
|
flushOnExit: true,
|
|
349
783
|
logger: console, // anything with debug, warn and error
|
|
@@ -364,10 +798,14 @@ Include `requestId` when you contact support.
|
|
|
364
798
|
|
|
365
799
|
```sh
|
|
366
800
|
pnpm install
|
|
367
|
-
pnpm check # typecheck, lint, tests
|
|
368
|
-
pnpm build # ESM and CommonJS into dist
|
|
801
|
+
pnpm check # typecheck, lint, tests (the integrations with their frameworks' real types)
|
|
802
|
+
pnpm build # ESM and CommonJS into dist/, one entry per integration
|
|
803
|
+
pnpm runtimes # the build on Deno, Bun, workerd and the Edge Runtime
|
|
804
|
+
pnpm --filter "./packages/*" check # the n8n and Flowise nodes
|
|
369
805
|
```
|
|
370
806
|
|
|
807
|
+
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.
|
|
808
|
+
|
|
371
809
|
## Documentation in Portuguese
|
|
372
810
|
|
|
373
811
|
The documentation is also available in Portuguese at [docs.niadra.com](https://docs.niadra.com),
|