@niadra/sdk 0.9.1 → 0.10.4

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 +69 -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 +331 -96
  9. package/dist/cli.js.map +1 -1
  10. package/dist/{client-CWMxmJHs.d.cts → client-7mkrdRI0.d.cts} +192 -7
  11. package/dist/{client-CWMxmJHs.d.ts → client-7mkrdRI0.d.ts} +192 -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 +341 -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 +341 -98
  35. package/dist/index.js.map +1 -1
  36. package/dist/{intercept-CGngQI8B.d.cts → intercept-CGQhwAom.d.cts} +1 -1
  37. package/dist/{intercept-Bv0nLpSw.d.ts → intercept-CT0vglsd.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-Bf84jr3Y.d.ts} +1 -1
  61. package/dist/{shared-Cq8mz-7m.d.cts → shared-CxAwOcZI.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-jTx_SegV.d.cts → webhook-DE3HsGkb.d.cts} +1 -1
  79. package/dist/{webhook-z2TjUzjv.d.ts → webhook-QxYgxE6_.d.ts} +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.4";
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,36 @@ 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;
531
+ /** Likely: an answer came within `keepAliveMs`. When not, the next budgeted call gets `coldAllowanceMs`. */
532
+ connectionOpen() {
533
+ return this.answeredAt !== void 0 && Date.now() - this.answeredAt <= (this.config.keepAliveMs ?? 0);
534
+ }
519
535
  async request(spec) {
520
- return spec.retry.kind === "read" ? this.read(spec, spec.retry) : this.write(spec, spec.retry);
536
+ if (spec.activity !== false) this.lastActivityAt = Date.now();
537
+ const budgeted = this.withAllowance(spec);
538
+ return budgeted.retry.kind === "read" ? this.read(budgeted, budgeted.retry) : this.write(budgeted, budgeted.retry);
539
+ }
540
+ /**
541
+ * `spec` with the allowance for opening a connection, when it has a budget and none is likely open. The calls
542
+ * that start while the first one opens it get it too; after that, none does until an answer comes, so an
543
+ * outage costs the allowance once, not on every turn.
544
+ */
545
+ withAllowance(spec) {
546
+ const allowance = this.config.coldAllowanceMs ?? 0;
547
+ const { retry } = spec;
548
+ if (allowance <= 0 || retry.kind === "write" && retry.totalMs === void 0) return spec;
549
+ const now = Date.now();
550
+ const keep = this.config.keepAliveMs ?? 0;
551
+ if (this.answeredAt !== void 0 && now - this.answeredAt <= keep) return spec;
552
+ this.grantedAt ??= now;
553
+ if (now - this.grantedAt > allowance) return spec;
554
+ const total = retry.kind === "write" && retry.totalMs !== void 0 ? { ...retry, totalMs: retry.totalMs + allowance } : retry;
555
+ return { ...spec, timeoutMs: spec.timeoutMs + allowance, retry: total };
521
556
  }
522
557
  async read(spec, policy) {
523
558
  const deadline = new Deadline(spec.timeoutMs, spec.signal);
@@ -588,7 +623,18 @@ var Transport = class {
588
623
  headers["content-type"] = "application/json";
589
624
  init.body = JSON.stringify(spec.body);
590
625
  }
591
- return this.exchange(this.url(spec.path, spec.query), init, deadline);
626
+ try {
627
+ const answer = await this.exchange(this.url(spec.path, spec.query), init, deadline);
628
+ this.answered();
629
+ return answer;
630
+ } catch (error) {
631
+ if (error instanceof NiadraAPIError) this.answered();
632
+ throw error;
633
+ }
634
+ }
635
+ answered() {
636
+ this.answeredAt = Date.now();
637
+ this.grantedAt = void 0;
592
638
  }
593
639
  async exchange(url, init, deadline) {
594
640
  let response;
@@ -4025,8 +4071,7 @@ var TurnSender = class {
4025
4071
  return true;
4026
4072
  }
4027
4073
  }
4028
- const code = error instanceof NiadraAPIError ? error.code : toNiadraError(error).name;
4029
- this.recorder.rejected(batch.frames.length, [code]);
4074
+ this.recorder.rejected(batch.frames.length, [explain(error)]);
4030
4075
  return true;
4031
4076
  }
4032
4077
  accepted(batch, answer) {
@@ -4034,8 +4079,8 @@ var TurnSender = class {
4034
4079
  this.resumeAt = 0;
4035
4080
  this.recorder.sent(answer.accepted, answer.duplicates);
4036
4081
  const again = [];
4037
- const codes2 = /* @__PURE__ */ new Set();
4038
- let refused2 = 0;
4082
+ const reasons = /* @__PURE__ */ new Set();
4083
+ let refused3 = 0;
4039
4084
  for (const error of answer.errors ?? []) {
4040
4085
  const frame = batch.frames[error.index];
4041
4086
  if (frame === void 0) continue;
@@ -4044,12 +4089,12 @@ var TurnSender = class {
4044
4089
  this.recorder.modeRefused();
4045
4090
  again.push(frame);
4046
4091
  } else {
4047
- refused2++;
4048
- codes2.add(error.code);
4092
+ refused3++;
4093
+ reasons.add(error.detail ? `${error.code}: ${error.detail.slice(0, 300)}` : error.code);
4049
4094
  }
4050
4095
  }
4051
4096
  if (again.length > 0) this.queue.requeue(again);
4052
- if (refused2 > 0) this.recorder.rejected(refused2, [...codes2]);
4097
+ if (refused3 > 0) this.recorder.rejected(refused3, [...reasons]);
4053
4098
  }
4054
4099
  };
