@andreprado/agentkit 0.1.0-alpha.15 → 0.1.0-alpha.17

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 (41) hide show
  1. package/README.md +8 -0
  2. package/docs/guides/add-channel.md +65 -0
  3. package/docs/guides/add-managed-composio.md +137 -0
  4. package/docs/guides/channel-security.md +32 -0
  5. package/docs/guides/channels-production-handoff.md +1 -1
  6. package/docs/guides/connect-telegram.md +60 -0
  7. package/docs/guides/connect-whatsapp-zapster.md +65 -0
  8. package/docs/guides/prepare-deploy.md +3 -1
  9. package/docs/llms-full.txt +13 -1
  10. package/docs/llms.txt +7 -0
  11. package/package.json +1 -1
  12. package/src/cli/commands/channels.ts +121 -7
  13. package/src/cli/commands/transcribe.ts +171 -0
  14. package/src/cli/deploy-readiness.ts +59 -3
  15. package/src/cli/help.ts +15 -0
  16. package/src/cli/index.ts +130 -0
  17. package/src/create-project.ts +4 -32
  18. package/src/index.ts +231 -0
  19. package/src/runtime/channel-test-harness.ts +2 -0
  20. package/src/runtime/channels/telegram.ts +326 -10
  21. package/src/runtime/channels/whatsapp-zapster.ts +319 -0
  22. package/src/runtime/channels.ts +47 -1
  23. package/src/runtime/chat.ts +2 -1
  24. package/src/runtime/config.ts +214 -4
  25. package/src/runtime/core/manifest.ts +61 -2
  26. package/src/runtime/deploy-readiness.ts +31 -1
  27. package/src/runtime/dev-server.ts +72 -4
  28. package/src/runtime/inspect.ts +142 -4
  29. package/src/runtime/integrations/composio.ts +257 -0
  30. package/src/runtime/skills.ts +95 -0
  31. package/src/runtime/targets/cloudflare/build.ts +162 -5
  32. package/src/runtime/tool-runner.ts +2 -1
  33. package/src/runtime/transcription.ts +483 -0
  34. package/src/templates/skills/agentkit-capsule/SKILL.md +1 -0
  35. package/src/templates/skills/agentkit-channels/SKILL.md +36 -1
  36. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +16 -1
  37. package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
  38. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +29 -0
  39. package/src/templates/skills/agentkit-deploy/SKILL.md +1 -1
  40. package/src/templates/skills/agentkit-integrations/SKILL.md +66 -0
  41. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +3 -2
@@ -1,5 +1,7 @@
1
1
  import type {
2
2
  ChannelAdapter,
3
+ ChannelMediaDownloadInput,
4
+ ChannelMediaDownloadResult,
3
5
  ChannelSendInput,
4
6
  ChannelSendResult,
5
7
  RawWebhookEvent,
@@ -105,6 +107,22 @@ export const zapsterWhatsappChannelAdapter: ChannelAdapter = {
105
107
  },
106
108
  };
107
109
 
110
+ if (messageType === "audio" && message.audio) {
111
+ return [
112
+ {
113
+ ...base,
114
+ kind: "message",
115
+ message: {
116
+ role: "user",
117
+ contentType: "audio",
118
+ content: "",
119
+ providerMessageId: message.id,
120
+ audio: message.audio,
121
+ },
122
+ },
123
+ ];
124
+ }
125
+
108
126
  if (messageType !== "text") {
109
127
  return [
110
128
  {
@@ -140,6 +158,9 @@ export const zapsterWhatsappChannelAdapter: ChannelAdapter = {
140
158
  },
141
159
  ];
142
160
  },
161
+ async downloadMedia(input) {
162
+ return downloadZapsterMedia(input);
163
+ },
143
164
  async sendMessage(input) {
144
165
  return sendZapsterMessage(input);
145
166
  },
@@ -169,6 +190,15 @@ type NormalizedZapsterWebhookMessage = {
169
190
  contactId?: string;
170
191
  type?: string;
171
192
  text?: string;
193
+ audio?: {
194
+ providerMediaId: string;
195
+ kind: "voice" | "audio";
196
+ mimeType?: string;
197
+ filename?: string;
198
+ durationSeconds?: number;
199
+ sizeBytes?: number;
200
+ downloadUrl?: string;
201
+ };
172
202
  };
173
203
 
174
204
  function normalizeZapsterOfficialMessage(body: Record<string, unknown>): NormalizedZapsterWebhookMessage | null {
@@ -185,17 +215,48 @@ function normalizeZapsterOfficialMessage(body: Record<string, unknown>): Normali
185
215
  contactId: readString(sender, "id"),
186
216
  type: readString(data, "type"),
187
217
  text: readString(content, "text"),
218
+ audio: normalizeZapsterAudio(content),
188
219
  };
189
220
  }
