@andreprado/agentkit 0.1.0-alpha.8 → 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 +1185 -50
  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 +68 -8
  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 +466 -23
  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 +89 -4
  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 +71 -6
  85. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +9 -2
  86. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +34 -5
  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 +39 -4
  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
@@ -13,25 +13,41 @@ import type {
13
13
  AgentChannel,
14
14
  ChannelProvider,
15
15
  ChannelType,
16
+ DiscordChannelMode,
16
17
  TelegramAllowedUpdate,
17
18
  AgentKnowledgeEmbeddingProvider,
18
19
  ChannelBufferMode,
20
+ AgentIntegration,
21
+ ChannelAudioMode,
22
+ TranscriptionProviderName,
23
+ ChannelReplyRouteConfig,
19
24
  } from "../index";
25
+ import { MANAGED_COMPOSIO_TOOL_NAME } from "./integrations/composio";
20
26
  import { AgentKitError } from "./errors";
27
+ import { isValidTimeZone } from "./prompt-context";
21
28
 
22
29
  const CONFIG_FILE = "agentkit.config.ts";
23
30
  const AGENT_RUNTIMES = new Set<AgentRuntime>(["local", "edge"]);
24
- const PROVIDERS = new Set<AgentProviderName>(["test", "openai", "anthropic", "openrouter", "custom"]);
31
+ const PROVIDERS = new Set<AgentProviderName>(["test", "openai", "openai-codex", "anthropic", "openrouter", "opencode", "opencode-go", "custom"]);
25
32
  const ACCESS_MODES = new Set<AccessMode>(["private", "token", "public"]);
26
33
  const TOOL_VISIBILITIES = new Set<ToolVisibility>(["user", "internal"]);
27
- const CHANNEL_TYPES = new Set<ChannelType>(["website", "telegram", "whatsapp"]);
28
- const CHANNEL_PROVIDERS = new Set<ChannelProvider>(["agentkit", "telegram", "zapster", "meta"]);
34
+ const CHANNEL_TYPES = new Set<ChannelType>(["website", "telegram", "whatsapp", "discord", "slack", "webhook"]);
35
+ const CHANNEL_PROVIDERS = new Set<ChannelProvider>(["agentkit", "telegram", "zapster", "meta", "uazapi", "evolution", "discord", "slack", "generic"]);
29
36
  const CHANNEL_BUFFER_MODES = new Set<ChannelBufferMode>(["off", "debounce"]);
37
+ const CHANNEL_AUDIO_MODES = new Set<ChannelAudioMode>(["off", "reject", "transcribe"]);
38
+ const DISCORD_CHANNEL_MODES = new Set<DiscordChannelMode>(["interactions", "bot"]);
30
39
  const TELEGRAM_ALLOWED_UPDATES = new Set<TelegramAllowedUpdate>(["message"]);
31
40
  const KNOWLEDGE_EMBEDDING_PROVIDERS = new Set<AgentKnowledgeEmbeddingProvider>(["none", "test", "openai"]);
41
+ const TRANSCRIPTION_PROVIDERS = new Set<TranscriptionProviderName>(["test", "openai", "groq"]);
32
42
  const CHANNEL_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
43
+ const CHANNEL_REPLY_PATH_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
44
+ const INTEGRATION_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
45
+ const COMPOSIO_TOOLKIT_PATTERN = /^[a-z][a-z0-9_-]*$/;
46
+ const COMPOSIO_TOOL_SLUG_PATTERN = /^[A-Z][A-Z0-9_]*$/;
33
47
  const SECRET_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
48
+ const HTTP_HEADER_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]*$/;
34
49
  const moduleDir = dirname(fileURLToPath(import.meta.url));
50
+ let configImportNonce = 0;
35
51
 
36
52
  export type LoadedAgentCapsule = {
37
53
  root: string;
@@ -108,15 +124,13 @@ function resolveLocalStoragePath(root: string, config: AgentConfig): string | un
108
124
  }
109
125
 
