@andreprado/agentkit 0.1.0-alpha.9 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +18 -1
  2. package/docs/guides/add-channel.md +251 -7
  3. package/docs/guides/add-knowledge.md +10 -0
  4. package/docs/guides/add-managed-composio.md +165 -0
  5. package/docs/guides/add-tool.md +10 -3
  6. package/docs/guides/channel-security.md +162 -32
  7. package/docs/guides/connect-discord.md +178 -0
  8. package/docs/guides/connect-slack.md +126 -0
  9. package/docs/guides/connect-telegram.md +61 -1
  10. package/docs/guides/connect-whatsapp-evolution.md +121 -0
  11. package/docs/guides/connect-whatsapp-uazapi.md +139 -0
  12. package/docs/guides/connect-whatsapp-zapster.md +119 -16
  13. package/docs/guides/create-agent.md +31 -4
  14. package/docs/guides/debug-channel.md +159 -0
  15. package/docs/guides/improve-from-production.md +151 -0
  16. package/docs/guides/prepare-deploy.md +32 -14
  17. package/docs/guides/replay-production-traces.md +72 -0
  18. package/docs/guides/run-evals.md +95 -25
  19. package/docs/guides/security-rules.md +9 -5
  20. package/docs/guides/send-feedback.md +135 -0
  21. package/docs/guides/use-jev.md +67 -0
  22. package/docs/guides/use-provider.md +70 -3
  23. package/docs/llms-full.txt +295 -25
  24. package/docs/llms.txt +54 -7
  25. package/package.json +3 -7
  26. package/src/cli/args.ts +23 -2
  27. package/src/cli/cloud-client.ts +121 -9
  28. package/src/cli/commands/channels.ts +856 -36
  29. package/src/cli/commands/feedback.ts +438 -0
  30. package/src/cli/commands/provider.ts +47 -0
  31. package/src/cli/commands/transcribe.ts +171 -0
  32. package/src/cli/deploy-chat-ui.ts +232 -18
  33. package/src/cli/deploy-readiness.ts +227 -14
  34. package/src/cli/help.ts +67 -9
  35. package/src/cli/index.ts +740 -35
  36. package/src/cli/new-command.ts +41 -0
  37. package/src/cloud/client.ts +4 -3
  38. package/src/cloud/contracts.ts +1 -1
  39. package/src/create-project.ts +18 -35
  40. package/src/index.ts +565 -11
  41. package/src/providers/codex-auth.ts +111 -0
  42. package/src/providers/pi.ts +88 -19
  43. package/src/providers/test.ts +36 -0
  44. package/src/providers/types.ts +8 -0
  45. package/src/runtime/channel-test-harness.ts +21 -1
  46. package/src/runtime/channels/discord.ts +904 -0
  47. package/src/runtime/channels/generic-webhook.ts +682 -0
  48. package/src/runtime/channels/net-guard.ts +480 -0
  49. package/src/runtime/channels/provider-fetch.ts +54 -0
  50. package/src/runtime/channels/slack.ts +652 -0
  51. package/src/runtime/channels/telegram.ts +379 -15
  52. package/src/runtime/channels/whatsapp-evolution.ts +1330 -0
  53. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  54. package/src/runtime/channels/whatsapp-uazapi.ts +1192 -0
  55. package/src/runtime/channels/whatsapp-zapster.ts +702 -40
  56. package/src/runtime/channels.ts +83 -3
  57. package/src/runtime/chat.ts +70 -44
  58. package/src/runtime/config.ts +512 -20
  59. package/src/runtime/core/manifest.ts +75 -5
  60. package/src/runtime/core/targets.ts +5 -5
  61. package/src/runtime/deploy-readiness.ts +34 -4
  62. package/src/runtime/dev-server.ts +639 -39
  63. package/src/runtime/env.ts +8 -3
  64. package/src/runtime/evals.ts +445 -74
  65. package/src/runtime/improve.ts +868 -0
  66. package/src/runtime/inspect.ts +173 -4
  67. package/src/runtime/integrations/composio.ts +425 -0
  68. package/src/runtime/knowledge/embeddings.ts +45 -7
  69. package/src/runtime/knowledge/ingest.ts +69 -6
  70. package/src/runtime/knowledge/retrieve.ts +25 -5
  71. package/src/runtime/knowledge/schema.ts +45 -1
  72. package/src/runtime/knowledge/vector.ts +30 -30
  73. package/src/runtime/prompt-context.ts +141 -0
  74. package/src/runtime/runtime-contract.ts +71 -7
  75. package/src/runtime/skills.ts +95 -0
  76. package/src/runtime/targets/cloudflare/build.ts +1010 -208
  77. package/src/runtime/targets/container/server.ts +1 -1
  78. package/src/runtime/targets/vps/deploy.ts +26 -9
  79. package/src/runtime/tool-runner.ts +9 -1
  80. package/src/runtime/tools.ts +26 -2
  81. package/src/runtime/transcription.ts +483 -0
  82. package/src/storage/sqlite.ts +7 -2
  83. package/src/templates/blank.ts +37 -9
  84. package/src/templates/dentista.ts +40 -14
  85. package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
  86. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  87. package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
  88. package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
  89. package/src/templates/skills/agentkit-channels/SKILL.md +66 -1
  90. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
  91. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +28 -3
  92. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  93. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  94. package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
  95. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
  96. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
  97. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
  98. package/src/templates/skills/agentkit-database/SKILL.md +11 -0
  99. package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
  100. package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
  101. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  102. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  103. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  104. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  105. package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
  106. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  107. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  108. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  109. package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
  110. package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
  111. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  112. package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
  113. package/src/templates/skills/agentkit-security/SKILL.md +5 -2
  114. package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
  115. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
  116. package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
  117. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
  118. package/src/templates/support.ts +42 -12
  119. package/docs/guides/agentkit-skills-architecture.md +0 -471
  120. package/docs/guides/channels-implementation-map.md +0 -243
  121. package/docs/guides/channels-production-handoff.md +0 -101
  122. package/docs/portable-deploy-release-checklist.md +0 -41