190
221
 
191
222
  function normalizeZapsterLegacyMessage(body: Record<string, unknown>): NormalizedZapsterWebhookMessage {
192
223
  const message = readRecord(body, "message");
224
+ const audio = readRecord(message, "audio");
193
225
 
194
226
  return {
195
227
  id: readString(message, "id"),
196
228
  contactId: readString(message, "from"),
197
229
  type: readString(message, "type"),
198
230
  text: readString(readRecord(message, "text"), "body"),
231
+ audio: normalizeZapsterAudio(audio),
232
+ };
233
+ }
234
+
235
+ function normalizeZapsterAudio(value: Record<string, unknown>): NormalizedZapsterWebhookMessage["audio"] {
236
+ const mediaId = readString(value, "id") ?? readString(value, "media_id") ?? readString(value, "mediaId");
237
+ const downloadUrl =
238
+ readString(value, "url") ??
239
+ readString(value, "download_url") ??
240
+ readString(value, "downloadUrl") ??
241
+ readString(value, "media_url") ??
242
+ readString(value, "mediaUrl");
243
+
244
+ if (!mediaId && !downloadUrl) {
245
+ return undefined;
246
+ }
247
+
248
+ return {
249
+ providerMediaId: mediaId ?? downloadUrl ?? "unknown",
250
+ kind: readString(value, "ptt") === "true" ? "voice" : "audio",
251
+ ...(readString(value, "mime_type") ?? readString(value, "mimeType")
252
+ ? { mimeType: readString(value, "mime_type") ?? readString(value, "mimeType") }
253
+ : {}),
254
+ filename: readString(value, "filename") ?? readString(value, "file_name") ?? defaultZapsterAudioFilename(value),
255
+ ...(readNumber(value, "duration") !== undefined ? { durationSeconds: readNumber(value, "duration") } : {}),
256
+ ...(readNumber(value, "file_size") ?? readNumber(value, "size")
257
+ ? { sizeBytes: readNumber(value, "file_size") ?? readNumber(value, "size") }
258
+ : {}),
259
+ ...(downloadUrl ? { downloadUrl } : {}),
199
260
  };
200
261
  }
201
262
 
