@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/index.cjs CHANGED
@@ -6,6 +6,46 @@ var __export = (target, all) => {
6
6
  __defProp(target, name, { get: all[name], enumerable: true });
7
7
  };
8
8
 
9
+ // src/ids.ts
10
+ var lastMs = -1;
11
+ var counter = 0;
12
+ function randomBytes(length) {
13
+ const bytes = new Uint8Array(length);
14
+ const webCrypto = globalThis.crypto;
15
+ if (webCrypto?.getRandomValues) {
16
+ webCrypto.getRandomValues(bytes);
17
+ } else {
18
+ for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
19
+ }
20
+ return bytes;
21
+ }
22
+ function uuidv7(now = Date.now()) {
23
+ let ms = now;
24
+ if (ms <= lastMs) {
25
+ ms = lastMs;
26
+ counter = counter + 1 & 4095;
27
+ if (counter === 0) ms += 1;
28
+ } else {
29
+ counter = (randomBytes(2)[0] ?? 0) & 127;
30
+ }
31
+ lastMs = ms;
32
+ const bytes = randomBytes(16);
33
+ const high = Math.floor(ms / 2 ** 16);
34
+ const low = ms % 2 ** 16;
35
+ bytes[0] = high >>> 24 & 255;
36
+ bytes[1] = high >>> 16 & 255;
37
+ bytes[2] = high >>> 8 & 255;
38
+ bytes[3] = high & 255;
39
+ bytes[4] = low >>> 8 & 255;
40
+ bytes[5] = low & 255;
41
+ bytes[6] = 112 | counter >>> 8 & 15;
42
+ bytes[7] = counter & 255;
43
+ bytes[8] = 128 | (bytes[8] ?? 0) & 63;
44
+ let hex2 = "";
45
+ for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
46
+ return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
47
+ }
48
+
9
49
  // src/errors.ts