4055
4100
 
@@ -4171,9 +4216,12 @@ var TurnRecorder = class {
4171
4216
  this.accepted += accepted;
4172
4217
  this.duplicates += duplicates;
4173
4218
  }
4174
- rejected(count2, codes2) {
4219
+ /** `reasons`: the API's code and detail of each refusal (field paths and rules, never a value). */
4220
+ rejected(count2, reasons) {
4175
4221
  this.rejectedTurns += count2;
4176
- this.logger.warn(`${count2} turn records were refused (${[...codes2].sort().join(", ")})`);
4222
+ const shown = [...new Set(reasons)].sort();
4223
+ const more = shown.length > 3 ? ` (and ${shown.length - 3} more)` : "";
4224
+ this.logger.warn(`${count2} turn records were refused: ${shown.slice(0, 3).join("; ")}${more}`);
4177
4225
  }
4178
4226
  /** The space does not record turns: these are dropped, and with `off` (a 404) recording stops a while. */
4179
4227
  notRecorded(count2, off) {
@@ -4243,6 +4291,10 @@ async function hex(text2) {
4243
4291
  }
4244
4292
 
4245
4293
  // src/coordination/client.ts
4294
+ var REFUSED = /* @__PURE__ */ new Set([400, 401, 403, 422]);
4295
+ function refused(error) {
4296
+ return error instanceof NiadraAPIError && REFUSED.has(error.status);
4297
+ }
4246
4298
  var FAIL_CLOSED = /* @__PURE__ */ new Set(["marketing", "retention", "collection"]);
4247
4299
  var CHECK_BUDGET_MS = 200;
4248
4300
  function fallback(request, suppressed, failOpen) {
@@ -4264,15 +4316,22 @@ function claimed(data, error) {
4264
4316
  return { held: false, claim: null, error: code };
4265
4317
  }
4266
4318
  var Coordinator = class {
4267
- constructor(outbox, suppressions, declareNow) {
4319
+ constructor(outbox, suppressions, declareNow, logger) {
4268
4320
  this.outbox = outbox;
4269
4321
  this.suppressions = suppressions;
4270
4322
  this.declareNow = declareNow;
4323
+ this.logger = logger;
4271
4324
  }
4272
4325
  outbox;
4273
4326
  suppressions;
4274
4327
  declareNow;
4328
+ logger;
4275
4329
  async failed(request, failOpen, error) {
4330
+ if (refused(error)) {
4331
+ this.logger?.warn(`the coordination check was refused: ${explain(error)}`);
4332
+ const decision = request.direction === "inbound" ? "allow" : "defer";
4333
+ return recorded({ decision, decision_id: uuidv7(), reasons: ["invalid_request"], valid_for_s: 0 });
4334
+ }
4276
4335
  const off = error instanceof NiadraAPIError && error.status === 404;
4277
4336
  const suppressed = request.subject != null && request.direction === "outbound" ? !await this.suppressions.mayContact(request.subject, request.purpose, { channel: request.channel ?? null, failOpen: true }) : false;
4278
4337
  const plain2 = off ? { ...request, effect_key: null } : request;
@@ -4293,7 +4352,7 @@ var Coordinator = class {
4293
4352
  if (who.subject) body.subject = who.subject;
4294
4353
  if (who.object) body.object = who.object;
4295
4354
  const key2 = uuidv7();
4296
- this.outbox.put({ send: () => this.declareNow(body, key2) });
4355
+ this.outbox.put({ send: () => this.declareNow(body, key2), route: "POST /v1/coordination/declare" });
4297
4356
  return key2;
4298
4357
  }
4299
4358
  };
@@ -4812,7 +4871,7 @@ var Outbox = class {
4812
4871
  return false;
4813
4872
  }
4814
4873
  this.writes.shift();
4815
- this.logger.warn(`a write was refused (${error instanceof NiadraAPIError ? error.code : String(error)})`);
4874
+ this.logger.warn(`${write.route ?? "a write"} was refused: ${explain(error)}`);
4816
4875
  this.settle(write, void 0, error);
4817
4876
  return true;
4818
4877
  }
@@ -6349,6 +6408,7 @@ var AgentSession = class {
6349
6408
  try {
6350
6409
  return this.host.coordinator.decided(await this.host.check(request, options.timeoutMs ?? CHECK_BUDGET_MS), request, this.checked);
6351
6410
  } catch (error) {
6411
+ if (this.host.strict && refused(error)) throw error;
6352
6412
  return this.host.coordinator.failed(request, options.failOpen, error);
6353
6413
  }
6354
6414
  }
@@ -6960,6 +7020,7 @@ var SessionState = class {
6960
7020
  /** The guards of the last read: they hold the answers to the turn they were written for. */
6961
7021
  guards = /* @__PURE__ */ new Map();
6962
7022
  lastReport = null;
7023
+ unlinkedSaid = false;
6963
7024
  /** After the first pack, every read also asks what changed since. */
6964
7025
  get wantsDelta() {
6965
7026
  return this.etag !== null;
@@ -6988,6 +7049,19 @@ var SessionState = class {
6988
7049
  this.last = absorbed;
6989
7050
  return absorbed;
6990
7051
  }
7052
+ /**
7053
+ * Once per session: the read named an organization (`about`) with no active link to the subject, so it went
7054
+ * on with the subject's own memory, never the organization's.
7055
+ */
7056
+ sayUnlinked(result2, logger) {
7057
+ if (result2.response?.about_unlinked && !this.unlinkedSaid) {
7058
+ this.unlinkedSaid = true;
7059
+ logger.warn(
7060
+ "`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."
7061
+ );
7062
+ }
7063
+ return result2;
7064
+ }
6991
7065
  /** The last backing check of an agent's answer. */
6992
7066
  get lastBacking() {
6993
7067
  return this.lastReport;
@@ -7235,7 +7309,7 @@ var Conversation = class {
7235
7309
  * `query` picks this read's slots by other words than the turn; the pack is the pinned one.
7236
7310
  */
7237
7311
  async context(options = {}) {
7238
- const { query, turn: turn2, format, explain, include, ...requestOptions } = options;
7312
+ const { query, turn: turn2, format, explain: explain2, include, ...requestOptions } = options;
7239
7313
  const params = {
7240
7314
  subject: this.subject,
7241
7315
  view: this.view,
@@ -7244,7 +7318,7 @@ var Conversation = class {
7244
7318
  ...this.params.about ? { about: this.params.about } : {},
7245
7319
  ...this.params.target ? { target: this.params.target } : {},
7246
7320
  ...format === "json" ? { format } : {},
7247
- ...explain ? { explain } : {}
7321
+ ...explain2 ? { explain: explain2 } : {}
7248
7322
  };
7249
7323
  if (query) params.query = query;
7250
7324
  if (include?.length) params.include = include;
@@ -7252,7 +7326,7 @@ var Conversation = class {
7252
7326
  params.turn = turn2 === void 0 ? this.turnText : turn2;
7253
7327
  const result2 = await this.client.context(params, requestOptions);
7254
7328
  this.features.observe(result2);
7255
- return this.state.observe(this.state.absorb(result2));
7329
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7256
7330
  }
7257
7331
  /**
7258
7332
  * Starts this conversation's first read now, in the background: call it when the call starts
@@ -7432,6 +7506,35 @@ function withHandles(base, extra) {
7432
7506
  return all;
7433
7507
  }
7434
7508
 
7509
+ // src/warm.ts
7510
+ var EVERY_MS = 1e5;
7511
+ var IDLE_MS = 9e4;
7512
+ var WARM_FOR_MS = 6e5;
7513
+ var KeepWarm = class {
7514
+ constructor(enabled) {
7515
+ this.enabled = enabled;
7516
+ }
7517
+ enabled;
7518
+ open = /* @__PURE__ */ new Map();
7519
+ openedAt = 0;
7520
+ /** Notes an open conversation or task. */
7521
+ add(scope, session, now) {
7522
+ if (!this.enabled || typeof WeakRef === "undefined") return;
7523
+ this.open.set(scope, new WeakRef(session));
7524
+ this.openedAt = now;
7525
+ }
7526
+ /** The conversation or task ended. */
7527
+ end(scope) {
7528
+ this.open.delete(scope);
7529
+ }
7530
+ step(now, lastActivityAt, idleMs = IDLE_MS, warmForMs = WARM_FOR_MS) {
7531
+ for (const [scope, ref] of this.open) if (ref.deref() === void 0) this.open.delete(scope);
7532
+ const used = Math.max(lastActivityAt ?? 0, this.openedAt);
7533
+ if (this.open.size === 0 || now - used > warmForMs) return "stop";
7534
+ return now - used >= idleMs ? "ping" : "wait";
7535
+ }
7536
+ };
7537
+
7435
7538
  // src/exit.ts
7436
7539
  var registered = /* @__PURE__ */ new Set();
7437
7540
  var installedOn = null;
@@ -7561,8 +7664,10 @@ var DEFAULT_TIMEOUTS = {
7561
7664
  write: 5e3,
7562
7665
  token: 2e3,
7563
7666
  upload: 6e4,
7564
- prefetch: 1e3
7667
+ prefetch: 1e3,
7668
+ connect: 1e3
7565
7669
  };
7670
+ var FETCH_KEEPALIVE_MS = 4e3;
7566
7671
  var DEFAULT_CACHE = {
7567
7672
  ttlMs: 1e4,
7568
7673
  staleWhileRevalidateMs: 10 * 6e4,
@@ -7669,7 +7774,7 @@ var Task = class {
7669
7774
  * Resolves with an empty result, never rejects, unless the client is strict.
7670
7775
  */
7671
7776
  async context(options = {}) {
7672
- const { query, format, explain, include, ...requestOptions } = options;
7777
+ const { query, format, explain: explain2, include, ...requestOptions } = options;
7673
7778
  const target = this.object ? { object: this.object } : this.params.subject ? { subject: this.params.subject } : {};
7674
7779
  const params = {
7675
7780
  ...target,
@@ -7679,18 +7784,18 @@ var Task = class {
7679
7784
  ...this.level ? { verification: this.level } : {},
7680
7785
  ...this.params.target ? { target: this.params.target } : {},
7681
7786
  ...format === "json" ? { format } : {},
7682
- ...explain ? { explain } : {},
7787
+ ...explain2 ? { explain: explain2 } : {},
7683
7788
  ...include?.length ? { include } : {}
7684
7789
  };
7685
7790
  if (query) {
7686
7791
  const answered = await this.client.context({ ...params, query }, requestOptions);
7687
7792
  this.features.observe(answered);
7688
- return this.state.observe(answered);
7793
+ return this.state.observe(this.state.sayUnlinked(answered, this.logger));
7689
7794
  }
7690
7795
  if (this.state.wantsDelta) params.delta = true;
7691
7796
  const result2 = await this.client.context(params, requestOptions);
7692
7797
  this.features.observe(result2);
7693
- return this.state.observe(this.state.absorb(result2));
7798
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7694
7799
  }
7695
7800
  /**
7696
7801
  * The agent's own working notes for this task's prompt, as `niadra.agentMemory()` with
@@ -7820,14 +7925,14 @@ var AGENT_MEMORY_TOOL_NAMES = {
7820
7925
  search: "search_agent_memory",
7821
7926
  remember: "remember"
7822
7927
  };
7823
- var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
7928
+ var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait", "system_event"];
7824
7929
  var NOTE_KINDS = ["procedure", "tool_note", "process_note", "pitfall"];
7825
7930
  var TOOL_DEFINITIONS = [
7826
7931
  {
7827
7932
  "type": "function",
7828
7933
  "function": {
7829
7934
  "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.",
7935
+ "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
7936
  "parameters": {
7832
7937
  "type": "object",
7833
7938
  "properties": {
@@ -7871,7 +7976,8 @@ var TOOL_DEFINITIONS = [
7871
7976
  "open_item",
7872
7977
  "action",
7873
7978
  "object",
7874
- "trait"
7979
+ "trait",
7980
+ "system_event"
7875
7981
  ]
7876
7982
  }
7877
7983
  },
@@ -7898,7 +8004,7 @@ var TOOL_DEFINITIONS = [
7898
8004
  "type": "function",
7899
8005
  "function": {
7900
8006
  "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.",
8007
+ "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
8008
  "parameters": {
7903
8009
  "type": "object",
7904
8010
  "properties": {
@@ -7946,7 +8052,8 @@ var TOOL_DEFINITIONS = [
7946
8052
  "open_item",
7947
8053
  "action",
7948
8054
  "object",
7949
- "trait"
8055
+ "trait",
8056
+ "system_event"
7950
8057
  ]
7951
8058
  }
7952
8059
  },
@@ -7965,7 +8072,7 @@ var TOOL_DEFINITIONS = [
7965
8072
  "type": "function",
7966
8073
  "function": {
7967
8074
  "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.",
8075
+ "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
8076
  "parameters": {
7970
8077
  "type": "object",
7971
8078
  "properties": {
@@ -8336,6 +8443,13 @@ function compose(body, read2) {
8336
8443
  const pack = body.pack ? { pack: { ...body.pack, slots: fetched?.pack?.slots ?? [] } } : {};
8337
8444
  return { ...body, slots: fetched?.slots ?? null, guards: fetched?.guards ?? [], ...pack };
8338
8445
  }
8446
+ var RTT_MARGIN_MS = 50;
8447
+ function budgetWarnings(rttMs, timeouts, explicit) {
8448
+ const ms = Math.round(rttMs);
8449
+ return [...explicit].sort().filter((name) => timeouts[name] < rttMs + RTT_MARGIN_MS).map(
8450
+ (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`
8451
+ );
8452
+ }
8339
8453
  function rttWarnings(rttMs, timeouts) {
8340
8454
  const ms = Math.round(rttMs);
8341
8455
  const found2 = [];
@@ -8373,6 +8487,14 @@ var Niadra = class _Niadra {
8373
8487
  enabled;
8374
8488
  core;
8375
8489
  timeouts;
8490
+ keepWarm;
8491
+ warmTimer;
8492
+ /** The read budgets the caller left at their defaults: they take the measured round trip on top. */
8493
+ defaultReads;
8494
+ voiceStarted = false;
8495
+ /** A probe is on its way: until it ends, default read budgets get `timeouts.connect` on top. */
8496
+ measuring = false;
8497
+ voiceWarned = false;
8376
8498
  strict;
8377
8499
  /** Where the client reports what it swallows in fail-open mode. */
8378
8500
  logger;
@@ -8417,6 +8539,8 @@ var Niadra = class _Niadra {
8417
8539
  this.strict = options.strict ?? false;
8418
8540
  this.logger = options.logger ?? consoleLogger;
8419
8541
  this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
8542
+ this.keepWarm = new KeepWarm(options.keepWarm ?? true);
8543
+ this.defaultReads = new Set(["context", "navigation"].filter((name) => options.timeouts?.[name] === void 0));
8420
8544
  this.voice = new VoiceLines(
8421
8545
  options.voice === false ? { ...DEFAULT_VOICE, enabled: false } : { ...DEFAULT_VOICE, ...options.voice }
8422
8546
  );
@@ -8442,13 +8566,14 @@ var Niadra = class _Niadra {
8442
8566
  this.coordinator = new Coordinator(
8443
8567
  this.outbox,
8444
8568
  this.suppressions,
8445
- (body, key2) => this.api.declare(body, { idempotency_key: key2 })
8569
+ (body, key2) => this.api.declare(body, { idempotency_key: key2 }),
8570
+ this.logger
8446
8571
  );
8447
8572
  this.states = new AgentStates(
8448
8573
  this.outbox,
8449
8574
  {
8450
- read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.timeouts.navigation }),
8451
- write: (write) => this.api.writeAgentState(write, { timeout: this.timeouts.navigation })
8575
+ read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.readBudget("navigation") }),
8576
+ write: (write) => this.api.writeAgentState(write, { timeout: this.readBudget("navigation") })
8452
8577
  },
8453
8578
  this.logger
8454
8579
  );
@@ -8475,6 +8600,7 @@ var Niadra = class _Niadra {
8475
8600
  this.core = setup;
8476
8601
  this.enabled = true;
8477
8602
  this.disabledReason = null;
8603
+ this.probe(setup);
8478
8604
  if (options.flushOnExit ?? true) this.unregisterExit = registerExitFlush(this);
8479
8605
  }
8480
8606
  setup(options) {
@@ -8497,7 +8623,9 @@ var Niadra = class _Niadra {
8497
8623
  apiKey,
8498
8624
  fetch: fetchImpl,
8499
8625
  defaultHeaders: options.defaultHeaders ?? {},
8500
- logger: this.logger
8626
+ logger: this.logger,
8627
+ coldAllowanceMs: this.timeouts.connect,
8628
+ keepAliveMs: options.keepAliveMs ?? FETCH_KEEPALIVE_MS
8501
8629
  });
8502
8630
  const queueOptions = { ...DEFAULT_QUEUE, ...options.queue };
8503
8631
  queueOptions.maxBatchSize = Math.min(queueOptions.maxBatchSize, 499);
@@ -8553,7 +8681,7 @@ var Niadra = class _Niadra {
8553
8681
  return result2;
8554
8682
  }
8555
8683
  readContext(core, params, request, options) {
8556
- const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
8684
+ const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8557
8685
  const { query: own2, ...pinned2 } = request;
8558
8686
  const query = turnText(own2 ?? params.turn);
8559
8687
  const voiceCache = this.voiceCache(core, pinned2, options.cache);
@@ -8569,7 +8697,7 @@ var Niadra = class _Niadra {
8569
8697
  */
8570
8698
  async profile() {
8571
8699
  if (!this.core) return null;
8572
- return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.timeouts.navigation }));
8700
+ return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.readBudget("navigation") }));
8573
8701
  }
8574
8702
  /**
8575
8703
  * Checks outputs against this claim contract instead of the one the profile serves (a company's own copy,
@@ -8594,7 +8722,7 @@ var Niadra = class _Niadra {
8594
8722
  */
8595
8723
  async mayContact(handle, purpose, options = {}) {
8596
8724
  if (this.core && this.suppressions.due()) {
8597
- const read2 = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.timeouts.navigation);
8725
+ const read2 = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.readBudget("navigation"));
8598
8726
  if (!this.suppressions.held) await read2;
8599
8727
  }
8600
8728
  const checkOptions = { channel: options.channel ?? null };
@@ -8604,8 +8732,8 @@ var Niadra = class _Niadra {
8604
8732
  readSuppressions(budgetMs) {
8605
8733
  return this.suppressions.read(
8606
8734
  {
8607
- salt: () => this.api.suppressionSalt({ timeout: this.timeouts.navigation }),
8608
- page: (cursor, limit2) => this.api.suppressions({ cursor, limit: limit2 }, { timeout: this.timeouts.navigation })
8735
+ salt: () => this.api.suppressionSalt({ timeout: this.readBudget("navigation") }),
8736
+ page: (cursor, limit2) => this.api.suppressions({ cursor, limit: limit2 }, { timeout: this.readBudget("navigation") })
8609
8737
  },
8610
8738
  budgetMs
8611
8739
  );
@@ -8684,6 +8812,7 @@ var Niadra = class _Niadra {
8684
8812
  }
8685
8813
  /** What a conversation or a task needs of its client for the agent features. */
8686
8814
  get agentHost() {
8815
+ const client = this;
8687
8816
  return {
8688
8817
  recorder: this.turns,
8689
8818
  coordinator: this.coordinator,
@@ -8697,7 +8826,10 @@ var Niadra = class _Niadra {
8697
8826
  claim: (request, timeoutMs) => this.api.claim(request, {}, { timeout: timeoutMs }),
8698
8827
  verifyClaim: (ref, field, value, options) => this.verifyClaim(ref, field, value, options),
8699
8828
  enabled: this.enabled,
8700
- navigationMs: this.timeouts.navigation
8829
+ strict: this.strict,
8830
+ get navigationMs() {
8831
+ return client.readBudget("navigation");
8832
+ }
8701
8833
  };
8702
8834
  }
8703
8835
  /**
@@ -8725,7 +8857,7 @@ var Niadra = class _Niadra {
8725
8857
  if (!cache) return false;
8726
8858
  const key2 = cacheKey(request);
8727
8859
  const line = this.voice.line(cacheScope(request) ?? "");
8728
- this.probe(core);
8860
+ this.startVoice(core);
8729
8861
  line.request = request;
8730
8862
  if (!cache.has(key2) && line.inFlight().length === 0) {
8731
8863
  this.voiceRead(core, cache, line, key2, request, null, this.timeouts.contextVoiceStart);
@@ -8862,13 +8994,13 @@ var Niadra = class _Niadra {
8862
8994
  if (!params.query || params.query.length > 2e3) {
8863
8995
  throw new NiadraValidationError("query must be 1 to 2000 characters");
8864
8996
  }
8865
- return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
8997
+ return this.readSpec("POST", "/v1/history/search", params, this.readBudget("navigation"), options);
8866
8998
  });
8867
8999
  }
8868
9000
  /** The customer's history, newest first, one line per item, paginated by cursor. */
8869
9001
  async timeline(params, options = {}) {
8870
9002
  return this.navigate(
8871
- () => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
9003
+ () => this.readSpec("POST", "/v1/history/timeline", params, this.readBudget("navigation"), options)
8872
9004
  );
8873
9005
  }
8874
9006
  /**
@@ -8881,9 +9013,10 @@ var Niadra = class _Niadra {
8881
9013
  if (!id) throw new NiadraValidationError("open() needs an item id");
8882
9014
  const body = { item_id: id };
8883
9015
  if (params.subject) body.subject = params.subject;
9016
+ if (params.about) body.about = params.about;
8884
9017
  if (params.verification) body.verification = params.verification;
8885
9018
  if (params.conversation_id) body.conversation_id = params.conversation_id;
8886
- return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
9019
+ return this.readSpec("POST", "/v1/history/open", body, this.readBudget("navigation"), options);
8887
9020
  });
8888
9021
  }
8889
9022
  /**
@@ -8895,7 +9028,7 @@ var Niadra = class _Niadra {
8895
9028
  * const { data: invoice } = await niadra.objectState("invoice:erp:0823");
8896
9029
  */
8897
9030
  async objectState(object, options = {}) {
8898
- return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.timeouts.navigation, options));
9031
+ return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.readBudget("navigation"), options));
8899
9032
  }
8900
9033
  /**
8901
9034
  * System events and agent actions about one object, newest first, one line each and never
@@ -8908,7 +9041,7 @@ var Niadra = class _Niadra {
8908
9041
  throw new NiadraValidationError("limit must be between 1 and 100");
8909
9042
  }
8910
9043
  const path = `${objectPath(object)}/timeline`;
8911
- const spec = this.readSpec("GET", path, void 0, this.timeouts.navigation, options);
9044
+ const spec = this.readSpec("GET", path, void 0, this.readBudget("navigation"), options);
8912
9045
  spec.query = { cursor: params.cursor, limit: String(limit2) };
8913
9046
  return spec;
8914
9047
  });
@@ -8927,6 +9060,7 @@ var Niadra = class _Niadra {
8927
9060
  timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
8928
9061
  open: (id, customer, bound, voice) => {
8929
9062
  const scope = { subject: customer };
9063
+ if (bound.about) scope.about = bound.about;
8930
9064
  if (bound.verification) scope.verification = bound.verification;
8931
9065
  if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
8932
9066
  return this.open(id, scope, this.voiceBudget(voice));
@@ -8961,7 +9095,7 @@ var Niadra = class _Niadra {
8961
9095
  const key2 = AgentMemoryCache.key(params);
8962
9096
  const fresh = cache?.fresh(key2);
8963
9097
  if (fresh) return blockResult(fresh, "cache");
8964
- const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
9098
+ const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8965
9099
  const spec = this.readSpec("GET", "/v1/agent-memory/block", void 0, timeout, options);
8966
9100
  spec.query = {
8967
9101
  max_tokens: String(params.max_tokens ?? 300),
@@ -8998,7 +9132,7 @@ var Niadra = class _Niadra {
8998
9132
  if (params.limit !== void 0) body.limit = params.limit;
8999
9133
  if (params.conversation_id) body.conversation_id = params.conversation_id;
9000
9134
  if (params.task_id) body.task_id = params.task_id;
9001
- return this.readSpec("POST", "/v1/agent-memory/search", body, this.timeouts.navigation, options);
9135
+ return this.readSpec("POST", "/v1/agent-memory/search", body, this.readBudget("navigation"), options);
9002
9136
  });
9003
9137
  return result2.error ? result2 : { data: result2.data.notes, error: null };
9004
9138
  }
@@ -9092,6 +9226,41 @@ var Niadra = class _Niadra {
9092
9226
  return { ok: false, idempotency_key: key2, error: this.swallow(error, "feedback") };
9093
9227
  }
9094
9228
  }
9229
+ /**
9230
+ * How the space's agents used the context they read (`GET /v1/context-use`): sessions, deliveries, use,
9231
+ * repetition, transfers and recontact, with intervals, grouped by `group_by`. A key of an `analyst` source
9232
+ * with the `analytics` scope reads every source of the space; a key with `admin` reads its own source.
9233
+ */
9234
+ contextUse(params = {}, options = {}) {
9235
+ return this.navigate(() => {
9236
+ const { group_by: groups, ...filters } = params;
9237
+ const query = { ...filters };
9238
+ if (groups?.length) query.group_by = groups;
9239
+ return { ...this.readSpec("GET", "/v1/context-use", void 0, this.timeouts.write, options), query };
9240
+ });
9241
+ }
9242
+ /**
9243
+ * Links a person to the organization they act for (an account or a partner), as a system of record that
9244
+ * knows who works for whom: a CRM, an HR system. Needs a key with the `identity:link` scope (or `admin`);
9245
+ * `can_see_contacts` needs `admin`. Reads with `about` reach the organization through the link.
9246
+ */
9247
+ link(params, options = {}) {
9248
+ const { idempotency_key: key2, ...rest } = params;
9249
+ const body = { can_see_contacts: false, method: "system_import", ...rest };
9250
+ return this.navigate(() => this.writeSpec("/v1/identity/links", body, key2 ?? uuidv7(), options));
9251
+ }
9252
+ /**
9253
+ * Ends a link, from `valid_to` (now when absent): the person no longer acts for the organization, and reads
9254
+ * with `about` for the pair go on with the person's own memory. Needs `identity:link` or `admin`.
9255
+ */
9256
+ endLink(linkId, params = {}, options = {}) {
9257
+ return this.navigate(() => {
9258
+ if (!linkId) throw new NiadraValidationError("endLink() needs a link id");
9259
+ const body = params.valid_to ? { valid_to: params.valid_to } : {};
9260
+ const path = `/v1/identity/links/${encodeURIComponent(linkId)}/end`;
9261
+ return this.writeSpec(path, body, params.idempotency_key ?? uuidv7(), options);
9262
+ });
9263
+ }
9095
9264
  /**
9096
9265
  * Up to 500 corrections in one call, each with its own idempotency key (minted when missing).
9097
9266
  * Resolves with `accepted`, `duplicates` for replayed keys and one error per refused item, by index.
@@ -9184,16 +9353,20 @@ var Niadra = class _Niadra {
9184
9353
  * emits `conversation.ended` when you call `end()`.
9185
9354
  */
9186
9355
  conversation(params) {
9187
- return new Conversation(this, params, {
9356
+ const conversation = new Conversation(this, params, {
9188
9357
  endConversation: (id) => this.endScope(buildConversationEnded(id), `conversation:${id}`)
9189
9358
  });
9359
+ this.warm(`conversation:${conversation.id}`, conversation);
9360
+ return conversation;
9190
9361
  }
9191
9362
  /** A helper for one internal-agent task: binds `task_id` to reads and writes and emits `task.ended`. */
9192
9363
  task(params) {
9193
- return new Task(this, params, {
9364
+ const task = new Task(this, params, {
9194
9365
  endTask: (id) => this.endScope(buildTaskEnded(id), `task:${id}`),
9195
9366
  verifyTask: (verify) => this.verifyWith(verify)
9196
9367
  });
9368
+ this.warm(`task:${task.id}`, task);
9369
+ return task;
9197
9370
  }
9198
9371
  /**
9199
9372
  * Sends every queued event and resolves when done. Call it before a serverless function
@@ -9213,6 +9386,8 @@ var Niadra = class _Niadra {
9213
9386
  */
9214
9387
  async shutdown() {
9215
9388
  this.unregisterExit();
9389
+ clearInterval(this.warmTimer);
9390
+ this.warmTimer = void 0;
9216
9391
  if (!this.core) return;
9217
9392
  await this.turnSender?.stop(this.timeouts.write);
9218
9393
  await this.outbox.stop(this.timeouts.write);
@@ -9316,9 +9491,9 @@ var Niadra = class _Niadra {
9316
9491
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", request, timeout, { signal, headers }));
9317
9492
  return normalizeContext(response.data);
9318
9493
  } catch (error) {
9319
- const refused2 = error instanceof NiadraAPIError && error.status === 404;
9494
+ const refused3 = error instanceof NiadraAPIError && error.status === 404;
9320
9495
  const left = timeout - (Date.now() - started);
9321
- if (!request.include?.length || !refused2 || left <= 0) throw error;
9496
+ if (!request.include?.length || !refused3 || left <= 0) throw error;
9322
9497
  for (const name of request.include) this.refusedBlocks.set(name, Date.now() + BLOCK_RECHECK_AFTER_MS);
9323
9498
  const { include: _dropped, ...plain2 } = request;
9324
9499
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", plain2, left, { signal, headers }));
@@ -9386,6 +9561,7 @@ var Niadra = class _Niadra {
9386
9561
  /** Measures the round trip to the region once per client, in the background. */
9387
9562
  probe(core) {
9388
9563
  if (!this.voice.claimProbe()) return;
9564
+ this.measuring = true;
9389
9565
  void (async () => {
9390
9566
  const samples = [];
9391
9567
  for (let i = 0; i < 2; i++) {
@@ -9399,16 +9575,44 @@ var Niadra = class _Niadra {
9399
9575
  });
9400
9576
  } catch (error) {
9401
9577
  this.logger.debug(`round trip probe failed: ${describe(toNiadraError(error))}`);
9578
+ this.measuring = false;
9402
9579
  return;
9403
9580
  }
9404
9581
  samples.push(Date.now() - started);
9405
9582
  }
9406
9583
  const rtt = Math.min(...samples);
9407
9584
  this.voice.rtt = rtt;
9585
+ this.measuring = false;
9408
9586
  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);
9587
+ const explicit = ["context", "navigation"].filter((name) => !this.defaultReads.has(name));
9588
+ for (const warning of budgetWarnings(rtt, this.timeouts, explicit)) this.logger.warn(warning);
9589
+ if (this.voiceStarted) this.warnVoice();
9410
9590
  })();
9411
9591
  }
9592
+ /**
9593
+ * A read budget: one the caller left at its default is what the API may take, and the measured round trip
9594
+ * to the region goes on top, so an agent far from the region (Sao Paulo, 170 ms from us-east-2) is not
9595
+ * timed out by the network; one the caller set is a ceiling.
9596
+ */
9597
+ readBudget(name) {
9598
+ if (!this.defaultReads.has(name)) return this.timeouts[name];
9599
+ const rtt = this.voice.rtt;
9600
+ if (rtt !== null) return this.timeouts[name] + rtt;
9601
+ const open = this.core?.transport.connectionOpen() ?? false;
9602
+ return this.measuring && open ? this.timeouts[name] + this.timeouts.connect : this.timeouts[name];
9603
+ }
9604
+ /** The client reads in voice: the voice budgets' warnings matter from now on. */
9605
+ startVoice(core) {
9606
+ this.voiceStarted = true;
9607
+ this.probe(core);
9608
+ this.warnVoice();
9609
+ }
9610
+ warnVoice() {
9611
+ const rtt = this.voice.rtt;
9612
+ if (this.voiceWarned || rtt === null) return;
9613
+ this.voiceWarned = true;
9614
+ for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9615
+ }
9412
9616
  /**
9413
9617
  * A voice turn: the pinned body from memory, and the slots of the read of its words when that
9414
9618
  * read lands within `timeout`. See `voice.ts`.
@@ -9418,7 +9622,7 @@ var Niadra = class _Niadra {
9418
9622
  const key2 = cacheKey(request);
9419
9623
  const deadline = Date.now() + timeout;
9420
9624
  const line = this.voice.line(scope);
9421
- this.probe(core);
9625
+ this.startVoice(core);
9422
9626
  line.request = request;
9423
9627
  const words2 = wordsOf(query);
9424
9628
  const background = this.timeouts.prefetch;
@@ -9604,8 +9808,39 @@ var Niadra = class _Niadra {
9604
9808
  core.queue.flushInBackground(true);
9605
9809
  });
9606
9810
  }
9811
+ /** Notes an open conversation or task; the first one starts the keep-warm timer (`warm.ts`). */
9812
+ warm(scope, session) {
9813
+ const core = this.core;
9814
+ if (!core || !this.keepWarm.enabled) return;
9815
+ this.keepWarm.add(scope, session, Date.now());
9816
+ if (this.warmTimer !== void 0) return;
9817
+ const timer = setInterval(() => {
9818
+ this.warmTick(core);
9819
+ }, EVERY_MS);
9820
+ timer.unref?.();
9821
+ this.warmTimer = timer;
9822
+ }
9823
+ warmTick(core) {
9824
+ const step = this.keepWarm.step(Date.now(), core.transport.lastActivityAt);
9825
+ if (step === "stop") {
9826
+ clearInterval(this.warmTimer);
9827
+ this.warmTimer = void 0;
9828
+ return;
9829
+ }
9830
+ if (step !== "ping") return;
9831
+ core.transport.request({
9832
+ method: "GET",
9833
+ path: "/healthz",
9834
+ timeoutMs: 2e3,
9835
+ retry: { kind: "read", maxAttempts: 1 },
9836
+ activity: false
9837
+ }).catch((error) => {
9838
+ this.logger.debug(`keep-warm ping failed: ${describe(toNiadraError(error))}`);
9839
+ });
9840
+ }
9607
9841
  async endScope(item, scope) {
9608
9842
  this.forgetScope(scope);
9843
+ this.keepWarm.end(scope);
9609
9844
  return this.sendNow(() => item);
9610
9845
  }
9611
9846
  async sendBatch(transport, items, maxAttempts, backoff) {
@@ -9910,7 +10145,7 @@ var Replayer = class {
9910
10145
  try {
9911
10146
  answer = await this.niadra.callRoute({ method: "POST", path: "/v1/scenario-runs", body, idempotencyKey: uuidv7() });
9912
10147
  } catch (error) {
9913
- if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused(scenarioIds, results);
10148
+ if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused2(scenarioIds, results);
9914
10149
  throw error;
9915
10150
  }
9916
10151
  let run = runOf(answer);
@@ -10013,7 +10248,7 @@ var Replayer = class {
10013
10248
  return result(scenarioId, turnId, caseId, run, "completed", { paraphrase: rephrased, assertions, divergent_calls: played.divergent, latency_ms: latency });
10014
10249
  }
10015
10250
  };
10016
- function refused(scenarioIds, results) {
10251
+ function refused2(scenarioIds, results) {
10017
10252
  const scenarios = scenarioIds.map((id) => {
10018
10253
  const mine = results.filter((r) => r.scenario_id === id);
10019
10254
  return {