@raindrop-ai/pi-agent 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,6 +39,15 @@ pi install npm:@raindrop-ai/pi-agent
39
39
 
40
40
  Set `RAINDROP_WRITE_KEY` in your environment. Traces appear automatically.
41
41
 
42
+ ## Payload size limits
43
+
44
+ Event input/output are capped at **1,000,000 characters per field** (span
45
+ attributes at 32 KB) and truncated with a `...[truncated by raindrop]` marker.
46
+ The cap is enforced before (or during) serialization, so oversized payloads
47
+ cost the cap — not the payload — on the host's event loop, and large events
48
+ land truncated instead of being rejected at the ingest size limit. Hook error
49
+ logs are rate-limited to one line per failure family per 30s.
50
+
42
51
  ## Documentation
43
52
 
44
53
  See the full [Pi Agent docs](https://www.raindrop.ai/docs/integrations/pi-agent/) for configuration, per-subscribe overrides, and extension settings.
@@ -1,4 +1,4 @@
1
- // ../core/dist/chunk-VUNUOE2X.js
1
+ // ../core/dist/chunk-U5HUTMR5.js
2
2
  function getCrypto() {
3
3
  const c = globalThis.crypto;
4
4
  return c;
@@ -56,6 +56,8 @@ function base64Encode(bytes) {
56
56
  function generateId() {
57
57
  return base64Encode(randomBytes(12)).replace(/[+/=]/g, "").slice(0, 16);
58
58
  }
59
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
60
+ var MAX_RETRY_DELAY_MS = 3e4;
59
61
  function wait(ms) {
60
62
  return new Promise((resolve) => setTimeout(resolve, ms));
61
63
  }
@@ -63,6 +65,46 @@ function formatEndpoint(endpoint) {
63
65
  if (!endpoint) return void 0;
64
66
  return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
65
67
  }
68
+ function redactUrlForLog(url) {
69
+ try {
70
+ const parsed = new URL(url);
71
+ parsed.username = "";
72
+ parsed.password = "";
73
+ parsed.search = "";
74
+ parsed.hash = "";
75
+ return parsed.toString();
76
+ } catch (e) {
77
+ return "<unparseable-url>";
78
+ }
79
+ }
80
+ var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
81
+ var rateLimitedLogLast = /* @__PURE__ */ new Map();
82
+ function rateLimitedLog(key, log) {
83
+ const now = Date.now();
84
+ const last = rateLimitedLogLast.get(key);
85
+ if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
86
+ return false;
87
+ }
88
+ rateLimitedLogLast.set(key, now);
89
+ log();
90
+ return true;
91
+ }
92
+ async function raceWithTimeout(promise, timeoutMs) {
93
+ let timer;
94
+ const settledInTime = await Promise.race([
95
+ promise.then(
96
+ () => true,
97
+ () => true
98
+ ),
99
+ new Promise((resolve) => {
100
+ var _a;
101
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
102
+ (_a = timer.unref) == null ? void 0 : _a.call(timer);
103
+ })
104
+ ]);
105
+ if (timer) clearTimeout(timer);
106
+ return settledInTime;
107
+ }
66
108
  function parseRetryAfter(headers) {
67
109
  var _a;
68
110
  const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
@@ -79,12 +121,12 @@ function parseRetryAfter(headers) {
79
121
  function getRetryDelayMs(attemptNumber, previousError) {
80
122
  if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
81
123
  const v = previousError.retryAfterMs;
82
- if (typeof v === "number") return Math.max(0, v);
124
+ if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
83
125
  }
84
126
  if (attemptNumber <= 1) return 0;
85
127
  const base = 500;
86
128
  const factor = Math.pow(2, attemptNumber - 2);
87
- return base * factor;
129
+ return Math.min(base * factor, MAX_RETRY_DELAY_MS);
88
130
  }
89
131
  async function withRetry(operation, opName, opts) {
90
132
  const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
@@ -119,7 +161,9 @@ async function withRetry(operation, opName, opts) {
119
161
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
120
162
  }
121
163
  async function postJson(url, body, headers, opts) {
122
- const opName = `POST ${url}`;
164
+ var _a;
165
+ const opName = `POST ${redactUrlForLog(url)}`;
166
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
123
167
  await withRetry(
124
168
  async () => {
125
169
  const resp = await fetch(url, {
@@ -128,7 +172,8 @@ async function postJson(url, body, headers, opts) {
128
172
  "Content-Type": "application/json",
129
173
  ...headers
130
174
  },
131
- body: JSON.stringify(body)
175
+ body: JSON.stringify(body),
176
+ signal: AbortSignal.timeout(timeoutMs)
132
177
  });
133
178
  if (!resp.ok) {
134
179
  const text = await resp.text().catch(() => "");
@@ -145,6 +190,27 @@ async function postJson(url, body, headers, opts) {
145
190
  opts
146
191
  );
147
192
  }
193
+ var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
194
+ var TRUNCATION_MARKER = "...[truncated by raindrop]";
195
+ var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
196
+ function resolveMaxTextFieldChars(value) {
197
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
198
+ return Math.floor(value);
199
+ }
200
+ return currentDefaultMaxTextFieldChars;
201
+ }
202
+ function truncateToLimit(text, limit) {
203
+ if (limit > TRUNCATION_MARKER.length) {
204
+ return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
205
+ }
206
+ return text.slice(0, Math.max(0, limit));
207
+ }
208
+ function capText(value, limit) {
209
+ if (typeof value !== "string") return value;
210
+ const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
211
+ if (value.length <= max) return value;
212
+ return truncateToLimit(value, max);
213
+ }
148
214
  var SpanStatusCode = {
149
215
  UNSET: 0,
150
216
  OK: 1,
@@ -287,6 +353,8 @@ function mirrorPartialEventToLocalDebugger(event, options = {}) {
287
353
  }).catch(() => {
288
354
  });
289
355
  }
356
+ var SHUTDOWN_DEADLINE_MS = 1e4;
357
+ var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
290
358
  function mergePatches(target, source) {
291
359
  var _a, _b, _c, _d;
292
360
  const out = { ...target, ...source };
@@ -304,6 +372,7 @@ var EventShipper = class {
304
372
  this.sticky = /* @__PURE__ */ new Map();
305
373
  this.timers = /* @__PURE__ */ new Map();
306
374
  this.inFlight = /* @__PURE__ */ new Set();
375
+ this.hasShutdown = false;
307
376
  var _a, _b, _c, _d, _e, _f, _g, _h;
308
377
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
309
378
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -313,6 +382,7 @@ var EventShipper = class {
313
382
  this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
314
383
  this.prefix = `[raindrop-ai/${this.sdkName}]`;
315
384
  this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
385
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
316
386
  this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
317
387
  if (this.debug && this.localDebuggerUrl) {
318
388
  console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
@@ -335,10 +405,52 @@ var EventShipper = class {
335
405
  authHeaders() {
336
406
  return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
337
407
  }
408
+ /**
409
+ * Build the retry/timeout options for one POST, honoring the shutdown
410
+ * deadline. Returns `null` when the shutdown drain window is exhausted —
411
+ * the caller must drop the payload (with a rate-limited warning) instead
412
+ * of issuing a request that could outlive process exit.
413
+ *
414
+ * Checked fresh on EVERY send, so a shutdown that begins while the flush
415
+ * path is mid-drain takes effect immediately: no further retries, and the
416
+ * per-attempt timeout is clamped to the remaining window. After
417
+ * `shutdown()` returns (deadline cleared, `hasShutdown` still set),
418
+ * sends — late callers, or flush work the deadline abandoned mid-drain —
419
+ * run as a single short attempt rather than regaining the full retry
420
+ * schedule.
421
+ */
422
+ requestOpts() {
423
+ if (this.shutdownDeadlineAt !== void 0) {
424
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
425
+ if (remainingMs <= 0) return null;
426
+ return {
427
+ maxAttempts: 1,
428
+ debug: this.debug,
429
+ sdkName: this.sdkName,
430
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
431
+ };
432
+ }
433
+ if (this.hasShutdown) {
434
+ return {
435
+ maxAttempts: 1,
436
+ debug: this.debug,
437
+ sdkName: this.sdkName,
438
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
439
+ };
440
+ }
441
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
442
+ }
338
443
  async patch(eventId, patch) {
339
444
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
340
445
  if (!this.enabled) return;
341
446
  if (!eventId || !eventId.trim()) return;
447
+ const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
448
+ if (typeof patch.input === "string" && patch.input.length > maxChars) {
449
+ patch = { ...patch, input: capText(patch.input, maxChars) };
450
+ }
451
+ if (typeof patch.output === "string" && patch.output.length > maxChars) {
452
+ patch = { ...patch, output: capText(patch.output, maxChars) };
453
+ }
342
454
  if (this.debug) {
343
455
  console.log(`${this.prefix} queue patch`, {
344
456
  eventId,
@@ -385,9 +497,16 @@ var EventShipper = class {
385
497
  })));
386
498
  }
387
499
  async shutdown() {
388
- for (const t of this.timers.values()) clearTimeout(t);
389
- this.timers.clear();
390
- await this.flush();
500
+ this.hasShutdown = true;
501
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
502
+ try {
503
+ for (const t of this.timers.values()) clearTimeout(t);
504
+ this.timers.clear();
505
+ const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
506
+ if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
507
+ } finally {
508
+ this.shutdownDeadlineAt = void 0;
509
+ }
391
510
  }
392
511
  async trackSignal(signal) {
393
512
  var _a, _b;
@@ -409,15 +528,19 @@ var EventShipper = class {
409
528
  ];
410
529
  if (!this.writeKey) return;
411
530
  const url = `${this.baseUrl}signals/track`;
531
+ const opts = this.requestOpts();
532
+ if (!opts) {
533
+ this.warnShutdownDrop("signal");
534
+ return;
535
+ }
412
536
  try {
413
- await postJson(url, body, this.authHeaders(), {
414
- maxAttempts: 3,
415
- debug: this.debug,
416
- sdkName: this.sdkName
417
- });
537
+ await postJson(url, body, this.authHeaders(), opts);
418
538
  } catch (err) {
419
539
  const msg = err instanceof Error ? err.message : String(err);
420
- console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`);
540
+ rateLimitedLog(
541
+ `${this.prefix}.send_signal_failed`,
542
+ () => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
543
+ );
421
544
  }
422
545
  }
423
546
  async identify(users) {
@@ -441,17 +564,27 @@ var EventShipper = class {
441
564
  if (!this.writeKey) return;
442
565
  if (body.length === 0) return;
443
566
  const url = `${this.baseUrl}users/identify`;
567
+ const opts = this.requestOpts();
568
+ if (!opts) {
569
+ this.warnShutdownDrop("identify");
570
+ return;
571
+ }
444
572
  try {
445
- await postJson(url, body, this.authHeaders(), {
446
- maxAttempts: 3,
447
- debug: this.debug,
448
- sdkName: this.sdkName
449
- });
573
+ await postJson(url, body, this.authHeaders(), opts);
450
574
  } catch (err) {
451
575
  const msg = err instanceof Error ? err.message : String(err);
452
- console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`);
576
+ rateLimitedLog(
577
+ `${this.prefix}.send_identify_failed`,
578
+ () => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
579
+ );
453
580
  }
454
581
  }
582
+ warnShutdownDrop(what) {
583
+ rateLimitedLog(
584
+ `${this.prefix}.shutdown_deadline`,
585
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
586
+ );
587
+ }
455
588
  async flushOne(eventId) {
456
589
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
457
590
  if (!this.enabled) return;
@@ -527,11 +660,13 @@ var EventShipper = class {
527
660
  if (!isPending) this.sticky.delete(eventId);
528
661
  return;
529
662
  }
530
- const p = postJson(url, payload, this.authHeaders(), {
531
- maxAttempts: 3,
532
- debug: this.debug,
533
- sdkName: this.sdkName
534
- });
663
+ const opts = this.requestOpts();
664
+ if (!opts) {
665
+ this.warnShutdownDrop(`track_partial ${eventId}`);
666
+ if (!isPending) this.sticky.delete(eventId);
667
+ return;
668
+ }
669
+ const p = postJson(url, payload, this.authHeaders(), opts);
535
670
  this.inFlight.add(p);
536
671
  try {
537
672
  try {
@@ -541,7 +676,10 @@ var EventShipper = class {
541
676
  }
542
677
  } catch (err) {
543
678
  const msg = err instanceof Error ? err.message : String(err);
544
- console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`);
679
+ rateLimitedLog(
680
+ `${this.prefix}.send_track_partial_failed`,
681
+ () => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
682
+ );
545
683
  }
546
684
  } finally {
547
685
  this.inFlight.delete(p);
@@ -640,10 +778,24 @@ function redactJsonAttributeValue(key, value) {
640
778
  if (scrubbedJson === json) return void 0;
641
779
  return { stringValue: scrubbedJson };
642
780
  }
781
+ function applyOtelSpanAttributeLimit(limit) {
782
+ var _a, _b;
783
+ try {
784
+ const raw = (_b = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env) == null ? void 0 : _b.OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT;
785
+ if (!raw) return limit;
786
+ const parsed = Number.parseInt(raw, 10);
787
+ if (Number.isFinite(parsed) && parsed > 0) {
788
+ return Math.min(limit, parsed);
789
+ }
790
+ } catch (e) {
791
+ }
792
+ return limit;
793
+ }
643
794
  var TraceShipper = class {
644
795
  constructor(opts) {
645
796
  this.queue = [];
646
797
  this.inFlight = /* @__PURE__ */ new Set();
798
+ this.hasShutdown = false;
647
799
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
648
800
  this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
649
801
  this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
@@ -663,6 +815,39 @@ var TraceShipper = class {
663
815
  }
664
816
  this.transformSpanHook = opts.transformSpan;
665
817
  this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
818
+ this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
819
+ }
820
+ /**
821
+ * Cap every string attribute value on the span. O(#attributes) length
822
+ * checks; only oversized values pay a slice. Runs AFTER the redaction
823
+ * pipeline so the default secret-scrub still sees parseable JSON in
824
+ * `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
825
+ * first could cut a JSON blob mid-way, fail the parse, and ship secrets
826
+ * in the surviving prefix).
827
+ *
828
+ * A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
829
+ * for span content, matching the Python SDK and the OTel SDK convention.
830
+ */
831
+ capSpanAttributes(span) {
832
+ var _a;
833
+ const maxChars = applyOtelSpanAttributeLimit(
834
+ resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
835
+ );
836
+ const attrs = span.attributes;
837
+ if (!attrs || attrs.length === 0) return span;
838
+ let nextAttrs;
839
+ for (let i = 0; i < attrs.length; i++) {
840
+ const attr = attrs[i];
841
+ const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
842
+ if (typeof value !== "string" || value.length <= maxChars) continue;
843
+ if (!nextAttrs) nextAttrs = attrs.slice();
844
+ nextAttrs[i] = {
845
+ key: attr.key,
846
+ value: { ...attr.value, stringValue: capText(value, maxChars) }
847
+ };
848
+ }
849
+ if (!nextAttrs) return span;
850
+ return { ...span, attributes: nextAttrs };
666
851
  }
667
852
  /**
668
853
  * Apply the user `transformSpan` hook (if any) followed by the default
@@ -696,7 +881,7 @@ var TraceShipper = class {
696
881
  if (!this.disableDefaultRedaction) {
697
882
  current = defaultTransformSpan(current);
698
883
  }
699
- return current;
884
+ return this.capSpanAttributes(current);
700
885
  }
701
886
  isDebugEnabled() {
702
887
  return this.debug;
@@ -817,6 +1002,16 @@ var TraceShipper = class {
817
1002
  while (this.queue.length > 0) {
818
1003
  const batch = this.queue.splice(0, this.maxBatchSize);
819
1004
  if (!this.writeKey) continue;
1005
+ const opts = this.requestOpts();
1006
+ if (!opts) {
1007
+ rateLimitedLog(
1008
+ `${this.prefix}.shutdown_deadline`,
1009
+ () => console.warn(
1010
+ `${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
1011
+ )
1012
+ );
1013
+ continue;
1014
+ }
820
1015
  const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
821
1016
  const url = `${this.baseUrl}traces`;
822
1017
  if (this.debug) {
@@ -825,11 +1020,7 @@ var TraceShipper = class {
825
1020
  endpoint: url
826
1021
  });
827
1022
  }
828
- const p = postJson(url, body, this.authHeaders(), {
829
- maxAttempts: 3,
830
- debug: this.debug,
831
- sdkName: this.sdkName
832
- });
1023
+ const p = postJson(url, body, this.authHeaders(), opts);
833
1024
  this.inFlight.add(p);
834
1025
  try {
835
1026
  try {
@@ -837,21 +1028,61 @@ var TraceShipper = class {
837
1028
  if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
838
1029
  } catch (err) {
839
1030
  const msg = err instanceof Error ? err.message : String(err);
840
- console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`);
1031
+ rateLimitedLog(
1032
+ `${this.prefix}.send_spans_failed`,
1033
+ () => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
1034
+ );
841
1035
  }
842
1036
  } finally {
843
1037
  this.inFlight.delete(p);
844
1038
  }
845
1039
  }
846
1040
  }
1041
+ /** See EventShipper.requestOpts — same shutdown-budget semantics. */
1042
+ requestOpts() {
1043
+ if (this.shutdownDeadlineAt !== void 0) {
1044
+ const remainingMs = this.shutdownDeadlineAt - Date.now();
1045
+ if (remainingMs <= 0) return null;
1046
+ return {
1047
+ maxAttempts: 1,
1048
+ debug: this.debug,
1049
+ sdkName: this.sdkName,
1050
+ timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
1051
+ };
1052
+ }
1053
+ if (this.hasShutdown) {
1054
+ return {
1055
+ maxAttempts: 1,
1056
+ debug: this.debug,
1057
+ sdkName: this.sdkName,
1058
+ timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
1059
+ };
1060
+ }
1061
+ return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
1062
+ }
847
1063
  async shutdown() {
848
- if (this.timer) {
849
- clearTimeout(this.timer);
850
- this.timer = void 0;
1064
+ this.hasShutdown = true;
1065
+ this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
1066
+ try {
1067
+ if (this.timer) {
1068
+ clearTimeout(this.timer);
1069
+ this.timer = void 0;
1070
+ }
1071
+ const drain = async () => {
1072
+ await this.flush();
1073
+ await Promise.all([...this.inFlight].map((p) => p.catch(() => {
1074
+ })));
1075
+ };
1076
+ const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
1077
+ if (!settled) {
1078
+ rateLimitedLog(
1079
+ `${this.prefix}.shutdown_deadline`,
1080
+ () => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
1081
+ );
1082
+ }
1083
+ } finally {
1084
+ this.shutdownDeadlineAt = void 0;
851
1085
  }
852
- await this.flush();
853
- await Promise.all([...this.inFlight].map((p) => p.catch(() => {
854
- })));
855
1086
  }
856
1087
  };
857
1088
 
@@ -862,7 +1093,7 @@ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage;
862
1093
  // package.json
863
1094
  var package_default = {
864
1095
  name: "@raindrop-ai/pi-agent",
865
- version: "0.0.4",
1096
+ version: "0.0.5",
866
1097
  description: "Raindrop observability for Pi Agent \u2014 automatic tracing via subscriber or pi-coding-agent extension",
867
1098
  type: "module",
868
1099
  license: "MIT",
@@ -1060,19 +1291,82 @@ function formatToolSpanName(toolName, args) {
1060
1291
  }
1061
1292
  return toolName;
1062
1293
  }
1063
- function safeStringify(value) {
1294
+ var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
1295
+ var MAX_TEXT_FIELD_CHARS = 1e6;
1296
+ var MAX_ATTR_LENGTH = 32768;
1297
+ var MAX_BOUNDED_DEPTH = 12;
1298
+ function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
1299
+ if (value.length <= limit) return value;
1300
+ if (limit > TRUNCATION_MARKER2.length) {
1301
+ return value.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
1302
+ }
1303
+ return value.slice(0, limit);
1304
+ }
1305
+ function boundedClone2(value, budget, depth) {
1306
+ if (budget.remaining <= 0) return TRUNCATION_MARKER2;
1307
+ if (typeof value === "string") {
1308
+ if (value.length > budget.remaining) {
1309
+ const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
1310
+ budget.remaining = 0;
1311
+ return taken;
1312
+ }
1313
+ budget.remaining -= Math.max(value.length, 1);
1314
+ return value;
1315
+ }
1316
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
1317
+ budget.remaining -= 8;
1318
+ return value;
1319
+ }
1320
+ if (typeof value !== "object") {
1321
+ budget.remaining -= 8;
1322
+ return value;
1323
+ }
1324
+ if (depth >= MAX_BOUNDED_DEPTH) {
1325
+ budget.remaining -= 16;
1326
+ return `<max depth: ${TRUNCATION_MARKER2}>`;
1327
+ }
1328
+ const withToJson = value;
1329
+ if (typeof withToJson.toJSON === "function") {
1330
+ try {
1331
+ return boundedClone2(withToJson.toJSON(), budget, depth + 1);
1332
+ } catch (e) {
1333
+ }
1334
+ }
1335
+ if (Array.isArray(value)) {
1336
+ const out2 = [];
1337
+ for (const item of value) {
1338
+ if (budget.remaining <= 0) {
1339
+ out2.push(TRUNCATION_MARKER2);
1340
+ break;
1341
+ }
1342
+ out2.push(boundedClone2(item, budget, depth + 1));
1343
+ }
1344
+ return out2;
1345
+ }
1346
+ const out = {};
1347
+ for (const key of Object.keys(value)) {
1348
+ if (budget.remaining <= 0) {
1349
+ out["..."] = TRUNCATION_MARKER2;
1350
+ break;
1351
+ }
1352
+ budget.remaining -= Math.max(key.length, 1);
1353
+ out[key] = boundedClone2(value[key], budget, depth + 1);
1354
+ }
1355
+ return out;
1356
+ }
1357
+ function safeStringify(value, limit = MAX_ATTR_LENGTH) {
1064
1358
  if (value === void 0 || value === null) return void 0;
1065
1359
  try {
1066
- return JSON.stringify(value);
1360
+ const pruned = boundedClone2(value, { remaining: limit + TRUNCATION_MARKER2.length + 256 }, 0);
1361
+ return JSON.stringify(pruned);
1067
1362
  } catch (e) {
1068
1363
  try {
1069
- return String(value);
1364
+ return capText2(String(value), limit);
1070
1365
  } catch (e2) {
1071
1366
  return "[unserializable]";
1072
1367
  }
1073
1368
  }
1074
1369
  }
1075
- var MAX_ATTR_LENGTH = 32768;
1076
1370
  function truncate(value) {
1077
1371
  if (value === void 0) return void 0;
1078
1372
  if (value.length <= MAX_ATTR_LENGTH) return value;
@@ -1118,6 +1412,7 @@ export {
1118
1412
  extractAssistantText,
1119
1413
  extractToolCallIds,
1120
1414
  formatToolSpanName,
1415
+ capText2 as capText,
1121
1416
  safeStringify,
1122
1417
  truncate,
1123
1418
  getHostname,