@andreprado/agentkit 0.1.0-alpha.9 → 0.1.0

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 (117) 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 +896 -0
  47. package/src/runtime/channels/generic-webhook.ts +974 -0
  48. package/src/runtime/channels/slack.ts +646 -0
  49. package/src/runtime/channels/telegram.ts +343 -12
  50. package/src/runtime/channels/whatsapp-evolution.ts +1357 -0
  51. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  52. package/src/runtime/channels/whatsapp-uazapi.ts +1323 -0
  53. package/src/runtime/channels/whatsapp-zapster.ts +674 -40
  54. package/src/runtime/channels.ts +83 -3
  55. package/src/runtime/chat.ts +70 -44
  56. package/src/runtime/config.ts +489 -20
  57. package/src/runtime/core/manifest.ts +75 -5
  58. package/src/runtime/core/targets.ts +5 -5
  59. package/src/runtime/deploy-readiness.ts +34 -4
  60. package/src/runtime/dev-server.ts +639 -39
  61. package/src/runtime/env.ts +8 -3
  62. package/src/runtime/evals.ts +445 -74
  63. package/src/runtime/improve.ts +868 -0
  64. package/src/runtime/inspect.ts +173 -4
  65. package/src/runtime/integrations/composio.ts +425 -0
  66. package/src/runtime/knowledge/retrieve.ts +25 -5
  67. package/src/runtime/knowledge/schema.ts +45 -1
  68. package/src/runtime/prompt-context.ts +141 -0
  69. package/src/runtime/runtime-contract.ts +71 -7
  70. package/src/runtime/skills.ts +95 -0
  71. package/src/runtime/targets/cloudflare/build.ts +1010 -208
  72. package/src/runtime/targets/container/server.ts +1 -1
  73. package/src/runtime/targets/vps/deploy.ts +26 -9
  74. package/src/runtime/tool-runner.ts +9 -1
  75. package/src/runtime/tools.ts +26 -2
  76. package/src/runtime/transcription.ts +483 -0
  77. package/src/storage/sqlite.ts +7 -2
  78. package/src/templates/blank.ts +37 -9
  79. package/src/templates/dentista.ts +40 -14
  80. package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
  81. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  82. package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
  83. package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
  84. package/src/templates/skills/agentkit-channels/SKILL.md +66 -1
  85. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
  86. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +28 -3
  87. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  88. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  89. package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
  90. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
  91. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
  92. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
  93. package/src/templates/skills/agentkit-database/SKILL.md +11 -0
  94. package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
  95. package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
  96. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  97. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  98. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  99. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  100. package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
  101. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  102. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  103. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  104. package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
  105. package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
  106. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  107. package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
  108. package/src/templates/skills/agentkit-security/SKILL.md +5 -2
  109. package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
  110. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
  111. package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
  112. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
  113. package/src/templates/support.ts +42 -12
  114. package/docs/guides/agentkit-skills-architecture.md +0 -471
  115. package/docs/guides/channels-implementation-map.md +0 -243
  116. package/docs/guides/channels-production-handoff.md +0 -101
  117. package/docs/portable-deploy-release-checklist.md +0 -41
