@niadra/sdk 0.9.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/ai-sdk.d.cts +3 -3
  3. package/dist/ai-sdk.d.ts +3 -3
  4. package/dist/anthropic.d.cts +4 -4
  5. package/dist/anthropic.d.ts +4 -4
  6. package/dist/bedrock.d.cts +4 -4
  7. package/dist/bedrock.d.ts +4 -4
  8. package/dist/cli.js +319 -96
  9. package/dist/cli.js.map +1 -1
  10. package/dist/{client-CWMxmJHs.d.cts → client-rmWhZJOx.d.cts} +190 -7
  11. package/dist/{client-CWMxmJHs.d.ts → client-rmWhZJOx.d.ts} +190 -7
  12. package/dist/cloudflare-agents.d.cts +3 -3
  13. package/dist/cloudflare-agents.d.ts +3 -3
  14. package/dist/elevenlabs.cjs +7 -5
  15. package/dist/elevenlabs.cjs.map +1 -1
  16. package/dist/elevenlabs.d.cts +5 -5
  17. package/dist/elevenlabs.d.ts +5 -5
  18. package/dist/elevenlabs.js +7 -5
  19. package/dist/elevenlabs.js.map +1 -1
  20. package/dist/genkit.d.cts +3 -3
  21. package/dist/genkit.d.ts +3 -3
  22. package/dist/google-adk.cjs +7 -5
  23. package/dist/google-adk.cjs.map +1 -1
  24. package/dist/google-adk.d.cts +3 -3
  25. package/dist/google-adk.d.ts +3 -3
  26. package/dist/google-adk.js +7 -5
  27. package/dist/google-adk.js.map +1 -1
  28. package/dist/google-genai.d.cts +4 -4
  29. package/dist/google-genai.d.ts +4 -4
  30. package/dist/index.cjs +329 -98
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +13 -5
  33. package/dist/index.d.ts +13 -5
  34. package/dist/index.js +329 -98
  35. package/dist/index.js.map +1 -1
  36. package/dist/{intercept-CGngQI8B.d.cts → intercept-C0XODsy9.d.cts} +1 -1
  37. package/dist/{intercept-Bv0nLpSw.d.ts → intercept-veMDudKV.d.ts} +1 -1
  38. package/dist/langchain.cjs.map +1 -1
  39. package/dist/langchain.d.cts +3 -3
  40. package/dist/langchain.d.ts +3 -3
  41. package/dist/langchain.js.map +1 -1
  42. package/dist/livekit.cjs +10 -2
  43. package/dist/livekit.cjs.map +1 -1
  44. package/dist/livekit.d.cts +3 -3
  45. package/dist/livekit.d.ts +3 -3
  46. package/dist/livekit.js +10 -2
  47. package/dist/livekit.js.map +1 -1
  48. package/dist/llamaindex.d.cts +3 -3
  49. package/dist/llamaindex.d.ts +3 -3
  50. package/dist/mastra.d.cts +3 -3
  51. package/dist/mastra.d.ts +3 -3
  52. package/dist/openai-agents.d.cts +3 -3
  53. package/dist/openai-agents.d.ts +3 -3
  54. package/dist/retell.cjs +7 -5
  55. package/dist/retell.cjs.map +1 -1
  56. package/dist/retell.d.cts +5 -5
  57. package/dist/retell.d.ts +5 -5
  58. package/dist/retell.js +7 -5
  59. package/dist/retell.js.map +1 -1
  60. package/dist/{shared-B4Ef2Wpg.d.ts → shared-Cs7h5C4Y.d.ts} +1 -1
  61. package/dist/{shared-Cq8mz-7m.d.cts → shared-zaTBum17.d.cts} +1 -1
  62. package/dist/strands.d.cts +3 -3
  63. package/dist/strands.d.ts +3 -3
  64. package/dist/twilio.cjs +10 -2
  65. package/dist/twilio.cjs.map +1 -1
  66. package/dist/twilio.d.cts +4 -4
  67. package/dist/twilio.d.ts +4 -4
  68. package/dist/twilio.js +10 -2
  69. package/dist/twilio.js.map +1 -1
  70. package/dist/vapi.cjs +7 -5
  71. package/dist/vapi.cjs.map +1 -1
  72. package/dist/vapi.d.cts +5 -5
  73. package/dist/vapi.d.ts +5 -5
  74. package/dist/vapi.js +7 -5
  75. package/dist/vapi.js.map +1 -1
  76. package/dist/voltagent.d.cts +3 -3
  77. package/dist/voltagent.d.ts +3 -3
  78. package/dist/{webhook-z2TjUzjv.d.ts → webhook-2V5QgnKI.d.ts} +1 -1
  79. package/dist/{webhook-jTx_SegV.d.cts → webhook-CX7nvQig.d.cts} +1 -1
  80. package/dist/whatsapp.cjs +10 -2
  81. package/dist/whatsapp.cjs.map +1 -1
  82. package/dist/whatsapp.d.cts +2 -2
  83. package/dist/whatsapp.d.ts +2 -2
  84. package/dist/whatsapp.js +10 -2
  85. package/dist/whatsapp.js.map +1 -1
  86. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5,6 +5,46 @@ import { resolve as resolvePath } from "path";
5
5
  import { pathToFileURL } from "url";
6
6
  import { parseArgs as parseArgs4 } from "util";
7
7
 
8
+ // src/ids.ts
9
+ var lastMs = -1;
10
+ var counter = 0;
11
+ function randomBytes(length) {
12
+ const bytes = new Uint8Array(length);
13
+ const webCrypto = globalThis.crypto;
14
+ if (webCrypto?.getRandomValues) {
15
+ webCrypto.getRandomValues(bytes);
16
+ } else {
17
+ for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
18
+ }
19
+ return bytes;
20
+ }
21
+ function uuidv7(now = Date.now()) {
22
+ let ms = now;
23
+ if (ms <= lastMs) {
24
+ ms = lastMs;
25
+ counter = counter + 1 & 4095;
26
+ if (counter === 0) ms += 1;
27
+ } else {
28
+ counter = (randomBytes(2)[0] ?? 0) & 127;
29
+ }
30
+ lastMs = ms;
31
+ const bytes = randomBytes(16);
32
+ const high = Math.floor(ms / 2 ** 16);
33
+ const low = ms % 2 ** 16;
34
+ bytes[0] = high >>> 24 & 255;
35
+ bytes[1] = high >>> 16 & 255;
36
+ bytes[2] = high >>> 8 & 255;
37
+ bytes[3] = high & 255;
38
+ bytes[4] = low >>> 8 & 255;
39
+ bytes[5] = low & 255;
40
+ bytes[6] = 112 | counter >>> 8 & 15;
41
+ bytes[7] = counter & 255;
42
+ bytes[8] = 128 | (bytes[8] ?? 0) & 63;
43
+ let hex2 = "";
44
+ for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
45
+ return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
46
+ }
47
+
8
48
  // src/errors.ts