110
126
  async function loadConfigModule(configPath: string): Promise<unknown> {
111
- const configUrl = pathToFileURL(configPath);
112
- configUrl.searchParams.set("t", String(Date.now()));
127
+ const source = await readFile(configPath, "utf8");
113
128
 
114
129
  try {
115
- const module = (await import(configUrl.href)) as { default?: unknown };
116
- return module.default;
130
+ return await importConfigSource(configPath, source);
117
131
  } catch (error) {
118
132
  if (isMissingAgentKitSelfImport(error)) {
119
- return loadConfigModuleWithLocalSelfImport(configPath, error);
133
+ return loadConfigModuleWithLocalSelfImport(configPath, source, error);
120
134
  }
121
135
 
122
136
  throw new AgentKitError(
@@ -127,8 +141,11 @@ async function loadConfigModule(configPath: string): Promise<unknown> {
127
141
  }
128
142
  }
129
143
 
130
- async function loadConfigModuleWithLocalSelfImport(configPath: string, originalError: unknown): Promise<unknown> {
131
- const source = await readFile(configPath, "utf8");
144
+ async function loadConfigModuleWithLocalSelfImport(
145
+ configPath: string,
146
+ source: string,
147
+ originalError: unknown,
148
+ ): Promise<unknown> {
132
149
  const localAgentKitUrl = pathToFileURL(resolve(moduleDir, "../index.ts")).href;
133
150
  const patchedSource = source.replace(
134
151
  /from\s+["']@andreprado\/agentkit["']/g,
@@ -143,15 +160,20 @@ async function loadConfigModuleWithLocalSelfImport(configPath: string, originalE
143
160
  );
144
161
  }
145
162
 
163
+ return importConfigSource(configPath, patchedSource);
164
+ }
165
+
166
+ async function importConfigSource(configPath: string, source: string): Promise<unknown> {
167
+ const nonce = nextConfigImportNonce();
146
168
  const tempConfigPath = join(
147
169
  dirname(configPath),
148
- `.agentkit.config.loader-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.ts`,
170
+ `.agentkit.config.loader-${process.pid}-${nonce}-${Math.random().toString(16).slice(2)}.ts`,
149
171
  );
150
172
 
151
173
  try {
152
- await writeFile(tempConfigPath, patchedSource);
174
+ await writeFile(tempConfigPath, source);
153
175
  const configUrl = pathToFileURL(tempConfigPath);
154
- configUrl.searchParams.set("t", String(Date.now()));
176
+ configUrl.searchParams.set("t", nonce);
155
177
  const module = (await import(configUrl.href)) as { default?: unknown };
156
178
  return module.default;
157
179
  } catch (error) {
@@ -165,6 +187,11 @@ async function loadConfigModuleWithLocalSelfImport(configPath: string, originalE
165
187
  }
166
188
  }
167
189
 
190
+ function nextConfigImportNonce(): string {
191
+ configImportNonce += 1;
192
+ return `${Date.now()}-${configImportNonce}`;
193
+ }
194
+
168
195
  function isMissingAgentKitSelfImport(error: unknown): boolean {
169
196
  const message = error instanceof Error ? error.message : String(error);
170
197
  return message.includes("@andreprado/agentkit");
@@ -195,8 +222,17 @@ function validateAgentConfig(value: unknown): asserts value is AgentConfig {
195
222
  throw invalidConfig('provider.model must be "fake" when provider.name is "test".');
196
223
  }
197
224
 
225
+ if (value.timeZone !== undefined) {
226
+ expectString(value.timeZone, "timeZone");
227
+
228
+ if (!isValidTimeZone(value.timeZone)) {
229
+ throw invalidConfig('timeZone must be a valid IANA time zone such as "UTC" or "America/New_York".');
230
+ }
231
+ }
232
+
198
233
  expectString(value.instructions, "instructions");
199
234
  expectStringArray(value.secrets, "secrets");
235
+ validateSecretNames(value.secrets, "secrets");
200
236
 
201
237
  if (value.tools !== undefined) {
202
238
  if (!Array.isArray(value.tools)) {
@@ -208,7 +244,9 @@ function validateAgentConfig(value: unknown): asserts value is AgentConfig {
208
244
  }
209
245
  }
210
246
 
211
- validateChannels(value.channels, value.runtime as AgentRuntime);
247
+ validateIntegrations(value.integrations, value.runtime as AgentRuntime, value.tools);
248
+ validateTranscriptionConfig(value.transcription);
249
+ validateChannels(value.channels, value.runtime as AgentRuntime, Boolean(value.transcription));
212
250
  validateKnowledgeConfig(value.knowledge);
213
251
 
214
252
  if (!isRecord(value.access) || !ACCESS_MODES.has(value.access.mode as AccessMode)) {
@@ -227,10 +265,181 @@ function validateAgentConfig(value: unknown): asserts value is AgentConfig {
227
265
  throw invalidConfig("storage.path must be a string when provided.");
228
266
  }
229
267
 
268
+ if (value.storage.driver === "sqlite" && typeof value.storage.path !== "string") {
269
+ throw invalidConfig('storage.path is required when storage.driver is "sqlite".');
270
+ }
271
+
230
272
  validateDatabaseConfig(value.storage.database);
231
273
  validateFilesConfig(value.storage.files);
232
274
  }
233
275
 
276
+ function validateIntegrations(
277
+ value: unknown,
278
+ runtime: AgentRuntime,
279
+ tools: AgentConfig["tools"] = [],
280
+ ): void {
281
+ if (value === undefined) {
282
+ return;
283
+ }
284
+
285
+ if (!Array.isArray(value)) {
286
+ throw invalidConfig("integrations must be an array when provided.");
287
+ }
288
+
289
+ if (value.length > 0 && runtime !== "edge") {
290
+ throw invalidConfig('managed integrations require runtime: "edge". Use BYO defineTool wrappers for local-only integrations.');
291
+ }
292
+
293
+ let composioCount = 0;
294
+
295
+ for (const [index, integration] of value.entries()) {
296
+ if (!isRecord(integration)) {
297
+ throw invalidConfig(`integrations[${index}] must be an object.`);
298
+ }
299
+
300
+ if (integration.provider !== "composio" || integration.type !== "composio" || integration.mode !== "managed") {
301
+ throw invalidConfig(`integrations[${index}] must be composioManaged({...}) or { type: "composio", provider: "composio", mode: "managed", ... }.`);
302
+ }
303
+
304
+ composioCount += 1;
305
+ validateComposioManagedIntegration(integration as unknown as AgentIntegration, index);
306
+ }
307
+
308
+ if (composioCount > 1) {
309
+ throw invalidConfig("Only one managed Composio integration is supported per agent.");
310
+ }
311
+
312
+ const hasExplicitComposioTools = value.some(
313
+ (integration) => isRecord(integration) && integration.provider === "composio" && isRecord(integration.tools),
314
+ );
315
+
316
+ if (hasExplicitComposioTools && tools.some((tool) => isRecord(tool) && tool.name === MANAGED_COMPOSIO_TOOL_NAME)) {
317
+ throw invalidConfig(`tools must not declare reserved managed Composio tool name "${MANAGED_COMPOSIO_TOOL_NAME}".`);
318
+ }
319
+ }
320
+
321
+ function validateComposioManagedIntegration(value: AgentIntegration, index: number): void {
322
+ expectString(value.name, `integrations[${index}].name`);
323
+
324
+ if (!INTEGRATION_NAME_PATTERN.test(value.name)) {
325
+ throw invalidConfig(`integrations[${index}].name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.`);
326
+ }
327
+
328
+ if (!Array.isArray(value.toolkits) || value.toolkits.length === 0) {
329
+ throw invalidConfig(`integrations[${index}].toolkits must include at least one Composio toolkit slug.`);
330
+ }
331
+
332
+ const toolkits = new Set<string>();
333
+
334
+ for (const [toolkitIndex, toolkit] of value.toolkits.entries()) {
335
+ expectString(toolkit, `integrations[${index}].toolkits[${toolkitIndex}]`);
336
+
337
+ if (!COMPOSIO_TOOLKIT_PATTERN.test(toolkit)) {
338
+ throw invalidConfig(`integrations[${index}].toolkits[${toolkitIndex}] must be a lowercase Composio toolkit slug.`);
339
+ }
340
+
341
+ if (toolkits.has(toolkit)) {
342
+ throw invalidConfig(`integrations[${index}].toolkits must not contain duplicates.`);
343
+ }
344
+
345
+ toolkits.add(toolkit);
346
+ }
347
+
348
+ if (value.tools !== undefined) {
349
+ if (!isRecord(value.tools)) {
350
+ throw invalidConfig(`integrations[${index}].tools must be an object when provided.`);
351
+ }
352
+
353
+ for (const [toolkit, tools] of Object.entries(value.tools)) {
354
+ if (!toolkits.has(toolkit)) {
355
+ throw invalidConfig(`integrations[${index}].tools.${toolkit} must reference a toolkit listed in integrations[${index}].toolkits.`);
356
+ }
357
+
358
+ if (!Array.isArray(tools) || tools.length === 0) {
359
+ throw invalidConfig(`integrations[${index}].tools.${toolkit} must include at least one Composio action slug.`);
360
+ }
361
+
362
+ const seenTools = new Set<string>();
363
+
364
+ for (const [toolIndex, tool] of tools.entries()) {
365
+ expectString(tool, `integrations[${index}].tools.${toolkit}[${toolIndex}]`);
366
+
367
+ if (!COMPOSIO_TOOL_SLUG_PATTERN.test(tool)) {
368
+ throw invalidConfig(`integrations[${index}].tools.${toolkit}[${toolIndex}] must be an uppercase Composio action slug.`);
369
+ }
370
+
371
+ if (seenTools.has(tool)) {
372
+ throw invalidConfig(`integrations[${index}].tools.${toolkit} must not contain duplicates.`);
373
+ }
374
+
375
+ seenTools.add(tool);
376
+ }
377
+ }
378
+ }
379
+
380
+ if (value.tags !== undefined) {
381
+ validateIntegrationTags(value.tags, `integrations[${index}].tags`);
382
+ }
383
+
384
+ if (value.confirmExternalWrites !== undefined && typeof value.confirmExternalWrites !== "boolean") {
385
+ throw invalidConfig(`integrations[${index}].confirmExternalWrites must be a boolean when provided.`);
386
+ }
387
+ }
388
+
389
+ function validateIntegrationTags(value: unknown, label: string): void {
390
+ if (!isRecord(value)) {
391
+ throw invalidConfig(`${label} must be an object when provided.`);
392
+ }
393
+
394
+ if (value.enable !== undefined) {
395
+ expectStringArray(value.enable, `${label}.enable`);
396
+ }
397
+
398
+ if (value.disable !== undefined) {
399
+ expectStringArray(value.disable, `${label}.disable`);
400
+ }
401
+ }
402
+
403
+ function validateTranscriptionConfig(value: unknown): void {
404
+ if (value === undefined) {
405
+ return;
406
+ }
407
+
408
+ if (!isRecord(value)) {
409
+ throw invalidConfig("transcription must be an object when provided.");
410
+ }
411
+
412
+ if (!TRANSCRIPTION_PROVIDERS.has(value.provider as TranscriptionProviderName)) {
413
+ throw invalidConfig(`transcription.provider must be one of: ${Array.from(TRANSCRIPTION_PROVIDERS).join(", ")}.`);
414
+ }
415
+
416
+ expectString(value.model, "transcription.model");
417
+
418
+ if (value.secret !== undefined) {
419
+ expectString(value.secret, "transcription.secret");
420
+ validateSecretNames([value.secret], "transcription.secret");
421
+ }
422
+
423
+ if (value.language !== undefined) {
424
+ expectString(value.language, "transcription.language");
425
+ }
426
+
427
+ if (value.prompt !== undefined) {
428
+ expectString(value.prompt, "transcription.prompt");
429
+ }
430
+
431
+ if (value.limits !== undefined) {
432
+ if (!isRecord(value.limits)) {
433
+ throw invalidConfig("transcription.limits must be an object when provided.");
434
+ }
435
+
436
+ validateOptionalPositiveInteger(value.limits.maxDurationSeconds, "transcription.limits.maxDurationSeconds");
437
+ validateOptionalPositiveInteger(value.limits.maxBytes, "transcription.limits.maxBytes");
438
+ }
439
+
440
+ validateOptionalPositiveInteger(value.rawAudioTtlSeconds, "transcription.rawAudioTtlSeconds");
441
+ }
442
+
234
443
  function validateKnowledgeConfig(value: unknown): void {
235
444
  if (value === undefined) {
236
445
  return;
@@ -359,7 +568,7 @@ function validateKnowledgeRetrieval(value: unknown): void {
359
568
  }
360
569
  }
361
570
 
362
- function validateChannels(value: unknown, runtime: AgentRuntime): void {
571
+ function validateChannels(value: unknown, runtime: AgentRuntime, hasTranscriptionConfig: boolean): void {
363
572
  if (value === undefined) {
364
573
  return;
365
574
  }
@@ -375,7 +584,7 @@ function validateChannels(value: unknown, runtime: AgentRuntime): void {
375
584
  const names = new Set<string>();
376
585
 
377
586
  for (const [index, channel] of value.entries()) {
378
- validateChannel(channel, index);
587
+ validateChannel(channel, index, hasTranscriptionConfig);
379
588
 
380
589
  if (names.has(channel.name)) {
381
590
  throw invalidConfig(`channels[${index}].name must be unique within the Agent Capsule.`);
@@ -383,9 +592,13 @@ function validateChannels(value: unknown, runtime: AgentRuntime): void {
383
592
 
384
593
  names.add(channel.name);
385
594
  }
595
+
596
+ for (const [index, channel] of value.entries()) {
597
+ validateChannelReplyRoute(channel, index, names);
598
+ }
386
599
  }
387
600
 
388
- function validateChannel(value: unknown, index: number): asserts value is AgentChannel {
601
+ function validateChannel(value: unknown, index: number, hasTranscriptionConfig: boolean): asserts value is AgentChannel {
389
602
  if (!isRecord(value)) {
390
603
  throw invalidConfig(`channels[${index}] must be an object.`);
391
604
  }
@@ -408,9 +621,27 @@ function validateChannel(value: unknown, index: number): asserts value is AgentC
408
621
 
409
622
  expectStringArray(value.secrets, `channels[${index}].secrets`);
410
623
  validateSecretNames(value.secrets, `channels[${index}].secrets`);
624
+ validateChannelReplyTo(value.replyTo, index);
411
625
  validateChannelLimits(value.limits, index);
412
626
  validateChannelBuffer(value.buffer, index);
413
627
 
628
+ if (value.type === "discord") {
629
+ validateDiscordChannel(value, index);
630
+ return;
631
+ }
632
+
633
+ if (value.type === "slack") {
634
+ validateSlackChannel(value, index);
635
+ return;
636
+ }
637
+
638
+ if (value.type === "webhook") {
639
+ validateGenericWebhookChannel(value, index);
640
+ return;
641
+ }
642
+
643
+ validateChannelAudio(value.audio, index, hasTranscriptionConfig);
644
+
414
645
  if (value.type === "website") {
415
646
  validateWebsiteChannel(value, index);
416
647
  return;
@@ -424,6 +655,31 @@ function validateChannel(value: unknown, index: number): asserts value is AgentC
424
655
  validateWhatsappChannel(value, index);
425
656
  }
426
657
 
658
+ function validateChannelAudio(value: unknown, index: number, hasTranscriptionConfig: boolean): void {
659
+ if (value === undefined) {
660
+ return;
661
+ }
662
+
663
+ if (!isRecord(value)) {
664
+ throw invalidConfig(`channels[${index}].audio must be an object when provided.`);
665
+ }
666
+
667
+ if (!CHANNEL_AUDIO_MODES.has(value.mode as ChannelAudioMode)) {
668
+ throw invalidConfig(`channels[${index}].audio.mode must be "off", "reject", or "transcribe".`);
669
+ }
670
+
671
+ if (value.mode === "transcribe" && !hasTranscriptionConfig) {
672
+ throw invalidConfig(`channels[${index}].audio.mode "transcribe" requires top-level transcription config.`);
673
+ }
674
+
675
+ if (value.mode !== "off") {
676
+ expectString(value.fallbackMessage, `channels[${index}].audio.fallbackMessage`);
677
+ }
678
+
679
+ validateOptionalPositiveInteger(value.maxDurationSeconds, `channels[${index}].audio.maxDurationSeconds`);
680
+ validateOptionalPositiveInteger(value.maxBytes, `channels[${index}].audio.maxBytes`);
681
+ }
682
+
427
683
  function validateWebsiteChannel(value: Record<string, unknown>, index: number): void {
428
684
  if (value.provider !== "agentkit") {
429
685
  throw invalidConfig(`channels[${index}].provider must be "agentkit" when type is "website".`);
@@ -456,11 +712,15 @@ function validateTelegramChannel(value: Record<string, unknown>, index: number):
456
712
 
457
713
  function validateWhatsappChannel(value: Record<string, unknown>, index: number): void {
458
714
  if (value.type !== "whatsapp") {
459
- throw invalidConfig(`channels[${index}].type must be "website", "telegram", or "whatsapp".`);
715
+ throw invalidConfig(`channels[${index}].type must be "website", "telegram", "whatsapp", "discord", "slack", or "webhook".`);
460
716
  }
461
717
 
462
718
  if (value.provider === "zapster") {
463
- expectRequiredSecrets(value.secrets, ["ZAPSTER_API_KEY", "ZAPSTER_WEBHOOK_SECRET"], `channels[${index}]`);
719
+ expectRequiredSecrets(
720
+ value.secrets,
721
+ ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID", "ZAPSTER_WEBHOOK_TOKEN"],
722
+ `channels[${index}]`,
723
+ );
464
724
  return;
465
725
  }
466
726
 
@@ -473,7 +733,205 @@ function validateWhatsappChannel(value: Record<string, unknown>, index: number):
473
733
  return;
474
734
  }
475
735
 
476
- throw invalidConfig(`channels[${index}].provider must be "zapster" or "meta" when type is "whatsapp".`);
736
+ if (value.provider === "uazapi") {
737
+ expectRequiredSecrets(
738
+ value.secrets,
739
+ ["UAZAPI_BASE_URL", "UAZAPI_TOKEN", "UAZAPI_WEBHOOK_TOKEN"],
740
+ `channels[${index}]`,
741
+ );
742
+ return;
743
+ }
744
+
745
+ if (value.provider === "evolution") {
746
+ expectRequiredSecrets(
747
+ value.secrets,
748
+ ["EVOLUTION_API_BASE_URL", "EVOLUTION_API_KEY", "EVOLUTION_INSTANCE_NAME", "EVOLUTION_WEBHOOK_TOKEN"],
749
+ `channels[${index}]`,
750
+ );
751
+ return;
752
+ }
753
+
754
+ throw invalidConfig(`channels[${index}].provider must be "zapster", "meta", "uazapi", or "evolution" when type is "whatsapp".`);
755
+ }
756
+
757
+ function validateDiscordChannel(value: Record<string, unknown>, index: number): void {
758
+ if (value.provider !== "discord") {
759
+ throw invalidConfig(`channels[${index}].provider must be "discord" when type is "discord".`);
760
+ }
761
+
762
+ if (value.mode !== undefined && !DISCORD_CHANNEL_MODES.has(value.mode as DiscordChannelMode)) {
763
+ throw invalidConfig(`channels[${index}].mode must be "interactions" or "bot" when type is "discord".`);
764
+ }
765
+
766
+ if (value.audio !== undefined) {
767
+ throw invalidConfig(`channels[${index}].audio is not supported when type is "discord".`);
768
+ }
769
+
770
+ expectRequiredSecrets(
771
+ value.secrets,
772
+ value.mode === "bot" ? ["DISCORD_BOT_TOKEN"] : ["DISCORD_PUBLIC_KEY"],
773
+ `channels[${index}]`,
774
+ );
775
+ }
776
+
777
+ function validateSlackChannel(value: Record<string, unknown>, index: number): void {
778
+ if (value.provider !== "slack") {
779
+ throw invalidConfig(`channels[${index}].provider must be "slack" when type is "slack".`);
780
+ }
781
+
782
+ if (value.audio !== undefined) {
783
+ throw invalidConfig(`channels[${index}].audio is not supported when type is "slack".`);
784
+ }
785
+
786
+ expectRequiredSecrets(value.secrets, ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"], `channels[${index}]`);
787
+ }
788
+
789
+ function validateGenericWebhookChannel(value: Record<string, unknown>, index: number): void {
790
+ if (value.provider !== "generic") {
791
+ throw invalidConfig(`channels[${index}].provider must be "generic" when type is "webhook".`);
792
+ }
793
+
794
+ if (value.audio !== undefined) {
795
+ throw invalidConfig(`channels[${index}].audio is not supported when type is "webhook".`);
796
+ }
797
+
798
+ if (value.direction !== undefined && value.direction !== "inbound" && value.direction !== "outbound") {
799
+ throw invalidConfig(`channels[${index}].direction must be "inbound" or "outbound" when type is "webhook".`);
800
+ }
801
+
802
+ if (value.direction === "outbound") {
803
+ if (value.buffer !== undefined) {
804
+ throw invalidConfig(`channels[${index}].buffer is not supported for outbound webhook channels.`);
805
+ }
806
+
807
+ if (value.replyTo !== undefined) {
808
+ throw invalidConfig(`channels[${index}].replyTo is not supported for outbound webhook channels.`);
809
+ }
810
+
811
+ expectString(value.urlSecret, `channels[${index}].urlSecret`);
812
+ validateSecretNames([value.urlSecret], `channels[${index}].urlSecret`);
813
+ validateGenericWebhookOutputAuth(value.auth, index);
814
+ expectRequiredSecrets(
815
+ value.secrets,
816
+ [value.urlSecret, ...genericWebhookOutputAuthSecrets(value.auth)],
817
+ `channels[${index}]`,
818
+ );
819
+ return;
820
+ }
821
+
822
+ if (value.urlSecret !== undefined || value.auth !== undefined) {
823
+ throw invalidConfig(`channels[${index}].urlSecret and auth are only supported for outbound webhook channels.`);
824
+ }
825
+
826
+ expectRequiredSecrets(value.secrets, ["AGENTKIT_WEBHOOK_SECRET"], `channels[${index}]`);
827
+ }
828
+
829
+ function validateChannelReplyTo(value: unknown, index: number): void {
830
+ if (value === undefined) {
831
+ return;
832
+ }
833
+
834
+ if (!isRecord(value)) {
835
+ throw invalidConfig(`channels[${index}].replyTo must be an object when provided.`);
836
+ }
837
+
838
+ expectString(value.channel, `channels[${index}].replyTo.channel`);
839
+
840
+ if (!CHANNEL_NAME_PATTERN.test(value.channel)) {
841
+ throw invalidConfig(`channels[${index}].replyTo.channel must be a valid channel name.`);
842
+ }
843
+
844
+ if (value.recipientFrom !== undefined) {
845
+ expectString(value.recipientFrom, `channels[${index}].replyTo.recipientFrom`);
846
+
847
+ if (!CHANNEL_REPLY_PATH_PATTERN.test(String(value.recipientFrom).replace(/^payload\./, ""))) {
848
+ throw invalidConfig(`channels[${index}].replyTo.recipientFrom must be a dotted JSON field path.`);
849
+ }
850
+ }
851
+ }
852
+
853
+ function validateChannelReplyRoute(
854
+ channel: unknown,
855
+ index: number,
856
+ names: Set<string>,
857
+ ): void {
858
+ if (!isRecord(channel) || !isRecord(channel.replyTo)) {
859
+ return;
860
+ }
861
+
862
+ const replyTo = channel.replyTo as ChannelReplyRouteConfig;
863
+
864
+ if (!names.has(replyTo.channel)) {
865
+ throw invalidConfig(`channels[${index}].replyTo.channel must name another channel in this Agent Capsule.`);
866
+ }
867
+
868
+ if (replyTo.channel === channel.name) {
869
+ throw invalidConfig(`channels[${index}].replyTo.channel must not point back to the same channel.`);
870
+ }
871
+ }
872
+
873
+ function validateGenericWebhookOutputAuth(value: unknown, index: number): void {
874
+ if (!isRecord(value)) {
875
+ throw invalidConfig(`channels[${index}].auth must be an object for outbound webhook channels.`);
876
+ }
877
+
878
+ if (value.type === "none") {
879
+ return;
880
+ }
881
+
882
+ if (value.type === "bearer") {
883
+ expectString(value.tokenSecret, `channels[${index}].auth.tokenSecret`);
884
+ validateSecretNames([value.tokenSecret], `channels[${index}].auth.tokenSecret`);
885
+ return;
886
+ }
887
+
888
+ if (value.type === "header") {
889
+ expectString(value.headerName, `channels[${index}].auth.headerName`);
890
+
891
+ if (!HTTP_HEADER_NAME_PATTERN.test(value.headerName)) {
892
+ throw invalidConfig(`channels[${index}].auth.headerName must be an HTTP header name.`);
893
+ }
894
+
895
+ expectString(value.valueSecret, `channels[${index}].auth.valueSecret`);
896
+ validateSecretNames([value.valueSecret], `channels[${index}].auth.valueSecret`);
897
+ return;
898
+ }
899
+
900
+ if (value.type === "hmac") {
901
+ if (value.headerName !== undefined) {
902
+ expectString(value.headerName, `channels[${index}].auth.headerName`);
903
+
904
+ if (!HTTP_HEADER_NAME_PATTERN.test(value.headerName)) {
905
+ throw invalidConfig(`channels[${index}].auth.headerName must be an HTTP header name.`);
906
+ }
907
+ }
908
+
909
+ expectString(value.secret, `channels[${index}].auth.secret`);
910
+ validateSecretNames([value.secret], `channels[${index}].auth.secret`);
911
+ return;
912
+ }
913
+
914
+ throw invalidConfig(`channels[${index}].auth.type must be "none", "bearer", "header", or "hmac".`);
915
+ }
916
+
917
+ function genericWebhookOutputAuthSecrets(value: unknown): string[] {
918
+ if (!isRecord(value)) {
919
+ return [];
920
+ }
921
+
922
+ if (value.type === "bearer" && typeof value.tokenSecret === "string") {
923
+ return [value.tokenSecret];
924
+ }
925
+
926
+ if (value.type === "header" && typeof value.valueSecret === "string") {
927
+ return [value.valueSecret];
928
+ }
929
+
930
+ if (value.type === "hmac" && typeof value.secret === "string") {
931
+ return [value.secret];
932
+ }
933
+
934
+ return [];
477
935
  }
478
936
 
479
937
  function validateChannelLimits(value: unknown, index: number): void {
@@ -536,6 +994,16 @@ function validateSecretNames(secrets: string[], field: string): void {
536
994
  }
537
995
  }
538
996
 
997
+ function validateOptionalPositiveInteger(value: unknown, field: string): void {
998
+ if (value === undefined) {
999
+ return;
1000
+ }
1001
+
1002
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
1003
+ throw invalidConfig(`${field} must be a positive integer when provided.`);
1004
+ }
1005
+ }
1006
+
539
1007
  function expectRequiredSecrets(value: unknown, requiredSecrets: string[], field: string): void {
540
1008
  const declaredSecrets = new Set(Array.isArray(value) ? value : []);
541
1009
  const missingSecrets = requiredSecrets.filter((secret) => !declaredSecrets.has(secret));
@@ -620,6 +1088,7 @@ function validateTool(value: unknown, index: number): asserts value is AgentTool
620
1088
 
621
1089
  if (value.secrets !== undefined) {
622
1090
  expectStringArray(value.secrets, `tools[${index}].secrets`);
1091
+ validateSecretNames(value.secrets, `tools[${index}].secrets`);
623
1092
  }
624
1093
 
625
1094
  if (value.permissions !== undefined) {