@@ -0,0 +1,896 @@
1
+ import type {
2
+ ChannelAdapter,
3
+ ChannelReplyTarget,
4
+ ChannelSendInput,
5
+ ChannelSendResult,
6
+ NormalizedChannelEvent,
7
+ RawWebhookEvent,
8
+ WebhookVerificationResult,
9
+ } from "../channels";
10
+ import { AgentKitError } from "../errors";
11
+
12
+ const DISCORD_INTERACTION_PING = 1;
13
+ const DISCORD_INTERACTION_APPLICATION_COMMAND = 2;
14
+ const DISCORD_API_BASE_URL = "https://discord.com/api/v10";
15
+
16
+ export const discordChannelAdapter: ChannelAdapter = {
17
+ type: "discord",
18
+ provider: "discord",
19
+ requiredSecrets: ["DISCORD_PUBLIC_KEY"],
20
+ async verifyWebhook(input): Promise<WebhookVerificationResult> {
21
+ return verifyDiscordWebhook(input);
22
+ },
23
+ getWebhookResponse(input) {
24
+ if (!isDiscordPingInteraction(input.rawBody)) {
25
+ return null;
26
+ }
27
+
28
+ return {
29
+ body: { type: 1 },
30
+ };
31
+ },
32
+ normalizeInbound(input) {
33
+ const body = parseJsonObject(input);
34
+ const interactionId = readString(body, "id");
35
+ const applicationId = readString(body, "application_id");
36
+ const interactionType = readNumber(body, "type");
37
+ const interactionToken = readString(body, "token");
38
+
39
+ if (interactionType === DISCORD_INTERACTION_PING) {
40
+ return [];
41
+ }
42
+
43
+ if (!interactionId) {
44
+ throw new AgentKitError("channel_payload_invalid", "Discord interaction payload is missing id.");
45
+ }
46
+
47
+ const identity = normalizeDiscordIdentity(body);
48
+ const base = {
49
+ type: "discord" as const,
50
+ provider: "discord" as const,
51
+ providerEventId: interactionId,
52
+ dedupeKey: `discord:${input.channelId}:${interactionId}`,
53
+ externalIdentity: identity,
54
+ receivedAt: input.receivedAt,
55
+ providerMetadata: {
56
+ interactionType,
57
+ ...(readString(body, "guild_id") ? { guildId: readString(body, "guild_id") } : {}),
58
+ ...(readString(body, "channel_id") ? { channelId: readString(body, "channel_id") } : {}),
59
+ },
60
+ };
61
+
62
+ if (interactionType !== DISCORD_INTERACTION_APPLICATION_COMMAND) {
63
+ return [
64
+ {
65
+ ...base,
66
+ kind: "unsupported",
67
+ unsupportedReason: `unsupported_interaction_type:${interactionType ?? "unknown"}`,
68
+ },
69
+ ];
70
+ }
71
+
72
+ if (!applicationId || !interactionToken) {
73
+ throw new AgentKitError(
74
+ "channel_payload_invalid",
75
+ "Discord application command payload is missing application_id or token.",
76
+ );
77
+ }
78
+
79
+ const data = readRecord(body, "data");
80
+ const commandName = readString(data, "name");
81
+ const content = normalizeDiscordCommandContent(data);
82
+ const replyTarget: ChannelReplyTarget = {
83
+ type: "discord_interaction",
84
+ applicationId,
85
+ interactionId,
86
+ interactionToken,
87
+ };
88
+
89
+ return [
90
+ {
91
+ ...base,
92
+ kind: "message",
93
+ replyTarget,
94
+ providerMetadata: {
95
+ ...base.providerMetadata,
96
+ ...(commandName ? { commandName } : {}),
97
+ },
98
+ message: {
99
+ role: "user",
100
+ contentType: "text",
101
+ content,
102
+ providerMessageId: interactionId,
103
+ },
104
+ },
105
+ ];
106
+ },
107
+ async sendMessage(input) {
108
+ return sendDiscordMessage(input);
109
+ },
110
+ getStatus(input) {
111
+ const channelMode = input.channel.type === "discord" && input.channel.mode === "bot" ? "bot" : "interactions";
112
+
113
+ if (channelMode === "bot") {
114
+ if (!input.secrets.DISCORD_BOT_TOKEN) {
115
+ return {
116
+ status: "needs_setup",
117
+ checkedAt: new Date().toISOString(),
118
+ message: "Secret DISCORD_BOT_TOKEN is missing.",
119
+ };
120
+ }
121
+
122
+ return {
123
+ status: "waiting_for_provider_event",
124
+ checkedAt: new Date().toISOString(),
125
+ message: "Invite the Discord bot to a server and enable Message Content Intent.",
126
+ };
127
+ }
128
+
129
+ if (!input.secrets.DISCORD_PUBLIC_KEY) {
130
+ return {
131
+ status: "needs_setup",
132
+ checkedAt: new Date().toISOString(),
133
+ message: "Secret DISCORD_PUBLIC_KEY is missing.",
134
+ };
135
+ }
136
+
137
+ return {
138
+ status: "waiting_for_provider_event",
139
+ checkedAt: new Date().toISOString(),
140
+ message: "Paste the AgentKit webhook URL into the Discord app Interactions Endpoint URL.",
141
+ };
142
+ },
143
+ };
144
+
145
+ export type DiscordFollowupMessageRequest = {
146
+ method: "POST";
147
+ url: string;
148
+ body: {
149
+ content: string;
150
+ allowed_mentions: {
151
+ parse: string[];
152
+ };
153
+ };
154
+ idempotencyKey: string;
155
+ };
156
+
157
+ export type DiscordBotMessageRequest = {
158
+ method: "POST";
159
+ url: string;
160
+ headers: {
161
+ Authorization: string;
162
+ "Content-Type": "application/json";
163
+ };
164
+ body: {
165
+ content: string;
166
+ allowed_mentions: {
167
+ parse: string[];
168
+ };
169
+ message_reference: {
170
+ message_id: string;
171
+ channel_id: string;
172
+ };
173
+ };
174
+ idempotencyKey: string;
175
+ };
176
+
177
+ export type DiscordGatewayMessageCreateInput = {
178
+ channelId: string;
179
+ receivedAt: string;
180
+ payload: Record<string, unknown>;
181
+ botUserId?: string;
182
+ };
183
+
184
+ type DiscordFetch = (url: string, init: RequestInit) => Promise<Response>;
185
+
186
+ export function isDiscordPingInteraction(rawBody: string): boolean {
187
+ try {
188
+ const parsed = JSON.parse(rawBody) as unknown;
189
+ return isRecord(parsed) && parsed.type === DISCORD_INTERACTION_PING;
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+
195
+ export function buildDiscordFollowupMessageRequest(input: ChannelSendInput): DiscordFollowupMessageRequest {
196
+ return buildDiscordFollowupMessageRequestInternal(input, {
197
+ redactToken: true,
198
+ });
199
+ }
200
+
201
+ export function buildDiscordBotMessageRequest(input: ChannelSendInput): DiscordBotMessageRequest {
202
+ return buildDiscordBotMessageRequestInternal(input, {
203
+ redactToken: true,
204
+ });
205
+ }
206
+
207
+ export function normalizeDiscordGatewayMessageCreate(
208
+ input: DiscordGatewayMessageCreateInput,
209
+ ): NormalizedChannelEvent[] {
210
+ const payload = input.payload;
211
+ const messageId = readString(payload, "id");
212
+ const discordChannelId = readString(payload, "channel_id");
213
+ const author = readRecord(payload, "author");
214
+ const authorId = readString(author, "id");
215
+ const content = readGatewayMessageContent(payload);
216
+
217
+ if (!messageId || !discordChannelId) {
218
+ throw new AgentKitError("channel_payload_invalid", "Discord MESSAGE_CREATE payload is missing id or channel_id.");
219
+ }
220
+
221
+ if (readBoolean(author, "bot") || (input.botUserId && authorId === input.botUserId)) {
222
+ return [];
223
+ }
224
+
225
+ const identity = normalizeDiscordMessageIdentity(payload);
226
+ const base = {
227
+ type: "discord" as const,
228
+ provider: "discord" as const,
229
+ providerEventId: messageId,
230
+ dedupeKey: `discord:${input.channelId}:${messageId}`,
231
+ externalIdentity: identity,
232
+ receivedAt: input.receivedAt,
233
+ providerMetadata: {
234
+ gatewayEvent: "MESSAGE_CREATE",
235
+ ...(readString(payload, "guild_id") ? { guildId: readString(payload, "guild_id") } : {}),
236
+ channelId: discordChannelId,
237
+ },
238
+ };
239
+
240
+ if (!content) {
241
+ return [
242
+ {
243
+ ...base,
244
+ kind: "unsupported",
245
+ unsupportedReason: "missing_or_empty_message_content",
246
+ },
247
+ ];
248
+ }
249
+
250
+ return [
251
+ {
252
+ ...base,
253
+ kind: "message",
254
+ replyTarget: {
255
+ type: "discord_message",
256
+ channelId: discordChannelId,
257
+ messageId,
258
+ },
259
+ message: {
260
+ role: "user",
261
+ contentType: "text",
262
+ content,
263
+ providerMessageId: messageId,
264
+ },
265
+ },
266
+ ];
267
+ }
268
+
269
+ export async function sendDiscordMessage(
270
+ input: ChannelSendInput,
271
+ fetcher: DiscordFetch = fetch,
272
+ ): Promise<ChannelSendResult> {
273
+ if (input.replyTarget?.type === "discord_interaction") {
274
+ return sendDiscordInteractionFollowup(input, fetcher);
275
+ }
276
+
277
+ if (input.replyTarget?.type !== "discord_message") {
278
+ return {
279
+ ok: false,
280
+ retryable: false,
281
+ code: "channel_send_failed",
282
+ message: "Discord sendMessage requires a Discord interaction or message reply target.",
283
+ };
284
+ }
285
+
286
+ if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
287
+ try {
288
+ const request = buildDiscordBotMessageRequest(input);
289
+
290
+ return {
291
+ ok: true,
292
+ status: "stubbed",
293
+ providerRequestId: request.idempotencyKey,
294
+ providerMetadata: {
295
+ method: request.method,
296
+ url: request.url,
297
+ headers: request.headers,
298
+ body: request.body,
299
+ },
300
+ };
301
+ } catch (error) {
302
+ return {
303
+ ok: false,
304
+ retryable: false,
305
+ code: "channel_send_failed",
306
+ message: error instanceof Error ? error.message : String(error),
307
+ };
308
+ }
309
+ }
310
+
311
+ let request: DiscordBotMessageRequest;
312
+ let actualRequest: DiscordBotMessageRequest;
313
+
314
+ try {
315
+ request = buildDiscordBotMessageRequest(input);
316
+ actualRequest = buildDiscordBotMessageRequestInternal(input, {
317
+ redactToken: false,
318
+ });
319
+ } catch (error) {
320
+ return {
321
+ ok: false,
322
+ retryable: false,
323
+ code: error instanceof AgentKitError && error.code === "channel_secret_missing"
324
+ ? "channel_secret_missing"
325
+ : "channel_send_failed",
326
+ message: error instanceof Error ? error.message : String(error),
327
+ };
328
+ }
329
+
330
+ let response: Response;
331
+
332
+ try {
333
+ response = await fetcher(actualRequest.url, {
334
+ method: actualRequest.method,
335
+ headers: actualRequest.headers,
336
+ body: JSON.stringify(actualRequest.body),
337
+ });
338
+ } catch (error) {
339
+ return {
340
+ ok: false,
341
+ retryable: true,
342
+ code: "channel_provider_unavailable",
343
+ message: `Discord bot message request failed before a response: ${redactDiscordToken(
344
+ error instanceof Error ? error.message : String(error),
345
+ input.secrets.DISCORD_BOT_TOKEN,
346
+ )}`,
347
+ providerRequestId: request.idempotencyKey,
348
+ providerMetadata: {
349
+ method: request.method,
350
+ url: request.url,
351
+ headers: request.headers,
352
+ body: request.body,
353
+ },
354
+ };
355
+ }
356
+
357
+ const payload = await response.json().catch(() => null);
358
+ const messageId = isRecord(payload) ? readString(payload, "id") : undefined;
359
+
360
+ if (!response.ok) {
361
+ return {
362
+ ok: false,
363
+ retryable: response.status === 429 || response.status >= 500,
364
+ code: "channel_send_failed",
365
+ message: redactDiscordToken(readDiscordErrorMessage(payload, response.status), input.secrets.DISCORD_BOT_TOKEN),
366
+ providerRequestId: request.idempotencyKey,
367
+ providerMetadata: {
368
+ method: request.method,
369
+ url: request.url,
370
+ headers: request.headers,
371
+ status: response.status,
372
+ body: redactDiscordValue(payload, input.secrets.DISCORD_BOT_TOKEN),
373
+ },
374
+ };
375
+ }
376
+
377
+ if (!messageId) {
378
+ return {
379
+ ok: false,
380
+ retryable: false,
381
+ code: "channel_send_failed",
382
+ message: "Discord bot message response did not return id.",
383
+ providerRequestId: request.idempotencyKey,
384
+ providerMetadata: {
385
+ method: request.method,
386
+ url: request.url,
387
+ headers: request.headers,
388
+ status: response.status,
389
+ body: redactDiscordValue(payload, input.secrets.DISCORD_BOT_TOKEN),
390
+ },
391
+ };
392
+ }
393
+
394
+ return {
395
+ ok: true,
396
+ status: "sent",
397
+ providerRequestId: request.idempotencyKey,
398
+ providerMessageId: messageId,
399
+ providerMetadata: {
400
+ method: request.method,
401
+ url: request.url,
402
+ headers: request.headers,
403
+ status: response.status,
404
+ body: redactDiscordValue(payload, input.secrets.DISCORD_BOT_TOKEN),
405
+ },
406
+ };
407
+ }
408
+
409
+ export async function sendDiscordInteractionFollowup(
410
+ input: ChannelSendInput,
411
+ fetcher: DiscordFetch = fetch,
412
+ ): Promise<ChannelSendResult> {
413
+ const replyTarget = input.replyTarget;
414
+
415
+ if (replyTarget?.type !== "discord_interaction") {
416
+ return {
417
+ ok: false,
418
+ retryable: false,
419
+ code: "channel_send_failed",
420
+ message: "Discord follow-up send requires a Discord interaction reply target.",
421
+ };
422
+ }
423
+
424
+ if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
425
+ const request = buildDiscordFollowupMessageRequest(input);
426
+
427
+ return {
428
+ ok: true,
429
+ status: "stubbed",
430
+ providerRequestId: request.idempotencyKey,
431
+ providerMetadata: {
432
+ method: request.method,
433
+ url: request.url,
434
+ body: request.body,
435
+ },
436
+ };
437
+ }
438
+
439
+ let request: DiscordFollowupMessageRequest;
440
+ let actualRequest: DiscordFollowupMessageRequest;
441
+
442
+ try {
443
+ request = buildDiscordFollowupMessageRequest(input);
444
+ actualRequest = buildDiscordFollowupMessageRequestInternal(input, {
445
+ redactToken: false,
446
+ });
447
+ } catch (error) {
448
+ return {
449
+ ok: false,
450
+ retryable: false,
451
+ code: "channel_send_failed",
452
+ message: error instanceof Error ? error.message : String(error),
453
+ };
454
+ }
455
+
456
+ let response: Response;
457
+
458
+ try {
459
+ response = await fetcher(actualRequest.url, {
460
+ method: actualRequest.method,
461
+ headers: {
462
+ "Content-Type": "application/json",
463
+ },
464
+ body: JSON.stringify(actualRequest.body),
465
+ });
466
+ } catch (error) {
467
+ return {
468
+ ok: false,
469
+ retryable: true,
470
+ code: "channel_provider_unavailable",
471
+ message: `Discord follow-up request failed before a response: ${redactDiscordToken(
472
+ error instanceof Error ? error.message : String(error),
473
+ replyTarget.interactionToken,
474
+ )}`,
475
+ providerRequestId: request.idempotencyKey,
476
+ providerMetadata: {
477
+ method: request.method,
478
+ url: request.url,
479
+ body: request.body,
480
+ },
481
+ };
482
+ }
483
+
484
+ const payload = await response.json().catch(() => null);
485
+ const messageId = isRecord(payload) ? readString(payload, "id") : undefined;
486
+
487
+ if (!response.ok) {
488
+ return {
489
+ ok: false,
490
+ retryable: response.status === 429 || response.status >= 500,
491
+ code: "channel_send_failed",
492
+ message: redactDiscordToken(readDiscordErrorMessage(payload, response.status), replyTarget.interactionToken),
493
+ providerRequestId: request.idempotencyKey,
494
+ providerMetadata: {
495
+ method: request.method,
496
+ url: request.url,
497
+ status: response.status,
498
+ body: redactDiscordValue(payload, replyTarget.interactionToken),
499
+ },
500
+ };
501
+ }
502
+
503
+ if (!messageId) {
504
+ return {
505
+ ok: false,
506
+ retryable: false,
507
+ code: "channel_send_failed",
508
+ message: "Discord follow-up response did not return id.",
509
+ providerRequestId: request.idempotencyKey,
510
+ providerMetadata: {
511
+ method: request.method,
512
+ url: request.url,
513
+ status: response.status,
514
+ body: redactDiscordValue(payload, replyTarget.interactionToken),
515
+ },
516
+ };
517
+ }
518
+
519
+ return {
520
+ ok: true,
521
+ status: "sent",
522
+ providerRequestId: request.idempotencyKey,
523
+ providerMessageId: messageId,
524
+ providerMetadata: {
525
+ method: request.method,
526
+ url: request.url,
527
+ status: response.status,
528
+ body: redactDiscordValue(payload, replyTarget.interactionToken),
529
+ },
530
+ };
531
+ }
532
+
533
+ async function verifyDiscordWebhook(input: {
534
+ headers: Headers;
535
+ rawBody: string;
536
+ secrets: Record<string, string>;
537
+ }): Promise<WebhookVerificationResult> {
538
+ const publicKey = input.secrets.DISCORD_PUBLIC_KEY;
539
+
540
+ if (!publicKey) {
541
+ return {
542
+ ok: false,
543
+ code: "channel_secret_missing",
544
+ message: "Secret DISCORD_PUBLIC_KEY is not set for this channel.",
545
+ };
546
+ }
547
+
548
+ const signature = input.headers.get("x-signature-ed25519");
549
+ const timestamp = input.headers.get("x-signature-timestamp");
550
+
551
+ if (!signature || !timestamp) {
552
+ return {
553
+ ok: false,
554
+ code: "channel_signature_invalid",
555
+ message: "Discord signature headers are missing.",
556
+ };
557
+ }
558
+
559
+ const publicKeyBytes = hexToBytes(publicKey);
560
+ const signatureBytes = hexToBytes(signature);
561
+
562
+ if (!publicKeyBytes || publicKeyBytes.byteLength !== 32 || !signatureBytes || signatureBytes.byteLength !== 64) {
563
+ return {
564
+ ok: false,
565
+ code: "channel_signature_invalid",
566
+ message: "Discord public key or signature is not valid hex.",
567
+ };
568
+ }
569
+
570
+ let key: CryptoKey;
571
+
572
+ try {
573
+ key = await crypto.subtle.importKey(
574
+ "raw",
575
+ bytesToArrayBuffer(publicKeyBytes),
576
+ { name: "Ed25519" } as AlgorithmIdentifier,
577
+ false,
578
+ ["verify"],
579
+ );
580
+ } catch {
581
+ return {
582
+ ok: false,
583
+ code: "channel_signature_invalid",
584
+ message: "Discord public key could not be imported for Ed25519 verification.",
585
+ };
586
+ }
587
+
588
+ const signedBody = new TextEncoder().encode(`${timestamp}${input.rawBody}`);
589
+ const ok = await crypto.subtle.verify(
590
+ { name: "Ed25519" } as AlgorithmIdentifier,
591
+ key,
592
+ bytesToArrayBuffer(signatureBytes),
593
+ bytesToArrayBuffer(signedBody),
594
+ );
595
+
596
+ if (!ok) {
597
+ return {
598
+ ok: false,
599
+ code: "channel_signature_invalid",
600
+ message: "Discord request signature did not match.",
601
+ };
602
+ }
603
+
604
+ return { ok: true };
605
+ }
606
+
607
+ function buildDiscordFollowupMessageRequestInternal(
608
+ input: ChannelSendInput,
609
+ options: { redactToken: boolean },
610
+ ): DiscordFollowupMessageRequest {
611
+ const replyTarget = input.replyTarget;
612
+
613
+ if (replyTarget?.type !== "discord_interaction") {
614
+ throw new Error("Discord sendMessage requires a Discord interaction reply target.");
615
+ }
616
+
617
+ const token = options.redactToken ? "<redacted>" : replyTarget.interactionToken;
618
+ const baseUrl = input.secrets.AGENTKIT_DISCORD_API_BASE_URL ?? DISCORD_API_BASE_URL;
619
+ const url = new URL(
620
+ `/api/v10/webhooks/${encodeURIComponent(replyTarget.applicationId)}/${encodeURIComponent(token)}`,
621
+ normalizeDiscordApiBaseUrl(baseUrl),
622
+ );
623
+
624
+ return {
625
+ method: "POST",
626
+ url: url.href,
627
+ body: {
628
+ content: input.message.content,
629
+ allowed_mentions: {
630
+ parse: [],
631
+ },
632
+ },
633
+ idempotencyKey: `discord:${input.channelId}:${input.conversationId}:${replyTarget.interactionId}`,
634
+ };
635
+ }
636
+
637
+ function buildDiscordBotMessageRequestInternal(
638
+ input: ChannelSendInput,
639
+ options: { redactToken: boolean },
640
+ ): DiscordBotMessageRequest {
641
+ const replyTarget = input.replyTarget;
642
+
643
+ if (replyTarget?.type !== "discord_message") {
644
+ throw new Error("Discord bot sendMessage requires a Discord message reply target.");
645
+ }
646
+
647
+ const botToken = input.secrets.DISCORD_BOT_TOKEN;
648
+
649
+ if (!botToken) {
650
+ throw new AgentKitError("channel_secret_missing", "Secret DISCORD_BOT_TOKEN is not set for this channel.");
651
+ }
652
+
653
+ const baseUrl = input.secrets.AGENTKIT_DISCORD_API_BASE_URL ?? DISCORD_API_BASE_URL;
654
+ const url = new URL(
655
+ `/api/v10/channels/${encodeURIComponent(replyTarget.channelId)}/messages`,
656
+ normalizeDiscordApiBaseUrl(baseUrl),
657
+ );
658
+
659
+ return {
660
+ method: "POST",
661
+ url: url.href,
662
+ headers: {
663
+ Authorization: `Bot ${options.redactToken ? "<redacted>" : botToken}`,
664
+ "Content-Type": "application/json",
665
+ },
666
+ body: {
667
+ content: input.message.content,
668
+ allowed_mentions: {
669
+ parse: [],
670
+ },
671
+ message_reference: {
672
+ message_id: replyTarget.messageId,
673
+ channel_id: replyTarget.channelId,
674
+ },
675
+ },
676
+ idempotencyKey: `discord:${input.channelId}:${input.conversationId}:${replyTarget.messageId}`,
677
+ };
678
+ }
679
+
680
+ function normalizeDiscordApiBaseUrl(value: string): string {
681
+ const url = new URL(value);
682
+
683
+ if (url.pathname.endsWith("/api/v10")) {
684
+ url.pathname = url.pathname.slice(0, -"/api/v10".length) || "/";
685
+ }
686
+
687
+ return url.href;
688
+ }
689
+
690
+ function bytesToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
691
+ const copy = new Uint8Array(bytes.byteLength);
692
+ copy.set(bytes);
693
+ return copy.buffer;
694
+ }
695
+
696
+ function normalizeDiscordIdentity(body: Record<string, unknown>) {
697
+ const guildId = readString(body, "guild_id") ?? "dm";
698
+ const channelId = readString(body, "channel_id") ?? "unknown";
699
+ const member = readRecord(body, "member");
700
+ const user = readRecord(member, "user");
701
+ const directUser = readRecord(body, "user");
702
+ const userId = readString(user, "id") ?? readString(directUser, "id") ?? "unknown";
703
+ const display =
704
+ readString(member, "nick") ??
705
+ readString(user, "global_name") ??
706
+ readString(user, "username") ??
707
+ readString(directUser, "global_name") ??
708
+ readString(directUser, "username");
709
+
710
+ return {
711
+ key: `discord:guild:${guildId}:channel:${channelId}:user:${userId}`,
712
+ ...(display ? { display } : {}),
713
+ redactedDisplay: `discord:guild:${redactId(guildId)}:channel:${redactId(channelId)}:user:${redactId(userId)}`,
714
+ };
715
+ }
716
+
717
+ function normalizeDiscordMessageIdentity(body: Record<string, unknown>) {
718
+ const guildId = readString(body, "guild_id") ?? "dm";
719
+ const channelId = readString(body, "channel_id") ?? "unknown";
720
+ const member = readRecord(body, "member");
721
+ const author = readRecord(body, "author");
722
+ const authorId = readString(author, "id") ?? "unknown";
723
+ const display =
724
+ readString(member, "nick") ??
725
+ readString(author, "global_name") ??
726
+ readString(author, "username");
727
+
728
+ return {
729
+ key: `discord:guild:${guildId}:channel:${channelId}:user:${authorId}`,
730
+ ...(display ? { display } : {}),
731
+ redactedDisplay: `discord:guild:${redactId(guildId)}:channel:${redactId(channelId)}:user:${redactId(authorId)}`,
732
+ };
733
+ }
734
+
735
+ function normalizeDiscordCommandContent(data: Record<string, unknown>): string {
736
+ const commandName = readString(data, "name") ?? "command";
737
+ const options = readArray(data, "options");
738
+ const renderedOptions = renderDiscordOptions(options);
739
+
740
+ if (renderedOptions.length === 1 && isPrimaryTextOption(renderedOptions[0]?.name)) {
741
+ return renderedOptions[0]?.value || `/${commandName}`;
742
+ }
743
+
744
+ if (renderedOptions.length === 0) {
745
+ return `/${commandName}`;
746
+ }
747
+
748
+ return [`/${commandName}`, ...renderedOptions.map((option) => `${option.name}: ${option.value}`)].join("\n");
749
+ }
750
+
751
+ function renderDiscordOptions(options: unknown[]): Array<{ name: string; value: string }> {
752
+ const rendered: Array<{ name: string; value: string }> = [];
753
+
754
+ for (const option of options) {
755
+ if (!isRecord(option)) {
756
+ continue;
757
+ }
758
+
759
+ const name = readString(option, "name") ?? "option";
760
+ const nested = readArray(option, "options");
761
+
762
+ if (nested.length > 0) {
763
+ for (const item of renderDiscordOptions(nested)) {
764
+ rendered.push({
765
+ name: `${name}.${item.name}`,
766
+ value: item.value,
767
+ });
768
+ }
769
+ continue;
770
+ }
771
+
772
+ const value = option.value;
773
+
774
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
775
+ rendered.push({
776
+ name,
777
+ value: String(value),
778
+ });
779
+ }
780
+ }
781
+
782
+ return rendered;
783
+ }
784
+
785
+ function isPrimaryTextOption(name: string | undefined): boolean {
786
+ return Boolean(name && ["message", "text", "prompt", "question", "query", "input"].includes(name));
787
+ }
788
+
789
+ function parseJsonObject(input: RawWebhookEvent): Record<string, unknown> {
790
+ try {
791
+ const parsed = JSON.parse(input.rawBody) as unknown;
792
+
793
+ if (isRecord(parsed)) {
794
+ return parsed;
795
+ }
796
+ } catch (error) {
797
+ throw new AgentKitError("channel_payload_invalid", "Discord interaction payload must be valid JSON.", {
798
+ cause: error,
799
+ });
800
+ }
801
+
802
+ throw new AgentKitError("channel_payload_invalid", "Discord interaction payload must be a JSON object.");
803
+ }
804
+
805
+ function readRecord(value: Record<string, unknown>, key: string): Record<string, unknown> {
806
+ const record = value[key];
807
+ return isRecord(record) ? record : {};
808
+ }
809
+
810
+ function readArray(value: Record<string, unknown>, key: string): unknown[] {
811
+ const field = value[key];
812
+ return Array.isArray(field) ? field : [];
813
+ }
814
+
815
+ function readString(value: Record<string, unknown>, key: string): string | undefined {
816
+ const field = value[key];
817
+ return typeof field === "string" && field.length > 0 ? field : undefined;
818
+ }
819
+
820
+ function readNumber(value: Record<string, unknown>, key: string): number | undefined {
821
+ const field = value[key];
822
+ return typeof field === "number" && Number.isFinite(field) ? field : undefined;
823
+ }
824
+
825
+ function readBoolean(value: Record<string, unknown>, key: string): boolean {
826
+ return value[key] === true;
827
+ }
828
+
829
+ function readGatewayMessageContent(value: Record<string, unknown>): string {
830
+ const content = readString(value, "content");
831
+ return content?.trim() ?? "";
832
+ }
833
+
834
+ function readDiscordErrorMessage(payload: unknown, status: number): string {
835
+ if (isRecord(payload)) {
836
+ const message = readString(payload, "message");
837
+
838
+ if (message) {
839
+ return message;
840
+ }
841
+ }
842
+
843
+ return `Discord follow-up request returned HTTP ${status}.`;
844
+ }
845
+
846
+ function redactDiscordToken(value: string, token: string | undefined): string {
847
+ return token ? value.replaceAll(token, "<redacted>") : value;
848
+ }
849
+
850
+ function redactDiscordValue(value: unknown, token: string | undefined): unknown {
851
+ if (typeof value === "string") {
852
+ return redactDiscordToken(value, token);
853
+ }
854
+
855
+ if (Array.isArray(value)) {
856
+ return value.map((item) => redactDiscordValue(item, token));
857
+ }
858
+
859
+ if (isRecord(value)) {
860
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactDiscordValue(item, token)]));
861
+ }
862
+
863
+ return value;
864
+ }
865
+
866
+ function redactId(value: string): string {
867
+ if (value === "unknown" || value === "dm") {
868
+ return value;
869
+ }
870
+
871
+ if (value.length <= 8) {
872
+ return "*".repeat(value.length);
873
+ }
874
+
875
+ return `${value.slice(0, 4)}******${value.slice(-4)}`;
876
+ }
877
+
878
+ function hexToBytes(value: string): Uint8Array | null {
879
+ const normalized = value.trim();
880
+
881
+ if (!/^[a-f0-9]+$/i.test(normalized) || normalized.length % 2 !== 0) {
882
+ return null;
883
+ }
884
+
885
+ const bytes = new Uint8Array(normalized.length / 2);
886
+
887
+ for (let index = 0; index < normalized.length; index += 2) {
888
+ bytes[index / 2] = Number.parseInt(normalized.slice(index, index + 2), 16);
889
+ }
890
+
891
+ return bytes;
892
+ }
893
+
894
+ function isRecord(value: unknown): value is Record<string, unknown> {
895
+ return typeof value === "object" && value !== null && !Array.isArray(value);
896
+ }