@niadra/sdk 0.1.0 → 0.1.1
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 +49 -0
- package/README.md +35 -18
- package/dist/index.cjs +169 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -13
- package/dist/index.d.ts +107 -13
- package/dist/index.js +167 -43
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the package follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [0.1.1] - 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- The agent's turn carries the usage the model provider reported for the call behind it, as
|
|
10
|
+
`usage` (`ModelUsage`: provider, model, prompt tokens with the cached ones included, cached
|
|
11
|
+
tokens, tokens written to the cache). `wrap()` reads it from every OpenAI-compatible response
|
|
12
|
+
and from the last chunk of a stream that asked for it (`stream_options: { include_usage: true }`),
|
|
13
|
+
and never changes the request. Niadra sums it per agent, vendor and model, and the Console shows
|
|
14
|
+
the prompt cache's hit rate and estimated savings.
|
|
15
|
+
- `agent(text, { usage })` on conversations and tasks takes the provider's response (OpenAI chat
|
|
16
|
+
completions or Responses, Anthropic messages) or a `ModelUsage`, for agents that do not use
|
|
17
|
+
`wrap()`; `modelUsage()` reads one yourself. A response without usage is left out; the turn is
|
|
18
|
+
recorded either way.
|
|
19
|
+
- CI runs the build on Deno (with no permissions), Bun, workerd (Cloudflare Workers) and the Vercel
|
|
20
|
+
Edge Runtime (`pnpm runtimes`), and the README names them.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `engines` asks for Node 20 or later, the versions the CI tests. The README no longer claims Node 18.
|
|
25
|
+
- A conversation turn (a message with a `conversation_id`) leaves the queue at most 200 ms after it
|
|
26
|
+
was queued, taking whatever else is waiting along, instead of up to a second: it is what the other
|
|
27
|
+
agents read in `live`. `queue.turnFlushIntervalMs` sets it; other items still wait for
|
|
28
|
+
`flushIntervalMs` (1 s) or `flushAt` (15).
|
|
29
|
+
- The timeline tool says the history comes newest first, as the server returns it, instead of
|
|
30
|
+
"in chronological order".
|
|
31
|
+
- The search tool no longer offers the model a `system_event` item kind, and its description names
|
|
32
|
+
business objects instead of system events: a system event is never an item, it changes its object,
|
|
33
|
+
so the filter is `object`. The server still reads `system_event` from 0.1.0 as `object`.
|
|
34
|
+
- `open()` sends `POST /v1/history/open` with the item id, the level and the conversation id in the
|
|
35
|
+
body, instead of `GET /v1/history/items/{id}` with the conversation id in the query: a
|
|
36
|
+
conversation id may be a phone number or an e-mail, and a URL reaches access logs.
|
|
37
|
+
- `open()` takes `subject`, the customer the item must belong to; the server opens any other item
|
|
38
|
+
as 404. The tool kit passes its bound customer, so `open_history_item` opens only that
|
|
39
|
+
customer's items.
|
|
40
|
+
- `task_id` on `open()` is no longer sent: the server never read it on this route. The field stays
|
|
41
|
+
in `OpenParams` for code written against 0.1.0.
|
|
42
|
+
- The `excerpt` field of an opened item says the server no longer sends it.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Building a client on Deno without `--allow-env` no longer throws: a runtime that refuses to read
|
|
47
|
+
the environment now counts as one without `NIADRA_API_KEY` and `NIADRA_BASE_URL`.
|
|
48
|
+
- `feedback()` and the reservation in `uploadMedia()` end within `timeouts.write` (5 s) in total,
|
|
49
|
+
retries and backoff included, instead of 5 s per attempt.
|
|
50
|
+
- The transfer in `uploadMedia()` ends within `timeouts.upload` (60 s) in total instead of per attempt.
|
|
51
|
+
- `identify()`, `verify()` and `handoff()` resolve by `timeouts.write` with a `NiadraTimeoutError`
|
|
52
|
+
when the queue could not confirm them in time; the item stays queued and is still sent.
|
|
53
|
+
|
|
5
54
|
## [0.1.0] - 2026-09-23
|
|
6
55
|
|
|
7
56
|
First public release, with the same surface as the Python SDK.
|
package/README.md
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
**Niadra is the shared customer memory for every AI agent in a company.** The WhatsApp agent, the
|
|
7
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
|
|
9
|
-
|
|
8
|
+
back what they said and did. This package connects a TypeScript or JavaScript agent to it, on
|
|
9
|
+
Node 20+, Deno, Bun, Cloudflare Workers and the Vercel Edge Runtime: it needs only `fetch` and
|
|
10
|
+
Web Crypto, and CI runs the build on each of them.
|
|
10
11
|
|
|
11
|
-
[Website](https://niadra.com/en) · [Documentation](https://niadra.com/en
|
|
12
|
+
[Website](https://niadra.com/en) · [Documentation](https://docs.niadra.com/en) ·
|
|
12
13
|
[Talk to us](https://niadra.com/en/enterprise) · [Python SDK](https://github.com/ainiadra/niadra-sdk-python)
|
|
13
14
|
|
|
14
15
|
```sh
|
|
@@ -90,7 +91,8 @@ exported on request. The SDK never logs handles or message text.
|
|
|
90
91
|
|
|
91
92
|
**Which models and frameworks does it work with?** Any. The context is text you place in your
|
|
92
93
|
prompt, the tools follow the common function-calling format, and `wrap()` covers
|
|
93
|
-
OpenAI-compatible clients
|
|
94
|
+
OpenAI-compatible clients; `agent(text, { usage })` takes the usage of an OpenAI or Anthropic
|
|
95
|
+
response.
|
|
94
96
|
|
|
95
97
|
## Concepts
|
|
96
98
|
|
|
@@ -168,6 +170,22 @@ const completion = await openai.chat.completions.create({ model: "gpt-4.1", mess
|
|
|
168
170
|
|
|
169
171
|
Every `chat.completions.create` and `chat.completions.parse` call through the wrapper, streaming or not, gets the pack after your leading system messages and the suffix at the end. The injection is stamped, and the model's answer (its first choice) is recorded as the agent's turn: at once, or when a stream ends or you stop reading it. `.withResponse()` keeps working and records too; `.asResponse()` returns the raw HTTP response, so nothing is recorded then. Pass a function instead of a conversation to pick one per call; when it returns `null`, the call passes through untouched. Nothing the wrapper does can fail your call: a context it cannot fetch is left out, and a failure to record the answer is logged, without content.
|
|
170
172
|
|
|
173
|
+
#### The provider's prompt cache
|
|
174
|
+
|
|
175
|
+
The agent's turn also carries the usage the provider reported for the call: every input token (`usage.prompt_tokens`), the ones read from its prompt cache (`usage.prompt_tokens_details.cached_tokens`) and, through gateways that pass Anthropic's fields along, the ones written to it. A stream reports usage only when you ask for it with `stream_options: { include_usage: true }`; the wrapper never changes your request to get it. Niadra sums the usage per agent, vendor and model, and the Console shows the cache's hit rate and the estimated savings next to the rest of the space's usage.
|
|
176
|
+
|
|
177
|
+
Without `wrap()`, pass the provider's response with the turn. An OpenAI response and an Anthropic one are both understood (Anthropic's `usage.input_tokens`, `cache_read_input_tokens` and `cache_creation_input_tokens`):
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const message = await anthropic.messages.create({ model: "claude-sonnet-4-5", max_tokens: 1024, system, messages });
|
|
181
|
+
convo.agent(textOf(message), { usage: message });
|
|
182
|
+
|
|
183
|
+
// or build it yourself
|
|
184
|
+
convo.agent(reply, { usage: { provider: "openai", model: "gpt-4.1", prompt_tokens: 3000, cached_tokens: 2048 } });
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`modelUsage(response)` reads one yourself. A response without usage is left out, and the turn is recorded either way.
|
|
188
|
+
|
|
171
189
|
### Tasks
|
|
172
190
|
|
|
173
191
|
Internal agents (billing, collections, triage) work in tasks rather than conversations. A task centers its pack on its object, keeps deltas and stamps like a conversation, scopes the cache, groups events and ends with `task.ended`.
|
|
@@ -191,6 +209,8 @@ await task.end();
|
|
|
191
209
|
| `handoff({ conversation_id, target })` | A transfer to a human or another agent | Sent at once |
|
|
192
210
|
| `feedback({ subject, action, ... })` | A correction of what Niadra derived: `retract_fact`, `correct_fact`, `resolve_open_item`, `conversation_outcome` | Sent at once |
|
|
193
211
|
|
|
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.
|
|
213
|
+
|
|
194
214
|
Every item carries an idempotency key: the provider's message id when you pass one, a UUIDv7 otherwise. Retrying the same event is harmless.
|
|
195
215
|
|
|
196
216
|
`identify()`, `verify()`, `handoff()` and `feedback()` resolve to `{ ok, idempotency_key, error }` once the server has answered. A correction is recorded as an event, so it is audited like any other. A `context()` call made after `identify()` or `verify()` resolves already reflects it.
|
|
@@ -211,6 +231,8 @@ if (first) {
|
|
|
211
231
|
|
|
212
232
|
`search()` also reports recurrence: how many times the same kind of issue came back, and how it was last resolved.
|
|
213
233
|
|
|
234
|
+
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
|
+
|
|
214
236
|
### Business objects
|
|
215
237
|
|
|
216
238
|
```ts
|
|
@@ -229,7 +251,7 @@ if (upload) {
|
|
|
229
251
|
}
|
|
230
252
|
```
|
|
231
253
|
|
|
232
|
-
Media never travels inside an event. `uploadMedia()` takes a `Uint8Array`, `ArrayBuffer` or `Blob`, reserves an upload, sends the bytes straight to storage over a signed URL (HTTPS only, with exactly the headers the signature covers and nothing else, so never your key or default headers; storage checks the body against the declared size and digest), and resolves with the reference and digest for the event. With `subject`, the file is stored under that person, so erasing them erases it even if no event ever references it. Hashing uses Web Crypto
|
|
254
|
+
Media never travels inside an event. `uploadMedia()` takes a `Uint8Array`, `ArrayBuffer` or `Blob`, reserves an upload, sends the bytes straight to storage over a signed URL (HTTPS only, with exactly the headers the signature covers and nothing else, so never your key or default headers; storage checks the body against the declared size and digest), and resolves with the reference and digest for the event. With `subject`, the file is stored under that person, so erasing them erases it even if no event ever references it. Hashing uses Web Crypto.
|
|
233
255
|
|
|
234
256
|
### Tools for any model
|
|
235
257
|
|
|
@@ -273,17 +295,17 @@ Memory should make an agent better, never make it fail. By default:
|
|
|
273
295
|
| Queue full (10,000 items) | New events are dropped and logged. |
|
|
274
296
|
| Server rejects one item of a batch (207) | Only that item fails; the rest are stored. |
|
|
275
297
|
|
|
276
|
-
Every
|
|
298
|
+
Every call you wait for has its own time budget for the whole call, retries and waits included, independent of your platform's:
|
|
277
299
|
|
|
278
300
|
| Call | Default |
|
|
279
301
|
| --- | --- |
|
|
280
302
|
| `context()` | 300 ms, 150 ms with `view: "voice"` |
|
|
281
303
|
| `search()`, `timeline()`, `open()`, `objectState()`, `objectTimeline()` | 600 ms, 300 ms through voice conversations and voice-bound tools |
|
|
282
304
|
| `subjectToken()` | 2 s |
|
|
283
|
-
|
|
|
284
|
-
|
|
|
305
|
+
| `identify()`, `verify()`, `handoff()`, `feedback()` and the reservation in `uploadMedia()` | 5 s |
|
|
306
|
+
| The transfer in `uploadMedia()` | 60 s |
|
|
285
307
|
|
|
286
|
-
Override them with `timeouts`, or per call with `{ timeout }`. Pass `{ signal }` to cancel a call.
|
|
308
|
+
An `identify()`, `verify()` or `handoff()` that runs out of time resolves with a `NiadraTimeoutError` and stays in the queue, which keeps sending it. `track()` never waits; each attempt of a background batch has 5 s. Override them with `timeouts`, or per call with `{ timeout }`. Pass `{ signal }` to cancel a call.
|
|
287
309
|
|
|
288
310
|
### The context cache
|
|
289
311
|
|
|
@@ -310,7 +332,6 @@ Configure it with `cache: { ttlMs, staleWhileRevalidateMs, maxStaleMs, maxEntrie
|
|
|
310
332
|
|
|
311
333
|
- **Long-running Node services:** queued events are flushed when the event loop runs out of work (`beforeExit`). That event does not fire on signals or `process.exit()`, so call `await niadra.shutdown()` in your SIGTERM handler.
|
|
312
334
|
- **Serverless functions:** `await niadra.flush()` before returning.
|
|
313
|
-
- **Edge runtimes:** pass the flush to the platform, for example `ctx.waitUntil(niadra.flush())`.
|
|
314
335
|
|
|
315
336
|
Create one client per process and share it: it owns the queue and the cache.
|
|
316
337
|
|
|
@@ -322,7 +343,7 @@ new Niadra({
|
|
|
322
343
|
baseURL: "http://localhost:4010", // default: NIADRA_BASE_URL, then derived from the key
|
|
323
344
|
timeouts: { context: 300, contextVoice: 150, navigation: 600, navigationVoice: 300, write: 5000, token: 2000, upload: 60_000 },
|
|
324
345
|
cache: { ttlMs: 10_000, staleWhileRevalidateMs: 600_000, maxStaleMs: 1_800_000, maxEntries: 1000 },
|
|
325
|
-
queue: { flushAt: 15, flushIntervalMs: 1000, maxBatchSize: 100, maxQueueSize: 10_000, maxAttempts: 3 },
|
|
346
|
+
queue: { flushAt: 15, flushIntervalMs: 1000, turnFlushIntervalMs: 200, maxBatchSize: 100, maxQueueSize: 10_000, maxAttempts: 3 },
|
|
326
347
|
strict: false,
|
|
327
348
|
flushOnExit: true,
|
|
328
349
|
logger: console, // anything with debug, warn and error
|
|
@@ -347,14 +368,10 @@ pnpm check # typecheck, lint, tests
|
|
|
347
368
|
pnpm build # ESM and CommonJS into dist/
|
|
348
369
|
```
|
|
349
370
|
|
|
350
|
-
##
|
|
371
|
+
## Documentation in Portuguese
|
|
351
372
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
o que disseram e fizeram. Este pacote conecta um agente em TypeScript ou JavaScript a essa memória:
|
|
355
|
-
`context()` antes de chamar o modelo, `track()` depois, e `action()` quando o agente faz algo num
|
|
356
|
-
sistema. Documentação em [niadra.com/docs](https://niadra.com/docs) e contato em
|
|
357
|
-
[niadra.com/enterprise](https://niadra.com/enterprise).
|
|
373
|
+
The documentation is also available in Portuguese at [docs.niadra.com](https://docs.niadra.com),
|
|
374
|
+
and the contact page in Portuguese at [niadra.com/enterprise](https://niadra.com/enterprise).
|
|
358
375
|
|
|
359
376
|
## License
|
|
360
377
|
|
package/dist/index.cjs
CHANGED
|
@@ -360,6 +360,15 @@ function checkCloses(closes) {
|
|
|
360
360
|
const byObject = Boolean(closes.object && closes.operation);
|
|
361
361
|
if (byId === byObject) fail("closes takes either item_id, or object and operation");
|
|
362
362
|
}
|
|
363
|
+
function checkUsage(usage) {
|
|
364
|
+
if (!/^[a-z0-9][a-z0-9_.-]{0,63}$/.test(usage.provider)) fail("usage.provider must be lowercase, like `openai`");
|
|
365
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,127}$/.test(usage.model)) fail("usage.model is not a model name");
|
|
366
|
+
const counts = [usage.prompt_tokens, usage.cached_tokens ?? 0, usage.cache_write_tokens ?? 0];
|
|
367
|
+
if (!counts.every((n) => Number.isInteger(n) && n >= 0)) fail("usage counts are whole numbers from 0");
|
|
368
|
+
if ((usage.cached_tokens ?? 0) + (usage.cache_write_tokens ?? 0) > usage.prompt_tokens) {
|
|
369
|
+
fail("cached and written tokens are part of prompt_tokens");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
363
372
|
function checkContent(content) {
|
|
364
373
|
if (!content) return;
|
|
365
374
|
if ((content.text?.length ?? 0) > MAX_EVENT_TEXT) fail(`content.text is longer than ${MAX_EVENT_TEXT}`);
|
|
@@ -390,6 +399,10 @@ function buildEvent(input) {
|
|
|
390
399
|
if ((input.action.result?.length ?? 0) > 2e3) fail("action.result is longer than 2000");
|
|
391
400
|
checkCloses(input.action.closes);
|
|
392
401
|
}
|
|
402
|
+
if (input.usage) {
|
|
403
|
+
if (kind !== "message" || speaker.role !== "ai_agent") fail("`usage` is only valid on a message of the `ai_agent`");
|
|
404
|
+
checkUsage(input.usage);
|
|
405
|
+
}
|
|
393
406
|
const event = {
|
|
394
407
|
type: "event",
|
|
395
408
|
kind,
|
|
@@ -405,6 +418,7 @@ function buildEvent(input) {
|
|
|
405
418
|
if (input.canonical_type) event.canonical_type = input.canonical_type;
|
|
406
419
|
if (input.fields) event.fields = input.fields;
|
|
407
420
|
if (input.action) event.action = input.action;
|
|
421
|
+
if (input.usage) event.usage = input.usage;
|
|
408
422
|
copyOptional(event, input);
|
|
409
423
|
assertSerializable(event);
|
|
410
424
|
return event;
|
|
@@ -579,6 +593,76 @@ function withDeltas(result, response, deltas) {
|
|
|
579
593
|
return { ...result, response: merged, suffix: renderSuffix(merged) };
|
|
580
594
|
}
|
|
581
595
|
|
|
596
|
+
// src/usage.ts
|
|
597
|
+
var PROVIDER = /^[a-z0-9][a-z0-9_.-]{0,63}$/;
|
|
598
|
+
var MODEL = /^[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,127}$/;
|
|
599
|
+
function get(value, name) {
|
|
600
|
+
return (typeof value === "object" || typeof value === "function") && value !== null ? value[name] : void 0;
|
|
601
|
+
}
|
|
602
|
+
function count(value) {
|
|
603
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
604
|
+
}
|
|
605
|
+
function tokenCounts(usage) {
|
|
606
|
+
const read = count(get(usage, "cache_read_input_tokens")) ?? 0;
|
|
607
|
+
const written = count(get(usage, "cache_creation_input_tokens")) ?? 0;
|
|
608
|
+
let prompt = count(get(usage, "prompt_tokens"));
|
|
609
|
+
let cached;
|
|
610
|
+
if (prompt !== null) {
|
|
611
|
+
const reported = count(get(get(usage, "prompt_tokens_details"), "cached_tokens"));
|
|
612
|
+
cached = reported !== null && reported > 0 ? reported : read;
|
|
613
|
+
} else {
|
|
614
|
+
const inputs = count(get(usage, "input_tokens"));
|
|
615
|
+
if (inputs === null) return null;
|
|
616
|
+
const details = get(usage, "input_tokens_details");
|
|
617
|
+
if (details !== void 0 && details !== null) {
|
|
618
|
+
prompt = inputs;
|
|
619
|
+
cached = count(get(details, "cached_tokens")) ?? 0;
|
|
620
|
+
} else {
|
|
621
|
+
prompt = inputs + read + written;
|
|
622
|
+
cached = read;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return { prompt_tokens: Math.max(prompt, cached + written), cached_tokens: cached, cache_write_tokens: written };
|
|
626
|
+
}
|
|
627
|
+
function providerOf(model, usage) {
|
|
628
|
+
const slash = model.indexOf("/");
|
|
629
|
+
if (slash > 0) return model.slice(0, slash).trim().toLowerCase();
|
|
630
|
+
const name = model.trim().toLowerCase();
|
|
631
|
+
if (name.startsWith("claude") || `.${name}`.includes(".anthropic.")) return "anthropic";
|
|
632
|
+
if (name.startsWith("gemini")) return "google";
|
|
633
|
+
if (get(usage, "prompt_tokens") === void 0 && get(usage, "cache_read_input_tokens") !== void 0) {
|
|
634
|
+
return "anthropic";
|
|
635
|
+
}
|
|
636
|
+
return "openai";
|
|
637
|
+
}
|
|
638
|
+
function modelUsage(response, options = {}) {
|
|
639
|
+
try {
|
|
640
|
+
let usage = get(response, "usage");
|
|
641
|
+
if (usage === void 0 || usage === null || typeof usage !== "object") usage = response;
|
|
642
|
+
const counts = tokenCounts(usage);
|
|
643
|
+
const name = options.model ?? get(response, "model");
|
|
644
|
+
if (!counts || typeof name !== "string" || !MODEL.test(name)) return null;
|
|
645
|
+
const provider = (options.provider ?? providerOf(name, usage)).toLowerCase();
|
|
646
|
+
if (!PROVIDER.test(provider)) return null;
|
|
647
|
+
return { provider, model: name, ...counts };
|
|
648
|
+
} catch {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function asModelUsage(value) {
|
|
653
|
+
if (value === void 0 || value === null) return null;
|
|
654
|
+
if (isModelUsage(value)) return value;
|
|
655
|
+
return modelUsage(value);
|
|
656
|
+
}
|
|
657
|
+
function isModelUsage(value) {
|
|
658
|
+
const provider = get(value, "provider");
|
|
659
|
+
const model = get(value, "model");
|
|
660
|
+
const prompt = count(get(value, "prompt_tokens"));
|
|
661
|
+
const cached = get(value, "cached_tokens") === void 0 ? 0 : count(get(value, "cached_tokens"));
|
|
662
|
+
const written = get(value, "cache_write_tokens") === void 0 ? 0 : count(get(value, "cache_write_tokens"));
|
|
663
|
+
return typeof provider === "string" && PROVIDER.test(provider) && typeof model === "string" && MODEL.test(model) && prompt !== null && cached !== null && written !== null && cached + written <= prompt;
|
|
664
|
+
}
|
|
665
|
+
|
|
582
666
|
// src/conversation.ts
|
|
583
667
|
var Conversation = class {
|
|
584
668
|
constructor(client, params, hooks) {
|
|
@@ -738,6 +822,8 @@ var Conversation = class {
|
|
|
738
822
|
if (options.visibility) event.visibility = options.visibility;
|
|
739
823
|
if (options.voice) event.voice = options.voice;
|
|
740
824
|
if (options.context_stamp) event.context_stamp = options.context_stamp;
|
|
825
|
+
const usage = role === "ai_agent" ? asModelUsage(options.usage) : null;
|
|
826
|
+
if (usage) event.usage = usage;
|
|
741
827
|
return this.client.track(event);
|
|
742
828
|
}
|
|
743
829
|
};
|
|
@@ -881,6 +967,7 @@ var DEFAULT_CACHE = {
|
|
|
881
967
|
var DEFAULT_QUEUE = {
|
|
882
968
|
flushAt: 15,
|
|
883
969
|
flushIntervalMs: 1e3,
|
|
970
|
+
turnFlushIntervalMs: 200,
|
|
884
971
|
maxBatchSize: 100,
|
|
885
972
|
maxQueueSize: 1e4,
|
|
886
973
|
maxAttempts: 3,
|
|
@@ -904,6 +991,7 @@ var EventQueue = class {
|
|
|
904
991
|
now;
|
|
905
992
|
items = [];
|
|
906
993
|
timer = null;
|
|
994
|
+
timerDueAt = Number.POSITIVE_INFINITY;
|
|
907
995
|
tail = Promise.resolve();
|
|
908
996
|
closed = false;
|
|
909
997
|
droppedSinceWarning = 0;
|
|
@@ -928,7 +1016,8 @@ var EventQueue = class {
|
|
|
928
1016
|
if (this.items.length >= this.options.flushAt) {
|
|
929
1017
|
this.flushInBackground();
|
|
930
1018
|
} else {
|
|
931
|
-
this.
|
|
1019
|
+
const { flushIntervalMs, turnFlushIntervalMs } = this.options;
|
|
1020
|
+
this.schedule(isTurn(item) ? Math.min(turnFlushIntervalMs, flushIntervalMs) : flushIntervalMs);
|
|
932
1021
|
}
|
|
933
1022
|
return true;
|
|
934
1023
|
}
|
|
@@ -1018,20 +1107,29 @@ var EventQueue = class {
|
|
|
1018
1107
|
this.sentInWindow = 0;
|
|
1019
1108
|
return item;
|
|
1020
1109
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1110
|
+
/** Sends what is waiting in `delayMs`, unless a send is already due sooner. */
|
|
1111
|
+
schedule(delayMs) {
|
|
1112
|
+
const dueAt = this.now() + delayMs;
|
|
1113
|
+
if (this.timer !== null && this.timerDueAt <= dueAt) return;
|
|
1114
|
+
this.cancelTimer();
|
|
1115
|
+
this.timerDueAt = dueAt;
|
|
1023
1116
|
this.timer = setTimeout(() => {
|
|
1024
1117
|
this.timer = null;
|
|
1118
|
+
this.timerDueAt = Number.POSITIVE_INFINITY;
|
|
1025
1119
|
this.flushInBackground();
|
|
1026
|
-
},
|
|
1120
|
+
}, delayMs);
|
|
1027
1121
|
unref(this.timer);
|
|
1028
1122
|
}
|
|
1029
1123
|
cancelTimer() {
|
|
1030
1124
|
if (this.timer === null) return;
|
|
1031
1125
|
clearTimeout(this.timer);
|
|
1032
1126
|
this.timer = null;
|
|
1127
|
+
this.timerDueAt = Number.POSITIVE_INFINITY;
|
|
1033
1128
|
}
|
|
1034
1129
|
};
|
|
1130
|
+
function isTurn(item) {
|
|
1131
|
+
return item.type === "event" && item.kind === "message" && Boolean(item.conversation_id);
|
|
1132
|
+
}
|
|
1035
1133
|
function unref(timer) {
|
|
1036
1134
|
if (typeof timer === "object" && timer !== null && "unref" in timer && typeof timer.unref === "function") {
|
|
1037
1135
|
timer.unref();
|
|
@@ -1108,6 +1206,8 @@ var Task = class {
|
|
|
1108
1206
|
if (options.idempotency_key) event.idempotency_key = options.idempotency_key;
|
|
1109
1207
|
if (options.occurred_at) event.occurred_at = options.occurred_at;
|
|
1110
1208
|
if (options.visibility) event.visibility = options.visibility;
|
|
1209
|
+
const usage = asModelUsage(options.usage);
|
|
1210
|
+
if (usage) event.usage = usage;
|
|
1111
1211
|
return this.track(event);
|
|
1112
1212
|
}
|
|
1113
1213
|
/**
|
|
@@ -1179,7 +1279,7 @@ var TOOL_NAMES = {
|
|
|
1179
1279
|
timeline: "get_customer_timeline",
|
|
1180
1280
|
open: "open_history_item"
|
|
1181
1281
|
};
|
|
1182
|
-
var ITEM_KINDS = ["episode", "fact", "open_item", "action", "
|
|
1282
|
+
var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
|
|
1183
1283
|
var period = {
|
|
1184
1284
|
since: { type: "string", format: "date-time", description: "Only items at or after this ISO 8601 time." },
|
|
1185
1285
|
until: { type: "string", format: "date-time", description: "Only items before this ISO 8601 time." },
|
|
@@ -1199,7 +1299,7 @@ var TOOL_DEFINITIONS = [
|
|
|
1199
1299
|
type: "function",
|
|
1200
1300
|
function: {
|
|
1201
1301
|
name: TOOL_NAMES.search,
|
|
1202
|
-
description: "Search this customer's past conversations, actions and
|
|
1302
|
+
description: "Search this customer's past conversations, actions and business objects by meaning and keywords. Use it when the customer refers to something that happened before and the details are not in the customer context you already have. Do not use it for facts already listed there. The result also says how often the same kind of issue came back. To read one result in full, call open_history_item.",
|
|
1203
1303
|
parameters: {
|
|
1204
1304
|
type: "object",
|
|
1205
1305
|
properties: {
|
|
@@ -1217,7 +1317,7 @@ var TOOL_DEFINITIONS = [
|
|
|
1217
1317
|
type: "function",
|
|
1218
1318
|
function: {
|
|
1219
1319
|
name: TOOL_NAMES.timeline,
|
|
1220
|
-
description: "List this customer's history
|
|
1320
|
+
description: "List this customer's history, newest first, one line per item. Use it when you need the sequence of events, for example what happened since a given date. Prefer search_customer_history when you are looking for something specific.",
|
|
1221
1321
|
parameters: {
|
|
1222
1322
|
type: "object",
|
|
1223
1323
|
properties: {
|
|
@@ -1311,7 +1411,7 @@ function bindTools(subject, binding, navigator, strict) {
|
|
|
1311
1411
|
case TOOL_NAMES.open: {
|
|
1312
1412
|
const id = str(args, "id");
|
|
1313
1413
|
if (!id) throw new NiadraValidationError("open_history_item needs an id");
|
|
1314
|
-
return navigator.open(id, binding, voice);
|
|
1414
|
+
return navigator.open(id, subject, binding, voice);
|
|
1315
1415
|
}
|
|
1316
1416
|
default:
|
|
1317
1417
|
throw new NiadraValidationError(`unknown tool: ${name}`);
|
|
@@ -1350,7 +1450,7 @@ function cloneDefinition(definition) {
|
|
|
1350
1450
|
}
|
|
1351
1451
|
|
|
1352
1452
|
// src/version.ts
|
|
1353
|
-
var VERSION = "0.1.
|
|
1453
|
+
var VERSION = "0.1.1";
|
|
1354
1454
|
|
|
1355
1455
|
// src/transport.ts
|
|
1356
1456
|
var RETRYABLE_WRITE_STATUS = /* @__PURE__ */ new Set([408, 421, 429, 500, 502, 503, 504]);
|
|
@@ -1403,13 +1503,17 @@ var Transport = class {
|
|
|
1403
1503
|
);
|
|
1404
1504
|
}
|
|
1405
1505
|
async retrying(timeoutMs, policy, signal, attempt) {
|
|
1406
|
-
|
|
1407
|
-
|
|
1506
|
+
const end = policy.totalMs === void 0 ? Number.POSITIVE_INFINITY : Date.now() + policy.totalMs;
|
|
1507
|
+
for (let count2 = 1; ; count2++) {
|
|
1508
|
+
const left = end - Date.now();
|
|
1509
|
+
if (left <= 0) throw new NiadraTimeoutError(policy.totalMs ?? timeoutMs);
|
|
1510
|
+
const deadline = new Deadline(Math.min(timeoutMs, left), signal);
|
|
1408
1511
|
try {
|
|
1409
1512
|
return await attempt(deadline);
|
|
1410
1513
|
} catch (error) {
|
|
1411
|
-
if (
|
|
1412
|
-
const delay = retryDelay(error,
|
|
1514
|
+
if (count2 >= policy.maxAttempts || !isTransient(error)) throw error;
|
|
1515
|
+
const delay = retryDelay(error, count2, policy.baseDelayMs, policy.maxDelayMs);
|
|
1516
|
+
if (Date.now() + delay >= end) throw error;
|
|
1413
1517
|
await sleep(delay, signal);
|
|
1414
1518
|
} finally {
|
|
1415
1519
|
deadline.clear();
|
|
@@ -1560,9 +1664,13 @@ function sleep(ms, signal) {
|
|
|
1560
1664
|
|
|
1561
1665
|
// src/client.ts
|
|
1562
1666
|
function readEnv(name) {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1667
|
+
try {
|
|
1668
|
+
const env = globalThis.process?.env;
|
|
1669
|
+
const value = env?.[name];
|
|
1670
|
+
return value === "" ? void 0 : value;
|
|
1671
|
+
} catch {
|
|
1672
|
+
return void 0;
|
|
1673
|
+
}
|
|
1566
1674
|
}
|
|
1567
1675
|
function describe(error) {
|
|
1568
1676
|
if (error instanceof NiadraAPIError) {
|
|
@@ -1702,7 +1810,7 @@ var Niadra = class {
|
|
|
1702
1810
|
return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
|
|
1703
1811
|
});
|
|
1704
1812
|
}
|
|
1705
|
-
/** The customer's history
|
|
1813
|
+
/** The customer's history, newest first, one line per item, paginated by cursor. */
|
|
1706
1814
|
async timeline(params, options = {}) {
|
|
1707
1815
|
return this.navigate(
|
|
1708
1816
|
() => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
|
|
@@ -1710,20 +1818,17 @@ var Niadra = class {
|
|
|
1710
1818
|
}
|
|
1711
1819
|
/**
|
|
1712
1820
|
* Opens one history item from `search()` or `timeline()`: summary, request, commitments,
|
|
1713
|
-
* outcome and resolution.
|
|
1714
|
-
*
|
|
1821
|
+
* outcome and resolution. Sent as `POST /v1/history/open`: the conversation id and `subject` go
|
|
1822
|
+
* in the body, never in a URL.
|
|
1715
1823
|
*/
|
|
1716
1824
|
async open(id, params = {}, options = {}) {
|
|
1717
1825
|
return this.navigate(() => {
|
|
1718
1826
|
if (!id) throw new NiadraValidationError("open() needs an item id");
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
task_id: params.task_id
|
|
1725
|
-
};
|
|
1726
|
-
return spec;
|
|
1827
|
+
const body = { item_id: id };
|
|
1828
|
+
if (params.subject) body.subject = params.subject;
|
|
1829
|
+
if (params.verification) body.verification = params.verification;
|
|
1830
|
+
if (params.conversation_id) body.conversation_id = params.conversation_id;
|
|
1831
|
+
return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
|
|
1727
1832
|
});
|
|
1728
1833
|
}
|
|
1729
1834
|
/**
|
|
@@ -1764,11 +1869,10 @@ var Niadra = class {
|
|
|
1764
1869
|
const navigator = {
|
|
1765
1870
|
search: (params, voice) => this.search(params, this.voiceBudget(voice)),
|
|
1766
1871
|
timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
|
|
1767
|
-
open: (id, bound, voice) => {
|
|
1768
|
-
const scope = {};
|
|
1872
|
+
open: (id, customer, bound, voice) => {
|
|
1873
|
+
const scope = { subject: customer };
|
|
1769
1874
|
if (bound.verification) scope.verification = bound.verification;
|
|
1770
1875
|
if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
|
|
1771
|
-
if (bound.task_id) scope.task_id = bound.task_id;
|
|
1772
1876
|
return this.open(id, scope, this.voiceBudget(voice));
|
|
1773
1877
|
}
|
|
1774
1878
|
};
|
|
@@ -1831,7 +1935,7 @@ var Niadra = class {
|
|
|
1831
1935
|
body: request,
|
|
1832
1936
|
headers: { "idempotency-key": key },
|
|
1833
1937
|
timeoutMs: this.timeouts.write,
|
|
1834
|
-
retry: core.writes
|
|
1938
|
+
retry: { ...core.writes, totalMs: this.timeouts.write }
|
|
1835
1939
|
});
|
|
1836
1940
|
const [rejected] = response.data?.errors ?? [];
|
|
1837
1941
|
if (rejected) {
|
|
@@ -1862,7 +1966,7 @@ var Niadra = class {
|
|
|
1862
1966
|
path: "/v1/media/uploads",
|
|
1863
1967
|
body: request,
|
|
1864
1968
|
timeoutMs: this.timeouts.write,
|
|
1865
|
-
retry: core.writes,
|
|
1969
|
+
retry: { ...core.writes, totalMs: this.timeouts.write },
|
|
1866
1970
|
signal: options.signal
|
|
1867
1971
|
});
|
|
1868
1972
|
const media_ref = reserved.data?.media_ref;
|
|
@@ -1875,7 +1979,7 @@ var Niadra = class {
|
|
|
1875
1979
|
body: bytes,
|
|
1876
1980
|
headers: uploadHeaders(reserved.data?.upload_headers, request.content_type),
|
|
1877
1981
|
timeoutMs: this.timeouts.upload,
|
|
1878
|
-
retry: core.writes,
|
|
1982
|
+
retry: { ...core.writes, totalMs: this.timeouts.upload },
|
|
1879
1983
|
signal: options.signal
|
|
1880
1984
|
});
|
|
1881
1985
|
}
|
|
@@ -2062,11 +2166,20 @@ var Niadra = class {
|
|
|
2062
2166
|
}
|
|
2063
2167
|
return new Promise((resolve, reject) => {
|
|
2064
2168
|
const key = item.idempotency_key;
|
|
2065
|
-
|
|
2169
|
+
let settled = false;
|
|
2170
|
+
const timer = setTimeout(() => {
|
|
2171
|
+
this.logger.warn(`write not confirmed within ${this.timeouts.write} ms; it stays queued`);
|
|
2172
|
+
settle(new NiadraTimeoutError(this.timeouts.write));
|
|
2173
|
+
}, this.timeouts.write);
|
|
2174
|
+
const settle = (error) => {
|
|
2175
|
+
if (settled) return;
|
|
2176
|
+
settled = true;
|
|
2177
|
+
clearTimeout(timer);
|
|
2066
2178
|
if (!error) resolve({ ok: true, idempotency_key: key, error: null });
|
|
2067
2179
|
else if (this.strict) reject(error);
|
|
2068
2180
|
else resolve({ ok: false, idempotency_key: key, error });
|
|
2069
|
-
}
|
|
2181
|
+
};
|
|
2182
|
+
core.queue.push(item, settle);
|
|
2070
2183
|
core.queue.flushInBackground();
|
|
2071
2184
|
});
|
|
2072
2185
|
}
|
|
@@ -2253,22 +2366,22 @@ var Answer = class {
|
|
|
2253
2366
|
parsed(value) {
|
|
2254
2367
|
try {
|
|
2255
2368
|
if (!this.stream) {
|
|
2256
|
-
this.record(messageText(value));
|
|
2369
|
+
this.record(messageText(value), modelUsage(value));
|
|
2257
2370
|
return value;
|
|
2258
2371
|
}
|
|
2259
|
-
return isAsyncIterable(value) ? captureStream(value, (text) => {
|
|
2260
|
-
this.record(text);
|
|
2372
|
+
return isAsyncIterable(value) ? captureStream(value, (text, usage) => {
|
|
2373
|
+
this.record(text, usage);
|
|
2261
2374
|
}) : value;
|
|
2262
2375
|
} catch (error) {
|
|
2263
2376
|
this.session.logger.warn(`could not capture the model's answer (${errorName(error)})`);
|
|
2264
2377
|
return value;
|
|
2265
2378
|
}
|
|
2266
2379
|
}
|
|
2267
|
-
record(text) {
|
|
2380
|
+
record(text, usage) {
|
|
2268
2381
|
if (this.recorded || !text) return;
|
|
2269
2382
|
this.recorded = true;
|
|
2270
2383
|
try {
|
|
2271
|
-
this.session.agent(text);
|
|
2384
|
+
this.session.agent(text, usage ? { usage } : {});
|
|
2272
2385
|
} catch (error) {
|
|
2273
2386
|
this.session.logger.warn(`could not record the model's answer (${errorName(error)})`);
|
|
2274
2387
|
}
|
|
@@ -2276,11 +2389,19 @@ var Answer = class {
|
|
|
2276
2389
|
};
|
|
2277
2390
|
function captureStream(stream, done) {
|
|
2278
2391
|
const parts = [];
|
|
2392
|
+
let usage = null;
|
|
2393
|
+
let model;
|
|
2279
2394
|
let finished = false;
|
|
2395
|
+
const observe = (chunk) => {
|
|
2396
|
+
const reported = property(chunk, "usage");
|
|
2397
|
+
if (isRecord(reported)) usage = reported;
|
|
2398
|
+
const name = property(chunk, "model");
|
|
2399
|
+
if (typeof name === "string" && name) model = name;
|
|
2400
|
+
};
|
|
2280
2401
|
const finish = () => {
|
|
2281
2402
|
if (finished) return;
|
|
2282
2403
|
finished = true;
|
|
2283
|
-
done(parts.join(""));
|
|
2404
|
+
done(parts.join(""), usage ? modelUsage(usage, model ? { model } : {}) : null);
|
|
2284
2405
|
};
|
|
2285
2406
|
return new Proxy(stream, {
|
|
2286
2407
|
get(target, prop) {
|
|
@@ -2292,7 +2413,10 @@ function captureStream(stream, done) {
|
|
|
2292
2413
|
try {
|
|
2293
2414
|
const step = await inner.next();
|
|
2294
2415
|
if (step.done) finish();
|
|
2295
|
-
else
|
|
2416
|
+
else {
|
|
2417
|
+
parts.push(deltaText(step.value));
|
|
2418
|
+
observe(step.value);
|
|
2419
|
+
}
|
|
2296
2420
|
return step;
|
|
2297
2421
|
} catch (error) {
|
|
2298
2422
|
finish();
|
|
@@ -2378,11 +2502,14 @@ exports.baseURLFromKey = baseURLFromKey;
|
|
|
2378
2502
|
exports.consoleLogger = consoleLogger;
|
|
2379
2503
|
exports.handles = handles;
|
|
2380
2504
|
exports.injectContext = injectContext;
|
|
2505
|
+
exports.modelUsage = modelUsage;
|
|
2381
2506
|
exports.parseApiKey = parseApiKey;
|
|
2507
|
+
exports.providerOf = providerOf;
|
|
2382
2508
|
exports.renderLive = renderLive;
|
|
2383
2509
|
exports.renderSuffix = renderSuffix;
|
|
2384
2510
|
exports.silentLogger = silentLogger;
|
|
2385
2511
|
exports.toObjectRef = toObjectRef;
|
|
2512
|
+
exports.tokenCounts = tokenCounts;
|
|
2386
2513
|
exports.uuidv7 = uuidv7;
|
|
2387
2514
|
exports.wrap = wrap;
|
|
2388
2515
|
//# sourceMappingURL=index.cjs.map
|