9
49
  var NiadraError = class extends Error {
10
50
  name = "NiadraError";
@@ -40,11 +80,12 @@ var NiadraAPIError = class extends NiadraError {
40
80
  constructor(status, problem, requestId, retryAfterMs = null) {
41
81
  const code = problem?.code ?? `http_${status}`;
42
82
  const detail = problem?.detail ? `: ${problem.detail}` : "";
43
- super(`${status} ${code}${detail}`);
83
+ const id = requestId ?? problem?.request_id ?? null;
84
+ super(`${status} ${code}${detail}${id ? ` (request ${id})` : ""}`);
44
85
  this.status = status;
45
86
  this.code = code;
46
87
  this.problem = problem;
47
- this.requestId = requestId ?? problem?.request_id ?? null;
88
+ this.requestId = id;
48
89
  this.retryAfterMs = retryAfterMs;
49
90
  }
50
91
  };
@@ -68,45 +109,11 @@ function toNiadraError(error) {
68
109
  const message = error instanceof Error ? error.message : String(error);
69
110
  return new NiadraError(message, { cause: error });
70
111
  }
71
-
72
- // src/ids.ts
73
- var lastMs = -1;
74
- var counter = 0;
75
- function randomBytes(length) {
76
- const bytes = new Uint8Array(length);
77
- const webCrypto = globalThis.crypto;
78
- if (webCrypto?.getRandomValues) {
79
- webCrypto.getRandomValues(bytes);
80
- } else {
81
- for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
82
- }
83
- return bytes;
84
- }
85
- function uuidv7(now = Date.now()) {
86
- let ms = now;
87
- if (ms <= lastMs) {
88
- ms = lastMs;
89
- counter = counter + 1 & 4095;
90
- if (counter === 0) ms += 1;
91
- } else {
92
- counter = (randomBytes(2)[0] ?? 0) & 127;
93
- }
94
- lastMs = ms;
95
- const bytes = randomBytes(16);
96
- const high = Math.floor(ms / 2 ** 16);
97
- const low = ms % 2 ** 16;
98
- bytes[0] = high >>> 24 & 255;
99
- bytes[1] = high >>> 16 & 255;
100
- bytes[2] = high >>> 8 & 255;
101
- bytes[3] = high & 255;
102
- bytes[4] = low >>> 8 & 255;
103
- bytes[5] = low & 255;
104
- bytes[6] = 112 | counter >>> 8 & 15;
105
- bytes[7] = counter & 255;
106
- bytes[8] = 128 | (bytes[8] ?? 0) & 63;
107
- let hex2 = "";
108
- for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
109
- return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
112
+ function explain(error) {
113
+ if (!(error instanceof NiadraAPIError)) return error instanceof Error ? error.name : String(error);
114
+ const detail = error.problem?.detail?.trim();
115
+ const said = `${error.status} ${error.code}${detail ? `: ${detail.slice(0, 300)}` : ""}`;
116
+ return error.requestId ? `${said} (request ${error.requestId})` : said;
110
117
  }
111
118
 
112
119
  // src/admin.ts
@@ -503,7 +510,7 @@ function internalRecord(span, ref, act) {
503
510
  }
504
511
 
505
512
  // src/version.ts
506
- var VERSION = "0.9.1";
513
+ var VERSION = "0.10.3";
507
514
 
508
515
  // src/transport.ts
509
516
  var RETRYABLE_WRITE_STATUS = /* @__PURE__ */ new Set([408, 421, 429, 500, 502, 503, 504]);
@@ -516,8 +523,32 @@ var Transport = class {
516
523
  }
517
524
  config;
518
525
  baseURL;
526
+ answeredAt;
527
+ /** When the allowance was first given since the last answer; it holds for the calls of that moment only. */
528
+ grantedAt;
529
+ /** When the client last sent a request of its own (`RequestSpec.activity`), in epoch milliseconds. */
530
+ lastActivityAt;
519
531
  async request(spec) {
520
- return spec.retry.kind === "read" ? this.read(spec, spec.retry) : this.write(spec, spec.retry);
532
+ if (spec.activity !== false) this.lastActivityAt = Date.now();
533
+ const budgeted = this.withAllowance(spec);
534
+ return budgeted.retry.kind === "read" ? this.read(budgeted, budgeted.retry) : this.write(budgeted, budgeted.retry);
535
+ }
536
+ /**
537
+ * `spec` with the allowance for opening a connection, when it has a budget and none is likely open. The calls
538
+ * that start while the first one opens it get it too; after that, none does until an answer comes, so an
539
+ * outage costs the allowance once, not on every turn.
540
+ */
541
+ withAllowance(spec) {
542
+ const allowance = this.config.coldAllowanceMs ?? 0;
543
+ const { retry } = spec;
544
+ if (allowance <= 0 || retry.kind === "write" && retry.totalMs === void 0) return spec;
545
+ const now = Date.now();
546
+ const keep = this.config.keepAliveMs ?? 0;
547
+ if (this.answeredAt !== void 0 && now - this.answeredAt <= keep) return spec;
548
+ this.grantedAt ??= now;
549
+ if (now - this.grantedAt > allowance) return spec;
550
+ const total = retry.kind === "write" && retry.totalMs !== void 0 ? { ...retry, totalMs: retry.totalMs + allowance } : retry;
551
+ return { ...spec, timeoutMs: spec.timeoutMs + allowance, retry: total };
521
552
  }
522
553
  async read(spec, policy) {
523
554
  const deadline = new Deadline(spec.timeoutMs, spec.signal);
@@ -588,7 +619,18 @@ var Transport = class {
588
619
  headers["content-type"] = "application/json";
589
620
  init.body = JSON.stringify(spec.body);
590
621
  }
591
- return this.exchange(this.url(spec.path, spec.query), init, deadline);
622
+ try {
623
+ const answer = await this.exchange(this.url(spec.path, spec.query), init, deadline);
624
+ this.answered();
625
+ return answer;
626
+ } catch (error) {
627
+ if (error instanceof NiadraAPIError) this.answered();
628
+ throw error;
629
+ }
630
+ }
631
+ answered() {
632
+ this.answeredAt = Date.now();
633
+ this.grantedAt = void 0;
592
634
  }
593
635
  async exchange(url, init, deadline) {
594
636
  let response;
@@ -4025,8 +4067,7 @@ var TurnSender = class {
4025
4067
  return true;
4026
4068
  }
4027
4069
  }
4028
- const code = error instanceof NiadraAPIError ? error.code : toNiadraError(error).name;
4029
- this.recorder.rejected(batch.frames.length, [code]);
4070
+ this.recorder.rejected(batch.frames.length, [explain(error)]);
4030
4071
  return true;
4031
4072
  }
4032
4073
  accepted(batch, answer) {
@@ -4034,8 +4075,8 @@ var TurnSender = class {
4034
4075
  this.resumeAt = 0;
4035
4076
  this.recorder.sent(answer.accepted, answer.duplicates);
4036
4077
  const again = [];
4037
- const codes2 = /* @__PURE__ */ new Set();
4038
- let refused2 = 0;
4078
+ const reasons = /* @__PURE__ */ new Set();
4079
+ let refused3 = 0;
4039
4080
  for (const error of answer.errors ?? []) {
4040
4081
  const frame = batch.frames[error.index];
4041
4082
  if (frame === void 0) continue;
@@ -4044,12 +4085,12 @@ var TurnSender = class {
4044
4085
  this.recorder.modeRefused();
4045
4086
  again.push(frame);
4046
4087
  } else {
4047
- refused2++;
4048
- codes2.add(error.code);
4088
+ refused3++;
4089
+ reasons.add(error.detail ? `${error.code}: ${error.detail.slice(0, 300)}` : error.code);
4049
4090
  }
4050
4091
  }
4051
4092
  if (again.length > 0) this.queue.requeue(again);
4052
- if (refused2 > 0) this.recorder.rejected(refused2, [...codes2]);
4093
+ if (refused3 > 0) this.recorder.rejected(refused3, [...reasons]);
4053
4094
  }
4054
4095
  };
4055
4096
 
@@ -4171,9 +4212,12 @@ var TurnRecorder = class {
4171
4212
  this.accepted += accepted;
4172
4213
  this.duplicates += duplicates;
4173
4214
  }
4174
- rejected(count2, codes2) {
4215
+ /** `reasons`: the API's code and detail of each refusal (field paths and rules, never a value). */
4216
+ rejected(count2, reasons) {
4175
4217
  this.rejectedTurns += count2;
4176
- this.logger.warn(`${count2} turn records were refused (${[...codes2].sort().join(", ")})`);
4218
+ const shown = [...new Set(reasons)].sort();
4219
+ const more = shown.length > 3 ? ` (and ${shown.length - 3} more)` : "";
4220
+ this.logger.warn(`${count2} turn records were refused: ${shown.slice(0, 3).join("; ")}${more}`);
4177
4221
  }
4178
4222
  /** The space does not record turns: these are dropped, and with `off` (a 404) recording stops a while. */
4179
4223
  notRecorded(count2, off) {
@@ -4243,6 +4287,10 @@ async function hex(text2) {
4243
4287
  }
4244
4288
 
4245
4289
  // src/coordination/client.ts
4290
+ var REFUSED = /* @__PURE__ */ new Set([400, 401, 403, 422]);
4291
+ function refused(error) {
4292
+ return error instanceof NiadraAPIError && REFUSED.has(error.status);
4293
+ }
4246
4294
  var FAIL_CLOSED = /* @__PURE__ */ new Set(["marketing", "retention", "collection"]);
4247
4295
  var CHECK_BUDGET_MS = 200;
4248
4296
  function fallback(request, suppressed, failOpen) {
@@ -4264,15 +4312,22 @@ function claimed(data, error) {
4264
4312
  return { held: false, claim: null, error: code };
4265
4313
  }
4266
4314
  var Coordinator = class {
4267
- constructor(outbox, suppressions, declareNow) {
4315
+ constructor(outbox, suppressions, declareNow, logger) {
4268
4316
  this.outbox = outbox;
4269
4317
  this.suppressions = suppressions;
4270
4318
  this.declareNow = declareNow;
4319
+ this.logger = logger;
4271
4320
  }
4272
4321
  outbox;
4273
4322
  suppressions;
4274
4323
  declareNow;
4324
+ logger;
4275
4325
  async failed(request, failOpen, error) {
4326
+ if (refused(error)) {
4327
+ this.logger?.warn(`the coordination check was refused: ${explain(error)}`);
4328
+ const decision = request.direction === "inbound" ? "allow" : "defer";
4329
+ return recorded({ decision, decision_id: uuidv7(), reasons: ["invalid_request"], valid_for_s: 0 });
4330
+ }
4276
4331
  const off = error instanceof NiadraAPIError && error.status === 404;
4277
4332
  const suppressed = request.subject != null && request.direction === "outbound" ? !await this.suppressions.mayContact(request.subject, request.purpose, { channel: request.channel ?? null, failOpen: true }) : false;
4278
4333
  const plain2 = off ? { ...request, effect_key: null } : request;
@@ -4293,7 +4348,7 @@ var Coordinator = class {
4293
4348
  if (who.subject) body.subject = who.subject;
4294
4349
  if (who.object) body.object = who.object;
4295
4350
  const key2 = uuidv7();
4296
- this.outbox.put({ send: () => this.declareNow(body, key2) });
4351
+ this.outbox.put({ send: () => this.declareNow(body, key2), route: "POST /v1/coordination/declare" });
4297
4352
  return key2;
4298
4353
  }
4299
4354
  };
@@ -4812,7 +4867,7 @@ var Outbox = class {
4812
4867
  return false;
4813
4868
  }
4814
4869
  this.writes.shift();
4815
- this.logger.warn(`a write was refused (${error instanceof NiadraAPIError ? error.code : String(error)})`);
4870
+ this.logger.warn(`${write.route ?? "a write"} was refused: ${explain(error)}`);
4816
4871
  this.settle(write, void 0, error);
4817
4872
  return true;
4818
4873
  }
@@ -6349,6 +6404,7 @@ var AgentSession = class {
6349
6404
  try {
6350
6405
  return this.host.coordinator.decided(await this.host.check(request, options.timeoutMs ?? CHECK_BUDGET_MS), request, this.checked);
6351
6406
  } catch (error) {
6407
+ if (this.host.strict && refused(error)) throw error;
6352
6408
  return this.host.coordinator.failed(request, options.failOpen, error);
6353
6409
  }
6354
6410
  }
@@ -6960,6 +7016,7 @@ var SessionState = class {
6960
7016
  /** The guards of the last read: they hold the answers to the turn they were written for. */
6961
7017
  guards = /* @__PURE__ */ new Map();
6962
7018
  lastReport = null;
7019
+ unlinkedSaid = false;
6963
7020
  /** After the first pack, every read also asks what changed since. */
6964
7021
  get wantsDelta() {
6965
7022
  return this.etag !== null;
@@ -6988,6 +7045,19 @@ var SessionState = class {
6988
7045
  this.last = absorbed;
6989
7046
  return absorbed;
6990
7047
  }
7048
+ /**
7049
+ * Once per session: the read named an organization (`about`) with no active link to the subject, so it went
7050
+ * on with the subject's own memory, never the organization's.
7051
+ */
7052
+ sayUnlinked(result2, logger) {
7053
+ if (result2.response?.about_unlinked && !this.unlinkedSaid) {
7054
+ this.unlinkedSaid = true;
7055
+ logger.warn(
7056
+ "`about` names an organization with no active link to the subject; the context is the subject's own, without that organization's memory. Create the link to read it."
7057
+ );
7058
+ }
7059
+ return result2;
7060
+ }
6991
7061
  /** The last backing check of an agent's answer. */
6992
7062
  get lastBacking() {
6993
7063
  return this.lastReport;
@@ -7235,7 +7305,7 @@ var Conversation = class {
7235
7305
  * `query` picks this read's slots by other words than the turn; the pack is the pinned one.
7236
7306
  */
7237
7307
  async context(options = {}) {
7238
- const { query, turn: turn2, format, explain, include, ...requestOptions } = options;
7308
+ const { query, turn: turn2, format, explain: explain2, include, ...requestOptions } = options;
7239
7309
  const params = {
7240
7310
  subject: this.subject,
7241
7311
  view: this.view,
@@ -7244,7 +7314,7 @@ var Conversation = class {
7244
7314
  ...this.params.about ? { about: this.params.about } : {},
7245
7315
  ...this.params.target ? { target: this.params.target } : {},
7246
7316
  ...format === "json" ? { format } : {},
7247
- ...explain ? { explain } : {}
7317
+ ...explain2 ? { explain: explain2 } : {}
7248
7318
  };
7249
7319
  if (query) params.query = query;
7250
7320
  if (include?.length) params.include = include;
@@ -7252,7 +7322,7 @@ var Conversation = class {
7252
7322
  params.turn = turn2 === void 0 ? this.turnText : turn2;
7253
7323
  const result2 = await this.client.context(params, requestOptions);
7254
7324
  this.features.observe(result2);
7255
- return this.state.observe(this.state.absorb(result2));
7325
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7256
7326
  }
7257
7327
  /**
7258
7328
  * Starts this conversation's first read now, in the background: call it when the call starts
@@ -7432,6 +7502,35 @@ function withHandles(base, extra) {
7432
7502
  return all;
7433
7503
  }
7434
7504
 
7505
+ // src/warm.ts
7506
+ var EVERY_MS = 1e5;
7507
+ var IDLE_MS = 9e4;
7508
+ var WARM_FOR_MS = 6e5;
7509
+ var KeepWarm = class {
7510
+ constructor(enabled) {
7511
+ this.enabled = enabled;
7512
+ }
7513
+ enabled;
7514
+ open = /* @__PURE__ */ new Map();
7515
+ openedAt = 0;
7516
+ /** Notes an open conversation or task. */
7517
+ add(scope, session, now) {
7518
+ if (!this.enabled || typeof WeakRef === "undefined") return;
7519
+ this.open.set(scope, new WeakRef(session));
7520
+ this.openedAt = now;
7521
+ }
7522
+ /** The conversation or task ended. */
7523
+ end(scope) {
7524
+ this.open.delete(scope);
7525
+ }
7526
+ step(now, lastActivityAt, idleMs = IDLE_MS, warmForMs = WARM_FOR_MS) {
7527
+ for (const [scope, ref] of this.open) if (ref.deref() === void 0) this.open.delete(scope);
7528
+ const used = Math.max(lastActivityAt ?? 0, this.openedAt);
7529
+ if (this.open.size === 0 || now - used > warmForMs) return "stop";
7530
+ return now - used >= idleMs ? "ping" : "wait";
7531
+ }
7532
+ };
7533
+
7435
7534
  // src/exit.ts
7436
7535
  var registered = /* @__PURE__ */ new Set();
7437
7536
  var installedOn = null;
@@ -7561,8 +7660,10 @@ var DEFAULT_TIMEOUTS = {
7561
7660
  write: 5e3,
7562
7661
  token: 2e3,
7563
7662
  upload: 6e4,
7564
- prefetch: 1e3
7663
+ prefetch: 1e3,
7664
+ connect: 1e3
7565
7665
  };
7666
+ var FETCH_KEEPALIVE_MS = 4e3;
7566
7667
  var DEFAULT_CACHE = {
7567
7668
  ttlMs: 1e4,
7568
7669
  staleWhileRevalidateMs: 10 * 6e4,
@@ -7669,7 +7770,7 @@ var Task = class {
7669
7770
  * Resolves with an empty result, never rejects, unless the client is strict.
7670
7771
  */
7671
7772
  async context(options = {}) {
7672
- const { query, format, explain, include, ...requestOptions } = options;
7773
+ const { query, format, explain: explain2, include, ...requestOptions } = options;
7673
7774
  const target = this.object ? { object: this.object } : this.params.subject ? { subject: this.params.subject } : {};
7674
7775
  const params = {
7675
7776
  ...target,
@@ -7679,18 +7780,18 @@ var Task = class {
7679
7780
  ...this.level ? { verification: this.level } : {},
7680
7781
  ...this.params.target ? { target: this.params.target } : {},
7681
7782
  ...format === "json" ? { format } : {},
7682
- ...explain ? { explain } : {},
7783
+ ...explain2 ? { explain: explain2 } : {},
7683
7784
  ...include?.length ? { include } : {}
7684
7785
  };
7685
7786
  if (query) {
7686
7787
  const answered = await this.client.context({ ...params, query }, requestOptions);
7687
7788
  this.features.observe(answered);
7688
- return this.state.observe(answered);
7789
+ return this.state.observe(this.state.sayUnlinked(answered, this.logger));
7689
7790
  }
7690
7791
  if (this.state.wantsDelta) params.delta = true;
7691
7792
  const result2 = await this.client.context(params, requestOptions);
7692
7793
  this.features.observe(result2);
7693
- return this.state.observe(this.state.absorb(result2));
7794
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7694
7795
  }
7695
7796
  /**
7696
7797
  * The agent's own working notes for this task's prompt, as `niadra.agentMemory()` with
@@ -7820,14 +7921,14 @@ var AGENT_MEMORY_TOOL_NAMES = {
7820
7921
  search: "search_agent_memory",
7821
7922
  remember: "remember"
7822
7923
  };
7823
- var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
7924
+ var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait", "system_event"];
7824
7925
  var NOTE_KINDS = ["procedure", "tool_note", "process_note", "pitfall"];
7825
7926
  var TOOL_DEFINITIONS = [
7826
7927
  {
7827
7928
  "type": "function",
7828
7929
  "function": {
7829
7930
  "name": "search_customer_history",
7830
- "description": "Search everything that already happened with this customer: past conversations, promises, agent actions, orders and invoices. Use it when the customer refers to something earlier or asks whether a problem happened before. Do not call it when the answer is already in the context block, including its 'Do hist\xF3rico' line, which already counts recurrences. Returns short items with date, channel and outcome, plus a recurrence count. Open one with open_history_item.",
7931
+ "description": "Search everything that already happened with this customer: past conversations, promises, agent actions, orders and invoices. Use it when the customer refers to something earlier or asks whether a problem happened before. Do not call it when the answer is already in the context block, including its 'Hist\xF3rico' line, which already counts recurrences. Returns short items with date, channel and outcome, plus a recurrence count. Open a conversation (`episode:`) or an object (`object:`) with open_history_item; the other items are complete as listed.",
7831
7932
  "parameters": {
7832
7933
  "type": "object",
7833
7934
  "properties": {
@@ -7871,7 +7972,8 @@ var TOOL_DEFINITIONS = [
7871
7972
  "open_item",
7872
7973
  "action",
7873
7974
  "object",
7874
- "trait"
7975
+ "trait",
7976
+ "system_event"
7875
7977
  ]
7876
7978
  }
7877
7979
  },
@@ -7898,7 +8000,7 @@ var TOOL_DEFINITIONS = [
7898
8000
  "type": "function",
7899
8001
  "function": {
7900
8002
  "name": "get_customer_timeline",
7901
- "description": "List this customer's conversations and agent actions in order, newest first, one line each. Use it to leaf through the history when you do not know what to search for. Pass next_cursor to continue. Prefer search_customer_history for a specific question.",
8003
+ "description": "List this customer's conversations, agent actions and system events in order, newest first, one line each. Use it to leaf through the history when you do not know what to search for; filters.item_kinds also lists the open items, facts, patterns or objects it names. Pass next_cursor to continue. Prefer search_customer_history for a specific question.",
7902
8004
  "parameters": {
7903
8005
  "type": "object",
7904
8006
  "properties": {
@@ -7946,7 +8048,8 @@ var TOOL_DEFINITIONS = [
7946
8048
  "open_item",
7947
8049
  "action",
7948
8050
  "object",
7949
- "trait"
8051
+ "trait",
8052
+ "system_event"
7950
8053
  ]
7951
8054
  }
7952
8055
  },
@@ -7965,7 +8068,7 @@ var TOOL_DEFINITIONS = [
7965
8068
  "type": "function",
7966
8069
  "function": {
7967
8070
  "name": "open_history_item",
7968
- "description": "Open one conversation or business object returned by search_customer_history or get_customer_timeline: what was asked, what was promised and by whom, the outcome and what memory came from it. Use it only after a search or timeline pointed to the item.",
8071
+ "description": "Open one conversation (`episode:`) or business object (`object:`) returned by search_customer_history or get_customer_timeline: what was asked, what was promised and by whom, the outcome and what memory came from it. Actions, events, facts and promises are complete as listed and do not open. Use it only after a search or timeline pointed to the item.",
7969
8072
  "parameters": {
7970
8073
  "type": "object",
7971
8074
  "properties": {
@@ -8336,6 +8439,13 @@ function compose(body, read2) {
8336
8439
  const pack = body.pack ? { pack: { ...body.pack, slots: fetched?.pack?.slots ?? [] } } : {};
8337
8440
  return { ...body, slots: fetched?.slots ?? null, guards: fetched?.guards ?? [], ...pack };
8338
8441
  }
8442
+ var RTT_MARGIN_MS = 50;
8443
+ function budgetWarnings(rttMs, timeouts, explicit) {
8444
+ const ms = Math.round(rttMs);
8445
+ return [...explicit].sort().filter((name) => timeouts[name] < rttMs + RTT_MARGIN_MS).map(
8446
+ (name) => `timeouts.${name} (${timeouts[name]} ms) is shorter than the round trip to the region (${ms} ms) plus ${RTT_MARGIN_MS} ms for the API: its reads will run out of time. Leave it at its default, which adds the measured round trip, or raise it`
8447
+ );
8448
+ }
8339
8449
  function rttWarnings(rttMs, timeouts) {
8340
8450
  const ms = Math.round(rttMs);
8341
8451
  const found2 = [];
@@ -8373,6 +8483,12 @@ var Niadra = class _Niadra {
8373
8483
  enabled;
8374
8484
  core;
8375
8485
  timeouts;
8486
+ keepWarm;
8487
+ warmTimer;
8488
+ /** The read budgets the caller left at their defaults: they take the measured round trip on top. */
8489
+ defaultReads;
8490
+ voiceStarted = false;
8491
+ voiceWarned = false;
8376
8492
  strict;
8377
8493
  /** Where the client reports what it swallows in fail-open mode. */
8378
8494
  logger;
@@ -8417,6 +8533,8 @@ var Niadra = class _Niadra {
8417
8533
  this.strict = options.strict ?? false;
8418
8534
  this.logger = options.logger ?? consoleLogger;
8419
8535
  this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
8536
+ this.keepWarm = new KeepWarm(options.keepWarm ?? true);
8537
+ this.defaultReads = new Set(["context", "navigation"].filter((name) => options.timeouts?.[name] === void 0));
8420
8538
  this.voice = new VoiceLines(
8421
8539
  options.voice === false ? { ...DEFAULT_VOICE, enabled: false } : { ...DEFAULT_VOICE, ...options.voice }
8422
8540
  );
@@ -8442,13 +8560,14 @@ var Niadra = class _Niadra {
8442
8560
  this.coordinator = new Coordinator(
8443
8561
  this.outbox,
8444
8562
  this.suppressions,
8445
- (body, key2) => this.api.declare(body, { idempotency_key: key2 })
8563
+ (body, key2) => this.api.declare(body, { idempotency_key: key2 }),
8564
+ this.logger
8446
8565
  );
8447
8566
  this.states = new AgentStates(
8448
8567
  this.outbox,
8449
8568
  {
8450
- read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.timeouts.navigation }),
8451
- write: (write) => this.api.writeAgentState(write, { timeout: this.timeouts.navigation })
8569
+ read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.readBudget("navigation") }),
8570
+ write: (write) => this.api.writeAgentState(write, { timeout: this.readBudget("navigation") })
8452
8571
  },
8453
8572
  this.logger
8454
8573
  );
@@ -8475,6 +8594,7 @@ var Niadra = class _Niadra {
8475
8594
  this.core = setup;
8476
8595
  this.enabled = true;
8477
8596
  this.disabledReason = null;
8597
+ this.probe(setup);
8478
8598
  if (options.flushOnExit ?? true) this.unregisterExit = registerExitFlush(this);
8479
8599
  }
8480
8600
  setup(options) {
@@ -8497,7 +8617,9 @@ var Niadra = class _Niadra {
8497
8617
  apiKey,
8498
8618
  fetch: fetchImpl,
8499
8619
  defaultHeaders: options.defaultHeaders ?? {},
8500
- logger: this.logger
8620
+ logger: this.logger,
8621
+ coldAllowanceMs: this.timeouts.connect,
8622
+ keepAliveMs: options.keepAliveMs ?? FETCH_KEEPALIVE_MS
8501
8623
  });
8502
8624
  const queueOptions = { ...DEFAULT_QUEUE, ...options.queue };
8503
8625
  queueOptions.maxBatchSize = Math.min(queueOptions.maxBatchSize, 499);
@@ -8553,7 +8675,7 @@ var Niadra = class _Niadra {
8553
8675
  return result2;
8554
8676
  }
8555
8677
  readContext(core, params, request, options) {
8556
- const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
8678
+ const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8557
8679
  const { query: own2, ...pinned2 } = request;
8558
8680
  const query = turnText(own2 ?? params.turn);
8559
8681
  const voiceCache = this.voiceCache(core, pinned2, options.cache);
@@ -8569,7 +8691,7 @@ var Niadra = class _Niadra {
8569
8691
  */
8570
8692
  async profile() {
8571
8693
  if (!this.core) return null;
8572
- return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.timeouts.navigation }));
8694
+ return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.readBudget("navigation") }));
8573
8695
  }
8574
8696
  /**
8575
8697
  * Checks outputs against this claim contract instead of the one the profile serves (a company's own copy,
@@ -8594,7 +8716,7 @@ var Niadra = class _Niadra {
8594
8716
  */
8595
8717
  async mayContact(handle, purpose, options = {}) {
8596
8718
  if (this.core && this.suppressions.due()) {
8597
- const read2 = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.timeouts.navigation);
8719
+ const read2 = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.readBudget("navigation"));
8598
8720
  if (!this.suppressions.held) await read2;
8599
8721
  }
8600
8722
  const checkOptions = { channel: options.channel ?? null };
@@ -8604,8 +8726,8 @@ var Niadra = class _Niadra {
8604
8726
  readSuppressions(budgetMs) {
8605
8727
  return this.suppressions.read(
8606
8728
  {
8607
- salt: () => this.api.suppressionSalt({ timeout: this.timeouts.navigation }),
8608
- page: (cursor, limit2) => this.api.suppressions({ cursor, limit: limit2 }, { timeout: this.timeouts.navigation })
8729
+ salt: () => this.api.suppressionSalt({ timeout: this.readBudget("navigation") }),
8730
+ page: (cursor, limit2) => this.api.suppressions({ cursor, limit: limit2 }, { timeout: this.readBudget("navigation") })
8609
8731
  },
8610
8732
  budgetMs
8611
8733
  );
@@ -8684,6 +8806,7 @@ var Niadra = class _Niadra {
8684
8806
  }
8685
8807
  /** What a conversation or a task needs of its client for the agent features. */
8686
8808
  get agentHost() {
8809
+ const client = this;
8687
8810
  return {
8688
8811
  recorder: this.turns,
8689
8812
  coordinator: this.coordinator,
@@ -8697,7 +8820,10 @@ var Niadra = class _Niadra {
8697
8820
  claim: (request, timeoutMs) => this.api.claim(request, {}, { timeout: timeoutMs }),
8698
8821
  verifyClaim: (ref, field, value, options) => this.verifyClaim(ref, field, value, options),
8699
8822
  enabled: this.enabled,
8700
- navigationMs: this.timeouts.navigation
8823
+ strict: this.strict,
8824
+ get navigationMs() {
8825
+ return client.readBudget("navigation");
8826
+ }
8701
8827
  };
8702
8828
  }
8703
8829
  /**
@@ -8725,7 +8851,7 @@ var Niadra = class _Niadra {
8725
8851
  if (!cache) return false;
8726
8852
  const key2 = cacheKey(request);
8727
8853
  const line = this.voice.line(cacheScope(request) ?? "");
8728
- this.probe(core);
8854
+ this.startVoice(core);
8729
8855
  line.request = request;
8730
8856
  if (!cache.has(key2) && line.inFlight().length === 0) {
8731
8857
  this.voiceRead(core, cache, line, key2, request, null, this.timeouts.contextVoiceStart);
@@ -8862,13 +8988,13 @@ var Niadra = class _Niadra {
8862
8988
  if (!params.query || params.query.length > 2e3) {
8863
8989
  throw new NiadraValidationError("query must be 1 to 2000 characters");
8864
8990
  }
8865
- return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
8991
+ return this.readSpec("POST", "/v1/history/search", params, this.readBudget("navigation"), options);
8866
8992
  });
8867
8993
  }
8868
8994
  /** The customer's history, newest first, one line per item, paginated by cursor. */
8869
8995
  async timeline(params, options = {}) {
8870
8996
  return this.navigate(
8871
- () => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
8997
+ () => this.readSpec("POST", "/v1/history/timeline", params, this.readBudget("navigation"), options)
8872
8998
  );
8873
8999
  }
8874
9000
  /**
@@ -8881,9 +9007,10 @@ var Niadra = class _Niadra {
8881
9007
  if (!id) throw new NiadraValidationError("open() needs an item id");
8882
9008
  const body = { item_id: id };
8883
9009
  if (params.subject) body.subject = params.subject;
9010
+ if (params.about) body.about = params.about;
8884
9011
  if (params.verification) body.verification = params.verification;
8885
9012
  if (params.conversation_id) body.conversation_id = params.conversation_id;
8886
- return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
9013
+ return this.readSpec("POST", "/v1/history/open", body, this.readBudget("navigation"), options);
8887
9014
  });
8888
9015
  }
8889
9016
  /**
@@ -8895,7 +9022,7 @@ var Niadra = class _Niadra {
8895
9022
  * const { data: invoice } = await niadra.objectState("invoice:erp:0823");
8896
9023
  */
8897
9024
  async objectState(object, options = {}) {
8898
- return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.timeouts.navigation, options));
9025
+ return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.readBudget("navigation"), options));
8899
9026
  }
8900
9027
  /**
8901
9028
  * System events and agent actions about one object, newest first, one line each and never
@@ -8908,7 +9035,7 @@ var Niadra = class _Niadra {
8908
9035
  throw new NiadraValidationError("limit must be between 1 and 100");
8909
9036
  }
8910
9037
  const path = `${objectPath(object)}/timeline`;
8911
- const spec = this.readSpec("GET", path, void 0, this.timeouts.navigation, options);
9038
+ const spec = this.readSpec("GET", path, void 0, this.readBudget("navigation"), options);
8912
9039
  spec.query = { cursor: params.cursor, limit: String(limit2) };
8913
9040
  return spec;
8914
9041
  });
@@ -8927,6 +9054,7 @@ var Niadra = class _Niadra {
8927
9054
  timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
8928
9055
  open: (id, customer, bound, voice) => {
8929
9056
  const scope = { subject: customer };
9057
+ if (bound.about) scope.about = bound.about;
8930
9058
  if (bound.verification) scope.verification = bound.verification;
8931
9059
  if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
8932
9060
  return this.open(id, scope, this.voiceBudget(voice));
@@ -8961,7 +9089,7 @@ var Niadra = class _Niadra {
8961
9089
  const key2 = AgentMemoryCache.key(params);
8962
9090
  const fresh = cache?.fresh(key2);
8963
9091
  if (fresh) return blockResult(fresh, "cache");
8964
- const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
9092
+ const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8965
9093
  const spec = this.readSpec("GET", "/v1/agent-memory/block", void 0, timeout, options);
8966
9094
  spec.query = {
8967
9095
  max_tokens: String(params.max_tokens ?? 300),
@@ -8998,7 +9126,7 @@ var Niadra = class _Niadra {
8998
9126
  if (params.limit !== void 0) body.limit = params.limit;
8999
9127
  if (params.conversation_id) body.conversation_id = params.conversation_id;
9000
9128
  if (params.task_id) body.task_id = params.task_id;
9001
- return this.readSpec("POST", "/v1/agent-memory/search", body, this.timeouts.navigation, options);
9129
+ return this.readSpec("POST", "/v1/agent-memory/search", body, this.readBudget("navigation"), options);
9002
9130
  });
9003
9131
  return result2.error ? result2 : { data: result2.data.notes, error: null };
9004
9132
  }
@@ -9092,6 +9220,41 @@ var Niadra = class _Niadra {
9092
9220
  return { ok: false, idempotency_key: key2, error: this.swallow(error, "feedback") };
9093
9221
  }
9094
9222
  }
9223
+ /**
9224
+ * How the space's agents used the context they read (`GET /v1/context-use`): sessions, deliveries, use,
9225
+ * repetition, transfers and recontact, with intervals, grouped by `group_by`. A key of an `analyst` source
9226
+ * with the `analytics` scope reads every source of the space; a key with `admin` reads its own source.
9227
+ */
9228
+ contextUse(params = {}, options = {}) {
9229
+ return this.navigate(() => {
9230
+ const { group_by: groups, ...filters } = params;
9231
+ const query = { ...filters };
9232
+ if (groups?.length) query.group_by = groups;
9233
+ return { ...this.readSpec("GET", "/v1/context-use", void 0, this.timeouts.write, options), query };
9234
+ });
9235
+ }
9236
+ /**
9237
+ * Links a person to the organization they act for (an account or a partner), as a system of record that
9238
+ * knows who works for whom: a CRM, an HR system. Needs a key with the `identity:link` scope (or `admin`);
9239
+ * `can_see_contacts` needs `admin`. Reads with `about` reach the organization through the link.
9240
+ */
9241
+ link(params, options = {}) {
9242
+ const { idempotency_key: key2, ...rest } = params;
9243
+ const body = { can_see_contacts: false, method: "system_import", ...rest };
9244
+ return this.navigate(() => this.writeSpec("/v1/identity/links", body, key2 ?? uuidv7(), options));
9245
+ }
9246
+ /**
9247
+ * Ends a link, from `valid_to` (now when absent): the person no longer acts for the organization, and reads
9248
+ * with `about` for the pair go on with the person's own memory. Needs `identity:link` or `admin`.
9249
+ */
9250
+ endLink(linkId, params = {}, options = {}) {
9251
+ return this.navigate(() => {
9252
+ if (!linkId) throw new NiadraValidationError("endLink() needs a link id");
9253
+ const body = params.valid_to ? { valid_to: params.valid_to } : {};
9254
+ const path = `/v1/identity/links/${encodeURIComponent(linkId)}/end`;
9255
+ return this.writeSpec(path, body, params.idempotency_key ?? uuidv7(), options);
9256
+ });
9257
+ }
9095
9258
  /**
9096
9259
  * Up to 500 corrections in one call, each with its own idempotency key (minted when missing).
9097
9260
  * Resolves with `accepted`, `duplicates` for replayed keys and one error per refused item, by index.
@@ -9184,16 +9347,20 @@ var Niadra = class _Niadra {
9184
9347
  * emits `conversation.ended` when you call `end()`.
9185
9348
  */
9186
9349
  conversation(params) {
9187
- return new Conversation(this, params, {
9350
+ const conversation = new Conversation(this, params, {
9188
9351
  endConversation: (id) => this.endScope(buildConversationEnded(id), `conversation:${id}`)
9189
9352
  });
9353
+ this.warm(`conversation:${conversation.id}`, conversation);
9354
+ return conversation;
9190
9355
  }
9191
9356
  /** A helper for one internal-agent task: binds `task_id` to reads and writes and emits `task.ended`. */
9192
9357
  task(params) {
9193
- return new Task(this, params, {
9358
+ const task = new Task(this, params, {
9194
9359
  endTask: (id) => this.endScope(buildTaskEnded(id), `task:${id}`),
9195
9360
  verifyTask: (verify) => this.verifyWith(verify)
9196
9361
  });
9362
+ this.warm(`task:${task.id}`, task);
9363
+ return task;
9197
9364
  }
9198
9365
  /**
9199
9366
  * Sends every queued event and resolves when done. Call it before a serverless function
@@ -9213,6 +9380,8 @@ var Niadra = class _Niadra {
9213
9380
  */
9214
9381
  async shutdown() {
9215
9382
  this.unregisterExit();
9383
+ clearInterval(this.warmTimer);
9384
+ this.warmTimer = void 0;
9216
9385
  if (!this.core) return;
9217
9386
  await this.turnSender?.stop(this.timeouts.write);
9218
9387
  await this.outbox.stop(this.timeouts.write);
@@ -9316,9 +9485,9 @@ var Niadra = class _Niadra {
9316
9485
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", request, timeout, { signal, headers }));
9317
9486
  return normalizeContext(response.data);
9318
9487
  } catch (error) {
9319
- const refused2 = error instanceof NiadraAPIError && error.status === 404;
9488
+ const refused3 = error instanceof NiadraAPIError && error.status === 404;
9320
9489
  const left = timeout - (Date.now() - started);
9321
- if (!request.include?.length || !refused2 || left <= 0) throw error;
9490
+ if (!request.include?.length || !refused3 || left <= 0) throw error;
9322
9491
  for (const name of request.include) this.refusedBlocks.set(name, Date.now() + BLOCK_RECHECK_AFTER_MS);
9323
9492
  const { include: _dropped, ...plain2 } = request;
9324
9493
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", plain2, left, { signal, headers }));
@@ -9406,9 +9575,32 @@ var Niadra = class _Niadra {
9406
9575
  const rtt = Math.min(...samples);
9407
9576
  this.voice.rtt = rtt;
9408
9577
  this.logger.debug(`round trip to the region ${Math.round(rtt)} ms`);
9409
- for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9578
+ const explicit = ["context", "navigation"].filter((name) => !this.defaultReads.has(name));
9579
+ for (const warning of budgetWarnings(rtt, this.timeouts, explicit)) this.logger.warn(warning);
9580
+ if (this.voiceStarted) this.warnVoice();
9410
9581
  })();
9411
9582
  }
9583
+ /**
9584
+ * A read budget: one the caller left at its default is what the API may take, and the measured round trip
9585
+ * to the region goes on top, so an agent far from the region (Sao Paulo, 170 ms from us-east-2) is not
9586
+ * timed out by the network; one the caller set is a ceiling.
9587
+ */
9588
+ readBudget(name) {
9589
+ const rtt = this.voice.rtt;
9590
+ return rtt !== null && this.defaultReads.has(name) ? this.timeouts[name] + rtt : this.timeouts[name];
9591
+ }
9592
+ /** The client reads in voice: the voice budgets' warnings matter from now on. */
9593
+ startVoice(core) {
9594
+ this.voiceStarted = true;
9595
+ this.probe(core);
9596
+ this.warnVoice();
9597
+ }
9598
+ warnVoice() {
9599
+ const rtt = this.voice.rtt;
9600
+ if (this.voiceWarned || rtt === null) return;
9601
+ this.voiceWarned = true;
9602
+ for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9603
+ }
9412
9604
  /**
9413
9605
  * A voice turn: the pinned body from memory, and the slots of the read of its words when that
9414
9606
  * read lands within `timeout`. See `voice.ts`.
@@ -9418,7 +9610,7 @@ var Niadra = class _Niadra {
9418
9610
  const key2 = cacheKey(request);
9419
9611
  const deadline = Date.now() + timeout;
9420
9612
  const line = this.voice.line(scope);
9421
- this.probe(core);
9613
+ this.startVoice(core);
9422
9614
  line.request = request;
9423
9615
  const words2 = wordsOf(query);
9424
9616
  const background = this.timeouts.prefetch;
@@ -9604,8 +9796,39 @@ var Niadra = class _Niadra {
9604
9796
  core.queue.flushInBackground(true);
9605
9797
  });
9606
9798
  }
9799
+ /** Notes an open conversation or task; the first one starts the keep-warm timer (`warm.ts`). */
9800
+ warm(scope, session) {
9801
+ const core = this.core;
9802
+ if (!core || !this.keepWarm.enabled) return;
9803
+ this.keepWarm.add(scope, session, Date.now());
9804
+ if (this.warmTimer !== void 0) return;
9805
+ const timer = setInterval(() => {
9806
+ this.warmTick(core);
9807
+ }, EVERY_MS);
9808
+ timer.unref?.();
9809
+ this.warmTimer = timer;
9810
+ }
9811
+ warmTick(core) {
9812
+ const step = this.keepWarm.step(Date.now(), core.transport.lastActivityAt);
9813
+ if (step === "stop") {
9814
+ clearInterval(this.warmTimer);
9815
+ this.warmTimer = void 0;
9816
+ return;
9817
+ }
9818
+ if (step !== "ping") return;
9819
+ core.transport.request({
9820
+ method: "GET",
9821
+ path: "/healthz",
9822
+ timeoutMs: 2e3,
9823
+ retry: { kind: "read", maxAttempts: 1 },
9824
+ activity: false
9825
+ }).catch((error) => {
9826
+ this.logger.debug(`keep-warm ping failed: ${describe(toNiadraError(error))}`);
9827
+ });
9828
+ }
9607
9829
  async endScope(item, scope) {
9608
9830
  this.forgetScope(scope);
9831
+ this.keepWarm.end(scope);
9609
9832
  return this.sendNow(() => item);
9610
9833
  }
9611
9834
  async sendBatch(transport, items, maxAttempts, backoff) {
@@ -9910,7 +10133,7 @@ var Replayer = class {
9910
10133
  try {
9911
10134
  answer = await this.niadra.callRoute({ method: "POST", path: "/v1/scenario-runs", body, idempotencyKey: uuidv7() });
9912
10135
  } catch (error) {
9913
- if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused(scenarioIds, results);
10136
+ if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused2(scenarioIds, results);
9914
10137
  throw error;
9915
10138
  }
9916
10139
  let run = runOf(answer);
@@ -10013,7 +10236,7 @@ var Replayer = class {
10013
10236
  return result(scenarioId, turnId, caseId, run, "completed", { paraphrase: rephrased, assertions, divergent_calls: played.divergent, latency_ms: latency });
10014
10237
  }
10015
10238
  };
10016
- function refused(scenarioIds, results) {
10239
+ function refused2(scenarioIds, results) {
10017
10240
  const scenarios = scenarioIds.map((id) => {
10018
10241
  const mine = results.filter((r) => r.scenario_id === id);
10019
10242
  return {