10
50
  var NiadraError = class extends Error {
11
51
  name = "NiadraError";
@@ -49,11 +89,12 @@ var NiadraAPIError = class extends NiadraError {
49
89
  constructor(status, problem, requestId, retryAfterMs = null) {
50
90
  const code = problem?.code ?? `http_${status}`;
51
91
  const detail = problem?.detail ? `: ${problem.detail}` : "";
52
- super(`${status} ${code}${detail}`);
92
+ const id = requestId ?? problem?.request_id ?? null;
93
+ super(`${status} ${code}${detail}${id ? ` (request ${id})` : ""}`);
53
94
  this.status = status;
54
95
  this.code = code;
55
96
  this.problem = problem;
56
- this.requestId = requestId ?? problem?.request_id ?? null;
97
+ this.requestId = id;
57
98
  this.retryAfterMs = retryAfterMs;
58
99
  }
59
100
  };
@@ -77,45 +118,11 @@ function toNiadraError(error) {
77
118
  const message = error instanceof Error ? error.message : String(error);
78
119
  return new NiadraError(message, { cause: error });
79
120
  }
80
-
81
- // src/ids.ts
82
- var lastMs = -1;
83
- var counter = 0;
84
- function randomBytes(length) {
85
- const bytes = new Uint8Array(length);
86
- const webCrypto = globalThis.crypto;
87
- if (webCrypto?.getRandomValues) {
88
- webCrypto.getRandomValues(bytes);
89
- } else {
90
- for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
91
- }
92
- return bytes;
93
- }
94
- function uuidv7(now = Date.now()) {
95
- let ms = now;
96
- if (ms <= lastMs) {
97
- ms = lastMs;
98
- counter = counter + 1 & 4095;
99
- if (counter === 0) ms += 1;
100
- } else {
101
- counter = (randomBytes(2)[0] ?? 0) & 127;
102
- }
103
- lastMs = ms;
104
- const bytes = randomBytes(16);
105
- const high = Math.floor(ms / 2 ** 16);
106
- const low = ms % 2 ** 16;
107
- bytes[0] = high >>> 24 & 255;
108
- bytes[1] = high >>> 16 & 255;
109
- bytes[2] = high >>> 8 & 255;
110
- bytes[3] = high & 255;
111
- bytes[4] = low >>> 8 & 255;
112
- bytes[5] = low & 255;
113
- bytes[6] = 112 | counter >>> 8 & 15;
114
- bytes[7] = counter & 255;
115
- bytes[8] = 128 | (bytes[8] ?? 0) & 63;
116
- let hex2 = "";
117
- for (const byte of bytes) hex2 += byte.toString(16).padStart(2, "0");
118
- return `${hex2.slice(0, 8)}-${hex2.slice(8, 12)}-${hex2.slice(12, 16)}-${hex2.slice(16, 20)}-${hex2.slice(20)}`;
121
+ function explain(error) {
122
+ if (!(error instanceof NiadraAPIError)) return error instanceof Error ? error.name : String(error);
123
+ const detail = error.problem?.detail?.trim();
124
+ const said = `${error.status} ${error.code}${detail ? `: ${detail.slice(0, 300)}` : ""}`;
125
+ return error.requestId ? `${said} (request ${error.requestId})` : said;
119
126
  }
120
127
 
121
128
  // src/admin.ts
@@ -518,7 +525,7 @@ function internalRecord(span, ref, act) {
518
525
  }
519
526
 
520
527
  // src/version.ts
521
- var VERSION = "0.9.1";
528
+ var VERSION = "0.10.4";
522
529
 
523
530
  // src/transport.ts
524
531
  var RETRYABLE_WRITE_STATUS = /* @__PURE__ */ new Set([408, 421, 429, 500, 502, 503, 504]);
@@ -531,8 +538,36 @@ var Transport = class {
531
538
  }
532
539
  config;
533
540
  baseURL;
541
+ answeredAt;
542
+ /** When the allowance was first given since the last answer; it holds for the calls of that moment only. */
543
+ grantedAt;
544
+ /** When the client last sent a request of its own (`RequestSpec.activity`), in epoch milliseconds. */
545
+ lastActivityAt;
546
+ /** Likely: an answer came within `keepAliveMs`. When not, the next budgeted call gets `coldAllowanceMs`. */
547
+ connectionOpen() {
548
+ return this.answeredAt !== void 0 && Date.now() - this.answeredAt <= (this.config.keepAliveMs ?? 0);
549
+ }
534
550
  async request(spec) {
535
- return spec.retry.kind === "read" ? this.read(spec, spec.retry) : this.write(spec, spec.retry);
551
+ if (spec.activity !== false) this.lastActivityAt = Date.now();
552
+ const budgeted = this.withAllowance(spec);
553
+ return budgeted.retry.kind === "read" ? this.read(budgeted, budgeted.retry) : this.write(budgeted, budgeted.retry);
554
+ }
555
+ /**
556
+ * `spec` with the allowance for opening a connection, when it has a budget and none is likely open. The calls
557
+ * that start while the first one opens it get it too; after that, none does until an answer comes, so an
558
+ * outage costs the allowance once, not on every turn.
559
+ */
560
+ withAllowance(spec) {
561
+ const allowance = this.config.coldAllowanceMs ?? 0;
562
+ const { retry } = spec;
563
+ if (allowance <= 0 || retry.kind === "write" && retry.totalMs === void 0) return spec;
564
+ const now = Date.now();
565
+ const keep = this.config.keepAliveMs ?? 0;
566
+ if (this.answeredAt !== void 0 && now - this.answeredAt <= keep) return spec;
567
+ this.grantedAt ??= now;
568
+ if (now - this.grantedAt > allowance) return spec;
569
+ const total = retry.kind === "write" && retry.totalMs !== void 0 ? { ...retry, totalMs: retry.totalMs + allowance } : retry;
570
+ return { ...spec, timeoutMs: spec.timeoutMs + allowance, retry: total };
536
571
  }
537
572
  async read(spec, policy) {
538
573
  const deadline = new Deadline(spec.timeoutMs, spec.signal);
@@ -603,7 +638,18 @@ var Transport = class {
603
638
  headers["content-type"] = "application/json";
604
639
  init.body = JSON.stringify(spec.body);
605
640
  }
606
- return this.exchange(this.url(spec.path, spec.query), init, deadline);
641
+ try {
642
+ const answer = await this.exchange(this.url(spec.path, spec.query), init, deadline);
643
+ this.answered();
644
+ return answer;
645
+ } catch (error) {
646
+ if (error instanceof NiadraAPIError) this.answered();
647
+ throw error;
648
+ }
649
+ }
650
+ answered() {
651
+ this.answeredAt = Date.now();
652
+ this.grantedAt = void 0;
607
653
  }
608
654
  async exchange(url, init, deadline) {
609
655
  let response;
@@ -4044,8 +4090,7 @@ var TurnSender = class {
4044
4090
  return true;
4045
4091
  }
4046
4092
  }
4047
- const code = error instanceof NiadraAPIError ? error.code : toNiadraError(error).name;
4048
- this.recorder.rejected(batch.frames.length, [code]);
4093
+ this.recorder.rejected(batch.frames.length, [explain(error)]);
4049
4094
  return true;
4050
4095
  }
4051
4096
  accepted(batch, answer) {
@@ -4053,8 +4098,8 @@ var TurnSender = class {
4053
4098
  this.resumeAt = 0;
4054
4099
  this.recorder.sent(answer.accepted, answer.duplicates);
4055
4100
  const again = [];
4056
- const codes2 = /* @__PURE__ */ new Set();
4057
- let refused2 = 0;
4101
+ const reasons = /* @__PURE__ */ new Set();
4102
+ let refused3 = 0;
4058
4103
  for (const error of answer.errors ?? []) {
4059
4104
  const frame = batch.frames[error.index];
4060
4105
  if (frame === void 0) continue;
@@ -4063,12 +4108,12 @@ var TurnSender = class {
4063
4108
  this.recorder.modeRefused();
4064
4109
  again.push(frame);
4065
4110
  } else {
4066
- refused2++;
4067
- codes2.add(error.code);
4111
+ refused3++;
4112
+ reasons.add(error.detail ? `${error.code}: ${error.detail.slice(0, 300)}` : error.code);
4068
4113
  }
4069
4114
  }
4070
4115
  if (again.length > 0) this.queue.requeue(again);
4071
- if (refused2 > 0) this.recorder.rejected(refused2, [...codes2]);
4116
+ if (refused3 > 0) this.recorder.rejected(refused3, [...reasons]);
4072
4117
  }
4073
4118
  };
4074
4119
 
@@ -4190,9 +4235,12 @@ var TurnRecorder = class {
4190
4235
  this.accepted += accepted;
4191
4236
  this.duplicates += duplicates;
4192
4237
  }
4193
- rejected(count3, codes2) {
4238
+ /** `reasons`: the API's code and detail of each refusal (field paths and rules, never a value). */
4239
+ rejected(count3, reasons) {
4194
4240
  this.rejectedTurns += count3;
4195
- this.logger.warn(`${count3} turn records were refused (${[...codes2].sort().join(", ")})`);
4241
+ const shown = [...new Set(reasons)].sort();
4242
+ const more = shown.length > 3 ? ` (and ${shown.length - 3} more)` : "";
4243
+ this.logger.warn(`${count3} turn records were refused: ${shown.slice(0, 3).join("; ")}${more}`);
4196
4244
  }
4197
4245
  /** The space does not record turns: these are dropped, and with `off` (a 404) recording stops a while. */
4198
4246
  notRecorded(count3, off) {
@@ -4262,6 +4310,10 @@ async function hex(text2) {
4262
4310
  }
4263
4311
 
4264
4312
  // src/coordination/client.ts
4313
+ var REFUSED = /* @__PURE__ */ new Set([400, 401, 403, 422]);
4314
+ function refused(error) {
4315
+ return error instanceof NiadraAPIError && REFUSED.has(error.status);
4316
+ }
4265
4317
  var FAIL_CLOSED = /* @__PURE__ */ new Set(["marketing", "retention", "collection"]);
4266
4318
  var CHECK_BUDGET_MS = 200;
4267
4319
  function fallback(request, suppressed, failOpen) {
@@ -4283,15 +4335,22 @@ function claimed(data, error) {
4283
4335
  return { held: false, claim: null, error: code };
4284
4336
  }
4285
4337
  var Coordinator = class {
4286
- constructor(outbox, suppressions, declareNow) {
4338
+ constructor(outbox, suppressions, declareNow, logger) {
4287
4339
  this.outbox = outbox;
4288
4340
  this.suppressions = suppressions;
4289
4341
  this.declareNow = declareNow;
4342
+ this.logger = logger;
4290
4343
  }
4291
4344
  outbox;
4292
4345
  suppressions;
4293
4346
  declareNow;
4347
+ logger;
4294
4348
  async failed(request, failOpen, error) {
4349
+ if (refused(error)) {
4350
+ this.logger?.warn(`the coordination check was refused: ${explain(error)}`);
4351
+ const decision = request.direction === "inbound" ? "allow" : "defer";
4352
+ return recorded({ decision, decision_id: uuidv7(), reasons: ["invalid_request"], valid_for_s: 0 });
4353
+ }
4295
4354
  const off = error instanceof NiadraAPIError && error.status === 404;
4296
4355
  const suppressed = request.subject != null && request.direction === "outbound" ? !await this.suppressions.mayContact(request.subject, request.purpose, { channel: request.channel ?? null, failOpen: true }) : false;
4297
4356
  const plain2 = off ? { ...request, effect_key: null } : request;
@@ -4312,7 +4371,7 @@ var Coordinator = class {
4312
4371
  if (who.subject) body.subject = who.subject;
4313
4372
  if (who.object) body.object = who.object;
4314
4373
  const key2 = uuidv7();
4315
- this.outbox.put({ send: () => this.declareNow(body, key2) });
4374
+ this.outbox.put({ send: () => this.declareNow(body, key2), route: "POST /v1/coordination/declare" });
4316
4375
  return key2;
4317
4376
  }
4318
4377
  };
@@ -4831,7 +4890,7 @@ var Outbox = class {
4831
4890
  return false;
4832
4891
  }
4833
4892
  this.writes.shift();
4834
- this.logger.warn(`a write was refused (${error instanceof NiadraAPIError ? error.code : String(error)})`);
4893
+ this.logger.warn(`${write.route ?? "a write"} was refused: ${explain(error)}`);
4835
4894
  this.settle(write, void 0, error);
4836
4895
  return true;
4837
4896
  }
@@ -5721,9 +5780,17 @@ var handles = {
5721
5780
  appUserId: (value, options) => build("app_user_id", value, void 0, options),
5722
5781
  /** The id of a person or organization in a system of record; `system` names that system, such as `crm`. */
5723
5782
  systemId: (value, system, options) => build("system_id", value, system, options),
5724
- /** An HMAC of a national document number; `country` is its ISO 3166-1 alpha-2 code. */
5783
+ /**
5784
+ * A person's national document, such as a CPF: `govIdHmac("529.982.247-25", "BR")`. Send the number
5785
+ * itself, never a hash of it: the server checks its check digits, then keeps only a keyed hash with your
5786
+ * space's secret and an encrypted copy, and shows it masked. `country` is its ISO 3166-1 alpha-2 code.
5787
+ */
5725
5788
  govIdHmac: (value, country, options) => build("gov_id_hmac", value, country, options),
5726
- /** An HMAC of a company registry number. Identifies an organization. */
5789
+ /**
5790
+ * A company's registry number, such as a CNPJ: `orgRegistryHmac("11.222.333/0001-81", "BR")`. As with
5791
+ * `govIdHmac`, send the number itself; the server checks it and keeps only a keyed hash. Identifies an
5792
+ * organization.
5793
+ */
5727
5794
  orgRegistryHmac: (value, country) => build("org_registry_hmac", value, country, { subjectKind: "account" }),
5728
5795
  /** An e-mail domain, such as `acme.com`. Identifies an organization. */
5729
5796
  emailDomain: (domain) => build("email_domain", domain, void 0, { subjectKind: "account" }),
@@ -6397,6 +6464,7 @@ var AgentSession = class {
6397
6464
  try {
6398
6465
  return this.host.coordinator.decided(await this.host.check(request, options.timeoutMs ?? CHECK_BUDGET_MS), request, this.checked);
6399
6466
  } catch (error) {
6467
+ if (this.host.strict && refused(error)) throw error;
6400
6468
  return this.host.coordinator.failed(request, options.failOpen, error);
6401
6469
  }
6402
6470
  }
@@ -7008,6 +7076,7 @@ var SessionState = class {
7008
7076
  /** The guards of the last read: they hold the answers to the turn they were written for. */
7009
7077
  guards = /* @__PURE__ */ new Map();
7010
7078
  lastReport = null;
7079
+ unlinkedSaid = false;
7011
7080
  /** After the first pack, every read also asks what changed since. */
7012
7081
  get wantsDelta() {
7013
7082
  return this.etag !== null;
@@ -7036,6 +7105,19 @@ var SessionState = class {
7036
7105
  this.last = absorbed;
7037
7106
  return absorbed;
7038
7107
  }
7108
+ /**
7109
+ * Once per session: the read named an organization (`about`) with no active link to the subject, so it went
7110
+ * on with the subject's own memory, never the organization's.
7111
+ */
7112
+ sayUnlinked(result2, logger) {
7113
+ if (result2.response?.about_unlinked && !this.unlinkedSaid) {
7114
+ this.unlinkedSaid = true;
7115
+ logger.warn(
7116
+ "`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."
7117
+ );
7118
+ }
7119
+ return result2;
7120
+ }
7039
7121
  /** The last backing check of an agent's answer. */
7040
7122
  get lastBacking() {
7041
7123
  return this.lastReport;
@@ -7283,7 +7365,7 @@ var Conversation = class {
7283
7365
  * `query` picks this read's slots by other words than the turn; the pack is the pinned one.
7284
7366
  */
7285
7367
  async context(options = {}) {
7286
- const { query, turn, format, explain, include, ...requestOptions } = options;
7368
+ const { query, turn, format, explain: explain2, include, ...requestOptions } = options;
7287
7369
  const params = {
7288
7370
  subject: this.subject,
7289
7371
  view: this.view,
@@ -7292,7 +7374,7 @@ var Conversation = class {
7292
7374
  ...this.params.about ? { about: this.params.about } : {},
7293
7375
  ...this.params.target ? { target: this.params.target } : {},
7294
7376
  ...format === "json" ? { format } : {},
7295
- ...explain ? { explain } : {}
7377
+ ...explain2 ? { explain: explain2 } : {}
7296
7378
  };
7297
7379
  if (query) params.query = query;
7298
7380
  if (include?.length) params.include = include;
@@ -7300,7 +7382,7 @@ var Conversation = class {
7300
7382
  params.turn = turn === void 0 ? this.turnText : turn;
7301
7383
  const result2 = await this.client.context(params, requestOptions);
7302
7384
  this.features.observe(result2);
7303
- return this.state.observe(this.state.absorb(result2));
7385
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7304
7386
  }
7305
7387
  /**
7306
7388
  * Starts this conversation's first read now, in the background: call it when the call starts
@@ -7480,6 +7562,35 @@ function withHandles(base, extra) {
7480
7562
  return all;
7481
7563
  }
7482
7564
 
7565
+ // src/warm.ts
7566
+ var EVERY_MS = 1e5;
7567
+ var IDLE_MS = 9e4;
7568
+ var WARM_FOR_MS = 6e5;
7569
+ var KeepWarm = class {
7570
+ constructor(enabled) {
7571
+ this.enabled = enabled;
7572
+ }
7573
+ enabled;
7574
+ open = /* @__PURE__ */ new Map();
7575
+ openedAt = 0;
7576
+ /** Notes an open conversation or task. */
7577
+ add(scope, session, now) {
7578
+ if (!this.enabled || typeof WeakRef === "undefined") return;
7579
+ this.open.set(scope, new WeakRef(session));
7580
+ this.openedAt = now;
7581
+ }
7582
+ /** The conversation or task ended. */
7583
+ end(scope) {
7584
+ this.open.delete(scope);
7585
+ }
7586
+ step(now, lastActivityAt, idleMs = IDLE_MS, warmForMs = WARM_FOR_MS) {
7587
+ for (const [scope, ref] of this.open) if (ref.deref() === void 0) this.open.delete(scope);
7588
+ const used = Math.max(lastActivityAt ?? 0, this.openedAt);
7589
+ if (this.open.size === 0 || now - used > warmForMs) return "stop";
7590
+ return now - used >= idleMs ? "ping" : "wait";
7591
+ }
7592
+ };
7593
+
7483
7594
  // src/exit.ts
7484
7595
  var registered = /* @__PURE__ */ new Set();
7485
7596
  var installedOn = null;
@@ -7610,8 +7721,10 @@ var DEFAULT_TIMEOUTS = {
7610
7721
  write: 5e3,
7611
7722
  token: 2e3,
7612
7723
  upload: 6e4,
7613
- prefetch: 1e3
7724
+ prefetch: 1e3,
7725
+ connect: 1e3
7614
7726
  };
7727
+ var FETCH_KEEPALIVE_MS = 4e3;
7615
7728
  var DEFAULT_CACHE = {
7616
7729
  ttlMs: 1e4,
7617
7730
  staleWhileRevalidateMs: 10 * 6e4,
@@ -7718,7 +7831,7 @@ var Task = class {
7718
7831
  * Resolves with an empty result, never rejects, unless the client is strict.
7719
7832
  */
7720
7833
  async context(options = {}) {
7721
- const { query, format, explain, include, ...requestOptions } = options;
7834
+ const { query, format, explain: explain2, include, ...requestOptions } = options;
7722
7835
  const target = this.object ? { object: this.object } : this.params.subject ? { subject: this.params.subject } : {};
7723
7836
  const params = {
7724
7837
  ...target,
@@ -7728,18 +7841,18 @@ var Task = class {
7728
7841
  ...this.level ? { verification: this.level } : {},
7729
7842
  ...this.params.target ? { target: this.params.target } : {},
7730
7843
  ...format === "json" ? { format } : {},
7731
- ...explain ? { explain } : {},
7844
+ ...explain2 ? { explain: explain2 } : {},
7732
7845
  ...include?.length ? { include } : {}
7733
7846
  };
7734
7847
  if (query) {
7735
7848
  const answered = await this.client.context({ ...params, query }, requestOptions);
7736
7849
  this.features.observe(answered);
7737
- return this.state.observe(answered);
7850
+ return this.state.observe(this.state.sayUnlinked(answered, this.logger));
7738
7851
  }
7739
7852
  if (this.state.wantsDelta) params.delta = true;
7740
7853
  const result2 = await this.client.context(params, requestOptions);
7741
7854
  this.features.observe(result2);
7742
- return this.state.observe(this.state.absorb(result2));
7855
+ return this.state.observe(this.state.sayUnlinked(this.state.absorb(result2), this.logger));
7743
7856
  }
7744
7857
  /**
7745
7858
  * The agent's own working notes for this task's prompt, as `niadra.agentMemory()` with
@@ -7869,14 +7982,14 @@ var AGENT_MEMORY_TOOL_NAMES = {
7869
7982
  search: "search_agent_memory",
7870
7983
  remember: "remember"
7871
7984
  };
7872
- var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait"];
7985
+ var ITEM_KINDS = ["episode", "fact", "open_item", "action", "object", "trait", "system_event"];
7873
7986
  var NOTE_KINDS = ["procedure", "tool_note", "process_note", "pitfall"];
7874
7987
  var TOOL_DEFINITIONS = [
7875
7988
  {
7876
7989
  "type": "function",
7877
7990
  "function": {
7878
7991
  "name": "search_customer_history",
7879
- "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.",
7992
+ "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.",
7880
7993
  "parameters": {
7881
7994
  "type": "object",
7882
7995
  "properties": {
@@ -7920,7 +8033,8 @@ var TOOL_DEFINITIONS = [
7920
8033
  "open_item",
7921
8034
  "action",
7922
8035
  "object",
7923
- "trait"
8036
+ "trait",
8037
+ "system_event"
7924
8038
  ]
7925
8039
  }
7926
8040
  },
@@ -7947,7 +8061,7 @@ var TOOL_DEFINITIONS = [
7947
8061
  "type": "function",
7948
8062
  "function": {
7949
8063
  "name": "get_customer_timeline",
7950
- "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.",
8064
+ "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.",
7951
8065
  "parameters": {
7952
8066
  "type": "object",
7953
8067
  "properties": {
@@ -7995,7 +8109,8 @@ var TOOL_DEFINITIONS = [
7995
8109
  "open_item",
7996
8110
  "action",
7997
8111
  "object",
7998
- "trait"
8112
+ "trait",
8113
+ "system_event"
7999
8114
  ]
8000
8115
  }
8001
8116
  },
@@ -8014,7 +8129,7 @@ var TOOL_DEFINITIONS = [
8014
8129
  "type": "function",
8015
8130
  "function": {
8016
8131
  "name": "open_history_item",
8017
- "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.",
8132
+ "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.",
8018
8133
  "parameters": {
8019
8134
  "type": "object",
8020
8135
  "properties": {
@@ -8385,6 +8500,13 @@ function compose(body, read) {
8385
8500
  const pack = body.pack ? { pack: { ...body.pack, slots: fetched?.pack?.slots ?? [] } } : {};
8386
8501
  return { ...body, slots: fetched?.slots ?? null, guards: fetched?.guards ?? [], ...pack };
8387
8502
  }
8503
+ var RTT_MARGIN_MS = 50;
8504
+ function budgetWarnings(rttMs, timeouts, explicit) {
8505
+ const ms = Math.round(rttMs);
8506
+ return [...explicit].sort().filter((name) => timeouts[name] < rttMs + RTT_MARGIN_MS).map(
8507
+ (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`
8508
+ );
8509
+ }
8388
8510
  function rttWarnings(rttMs, timeouts) {
8389
8511
  const ms = Math.round(rttMs);
8390
8512
  const found2 = [];
@@ -8422,6 +8544,14 @@ var Niadra = class _Niadra {
8422
8544
  enabled;
8423
8545
  core;
8424
8546
  timeouts;
8547
+ keepWarm;
8548
+ warmTimer;
8549
+ /** The read budgets the caller left at their defaults: they take the measured round trip on top. */
8550
+ defaultReads;
8551
+ voiceStarted = false;
8552
+ /** A probe is on its way: until it ends, default read budgets get `timeouts.connect` on top. */
8553
+ measuring = false;
8554
+ voiceWarned = false;
8425
8555
  strict;
8426
8556
  /** Where the client reports what it swallows in fail-open mode. */
8427
8557
  logger;
@@ -8466,6 +8596,8 @@ var Niadra = class _Niadra {
8466
8596
  this.strict = options.strict ?? false;
8467
8597
  this.logger = options.logger ?? consoleLogger;
8468
8598
  this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
8599
+ this.keepWarm = new KeepWarm(options.keepWarm ?? true);
8600
+ this.defaultReads = new Set(["context", "navigation"].filter((name) => options.timeouts?.[name] === void 0));
8469
8601
  this.voice = new VoiceLines(
8470
8602
  options.voice === false ? { ...DEFAULT_VOICE, enabled: false } : { ...DEFAULT_VOICE, ...options.voice }
8471
8603
  );
@@ -8491,13 +8623,14 @@ var Niadra = class _Niadra {
8491
8623
  this.coordinator = new Coordinator(
8492
8624
  this.outbox,
8493
8625
  this.suppressions,
8494
- (body, key2) => this.api.declare(body, { idempotency_key: key2 })
8626
+ (body, key2) => this.api.declare(body, { idempotency_key: key2 }),
8627
+ this.logger
8495
8628
  );
8496
8629
  this.states = new AgentStates(
8497
8630
  this.outbox,
8498
8631
  {
8499
- read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.timeouts.navigation }),
8500
- write: (write) => this.api.writeAgentState(write, { timeout: this.timeouts.navigation })
8632
+ read: (scope, agent) => this.api.readAgentState({ scope, agent }, { timeout: this.readBudget("navigation") }),
8633
+ write: (write) => this.api.writeAgentState(write, { timeout: this.readBudget("navigation") })
8501
8634
  },
8502
8635
  this.logger
8503
8636
  );
@@ -8524,6 +8657,7 @@ var Niadra = class _Niadra {
8524
8657
  this.core = setup;
8525
8658
  this.enabled = true;
8526
8659
  this.disabledReason = null;
8660
+ this.probe(setup);
8527
8661
  if (options.flushOnExit ?? true) this.unregisterExit = registerExitFlush(this);
8528
8662
  }
8529
8663
  setup(options) {
@@ -8546,7 +8680,9 @@ var Niadra = class _Niadra {
8546
8680
  apiKey,
8547
8681
  fetch: fetchImpl,
8548
8682
  defaultHeaders: options.defaultHeaders ?? {},
8549
- logger: this.logger
8683
+ logger: this.logger,
8684
+ coldAllowanceMs: this.timeouts.connect,
8685
+ keepAliveMs: options.keepAliveMs ?? FETCH_KEEPALIVE_MS
8550
8686
  });
8551
8687
  const queueOptions = { ...DEFAULT_QUEUE, ...options.queue };
8552
8688
  queueOptions.maxBatchSize = Math.min(queueOptions.maxBatchSize, 499);
@@ -8602,7 +8738,7 @@ var Niadra = class _Niadra {
8602
8738
  return result2;
8603
8739
  }
8604
8740
  readContext(core, params, request, options) {
8605
- const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
8741
+ const timeout = options.timeout ?? (request.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
8606
8742
  const { query: own3, ...pinned2 } = request;
8607
8743
  const query = turnText(own3 ?? params.turn);
8608
8744
  const voiceCache = this.voiceCache(core, pinned2, options.cache);
@@ -8618,7 +8754,7 @@ var Niadra = class _Niadra {
8618
8754
  */
8619
8755
  async profile() {
8620
8756
  if (!this.core) return null;
8621
- return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.timeouts.navigation }));
8757
+ return this.profileCache.refresh(() => this.api.sdkProfile({ timeout: this.readBudget("navigation") }));
8622
8758
  }
8623
8759
  /**
8624
8760
  * Checks outputs against this claim contract instead of the one the profile serves (a company's own copy,
@@ -8643,7 +8779,7 @@ var Niadra = class _Niadra {
8643
8779
  */
8644
8780
  async mayContact(handle, purpose, options = {}) {
8645
8781
  if (this.core && this.suppressions.due()) {
8646
- const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.timeouts.navigation);
8782
+ const read = this.readSuppressions(this.suppressions.held ? this.timeouts.write : this.readBudget("navigation"));
8647
8783
  if (!this.suppressions.held) await read;
8648
8784
  }
8649
8785
  const checkOptions = { channel: options.channel ?? null };
@@ -8653,8 +8789,8 @@ var Niadra = class _Niadra {
8653
8789
  readSuppressions(budgetMs) {
8654
8790
  return this.suppressions.read(
8655
8791
  {
8656
- salt: () => this.api.suppressionSalt({ timeout: this.timeouts.navigation }),
8657
- page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.timeouts.navigation })
8792
+ salt: () => this.api.suppressionSalt({ timeout: this.readBudget("navigation") }),
8793
+ page: (cursor, limit3) => this.api.suppressions({ cursor, limit: limit3 }, { timeout: this.readBudget("navigation") })
8658
8794
  },
8659
8795
  budgetMs
8660
8796
  );
@@ -8733,6 +8869,7 @@ var Niadra = class _Niadra {
8733
8869
  }
8734
8870
  /** What a conversation or a task needs of its client for the agent features. */
8735
8871
  get agentHost() {
8872
+ const client = this;
8736
8873
  return {
8737
8874
  recorder: this.turns,
8738
8875
  coordinator: this.coordinator,
@@ -8746,7 +8883,10 @@ var Niadra = class _Niadra {
8746
8883
  claim: (request, timeoutMs) => this.api.claim(request, {}, { timeout: timeoutMs }),
8747
8884
  verifyClaim: (ref, field, value, options) => this.verifyClaim(ref, field, value, options),
8748
8885
  enabled: this.enabled,
8749
- navigationMs: this.timeouts.navigation
8886
+ strict: this.strict,
8887
+ get navigationMs() {
8888
+ return client.readBudget("navigation");
8889
+ }
8750
8890
  };
8751
8891
  }
8752
8892
  /**
@@ -8774,7 +8914,7 @@ var Niadra = class _Niadra {
8774
8914
  if (!cache) return false;
8775
8915
  const key2 = cacheKey(request);
8776
8916
  const line = this.voice.line(cacheScope(request) ?? "");
8777
- this.probe(core);
8917
+ this.startVoice(core);
8778
8918
  line.request = request;
8779
8919
  if (!cache.has(key2) && line.inFlight().length === 0) {
8780
8920
  this.voiceRead(core, cache, line, key2, request, null, this.timeouts.contextVoiceStart);
@@ -8911,13 +9051,13 @@ var Niadra = class _Niadra {
8911
9051
  if (!params.query || params.query.length > 2e3) {
8912
9052
  throw new NiadraValidationError("query must be 1 to 2000 characters");
8913
9053
  }
8914
- return this.readSpec("POST", "/v1/history/search", params, this.timeouts.navigation, options);
9054
+ return this.readSpec("POST", "/v1/history/search", params, this.readBudget("navigation"), options);
8915
9055
  });
8916
9056
  }
8917
9057
  /** The customer's history, newest first, one line per item, paginated by cursor. */
8918
9058
  async timeline(params, options = {}) {
8919
9059
  return this.navigate(
8920
- () => this.readSpec("POST", "/v1/history/timeline", params, this.timeouts.navigation, options)
9060
+ () => this.readSpec("POST", "/v1/history/timeline", params, this.readBudget("navigation"), options)
8921
9061
  );
8922
9062
  }
8923
9063
  /**
@@ -8930,9 +9070,10 @@ var Niadra = class _Niadra {
8930
9070
  if (!id) throw new NiadraValidationError("open() needs an item id");
8931
9071
  const body = { item_id: id };
8932
9072
  if (params.subject) body.subject = params.subject;
9073
+ if (params.about) body.about = params.about;
8933
9074
  if (params.verification) body.verification = params.verification;
8934
9075
  if (params.conversation_id) body.conversation_id = params.conversation_id;
8935
- return this.readSpec("POST", "/v1/history/open", body, this.timeouts.navigation, options);
9076
+ return this.readSpec("POST", "/v1/history/open", body, this.readBudget("navigation"), options);
8936
9077
  });
8937
9078
  }
8938
9079
  /**
@@ -8944,7 +9085,7 @@ var Niadra = class _Niadra {
8944
9085
  * const { data: invoice } = await niadra.objectState("invoice:erp:0823");
8945
9086
  */
8946
9087
  async objectState(object, options = {}) {
8947
- return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.timeouts.navigation, options));
9088
+ return this.navigate(() => this.readSpec("GET", objectPath(object), void 0, this.readBudget("navigation"), options));
8948
9089
  }
8949
9090
  /**
8950
9091
  * System events and agent actions about one object, newest first, one line each and never
@@ -8957,7 +9098,7 @@ var Niadra = class _Niadra {
8957
9098
  throw new NiadraValidationError("limit must be between 1 and 100");
8958
9099
  }
8959
9100
  const path = `${objectPath(object)}/timeline`;
8960
- const spec = this.readSpec("GET", path, void 0, this.timeouts.navigation, options);
9101
+ const spec = this.readSpec("GET", path, void 0, this.readBudget("navigation"), options);
8961
9102
  spec.query = { cursor: params.cursor, limit: String(limit3) };
8962
9103
  return spec;
8963
9104
  });
@@ -8976,6 +9117,7 @@ var Niadra = class _Niadra {
8976
9117
  timeline: (params, voice) => this.timeline(params, this.voiceBudget(voice)),
8977
9118
  open: (id, customer, bound, voice) => {
8978
9119
  const scope = { subject: customer };
9120
+ if (bound.about) scope.about = bound.about;
8979
9121
  if (bound.verification) scope.verification = bound.verification;
8980
9122
  if (bound.conversation_id) scope.conversation_id = bound.conversation_id;
8981
9123
  return this.open(id, scope, this.voiceBudget(voice));
@@ -9010,7 +9152,7 @@ var Niadra = class _Niadra {
9010
9152
  const key2 = AgentMemoryCache.key(params);
9011
9153
  const fresh = cache?.fresh(key2);
9012
9154
  if (fresh) return blockResult(fresh, "cache");
9013
- const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.timeouts.context);
9155
+ const timeout = options.timeout ?? (params.view === "voice" ? this.timeouts.contextVoice : this.readBudget("context"));
9014
9156
  const spec = this.readSpec("GET", "/v1/agent-memory/block", void 0, timeout, options);
9015
9157
  spec.query = {
9016
9158
  max_tokens: String(params.max_tokens ?? 300),
@@ -9047,7 +9189,7 @@ var Niadra = class _Niadra {
9047
9189
  if (params.limit !== void 0) body.limit = params.limit;
9048
9190
  if (params.conversation_id) body.conversation_id = params.conversation_id;
9049
9191
  if (params.task_id) body.task_id = params.task_id;
9050
- return this.readSpec("POST", "/v1/agent-memory/search", body, this.timeouts.navigation, options);
9192
+ return this.readSpec("POST", "/v1/agent-memory/search", body, this.readBudget("navigation"), options);
9051
9193
  });
9052
9194
  return result2.error ? result2 : { data: result2.data.notes, error: null };
9053
9195
  }
@@ -9141,6 +9283,41 @@ var Niadra = class _Niadra {
9141
9283
  return { ok: false, idempotency_key: key2, error: this.swallow(error, "feedback") };
9142
9284
  }
9143
9285
  }
9286
+ /**
9287
+ * How the space's agents used the context they read (`GET /v1/context-use`): sessions, deliveries, use,
9288
+ * repetition, transfers and recontact, with intervals, grouped by `group_by`. A key of an `analyst` source
9289
+ * with the `analytics` scope reads every source of the space; a key with `admin` reads its own source.
9290
+ */
9291
+ contextUse(params = {}, options = {}) {
9292
+ return this.navigate(() => {
9293
+ const { group_by: groups, ...filters } = params;
9294
+ const query = { ...filters };
9295
+ if (groups?.length) query.group_by = groups;
9296
+ return { ...this.readSpec("GET", "/v1/context-use", void 0, this.timeouts.write, options), query };
9297
+ });
9298
+ }
9299
+ /**
9300
+ * Links a person to the organization they act for (an account or a partner), as a system of record that
9301
+ * knows who works for whom: a CRM, an HR system. Needs a key with the `identity:link` scope (or `admin`);
9302
+ * `can_see_contacts` needs `admin`. Reads with `about` reach the organization through the link.
9303
+ */
9304
+ link(params, options = {}) {
9305
+ const { idempotency_key: key2, ...rest } = params;
9306
+ const body = { can_see_contacts: false, method: "system_import", ...rest };
9307
+ return this.navigate(() => this.writeSpec("/v1/identity/links", body, key2 ?? uuidv7(), options));
9308
+ }
9309
+ /**
9310
+ * Ends a link, from `valid_to` (now when absent): the person no longer acts for the organization, and reads
9311
+ * with `about` for the pair go on with the person's own memory. Needs `identity:link` or `admin`.
9312
+ */
9313
+ endLink(linkId, params = {}, options = {}) {
9314
+ return this.navigate(() => {
9315
+ if (!linkId) throw new NiadraValidationError("endLink() needs a link id");
9316
+ const body = params.valid_to ? { valid_to: params.valid_to } : {};
9317
+ const path = `/v1/identity/links/${encodeURIComponent(linkId)}/end`;
9318
+ return this.writeSpec(path, body, params.idempotency_key ?? uuidv7(), options);
9319
+ });
9320
+ }
9144
9321
  /**
9145
9322
  * Up to 500 corrections in one call, each with its own idempotency key (minted when missing).
9146
9323
  * Resolves with `accepted`, `duplicates` for replayed keys and one error per refused item, by index.
@@ -9233,16 +9410,20 @@ var Niadra = class _Niadra {
9233
9410
  * emits `conversation.ended` when you call `end()`.
9234
9411
  */
9235
9412
  conversation(params) {
9236
- return new Conversation(this, params, {
9413
+ const conversation = new Conversation(this, params, {
9237
9414
  endConversation: (id) => this.endScope(buildConversationEnded(id), `conversation:${id}`)
9238
9415
  });
9416
+ this.warm(`conversation:${conversation.id}`, conversation);
9417
+ return conversation;
9239
9418
  }
9240
9419
  /** A helper for one internal-agent task: binds `task_id` to reads and writes and emits `task.ended`. */
9241
9420
  task(params) {
9242
- return new Task(this, params, {
9421
+ const task = new Task(this, params, {
9243
9422
  endTask: (id) => this.endScope(buildTaskEnded(id), `task:${id}`),
9244
9423
  verifyTask: (verify) => this.verifyWith(verify)
9245
9424
  });
9425
+ this.warm(`task:${task.id}`, task);
9426
+ return task;
9246
9427
  }
9247
9428
  /**
9248
9429
  * Sends every queued event and resolves when done. Call it before a serverless function
@@ -9262,6 +9443,8 @@ var Niadra = class _Niadra {
9262
9443
  */
9263
9444
  async shutdown() {
9264
9445
  this.unregisterExit();
9446
+ clearInterval(this.warmTimer);
9447
+ this.warmTimer = void 0;
9265
9448
  if (!this.core) return;
9266
9449
  await this.turnSender?.stop(this.timeouts.write);
9267
9450
  await this.outbox.stop(this.timeouts.write);
@@ -9365,9 +9548,9 @@ var Niadra = class _Niadra {
9365
9548
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", request, timeout, { signal, headers }));
9366
9549
  return normalizeContext(response.data);
9367
9550
  } catch (error) {
9368
- const refused2 = error instanceof NiadraAPIError && error.status === 404;
9551
+ const refused3 = error instanceof NiadraAPIError && error.status === 404;
9369
9552
  const left = timeout - (Date.now() - started);
9370
- if (!request.include?.length || !refused2 || left <= 0) throw error;
9553
+ if (!request.include?.length || !refused3 || left <= 0) throw error;
9371
9554
  for (const name of request.include) this.refusedBlocks.set(name, Date.now() + BLOCK_RECHECK_AFTER_MS);
9372
9555
  const { include: _dropped, ...plain2 } = request;
9373
9556
  const response = await core.transport.request(this.readSpec("POST", "/v1/context", plain2, left, { signal, headers }));
@@ -9435,6 +9618,7 @@ var Niadra = class _Niadra {
9435
9618
  /** Measures the round trip to the region once per client, in the background. */
9436
9619
  probe(core) {
9437
9620
  if (!this.voice.claimProbe()) return;
9621
+ this.measuring = true;
9438
9622
  void (async () => {
9439
9623
  const samples = [];
9440
9624
  for (let i = 0; i < 2; i++) {
@@ -9448,16 +9632,44 @@ var Niadra = class _Niadra {
9448
9632
  });
9449
9633
  } catch (error) {
9450
9634
  this.logger.debug(`round trip probe failed: ${describe(toNiadraError(error))}`);
9635
+ this.measuring = false;
9451
9636
  return;
9452
9637
  }
9453
9638
  samples.push(Date.now() - started);
9454
9639
  }
9455
9640
  const rtt = Math.min(...samples);
9456
9641
  this.voice.rtt = rtt;
9642
+ this.measuring = false;
9457
9643
  this.logger.debug(`round trip to the region ${Math.round(rtt)} ms`);
9458
- for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9644
+ const explicit = ["context", "navigation"].filter((name) => !this.defaultReads.has(name));
9645
+ for (const warning of budgetWarnings(rtt, this.timeouts, explicit)) this.logger.warn(warning);
9646
+ if (this.voiceStarted) this.warnVoice();
9459
9647
  })();
9460
9648
  }
9649
+ /**
9650
+ * A read budget: one the caller left at its default is what the API may take, and the measured round trip
9651
+ * to the region goes on top, so an agent far from the region (Sao Paulo, 170 ms from us-east-2) is not
9652
+ * timed out by the network; one the caller set is a ceiling.
9653
+ */
9654
+ readBudget(name) {
9655
+ if (!this.defaultReads.has(name)) return this.timeouts[name];
9656
+ const rtt = this.voice.rtt;
9657
+ if (rtt !== null) return this.timeouts[name] + rtt;
9658
+ const open = this.core?.transport.connectionOpen() ?? false;
9659
+ return this.measuring && open ? this.timeouts[name] + this.timeouts.connect : this.timeouts[name];
9660
+ }
9661
+ /** The client reads in voice: the voice budgets' warnings matter from now on. */
9662
+ startVoice(core) {
9663
+ this.voiceStarted = true;
9664
+ this.probe(core);
9665
+ this.warnVoice();
9666
+ }
9667
+ warnVoice() {
9668
+ const rtt = this.voice.rtt;
9669
+ if (this.voiceWarned || rtt === null) return;
9670
+ this.voiceWarned = true;
9671
+ for (const warning of rttWarnings(rtt, this.timeouts)) this.logger.warn(warning);
9672
+ }
9461
9673
  /**
9462
9674
  * A voice turn: the pinned body from memory, and the slots of the read of its words when that
9463
9675
  * read lands within `timeout`. See `voice.ts`.
@@ -9467,7 +9679,7 @@ var Niadra = class _Niadra {
9467
9679
  const key2 = cacheKey(request);
9468
9680
  const deadline = Date.now() + timeout;
9469
9681
  const line = this.voice.line(scope);
9470
- this.probe(core);
9682
+ this.startVoice(core);
9471
9683
  line.request = request;
9472
9684
  const words2 = wordsOf(query);
9473
9685
  const background = this.timeouts.prefetch;
@@ -9653,8 +9865,39 @@ var Niadra = class _Niadra {
9653
9865
  core.queue.flushInBackground(true);
9654
9866
  });
9655
9867
  }
9868
+ /** Notes an open conversation or task; the first one starts the keep-warm timer (`warm.ts`). */
9869
+ warm(scope, session) {
9870
+ const core = this.core;
9871
+ if (!core || !this.keepWarm.enabled) return;
9872
+ this.keepWarm.add(scope, session, Date.now());
9873
+ if (this.warmTimer !== void 0) return;
9874
+ const timer = setInterval(() => {
9875
+ this.warmTick(core);
9876
+ }, EVERY_MS);
9877
+ timer.unref?.();
9878
+ this.warmTimer = timer;
9879
+ }
9880
+ warmTick(core) {
9881
+ const step = this.keepWarm.step(Date.now(), core.transport.lastActivityAt);
9882
+ if (step === "stop") {
9883
+ clearInterval(this.warmTimer);
9884
+ this.warmTimer = void 0;
9885
+ return;
9886
+ }
9887
+ if (step !== "ping") return;
9888
+ core.transport.request({
9889
+ method: "GET",
9890
+ path: "/healthz",
9891
+ timeoutMs: 2e3,
9892
+ retry: { kind: "read", maxAttempts: 1 },
9893
+ activity: false
9894
+ }).catch((error) => {
9895
+ this.logger.debug(`keep-warm ping failed: ${describe(toNiadraError(error))}`);
9896
+ });
9897
+ }
9656
9898
  async endScope(item, scope) {
9657
9899
  this.forgetScope(scope);
9900
+ this.keepWarm.end(scope);
9658
9901
  return this.sendNow(() => item);
9659
9902
  }
9660
9903
  async sendBatch(transport, items2, maxAttempts, backoff) {
@@ -11509,7 +11752,7 @@ var Replayer = class {
11509
11752
  try {
11510
11753
  answer = await this.niadra.callRoute({ method: "POST", path: "/v1/scenario-runs", body, idempotencyKey: uuidv7() });
11511
11754
  } catch (error) {
11512
- if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused(scenarioIds, results);
11755
+ if (error instanceof NiadraAPIError && error.code === "pin_mismatch") return refused2(scenarioIds, results);
11513
11756
  throw error;
11514
11757
  }
11515
11758
  let run = runOf(answer);
@@ -11612,7 +11855,7 @@ var Replayer = class {
11612
11855
  return result(scenarioId, turnId, caseId, run, "completed", { paraphrase: rephrased, assertions, divergent_calls: played.divergent, latency_ms: latency });
11613
11856
  }
11614
11857
  };
11615
- function refused(scenarioIds, results) {
11858
+ function refused2(scenarioIds, results) {
11616
11859
  const scenarios = scenarioIds.map((id) => {
11617
11860
  const mine = results.filter((r) => r.scenario_id === id);
11618
11861
  return {