@@ -249,6 +310,8 @@ export type ZapsterSendMessageRequest = {
249
310
  type ZapsterFetch = (url: string, init: RequestInit) => Promise<Response>;
250
311
 
251
312
  const ZAPSTER_SEND_MESSAGE_URL = "https://api.zapsterapi.com/v1/wa/messages";
313
+ const ZAPSTER_DEFAULT_MEDIA_HOSTS = ["api.zapsterapi.com"];
314
+ const ZAPSTER_MAX_DOWNLOAD_BYTES = 25_000_000;
252
315
 
253
316
  export function buildZapsterSendMessageRequest(input: ChannelSendInput): ZapsterSendMessageRequest {
254
317
  return buildZapsterSendMessageRequestInternal(input, "<redacted>");
@@ -381,6 +444,239 @@ export async function sendZapsterMessage(
381
444
  };
382
445
  }
383
446
 
447
+ export async function downloadZapsterMedia(
448
+ input: ChannelMediaDownloadInput,
449
+ fetcher: ZapsterFetch = fetch,
450
+ ): Promise<ChannelMediaDownloadResult> {
451
+ const apiKey = input.secrets.ZAPSTER_API_KEY;
452
+ const audio = input.message.audio;
453
+
454
+ if (!apiKey) {
455
+ return {
456
+ ok: false,
457
+ retryable: false,
458
+ code: "channel_secret_missing",
459
+ message: "Secret ZAPSTER_API_KEY is missing.",
460
+ };
461
+ }
462
+
463
+ if (!audio?.downloadUrl) {
464
+ return {
465
+ ok: false,
466
+ retryable: false,
467
+ code: "channel_audio_download_unavailable",
468
+ message: "Zapster audio payload did not include a media download URL.",
469
+ };
470
+ }
471
+
472
+ const mediaUrl = validateZapsterMediaDownloadUrl(audio.downloadUrl);
473
+
474
+ if (!mediaUrl.ok) {
475
+ return mediaUrl;
476
+ }
477
+
478
+ let response: Response;
479
+
480
+ try {
481
+ response = await fetcher(mediaUrl.url, {
482
+ method: "GET",
483
+ headers: {
484
+ Authorization: `Bearer ${apiKey}`,
485
+ },
486
+ });
487
+ } catch (error) {
488
+ return {
489
+ ok: false,
490
+ retryable: true,
491
+ code: "channel_provider_unavailable",
492
+ message: `Zapster media download failed before a response: ${redactString(
493
+ error instanceof Error ? error.message : String(error),
494
+ [apiKey, mediaUrl.url],
495
+ )}`,
496
+ };
497
+ }
498
+
499
+ if (!response.ok) {
500
+ return {
501
+ ok: false,
502
+ retryable: response.status === 429 || response.status >= 500,
503
+ code: response.status === 429 || response.status >= 500
504
+ ? "channel_provider_unavailable"
505
+ : "channel_media_download_failed",
506
+ message: `Zapster media download returned HTTP ${response.status}.`,
507
+ providerMetadata: {
508
+ status: response.status,
509
+ },
510
+ };
511
+ }
512
+
513
+ const maxDownloadBytes = input.maxBytes ?? ZAPSTER_MAX_DOWNLOAD_BYTES;
514
+ const bytes = await readResponseBytesWithLimit(response, maxDownloadBytes);
515
+
516
+ if (!bytes.ok) {
517
+ return bytes;
518
+ }
519
+
520
+ return {
521
+ ok: true,
522
+ audio: bytes.audio,
523
+ filename: audio.filename ?? `${audio.providerMediaId}.ogg`,
524
+ ...(audio.mimeType ? { mimeType: audio.mimeType } : {}),
525
+ ...(audio.durationSeconds !== undefined ? { durationSeconds: audio.durationSeconds } : {}),
526
+ sizeBytes: bytes.audio.byteLength,
527
+ providerMetadata: {
528
+ status: response.status,
529
+ },
530
+ };
531
+ }
532
+
533
+ function validateZapsterMediaDownloadUrl(rawUrl: string):
534
+ | {
535
+ ok: true;
536
+ url: string;
537
+ }
538
+ | {
539
+ ok: false;
540
+ retryable: false;
541
+ code: "channel_audio_download_unavailable";
542
+ message: string;
543
+ } {
544
+ let url: URL;
545
+
546
+ try {
547
+ url = new URL(rawUrl);
548
+ } catch {
549
+ return {
550
+ ok: false,
551
+ retryable: false,
552
+ code: "channel_audio_download_unavailable",
553
+ message: "Zapster audio download URL is not a valid URL.",
554
+ };
555
+ }
556
+
557
+ if (url.protocol !== "https:") {
558
+ return {
559
+ ok: false,
560
+ retryable: false,
561
+ code: "channel_audio_download_unavailable",
562
+ message: "Zapster audio download URL must use HTTPS.",
563
+ };
564
+ }
565
+
566
+ const hostname = url.hostname.toLowerCase();
567
+ const allowed = ZAPSTER_DEFAULT_MEDIA_HOSTS.some((host) => hostname === host || hostname.endsWith(`.${host}`));
568
+
569
+ if (!allowed) {
570
+ return {
571
+ ok: false,
572
+ retryable: false,
573
+ code: "channel_audio_download_unavailable",
574
+ message: "Zapster audio download URL host is not allowed.",
575
+ };
576
+ }
577
+
578
+ url.username = "";
579
+ url.password = "";
580
+
581
+ return {
582
+ ok: true,
583
+ url: url.href,
584
+ };
585
+ }
586
+
587
+ async function readResponseBytesWithLimit(
588
+ response: Response,
589
+ maxBytes: number,
590
+ ): Promise<
591
+ | {
592
+ ok: true;
593
+ audio: Uint8Array;
594
+ }
595
+ | {
596
+ ok: false;
597
+ retryable: false;
598
+ code: "channel_audio_too_large";
599
+ message: string;
600
+ providerMetadata?: Record<string, unknown>;
601
+ }
602
+ > {
603
+ const contentLength = response.headers.get("content-length");
604
+ const declaredBytes = contentLength ? Number(contentLength) : undefined;
605
+
606
+ if (declaredBytes !== undefined && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
607
+ return {
608
+ ok: false,
609
+ retryable: false,
610
+ code: "channel_audio_too_large",
611
+ message: `Audio response declares ${declaredBytes} bytes, which exceeds the configured ${maxBytes} byte limit.`,
612
+ providerMetadata: {
613
+ sizeBytes: declaredBytes,
614
+ },
615
+ };
616
+ }
617
+
618
+ if (!response.body) {
619
+ const audio = new Uint8Array(await response.arrayBuffer());
620
+
621
+ if (audio.byteLength > maxBytes) {
622
+ return {
623
+ ok: false,
624
+ retryable: false,
625
+ code: "channel_audio_too_large",
626
+ message: `Audio response is ${audio.byteLength} bytes, which exceeds the configured ${maxBytes} byte limit.`,
627
+ providerMetadata: {
628
+ sizeBytes: audio.byteLength,
629
+ },
630
+ };
631
+ }
632
+
633
+ return { ok: true, audio };
634
+ }
635
+
636
+ const reader = response.body.getReader();
637
+ const chunks: Uint8Array[] = [];
638
+ let totalBytes = 0;
639
+
640
+ try {
641
+ while (true) {
642
+ const chunk = await reader.read();
643
+
644
+ if (chunk.done) {
645
+ break;
646
+ }
647
+
648
+ totalBytes += chunk.value.byteLength;
649
+
650
+ if (totalBytes > maxBytes) {
651
+ await reader.cancel().catch(() => undefined);
652
+ return {
653
+ ok: false,
654
+ retryable: false,
655
+ code: "channel_audio_too_large",
656
+ message: `Audio response exceeded the configured ${maxBytes} byte limit while downloading.`,
657
+ providerMetadata: {
658
+ sizeBytes: totalBytes,
659
+ },
660
+ };
661
+ }
662
+
663
+ chunks.push(chunk.value);
664
+ }
665
+ } finally {
666
+ reader.releaseLock();
667
+ }
668
+
669
+ const audio = new Uint8Array(totalBytes);
670
+ let offset = 0;
671
+
672
+ for (const chunk of chunks) {
673
+ audio.set(chunk, offset);
674
+ offset += chunk.byteLength;
675
+ }
676
+
677
+ return { ok: true, audio };
678
+ }
679
+
384
680
  function buildZapsterSendMessageRequestInternal(
385
681
  input: ChannelSendInput,
386
682
  instanceId: string,
@@ -494,6 +790,29 @@ function readString(value: Record<string, unknown>, key: string): string | undef
494
790
  return typeof field === "string" && field.length > 0 ? field : undefined;
495
791
  }
496
792
 
793
+ function readNumber(value: Record<string, unknown>, key: string): number | undefined {
794
+ const field = value[key];
795
+ return typeof field === "number" && Number.isFinite(field) ? field : undefined;
796
+ }
797
+
798
+ function defaultZapsterAudioFilename(value: Record<string, unknown>): string {
799
+ const mimeType = readString(value, "mime_type") ?? readString(value, "mimeType");
800
+
801
+ if (mimeType?.includes("ogg")) {
802
+ return "audio.ogg";
803
+ }
804
+
805
+ if (mimeType?.includes("mpeg") || mimeType?.includes("mp3")) {
806
+ return "audio.mp3";
807
+ }
808
+
809
+ if (mimeType?.includes("wav")) {
810
+ return "audio.wav";
811
+ }
812
+
813
+ return "audio.ogg";
814
+ }
815
+
497
816
  function isPositiveIntegerHeader(value: string | null): value is string {
498
817
  return typeof value === "string" && /^[1-9]\d*$/.test(value);
499
818
  }
@@ -1,13 +1,17 @@
1
1
  import type { AgentChannel, ChannelProvider, ChannelType } from "../index";
2
2
 
3
3
  export type ChannelMessageRole = "user" | "assistant";
4
- export type ChannelMessageContentType = "text";
4
+ export type ChannelMessageContentType = "text" | "audio";
5
5
  export type ChannelEventKind = "message" | "delivery_status" | "unsupported";
6
6
  export type ChannelDeliveryDirection = "inbound" | "outbound";
7
7
  export type ChannelDeliveryState =
8
8
  | "webhook_received"
9
9
  | "received"
10
10
  | "validated"
11
+ | "audio_received"
12
+ | "audio_downloaded"
13
+ | "transcribing"
14
+ | "transcribed"
11
15
  | "duplicate"
12
16
  | "buffered"
13
17
  | "queued"
@@ -45,11 +49,22 @@ export type ChannelExternalIdentity = {
45
49
  redactedDisplay?: string;
46
50
  };
47
51
 
52
+ export type ChannelAudioAttachment = {
53
+ providerMediaId: string;
54
+ kind: "voice" | "audio";
55
+ mimeType?: string;
56
+ filename?: string;
57
+ durationSeconds?: number;
58
+ sizeBytes?: number;
59
+ downloadUrl?: string;
60
+ };
61
+
48
62
  export type NormalizedChannelMessage = {
49
63
  role: ChannelMessageRole;
50
64
  contentType: ChannelMessageContentType;
51
65
  content: string;
52
66
  providerMessageId?: string;
67
+ audio?: ChannelAudioAttachment;
53
68
  };
54
69
 
55
70
  export type NormalizedChannelEvent = ChannelAdapterId & {
@@ -87,6 +102,36 @@ export type ChannelSendInput = ChannelAdapterId & {
87
102
  secrets: Record<string, string>;
88
103
  };
89
104
 
105
+ export type ChannelMediaDownloadInput = ChannelAdapterId & {
106
+ channelId: string;
107
+ message: NormalizedChannelMessage;
108
+ secrets: Record<string, string>;
109
+ maxBytes?: number;
110
+ };
111
+
112
+ export type ChannelMediaDownloadResult =
113
+ | {
114
+ ok: true;
115
+ audio: Uint8Array;
116
+ filename: string;
117
+ mimeType?: string;
118
+ durationSeconds?: number;
119
+ sizeBytes?: number;
120
+ providerMetadata?: Record<string, unknown>;
121
+ }
122
+ | {
123
+ ok: false;
124
+ retryable: boolean;
125
+ code:
126
+ | "channel_secret_missing"
127
+ | "channel_audio_download_unavailable"
128
+ | "channel_audio_too_large"
129
+ | "channel_provider_unavailable"
130
+ | "channel_media_download_failed";
131
+ message: string;
132
+ providerMetadata?: Record<string, unknown>;
133
+ };
134
+
90
135
  export type ChannelSendResult =
91
136
  | {
92
137
  ok: true;
@@ -120,6 +165,7 @@ export type ChannelStatusResult = {
120
165
  export type ChannelAdapter = ChannelAdapterId & {
121
166
  requiredSecrets: string[];
122
167
  normalizeInbound(input: RawWebhookEvent): Promise<NormalizedChannelEvent[]> | NormalizedChannelEvent[];
168
+ downloadMedia?(input: ChannelMediaDownloadInput): Promise<ChannelMediaDownloadResult>;
123
169
  sendMessage(input: ChannelSendInput): Promise<ChannelSendResult>;
124
170
  verifyWebhook?(input: WebhookVerificationInput): Promise<WebhookVerificationResult> | WebhookVerificationResult;
125
171
  getStatus?(input: ChannelStatusInput): Promise<ChannelStatusResult> | ChannelStatusResult;
@@ -7,6 +7,7 @@ import { loadAgentCapsule } from "./config";
7
7
  import { openPreparedCapsuleStore } from "./database";
8
8
  import { loadCapsuleEnv } from "./env";
9
9
  import { AgentKitError, isAgentKitError } from "./errors";
10
+ import { createManagedComposioTools } from "./integrations/composio";
10
11
  import { syncConfiguredKnowledgeSources } from "./knowledge/ingest";
11
12
  import { appendKnowledgePromptPolicy } from "./knowledge/prompt-policy";
12
13
  import { createKnowledgeSearchTool } from "./knowledge/tool";
@@ -145,7 +146,7 @@ export async function runAgentMessage(
145
146
  }
146
147
 
147
148
  function withKnowledgeTools(capsule: LoadedAgentCapsule): NonNullable<LoadedAgentCapsule["config"]["tools"]> {
148
- const tools = [...(capsule.config.tools ?? [])];
149
+ const tools = [...createManagedComposioTools(capsule.config), ...(capsule.config.tools ?? [])];
149
150
  const knowledgeTool = createKnowledgeSearchTool(capsule);
150
151
 
151
152
  if (knowledgeTool) {