@@ -1,11 +1,19 @@
1
1
  import type {
2
2
  ChannelAdapter,
3
+ ChannelMediaDownloadInput,
4
+ ChannelMediaDownloadResult,
3
5
  ChannelSendInput,
4
6
  ChannelStatusResult,
5
7
  RawWebhookEvent,
6
8
  WebhookVerificationResult,
7
9
  } from "../channels";
8
10
  import { AgentKitError } from "../errors";
11
+ import {
12
+ CHANNEL_DOWNLOAD_TIMEOUT_MS,
13
+ CHANNEL_SEND_TIMEOUT_MS,
14
+ classifySendRequestError,
15
+ isRetryableReadRequestError,
16
+ } from "./provider-fetch";
9
17
 
10
18
  export const telegramChannelAdapter: ChannelAdapter = {
11
19
  type: "telegram",
@@ -86,13 +94,37 @@ export const telegramChannelAdapter: ChannelAdapter = {
86
94
 
87
95
  const text = readString(message, "text");
88
96
 
89
- if (!text) {
97
+ if (text) {
90
98
  return [
91
99
  {
92
100
  ...base,
93
- kind: "unsupported",
101
+ kind: "message",
94
102
  externalIdentity: identity,
95
- unsupportedReason: "unsupported_message_type:non_text",
103
+ message: {
104
+ role: "user",
105
+ contentType: "text",
106
+ content: text,
107
+ providerMessageId: readNumberishString(message, "message_id"),
108
+ },
109
+ },
110
+ ];
111
+ }
112
+
113
+ const audio = normalizeTelegramAudio(message);
114
+
115
+ if (audio) {
116
+ return [
117
+ {
118
+ ...base,
119
+ kind: "message",
120
+ externalIdentity: identity,
121
+ message: {
122
+ role: "user",
123
+ contentType: "audio",
124
+ content: "",
125
+ providerMessageId: readNumberishString(message, "message_id"),
126
+ audio,
127
+ },
96
128
  },
97
129
  ];
98
130
  }
@@ -100,17 +132,15 @@ export const telegramChannelAdapter: ChannelAdapter = {
100
132
  return [
101
133
  {
102
134
  ...base,
103
- kind: "message",
135
+ kind: "unsupported",
104
136
  externalIdentity: identity,
105
- message: {
106
- role: "user",
107
- contentType: "text",
108
- content: text,
109
- providerMessageId: readNumberishString(message, "message_id"),
110
- },
137
+ unsupportedReason: "unsupported_message_type:non_text",
111
138
  },
112
139
  ];
113
140
  },
141
+ async downloadMedia(input) {
142
+ return downloadTelegramMedia(input);
143
+ },
114
144
  async sendMessage(input) {
115
145
  return sendTelegramMessage(input);
116
146
  },
@@ -142,6 +172,7 @@ export type TelegramSendMessageRequest = {
142
172
  };
143
173
 
144
174
  type TelegramFetch = (url: string, init: RequestInit) => Promise<Response>;
175
+ const TELEGRAM_MAX_DOWNLOAD_BYTES = 20_000_000;
145
176
 
146
177
  export function buildTelegramSendMessageRequest(input: ChannelSendInput): TelegramSendMessageRequest {
147
178
  return buildTelegramSendMessageRequestInternal(input, "<redacted>");
@@ -156,7 +187,7 @@ export async function sendTelegramMessage(
156
187
 
157
188
  return {
158
189
  ok: true,
159
- status: "sent",
190
+ status: "stubbed",
160
191
  providerRequestId: request.idempotencyKey,
161
192
  providerMetadata: {
162
193
  method: request.method,
@@ -196,6 +227,7 @@ export async function sendTelegramMessage(
196
227
 
197
228
  try {
198
229
  response = await fetcher(actualRequest.url, {
230
+ signal: AbortSignal.timeout(CHANNEL_SEND_TIMEOUT_MS),
199
231
  method: actualRequest.method,
200
232
  headers: {
201
233
  "Content-Type": "application/json",
@@ -203,13 +235,14 @@ export async function sendTelegramMessage(
203
235
  body: JSON.stringify(actualRequest.body),
204
236
  });
205
237
  } catch (error) {
238
+ const failure = classifySendRequestError(error);
206
239
  const errorMessage = (error instanceof Error ? error.message : String(error)).replace(token, "<redacted>");
207
240
 
208
241
  return {
209
242
  ok: false,
210
- retryable: true,
211
- code: "channel_provider_unavailable",
212
- message: `Telegram sendMessage request failed before a response: ${errorMessage}`,
243
+ retryable: failure.retryable,
244
+ code: failure.code,
245
+ message: `Telegram sendMessage request ${failure.reason}: ${errorMessage}`,
213
246
  providerRequestId: request.idempotencyKey,
214
247
  providerMetadata: {
215
248
  method: request.method,
@@ -247,11 +280,26 @@ export async function sendTelegramMessage(
247
280
  const result = isRecord(payload.result) ? payload.result : undefined;
248
281
  const messageId = readNumberishString(result ?? {}, "message_id");
249
282
 
283
+ if (!messageId) {
284
+ return {
285
+ ok: false,
286
+ retryable: false,
287
+ code: "channel_send_failed",
288
+ message: "Telegram sendMessage did not return result.message_id.",
289
+ providerRequestId: request.idempotencyKey,
290
+ providerMetadata: {
291
+ method: request.method,
292
+ url: request.url,
293
+ body: request.body,
294
+ },
295
+ };
296
+ }
297
+
250
298
  return {
251
299
  ok: true,
252
300
  status: "sent",
253
301
  providerRequestId: request.idempotencyKey,
254
- ...(messageId ? { providerMessageId: messageId } : {}),
302
+ providerMessageId: messageId,
255
303
  providerMetadata: {
256
304
  method: request.method,
257
305
  url: request.url,
@@ -260,6 +308,155 @@ export async function sendTelegramMessage(
260
308
  };
261
309
  }
262
310
 
311
+ export async function downloadTelegramMedia(
312
+ input: ChannelMediaDownloadInput,
313
+ fetcher: TelegramFetch = fetch,
314
+ ): Promise<ChannelMediaDownloadResult> {
315
+ const token = input.secrets.TELEGRAM_BOT_TOKEN;
316
+
317
+ if (!token) {
318
+ return {
319
+ ok: false,
320
+ retryable: false,
321
+ code: "channel_secret_missing",
322
+ message: "Secret TELEGRAM_BOT_TOKEN is not set for this channel.",
323
+ };
324
+ }
325
+
326
+ const audio = input.message.audio;
327
+
328
+ if (!audio?.providerMediaId) {
329
+ return {
330
+ ok: false,
331
+ retryable: false,
332
+ code: "channel_audio_download_unavailable",
333
+ message: "Telegram audio message is missing file_id.",
334
+ };
335
+ }
336
+
337
+ let fileResponse: Response;
338
+
339
+ try {
340
+ const url = new URL(`/bot${token}/getFile`, telegramApiBaseUrl(input.secrets));
341
+ url.searchParams.set("file_id", audio.providerMediaId);
342
+ fileResponse = await fetcher(url.href, {
343
+ method: "GET",
344
+ signal: AbortSignal.timeout(CHANNEL_DOWNLOAD_TIMEOUT_MS),
345
+ });
346
+ } catch (error) {
347
+ return {
348
+ ok: false,
349
+ retryable: isRetryableReadRequestError(error),
350
+ code: "channel_provider_unavailable",
351
+ message: `Telegram getFile request failed before a response: ${redactToken(
352
+ error instanceof Error ? error.message : String(error),
353
+ token,
354
+ )}`,
355
+ };
356
+ }
357
+
358
+ const filePayload = await fileResponse.json().catch(() => null);
359
+
360
+ if (!fileResponse.ok || !isRecord(filePayload) || filePayload.ok !== true || !isRecord(filePayload.result)) {
361
+ const description =
362
+ isRecord(filePayload) && typeof filePayload.description === "string"
363
+ ? filePayload.description
364
+ : `Telegram getFile returned HTTP ${fileResponse.status}.`;
365
+
366
+ return {
367
+ ok: false,
368
+ retryable: fileResponse.status === 429 || fileResponse.status >= 500,
369
+ code: fileResponse.status === 429 || fileResponse.status >= 500
370
+ ? "channel_provider_unavailable"
371
+ : "channel_media_download_failed",
372
+ message: redactToken(description, token),
373
+ providerMetadata: {
374
+ status: fileResponse.status,
375
+ },
376
+ };
377
+ }
378
+
379
+ const filePath = readString(filePayload.result, "file_path");
380
+
381
+ if (!filePath) {
382
+ return {
383
+ ok: false,
384
+ retryable: false,
385
+ code: "channel_media_download_failed",
386
+ message: "Telegram getFile did not return result.file_path.",
387
+ providerMetadata: {
388
+ status: fileResponse.status,
389
+ },
390
+ };
391
+ }
392
+
393
+ const maxDownloadBytes = Math.min(input.maxBytes ?? TELEGRAM_MAX_DOWNLOAD_BYTES, TELEGRAM_MAX_DOWNLOAD_BYTES);
394
+ const fileSize = readNumber(filePayload.result, "file_size") ?? audio.sizeBytes;
395
+
396
+ if (fileSize !== undefined && fileSize > maxDownloadBytes) {
397
+ return {
398
+ ok: false,
399
+ retryable: false,
400
+ code: "channel_audio_too_large",
401
+ message: `Telegram audio is ${fileSize} bytes, which exceeds the configured ${maxDownloadBytes} byte limit.`,
402
+ providerMetadata: {
403
+ sizeBytes: fileSize,
404
+ },
405
+ };
406
+ }
407
+
408
+ let mediaResponse: Response;
409
+
410
+ try {
411
+ mediaResponse = await fetcher(`${telegramApiBaseUrl(input.secrets)}/file/bot${token}/${filePath}`, {
412
+ method: "GET",
413
+ signal: AbortSignal.timeout(CHANNEL_DOWNLOAD_TIMEOUT_MS),
414
+ });
415
+ } catch (error) {
416
+ return {
417
+ ok: false,
418
+ retryable: isRetryableReadRequestError(error),
419
+ code: "channel_provider_unavailable",
420
+ message: `Telegram file download failed before a response: ${redactToken(
421
+ error instanceof Error ? error.message : String(error),
422
+ token,
423
+ )}`,
424
+ };
425
+ }
426
+
427
+ if (!mediaResponse.ok) {
428
+ return {
429
+ ok: false,
430
+ retryable: mediaResponse.status === 429 || mediaResponse.status >= 500,
431
+ code: mediaResponse.status === 429 || mediaResponse.status >= 500
432
+ ? "channel_provider_unavailable"
433
+ : "channel_media_download_failed",
434
+ message: `Telegram file download returned HTTP ${mediaResponse.status}.`,
435
+ providerMetadata: {
436
+ status: mediaResponse.status,
437
+ },
438
+ };
439
+ }
440
+
441
+ const bytes = await readResponseBytesWithLimit(mediaResponse, maxDownloadBytes);
442
+
443
+ if (!bytes.ok) {
444
+ return bytes;
445
+ }
446
+
447
+ return {
448
+ ok: true,
449
+ audio: bytes.audio,
450
+ filename: audio.filename ?? basename(filePath) ?? `${audio.providerMediaId}.ogg`,
451
+ ...(audio.mimeType ? { mimeType: audio.mimeType } : {}),
452
+ ...(audio.durationSeconds !== undefined ? { durationSeconds: audio.durationSeconds } : {}),
453
+ sizeBytes: bytes.audio.byteLength,
454
+ providerMetadata: {
455
+ fileSize: fileSize ?? bytes.audio.byteLength,
456
+ },
457
+ };
458
+ }
459
+
263
460
  function isSyntheticTelegramChatId(chatId: string): boolean {
264
461
  return chatId === "456" || chatId.startsWith("agentkit-synthetic-");
265
462
  }
@@ -282,6 +479,48 @@ function buildTelegramSendMessageRequestInternal(input: ChannelSendInput, token:
282
479
  };
283
480
  }
284
481
 
482
+ function normalizeTelegramAudio(message: Record<string, unknown>) {
483
+ const voice = readRecord(message, "voice");
484
+
485
+ if (Object.keys(voice).length > 0) {
486
+ const fileId = readString(voice, "file_id");
487
+
488
+ if (!fileId) {
489
+ return null;
490
+ }
491
+
492
+ return {
493
+ providerMediaId: fileId,
494
+ kind: "voice" as const,
495
+ mimeType: readString(voice, "mime_type") ?? "audio/ogg",
496
+ filename: `${fileId}.ogg`,
497
+ ...(readNumber(voice, "duration") !== undefined ? { durationSeconds: readNumber(voice, "duration") } : {}),
498
+ ...(readNumber(voice, "file_size") !== undefined ? { sizeBytes: readNumber(voice, "file_size") } : {}),
499
+ };
500
+ }
501
+
502
+ const audio = readRecord(message, "audio");
503
+
504
+ if (Object.keys(audio).length > 0) {
505
+ const fileId = readString(audio, "file_id");
506
+
507
+ if (!fileId) {
508
+ return null;
509
+ }
510
+
511
+ return {
512
+ providerMediaId: fileId,
513
+ kind: "audio" as const,
514
+ ...(readString(audio, "mime_type") ? { mimeType: readString(audio, "mime_type") } : {}),
515
+ filename: readString(audio, "file_name") ?? `${fileId}.mp3`,
516
+ ...(readNumber(audio, "duration") !== undefined ? { durationSeconds: readNumber(audio, "duration") } : {}),
517
+ ...(readNumber(audio, "file_size") !== undefined ? { sizeBytes: readNumber(audio, "file_size") } : {}),
518
+ };
519
+ }
520
+
521
+ return null;
522
+ }
523
+
285
524
  export function parseTelegramWebhookInfo(input: unknown, checkedAt: string): ChannelStatusResult {
286
525
  if (!isRecord(input) || input.ok !== true || !isRecord(input.result)) {
287
526
  return {
@@ -347,6 +586,131 @@ function readNumberishString(value: Record<string, unknown>, key: string): strin
347
586
  return undefined;
348
587
  }
349
588
 
589
+ function telegramApiBaseUrl(env: Record<string, string | undefined>): string {
590
+ return env.AGENTKIT_TELEGRAM_API_BASE_URL ?? "https://api.telegram.org";
591
+ }
592
+
593
+ function basename(path: string): string | null {
594
+ const name = path.split("/").filter(Boolean).at(-1);
595
+ return name && name.length > 0 ? name : null;
596
+ }
597
+
598
+ async function readResponseBytesWithLimit(
599
+ response: Response,
600
+ maxBytes: number,
601
+ ): Promise<
602
+ | {
603
+ ok: true;
604
+ audio: Uint8Array;
605
+ }
606
+ | {
607
+ ok: false;
608
+ retryable: boolean;
609
+ code: "channel_audio_too_large" | "channel_provider_unavailable";
610
+ message: string;
611
+ providerMetadata?: Record<string, unknown>;
612
+ }
613
+ > {
614
+ const contentLength = response.headers.get("content-length");
615
+ const declaredBytes = contentLength ? Number(contentLength) : undefined;
616
+
617
+ if (declaredBytes !== undefined && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
618
+ return {
619
+ ok: false,
620
+ retryable: false,
621
+ code: "channel_audio_too_large",
622
+ message: `Audio response declares ${declaredBytes} bytes, which exceeds the configured ${maxBytes} byte limit.`,
623
+ providerMetadata: {
624
+ sizeBytes: declaredBytes,
625
+ },
626
+ };
627
+ }
628
+
629
+ if (!response.body) {
630
+ let audio: Uint8Array;
631
+
632
+ try {
633
+ audio = new Uint8Array(await response.arrayBuffer());
634
+ } catch (error) {
635
+ return {
636
+ ok: false,
637
+ retryable: true,
638
+ code: "channel_provider_unavailable",
639
+ message: `Media download response could not be read: ${error instanceof Error ? error.message : String(error)}`,
640
+ };
641
+ }
642
+
643
+ if (audio.byteLength > maxBytes) {
644
+ return {
645
+ ok: false,
646
+ retryable: false,
647
+ code: "channel_audio_too_large",
648
+ message: `Audio response is ${audio.byteLength} bytes, which exceeds the configured ${maxBytes} byte limit.`,
649
+ providerMetadata: {
650
+ sizeBytes: audio.byteLength,
651
+ },
652
+ };
653
+ }
654
+
655
+ return { ok: true, audio };
656
+ }
657
+
658
+ const reader = response.body.getReader();
659
+ const chunks: Uint8Array[] = [];
660
+ let totalBytes = 0;
661
+
662
+ try {
663
+ while (true) {
664
+ const chunk = await reader.read();
665
+
666
+ if (chunk.done) {
667
+ break;
668
+ }
669
+
670
+ totalBytes += chunk.value.byteLength;
671
+
672
+ if (totalBytes > maxBytes) {
673
+ await reader.cancel().catch(() => undefined);
674
+ return {
675
+ ok: false,
676
+ retryable: false,
677
+ code: "channel_audio_too_large",
678
+ message: `Audio response exceeded the configured ${maxBytes} byte limit while downloading.`,
679
+ providerMetadata: {
680
+ sizeBytes: totalBytes,
681
+ },
682
+ };
683
+ }
684
+
685
+ chunks.push(chunk.value);
686
+ }
687
+ } catch (error) {
688
+ await reader.cancel().catch(() => undefined);
689
+ return {
690
+ ok: false,
691
+ retryable: true,
692
+ code: "channel_provider_unavailable",
693
+ message: `Media download response could not be read: ${error instanceof Error ? error.message : String(error)}`,
694
+ };
695
+ } finally {
696
+ reader.releaseLock();
697
+ }
698
+
699
+ const audio = new Uint8Array(totalBytes);
700
+ let offset = 0;
701
+
702
+ for (const chunk of chunks) {
703
+ audio.set(chunk, offset);
704
+ offset += chunk.byteLength;
705
+ }
706
+
707
+ return { ok: true, audio };
708
+ }
709
+
710
+ function redactToken(value: string, token: string): string {
711
+ return token ? value.replaceAll(token, "<redacted>") : value;
712
+ }
713
+
350
714
  function redactId(value: string): string {
351
715
  if (value.length <= 2) {
352
716
  return "*".repeat(value.length);