@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/dist/index.js CHANGED
@@ -358,6 +358,15 @@ function checkCloses(closes) {
358
358
  const byObject = Boolean(closes.object && closes.operation);
359
359
  if (byId === byObject) fail("closes takes either item_id, or object and operation");
360
360
  }
361
+ function checkUsage(usage) {
362
+ if (!/^[a-z0-9][a-z0-9_.-]{0,63}$/.test(usage.provider)) fail("usage.provider must be lowercase, like `openai`");
363
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,127}$/.test(usage.model)) fail("usage.model is not a model name");
364
+ const counts = [usage.prompt_tokens, usage.cached_tokens ?? 0, usage.cache_write_tokens ?? 0];
365
+ if (!counts.every((n) => Number.isInteger(n) && n >= 0)) fail("usage counts are whole numbers from 0");
366
+ if ((usage.cached_tokens ?? 0) + (usage.cache_write_tokens ?? 0) > usage.prompt_tokens) {
367
+ fail("cached and written tokens are part of prompt_tokens");
368
+ }
369
+ }
361
370
  function checkContent(content) {
362
371
  if (!content) return;
363
372
  if ((content.text?.length ?? 0) > MAX_EVENT_TEXT) fail(`content.text is longer than ${MAX_EVENT_TEXT}`);
@@ -388,6 +397,10 @@ function buildEvent(input) {
388
397
  if ((input.action.result?.length ?? 0) > 2e3) fail("action.result is longer than 2000");
389
398
  checkCloses(input.action.closes);
390
399
  }
400
+ if (input.usage) {
401
+ if (kind !== "message" || speaker.role !== "ai_agent") fail("`usage` is only valid on a message of the `ai_agent`");
402
+ checkUsage(input.usage);
403
+ }
391
404
  const event = {
392
405
  type: "event",
393
406
  kind,
@@ -403,6 +416,7 @@ function buildEvent(input) {
403
416
  if (input.canonical_type) event.canonical_type = input.canonical_type;
404
417
  if (input.fields) event.fields = input.fields;
405
418
  if (input.action) event.action = input.action;
419
+ if (input.usage) event.usage = input.usage;
406
420
  copyOptional(event, input);
407
421
  assertSerializable(event);
408
422
  return event;
@@ -577,6 +591,76 @@ function withDeltas(result, response, deltas) {
577
591
  return { ...result, response: merged, suffix: renderSuffix(merged) };
578
592
  }
579
593
 
594
+ // src/usage.ts
595
+ var PROVIDER = /^[a-z0-9][a-z0-9_.-]{0,63}$/;
596
+ var MODEL = /^[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,127}$/;
597
+ function get(value, name) {
598
+ return (typeof value === "object" || typeof value === "function") && value !== null ? value[name] : void 0;
599
+ }
600
+ function count(value) {
601
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
602
+ }
603
+ function tokenCounts(usage) {
604
+ const read = count(get(usage, "cache_read_input_tokens")) ?? 0;
605
+ const written = count(get(usage, "cache_creation_input_tokens")) ?? 0;
606
+ let prompt = count(get(usage, "prompt_tokens"));
607
+ let cached;
608
+ if (prompt !== null) {
609
+ const reported = count(get(get(usage, "prompt_tokens_details"), "cached_tokens"));
610
+ cached = reported !== null && reported > 0 ? reported : read;
611
+ } else {
612
+ const inputs = count(get(usage, "input_tokens"));
613
+ if (inputs === null) return null;
614
+ const details = get(usage, "input_tokens_details");
615
+ if (details !== void 0 && details !== null) {
616
+ prompt = inputs;
617
+ cached = count(get(details, "cached_tokens")) ?? 0;
618
+ } else {
619
+ prompt = inputs + read + written;
620
+ cached = read;
621
+ }
622
+ }
623
+ return { prompt_tokens: Math.max(prompt, cached + written), cached_tokens: cached, cache_write_tokens: written };
624
+ }
625
+ function providerOf(model, usage) {
626
+ const slash = model.indexOf("/");
627
+ if (slash > 0) return model.slice(0, slash).trim().toLowerCase();
628
+ const name = model.trim().toLowerCase();
629
+ if (name.startsWith("claude") || `.${name}`.includes(".anthropic.")) return "anthropic";
630
+ if (name.startsWith("gemini")) return "google";
631
+ if (get(usage, "prompt_tokens") === void 0 && get(usage, "cache_read_input_tokens") !== void 0) {
632
+ return "anthropic";
633
+ }
634
+ return "openai";
635
+ }
636
+ function modelUsage(response, options = {}) {
637
+ try {
638
+ let usage = get(response, "usage");
639
+ if (usage === void 0 || usage === null || typeof usage !== "object") usage = response;
640
+ const counts = tokenCounts(usage);
641
+ const name = options.model ?? get(response, "model");
642
+ if (!counts || typeof name !== "string" || !MODEL.test(name)) return null;
643
+ const provider = (options.provider ?? providerOf(name, usage)).toLowerCase();
644
+ if (!PROVIDER.test(provider)) return null;
645
+ return { provider, model: name, ...counts };
646
+ } catch {
647
+ return null;
648
+ }
649
+ }
650
+ function asModelUsage(value) {
651
+ if (value === void 0 || value === null) return null;
652
+ if (isModelUsage(value)) return value;
653
+ return modelUsage(value);
654
+ }
655
+ function isModelUsage(value) {
656
+ const provider = get(value, "provider");
657
+ const model = get(value, "model");
658
+ const prompt = count(get(value, "prompt_tokens"));
659
+ const cached = get(value, "cached_tokens") === void 0 ? 0 : count(get(value, "cached_tokens"));
660
+ const written = get(value, "cache_write_tokens") === void 0 ? 0 : count(get(value, "cache_write_tokens"));
661
+ return typeof provider === "string" && PROVIDER.test(provider) && typeof model === "string" && MODEL.test(model) && prompt !== null && cached !== null && written !== null && cached + written <= prompt;
662
+ }
663
+
580
664
  // src/conversation.ts
581
665
  var Conversation = class {
582
666
  constructor(client, params, hooks) {
@@ -736,6 +820,8 @@ var Conversation = class {
736
820
  if (options.visibility) event.visibility = options.visibility;
737
821
  if (options.voice) event.voice = options.voice;
738
822
  if (options.context_stamp) event.context_stamp = options.context_stamp;
823
+ const usage = role === "ai_agent" ? asModelUsage(options.usage) : null;
824
+ if (usage) event.usage = usage;
739
825
  return this.client.track(event);
740
826
  }
741
827
  };
@@ -879,6 +965,7 @@ var DEFAULT_CACHE = {
879
965
  var DEFAULT_QUEUE = {
880
966
  flushAt: 15,
881
967
  flushIntervalMs: 1e3,
968
+ turnFlushIntervalMs: 200,
882
969
  maxBatchSize: 100,
883
970
  maxQueueSize: 1e4,
884
971
  maxAttempts: 3,
@@ -902,6 +989,7 @@ var EventQueue = class {
902
989
  now;
903
990
  items = [];
904
991
  timer = null;
992
+ timerDueAt = Number.POSITIVE_INFINITY;
905
993
  tail = Promise.resolve();
906
994
  closed = false;
907
995
  droppedSinceWarning = 0;
@@ -926,7 +1014,8 @@ var EventQueue = class {
926
1014
  if (this.items.length >= this.options.flushAt) {
927
1015
  this.flushInBackground();
928
1016
  } else {
929
- this.schedule();
1017
+ const { flushIntervalMs, turnFlushIntervalMs } = this.options;
1018
+ this.schedule(isTurn(item) ? Math.min(turnFlushIntervalMs, flushIntervalMs) : flushIntervalMs);
930
1019
  }
931
1020
  return true;
932
1021
  }
@@ -1016,20 +1105,29 @@ var EventQueue = class {
1016
1105
  this.sentInWindow = 0;
1017
1106
  return item;
1018
1107
  }
1019
- schedule() {
1020
- if (this.timer !== null) return;
1108
+ /** Sends what is waiting in `delayMs`, unless a send is already due sooner. */
1109
+ schedule(delayMs) {
1110
+ const dueAt = this.now() + delayMs;
1111
+ if (this.timer !== null && this.timerDueAt <= dueAt) return;
1112
+ this.cancelTimer();
1113
+ this.timerDueAt = dueAt;
1021
1114
  this.timer = setTimeout(() => {
1022
1115
  this.timer = null;
1116
+ this.timerDueAt = Number.POSITIVE_INFINITY;
1023
1117
  this.flushInBackground();
1024
- }, this.options.flushIntervalMs);
1118
+ }, delayMs);
1025
1119
  unref(this.timer);
1026
1120
  }
1027
1121
  cancelTimer() {
1028
1122
  if (this.timer === null) return;
1029
1123
  clearTimeout(this.timer);
1030
1124
  this.timer = null;
1125
+ this.timerDueAt = Number.POSITIVE_INFINITY;
1031
1126
  }
1032
1127
  };
1128
+ function isTurn(item) {
1129
+ return item.type === "event" && item.kind === "message" && Boolean(item.conversation_id);
1130
+ }
1033
1131
  function unref(timer) {
1034
1132
  if (typeof timer === "object" && timer !== null && "unref" in timer && typeof timer.unref === "function") {
1035
1133
  timer.unref();
@@ -1106,6 +1204,8 @@ var Task = class {
1106
1204
  if (options.idempotency_key) event.idempotency_key = options.idempotency_key;
1107
1205
  if (options.occurred_at) event.occurred_at = options.occurred_at;
1108
1206
  if (options.visibility) event.visibility = options.visibility;
1207
+ const usage = asModelUsage(options.usage);
1208
+ if (usage) event.usage = usage;
1109
1209
  return this.track(event);
1110
1210
  }
1111
1211
  /**
@@ -1177,7 +1277,7 @@ var TOOL_NAMES = {
1177
1277
  timeline: "get_customer_timeline",
1178
1278
  open: "open_history_item"
1179
1279
  };
1180
- var ITEM_KINDS = ["episode", "fact", "open_item", "action", "system_event", "object", "trait"];
1280
+ var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
1181
1281
  var period = {
1182
1282
  since: { type: "string", format: "date-time", description: "Only items at or after this ISO 8601 time." },
1183
1283
  until: { type: "string", format: "date-time", description: "Only items before this ISO 8601 time." },
@@ -1197,7 +1297,7 @@ var TOOL_DEFINITIONS = [
1197
1297
  type: "function",
1198
1298
  function: {
1199
1299
  name: TOOL_NAMES.search,
1200
- description: "Search this customer's past conversations, actions and system events 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.",
1300
+ 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.",
1201
1301
  parameters: {
1202
1302
  type: "object",
1203
1303
  properties: {
@@ -1215,7 +1315,7 @@ var TOOL_DEFINITIONS = [
1215
1315
  type: "function",
1216
1316
  function: {
1217
1317
  name: TOOL_NAMES.timeline,
1218
- description: "List this customer's history in chronological order, 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.",
1318
+ 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.",
1219
1319
  parameters: {
1220
1320
  type: "object",
1221
1321
  properties: {
@@ -1309,7 +1409,7 @@ function bindTools(subject, binding, navigator, strict) {
1309
1409
  case TOOL_NAMES.open: {
1310
1410
  const id = str(args, "id");
1311
1411
  if (!id) throw new NiadraValidationError("open_history_item needs an id");
1312
- return navigator.open(id, binding, voice);
1412
+ return navigator.open(id, subject, binding, voice);
1313
1413
  }
1314
1414
  default:
1315
1415
  throw new NiadraValidationError(`unknown tool: ${name}`);
@@ -1348,7 +1448,7 @@ function cloneDefinition(definition) {
1348
1448
  }
1349
1449
 
1350
1450
  // src/version.ts
1351
- var VERSION = "0.1.0";
1451
+ var VERSION = "0.1.1";
1352
1452
 
1353
1453
  // src/transport.ts
1354
1454
  var RETRYABLE_WRITE_STATUS = /* @__PURE__ */ new Set([408, 421, 429, 500, 502, 503, 504]);
@@ -1401,13 +1501,17 @@ var Transport = class {
1401
1501
  );
1402
1502
  }
1403
1503
  async retrying(timeoutMs, policy, signal, attempt) {
1404
- for (let count = 1; ; count++) {
1405
- const deadline = new Deadline(timeoutMs, signal);
1504
+ const end = policy.totalMs === void 0 ? Number.POSITIVE_INFINITY : Date.now() + policy.totalMs;
1505
+ for (let count2 = 1; ; count2++) {
1506
+ const left = end - Date.now();
1507
+ if (left <= 0) throw new NiadraTimeoutError(policy.totalMs ?? timeoutMs);
1508
+ const deadline = new Deadline(Math.min(timeoutMs, left), signal);
1406
1509
  try {
1407
1510
  return await attempt(deadline);
1408
1511
  } catch (error) {
1409
- if (count >= policy.maxAttempts || !isTransient(error)) throw error;
1410
- const delay = retryDelay(error, count, policy.baseDelayMs, policy.maxDelayMs);
1512
+ if (count2 >= policy.maxAttempts || !isTransient(error)) throw error;
1513
+ const delay = retryDelay(error, count2, policy.baseDelayMs, policy.maxDelayMs);
1514
+ if (Date.now() + delay >= end) throw error;
1411
1515
  await sleep(delay, signal);
1412
1516
  } finally {
1413
1517
  deadline.clear();
@@ -1558,9 +1662,13 @@ function sleep(ms, signal) {
1558
1662
 
1559
1663
  // src/client.ts
1560
1664
  function readEnv(name) {
1561
- const env = globalThis.process?.env;
1562
- const value = env?.[name];
1563
- return value === "" ? void 0 : value;
1665
+ try {
1666
+ const env = globalThis.process?.env;
1667
+ const value = env?.[name];
1668
+ return value === "" ? void 0 : value;
1669
+ } catch {
1670
+ return void 0;
1671
+ }
1564
1672
  }
1565
1673
  function describe(error) {
1566
1674
  if (error instanceof NiadraAPIError) {
@@ -1700,7 +1808,7 @@ var Niadra = class {
1700
1808
  return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
1701
1809
  });
1702
1810
  }
1703
- /** The customer's history in chronological order, one line per item, paginated by cursor. */
1811
+ /** The customer's history, newest first, one line per item, paginated by cursor. */
1704
1812
  async timeline(params, options = {}) {
1705
1813
  return this.navigate(
1706
1814
  () => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
@@ -1708,20 +1816,17 @@ var Niadra = class {
1708
1816
  }
1709
1817
  /**
1710
1818
  * Opens one history item from `search()` or `timeline()`: summary, request, commitments,
1711
- * outcome and resolution. The literal transcript excerpt only comes back to keys with an
1712
- * elevated scope.
1819
+ * outcome and resolution. Sent as `POST /v1/history/open`: the conversation id and `subject` go
1820
+ * in the body, never in a URL.
1713
1821
  */
1714
1822
  async open(id, params = {}, options = {}) {
1715
1823
  return this.navigate(() => {
1716
1824
  if (!id) throw new NiadraValidationError("open() needs an item id");
1717
- const path = `/v1/history/items/${encodeURIComponent(id)}`;
1718
- const spec = this.readSpec("GET", path, void 0, this.timeouts.navigation, options);
1719
- spec.query = {
1720
- verification: params.verification,
1721
- conversation_id: params.conversation_id,
1722
- task_id: params.task_id
1723
- };
1724
- return spec;
1825
+ const body = { item_id: id };
1826
+ if (params.subject) body.subject = params.subject;
1827
+ if (params.verification) body.verification = params.verification;
1828
+ if (params.conversation_id) body.conversation_id = params.conversation_id;
1829
+ return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
1725
1830
  });
1726
1831
  }
1727
1832
  /**
@@ -1762,11 +1867,10 @@ var Niadra = class {
1762
1867
  const navigator = {
1763
1868
  search: (params, voice) => this.search(params, this.voiceBudget(voice)),
1764
1869
  timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
1765
- open: (id, bound, voice) => {
1766
- const scope = {};
1870
+ open: (id, customer, bound, voice) => {
1871
+ const scope = { subject: customer };
1767
1872
  if (bound.verification) scope.verification = bound.verification;
1768
1873
  if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
1769
- if (bound.task_id) scope.task_id = bound.task_id;
1770
1874
  return this.open(id, scope, this.voiceBudget(voice));
1771
1875
  }
1772
1876
  };
@@ -1829,7 +1933,7 @@ var Niadra = class {
1829
1933
  body: request,
1830
1934
  headers: { "idempotency-key": key },
1831
1935
  timeoutMs: this.timeouts.write,
1832
- retry: core.writes
1936
+ retry: { ...core.writes, totalMs: this.timeouts.write }
1833
1937
  });
1834
1938
  const [rejected] = response.data?.errors ?? [];
1835
1939
  if (rejected) {
@@ -1860,7 +1964,7 @@ var Niadra = class {
1860
1964
  path: "/v1/media/uploads",
1861
1965
  body: request,
1862
1966
  timeoutMs: this.timeouts.write,
1863
- retry: core.writes,
1967
+ retry: { ...core.writes, totalMs: this.timeouts.write },
1864
1968
  signal: options.signal
1865
1969
  });
1866
1970
  const media_ref = reserved.data?.media_ref;
@@ -1873,7 +1977,7 @@ var Niadra = class {
1873
1977
  body: bytes,
1874
1978
  headers: uploadHeaders(reserved.data?.upload_headers, request.content_type),
1875
1979
  timeoutMs: this.timeouts.upload,
1876
- retry: core.writes,
1980
+ retry: { ...core.writes, totalMs: this.timeouts.upload },
1877
1981
  signal: options.signal
1878
1982
  });
1879
1983
  }
@@ -2060,11 +2164,20 @@ var Niadra = class {
2060
2164
  }
2061
2165
  return new Promise((resolve, reject) => {
2062
2166
  const key = item.idempotency_key;
2063
- core.queue.push(item, (error) => {
2167
+ let settled = false;
2168
+ const timer = setTimeout(() => {
2169
+ this.logger.warn(`write not confirmed within ${this.timeouts.write} ms; it stays queued`);
2170
+ settle(new NiadraTimeoutError(this.timeouts.write));
2171
+ }, this.timeouts.write);
2172
+ const settle = (error) => {
2173
+ if (settled) return;
2174
+ settled = true;
2175
+ clearTimeout(timer);
2064
2176
  if (!error) resolve({ ok: true, idempotency_key: key, error: null });
2065
2177
  else if (this.strict) reject(error);
2066
2178
  else resolve({ ok: false, idempotency_key: key, error });
2067
- });
2179
+ };
2180
+ core.queue.push(item, settle);
2068
2181
  core.queue.flushInBackground();
2069
2182
  });
2070
2183
  }
@@ -2251,22 +2364,22 @@ var Answer = class {
2251
2364
  parsed(value) {
2252
2365
  try {
2253
2366
  if (!this.stream) {
2254
- this.record(messageText(value));
2367
+ this.record(messageText(value), modelUsage(value));
2255
2368
  return value;
2256
2369
  }
2257
- return isAsyncIterable(value) ? captureStream(value, (text) => {
2258
- this.record(text);
2370
+ return isAsyncIterable(value) ? captureStream(value, (text, usage) => {
2371
+ this.record(text, usage);
2259
2372
  }) : value;
2260
2373
  } catch (error) {
2261
2374
  this.session.logger.warn(`could not capture the model's answer (${errorName(error)})`);
2262
2375
  return value;
2263
2376
  }
2264
2377
  }
2265
- record(text) {
2378
+ record(text, usage) {
2266
2379
  if (this.recorded || !text) return;
2267
2380
  this.recorded = true;
2268
2381
  try {
2269
- this.session.agent(text);
2382
+ this.session.agent(text, usage ? { usage } : {});
2270
2383
  } catch (error) {
2271
2384
  this.session.logger.warn(`could not record the model's answer (${errorName(error)})`);
2272
2385
  }
@@ -2274,11 +2387,19 @@ var Answer = class {
2274
2387
  };
2275
2388
  function captureStream(stream, done) {
2276
2389
  const parts = [];
2390
+ let usage = null;
2391
+ let model;
2277
2392
  let finished = false;
2393
+ const observe = (chunk) => {
2394
+ const reported = property(chunk, "usage");
2395
+ if (isRecord(reported)) usage = reported;
2396
+ const name = property(chunk, "model");
2397
+ if (typeof name === "string" && name) model = name;
2398
+ };
2278
2399
  const finish = () => {
2279
2400
  if (finished) return;
2280
2401
  finished = true;
2281
- done(parts.join(""));
2402
+ done(parts.join(""), usage ? modelUsage(usage, model ? { model } : {}) : null);
2282
2403
  };
2283
2404
  return new Proxy(stream, {
2284
2405
  get(target, prop) {
@@ -2290,7 +2411,10 @@ function captureStream(stream, done) {
2290
2411
  try {
2291
2412
  const step = await inner.next();
2292
2413
  if (step.done) finish();
2293
- else parts.push(deltaText(step.value));
2414
+ else {
2415
+ parts.push(deltaText(step.value));
2416
+ observe(step.value);
2417
+ }
2294
2418
  return step;
2295
2419
  } catch (error) {
2296
2420
  finish();
@@ -2350,6 +2474,6 @@ function errorName(error) {
2350
2474
  return error instanceof Error ? error.name : typeof error;
2351
2475
  }
2352
2476
 
2353
- export { Conversation, DEFAULT_CACHE, DEFAULT_QUEUE, DEFAULT_TIMEOUTS, MAX_BATCH_ITEMS, MAX_EVENT_TEXT, MAX_MEDIA_BYTES, Niadra, NiadraAPIError, NiadraAbortError, NiadraAuthenticationError, NiadraConfigError, NiadraConnectionError, NiadraError, NiadraPermissionError, NiadraRateLimitError, NiadraTimeoutError, NiadraValidationError, TOOL_DEFINITIONS, TOOL_NAMES, Task, VERSION, baseURLFromKey, consoleLogger, handles, injectContext, parseApiKey, renderLive, renderSuffix, silentLogger, toObjectRef, uuidv7, wrap };
2477
+ export { Conversation, DEFAULT_CACHE, DEFAULT_QUEUE, DEFAULT_TIMEOUTS, MAX_BATCH_ITEMS, MAX_EVENT_TEXT, MAX_MEDIA_BYTES, Niadra, NiadraAPIError, NiadraAbortError, NiadraAuthenticationError, NiadraConfigError, NiadraConnectionError, NiadraError, NiadraPermissionError, NiadraRateLimitError, NiadraTimeoutError, NiadraValidationError, TOOL_DEFINITIONS, TOOL_NAMES, Task, VERSION, baseURLFromKey, consoleLogger, handles, injectContext, modelUsage, parseApiKey, providerOf, renderLive, renderSuffix, silentLogger, toObjectRef, tokenCounts, uuidv7, wrap };
2354
2478
  //# sourceMappingURL=index.js.map
2355
2479
  //# sourceMappingURL=index.js.map