@andreprado/agentkit 0.1.0-alpha.19 → 0.1.0-alpha.20

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.
@@ -30,8 +30,8 @@ const AGENT_RUNTIMES = new Set<AgentRuntime>(["local", "edge"]);
30
30
  const PROVIDERS = new Set<AgentProviderName>(["test", "openai", "anthropic", "openrouter", "custom"]);
31
31
  const ACCESS_MODES = new Set<AccessMode>(["private", "token", "public"]);
32
32
  const TOOL_VISIBILITIES = new Set<ToolVisibility>(["user", "internal"]);
33
- const CHANNEL_TYPES = new Set<ChannelType>(["website", "telegram", "whatsapp", "discord"]);
34
- const CHANNEL_PROVIDERS = new Set<ChannelProvider>(["agentkit", "telegram", "zapster", "meta", "discord"]);
33
+ const CHANNEL_TYPES = new Set<ChannelType>(["website", "telegram", "whatsapp", "discord", "slack"]);
34
+ const CHANNEL_PROVIDERS = new Set<ChannelProvider>(["agentkit", "telegram", "zapster", "meta", "discord", "slack"]);
35
35
  const CHANNEL_BUFFER_MODES = new Set<ChannelBufferMode>(["off", "debounce"]);
36
36
  const CHANNEL_AUDIO_MODES = new Set<ChannelAudioMode>(["off", "reject", "transcribe"]);
37
37
  const DISCORD_CHANNEL_MODES = new Set<DiscordChannelMode>(["interactions", "bot"]);
@@ -622,6 +622,11 @@ function validateChannel(value: unknown, index: number, hasTranscriptionConfig:
622
622
  return;
623
623
  }
624
624
 
625
+ if (value.type === "slack") {
626
+ validateSlackChannel(value, index);
627
+ return;
628
+ }
629
+
625
630
  validateChannelAudio(value.audio, index, hasTranscriptionConfig);
626
631
 
627
632
  if (value.type === "website") {
@@ -694,7 +699,7 @@ function validateTelegramChannel(value: Record<string, unknown>, index: number):
694
699
 
695
700
  function validateWhatsappChannel(value: Record<string, unknown>, index: number): void {
696
701
  if (value.type !== "whatsapp") {
697
- throw invalidConfig(`channels[${index}].type must be "website", "telegram", "whatsapp", or "discord".`);
702
+ throw invalidConfig(`channels[${index}].type must be "website", "telegram", "whatsapp", "discord", or "slack".`);
698
703
  }
699
704
 
700
705
  if (value.provider === "zapster") {
@@ -738,6 +743,18 @@ function validateDiscordChannel(value: Record<string, unknown>, index: number):
738
743
  );
739
744
  }
740
745
 
746
+ function validateSlackChannel(value: Record<string, unknown>, index: number): void {
747
+ if (value.provider !== "slack") {
748
+ throw invalidConfig(`channels[${index}].provider must be "slack" when type is "slack".`);
749
+ }
750
+
751
+ if (value.audio !== undefined) {
752
+ throw invalidConfig(`channels[${index}].audio is not supported when type is "slack".`);
753
+ }
754
+
755
+ expectRequiredSecrets(value.secrets, ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"], `channels[${index}]`);
756
+ }
757
+
741
758
  function validateChannelLimits(value: unknown, index: number): void {
742
759
  if (value === undefined) {
743
760
  return;
@@ -6,7 +6,7 @@ export type RuntimeTargetId = (typeof runtimeTargetIds)[number];
6
6
  export type RuntimeTargetCapabilityState = "available" | "planned" | "unsupported";
7
7
  export type RuntimeStorageCapability = "agentkit" | "durable-object-sqlite" | "local-sqlite" | "postgres";
8
8
  export type RuntimeFilesCapability = "none" | "r2" | "local-disk" | "s3-compatible";
9
- export type RuntimeChannelCapability = "website" | "telegram" | "whatsapp";
9
+ export type RuntimeChannelCapability = "website" | "telegram" | "whatsapp" | "discord" | "slack";
10
10
  export type RuntimeProviderSupport = "pi-local" | "edge-compatible" | "server-env";
11
11
 
12
12
  export type RuntimeTargetCapabilities = {
@@ -35,7 +35,7 @@ export const runtimeTargets: Readonly<Record<RuntimeTargetId, RuntimeTarget>> =
35
35
  dev: "available",
36
36
  storage: ["local-sqlite"],
37
37
  files: ["local-disk"],
38
- channels: ["website"],
38
+ channels: ["website", "slack"],
39
39
  providers: ["pi-local"],
40
40
  },
41
41
  },
@@ -48,7 +48,7 @@ export const runtimeTargets: Readonly<Record<RuntimeTargetId, RuntimeTarget>> =
48
48
  dev: "planned",
49
49
  storage: ["agentkit", "durable-object-sqlite", "postgres"],
50
50
  files: ["none", "r2"],
51
- channels: ["website", "telegram", "whatsapp"],
51
+ channels: ["website", "telegram", "whatsapp", "discord", "slack"],
52
52
  providers: ["edge-compatible"],
53
53
  },
54
54
  },
@@ -61,7 +61,7 @@ export const runtimeTargets: Readonly<Record<RuntimeTargetId, RuntimeTarget>> =
61
61
  dev: "planned",
62
62
  storage: ["local-sqlite", "postgres"],
63
63
  files: ["none", "local-disk", "s3-compatible"],
64
- channels: ["website", "telegram", "whatsapp"],
64
+ channels: ["website", "telegram", "whatsapp", "discord", "slack"],
65
65
  providers: ["server-env"],
66
66
  },
67
67
  },
@@ -74,7 +74,7 @@ export const runtimeTargets: Readonly<Record<RuntimeTargetId, RuntimeTarget>> =
74
74
  dev: "unsupported",
75
75
  storage: ["local-sqlite", "postgres"],
76
76
  files: ["none", "local-disk", "s3-compatible"],
77
- channels: ["website", "telegram", "whatsapp"],
77
+ channels: ["website", "telegram", "whatsapp", "discord", "slack"],
78
78
  providers: ["server-env"],
79
79
  },
80
80
  },
@@ -8,7 +8,8 @@ import { Readable } from "node:stream";
8
8
  import { openCapsuleStore, type ConversationRecord } from "../storage/sqlite";
9
9
  import type { AgentChannel, ChannelProvider, ChannelType } from "../index";
10
10
  import type { ChannelAdapter, NormalizedChannelMessage, RawWebhookEvent } from "./channels";
11
- import { discordChannelAdapter, isDiscordPingInteraction } from "./channels/discord";
11
+ import { discordChannelAdapter } from "./channels/discord";
12
+ import { slackChannelAdapter } from "./channels/slack";
12
13
  import { telegramChannelAdapter } from "./channels/telegram";
13
14
  import { websiteChannelAdapter } from "./channels/website";
14
15
  import { metaWhatsappChannelAdapter } from "./channels/whatsapp-meta";
@@ -319,7 +320,7 @@ async function handleDevServerRequest(
319
320
  return await handleLocalFile(capsule, request, decodeURIComponent(fileMatch[1]));
320
321
  }
321
322
 
322
- const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp|discord)\/([^/]+)\/webhook$/);
323
+ const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp|discord|slack)\/([^/]+)\/webhook$/);
323
324
 
324
325
  if (channelMatch) {
325
326
  return await handlePortableChannel(capsule, request, channelRuntime, {
@@ -504,12 +505,12 @@ async function handlePortableChannel(
504
505
  throw new AgentKitError(verification.code, verification.message);
505
506
  }
506
507
 
507
- if (channel.type === "whatsapp" && channel.provider === "meta" && request.method === "GET") {
508
- return new Response(new URL(request.url).searchParams.get("hub.challenge") ?? "");
509
- }
508
+ const webhookResponse = adapter.getWebhookResponse
509
+ ? await adapter.getWebhookResponse({ ...rawEvent, channel, secrets })
510
+ : null;
510
511
 
511
- if (channel.type === "discord" && channel.provider === "discord" && isDiscordPingInteraction(rawBody)) {
512
- return jsonResponse({ type: 1 });
512
+ if (webhookResponse) {
513
+ return channelAdapterResponse(webhookResponse);
513
514
  }
514
515
 
515
516
  const events = await adapter.normalizeInbound(rawEvent);
@@ -1140,9 +1141,31 @@ function adapterForChannel(type: ChannelType, provider: ChannelProvider): Channe
1140
1141
  return discordChannelAdapter;
1141
1142
  }
1142
1143
 
1144
+ if (type === "slack" && provider === "slack") {
1145
+ return slackChannelAdapter;
1146
+ }
1147
+
1143
1148
  throw new AgentKitError("channel_not_found", `No channel adapter for ${type}/${provider}.`);
1144
1149
  }
1145
1150
 
1151
+ function channelAdapterResponse(response: {
1152
+ status?: number;
1153
+ headers?: Record<string, string>;
1154
+ body: string | Record<string, unknown>;
1155
+ }): Response {
1156
+ if (typeof response.body === "string") {
1157
+ return new Response(response.body, {
1158
+ status: response.status ?? 200,
1159
+ headers: response.headers,
1160
+ });
1161
+ }
1162
+
1163
+ return jsonResponse(response.body, {
1164
+ status: response.status ?? 200,
1165
+ headers: response.headers,
1166
+ });
1167
+ }
1168
+
1146
1169
  function discordLocalInteractionResponse(
1147
1170
  deliveries: Array<{
1148
1171
  state: string;
@@ -451,6 +451,17 @@ export function discordChannel(input) {
451
451
  };
452
452
  }
453
453
 
454
+ export function slackChannel(input) {
455
+ return {
456
+ type: "slack",
457
+ provider: "slack",
458
+ name: input.name,
459
+ secrets: normalizeChannelSecrets(input.secrets ?? ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"], "slack channel"),
460
+ ...(input.limits ? { limits: input.limits } : {}),
461
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "slack channel") } : {}),
462
+ };
463
+ }
464
+
454
465
  function defaultWhatsappChannelSecrets(provider) {
455
466
  if (provider === "zapster") {
456
467
  return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"];
@@ -715,6 +726,10 @@ export class AgentKitConversationStore extends DurableObject {
715
726
  status TEXT NOT NULL,
716
727
  error_code TEXT,
717
728
  error_message TEXT,
729
+ input_tokens INTEGER,
730
+ output_tokens INTEGER,
731
+ total_tokens INTEGER,
732
+ cost_json TEXT,
718
733
  created_at TEXT NOT NULL,
719
734
  completed_at TEXT
720
735
  );
@@ -738,6 +753,18 @@ export class AgentKitConversationStore extends DurableObject {
738
753
  } catch {
739
754
  // Existing Durable Object tables either already have this column or do not need migration.
740
755
  }
756
+ for (const statement of [
757
+ "ALTER TABLE runs ADD COLUMN input_tokens INTEGER",
758
+ "ALTER TABLE runs ADD COLUMN output_tokens INTEGER",
759
+ "ALTER TABLE runs ADD COLUMN total_tokens INTEGER",
760
+ "ALTER TABLE runs ADD COLUMN cost_json TEXT",
761
+ ]) {
762
+ try {
763
+ this.sql.exec(statement);
764
+ } catch {
765
+ // Existing Durable Object tables either already have this column or do not need migration.
766
+ }
767
+ }
741
768
  }
742
769
 
743
770
  async fetch(request) {
@@ -835,7 +862,7 @@ export class AgentKitConversationStore extends DurableObject {
835
862
  }
836
863
 
837
864
  const runs = this.sql.exec(
838
- "SELECT id, conversation_id, provider, model, status, error_code, error_message, created_at, completed_at FROM runs WHERE conversation_id = ? ORDER BY rowid ASC",
865
+ "SELECT id, conversation_id, provider, model, status, error_code, error_message, input_tokens, output_tokens, total_tokens, cost_json, created_at, completed_at FROM runs WHERE conversation_id = ? ORDER BY rowid ASC",
839
866
  conversationId,
840
867
  ).toArray();
841
868
 
@@ -849,6 +876,7 @@ export class AgentKitConversationStore extends DurableObject {
849
876
  status: String(run.status),
850
877
  errorCode: run.error_code === null || run.error_code === undefined ? null : String(run.error_code),
851
878
  errorMessage: run.error_message === null || run.error_message === undefined ? null : String(run.error_message),
879
+ usage: readStoredUsage(run),
852
880
  startedAt: String(run.created_at),
853
881
  completedAt: run.completed_at === null || run.completed_at === undefined ? null : String(run.completed_at),
854
882
  toolCalls: this.readToolCalls(String(run.id)),
@@ -920,6 +948,7 @@ export class AgentKitConversationStore extends DurableObject {
920
948
  const userMessageId = crypto.randomUUID();
921
949
  const assistantMessageId = crypto.randomUUID();
922
950
  const title = titleFromMessage(String(input.userMessage || ""));
951
+ const usage = normalizeRuntimeUsage(input.usage);
923
952
 
924
953
  this.sql.exec(
925
954
  "INSERT OR IGNORE INTO conversations (id, agent_name, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
@@ -939,7 +968,7 @@ export class AgentKitConversationStore extends DurableObject {
939
968
  now,
940
969
  );
941
970
  this.sql.exec(
942
- "INSERT INTO runs (id, conversation_id, provider, model, status, error_code, error_message, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
971
+ "INSERT INTO runs (id, conversation_id, provider, model, status, error_code, error_message, input_tokens, output_tokens, total_tokens, cost_json, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
943
972
  runId,
944
973
  conversationId,
945
974
  manifest.provider.name,
@@ -947,6 +976,10 @@ export class AgentKitConversationStore extends DurableObject {
947
976
  String(input.status || "completed"),
948
977
  input.errorCode ? String(input.errorCode) : null,
949
978
  input.errorMessage ? String(input.errorMessage) : null,
979
+ usage.inputTokens ?? null,
980
+ usage.outputTokens ?? null,
981
+ usage.totalTokens ?? null,
982
+ usage.cost && Object.keys(usage.cost).length > 0 ? JSON.stringify(usage.cost) : null,
950
983
  now,
951
984
  now,
952
985
  );
@@ -990,7 +1023,7 @@ export class AgentKitConversationStore extends DurableObject {
990
1023
  }
991
1024
 
992
1025
  export default {
993
- async fetch(request, env) {
1026
+ async fetch(request, env, ctx) {
994
1027
  const url = new URL(request.url);
995
1028
 
996
1029
  if (request.method === "GET" && url.pathname === "/_agentkit") {
@@ -1062,7 +1095,7 @@ export default {
1062
1095
  }
1063
1096
 
1064
1097
  if (request.method === "POST" && (url.pathname === "/chat" || url.pathname === "/v1/chat")) {
1065
- return handleChat(request, env);
1098
+ return handleChat(request, env, ctx);
1066
1099
  }
1067
1100
 
1068
1101
  if (request.method === "GET" && (url.pathname === "/conversations" || url.pathname === "/v1/conversations")) {
@@ -1095,8 +1128,8 @@ export default {
1095
1128
 
1096
1129
  function isHostedChannelWebhookPath(pathname) {
1097
1130
  return (
1098
- /^\\/channels\\/[^/]+\\/(?:website\\/agentkit|telegram\\/telegram|whatsapp\\/(?:zapster|meta)|discord\\/discord)\\/webhook$/.test(pathname) ||
1099
- /^\\/channels\\/[^/]+\\/(?:website|telegram|discord)\\/webhook$/.test(pathname) ||
1131
+ /^\\/channels\\/[^/]+\\/(?:website\\/agentkit|telegram\\/telegram|whatsapp\\/(?:zapster|meta)|discord\\/discord|slack\\/slack)\\/webhook$/.test(pathname) ||
1132
+ /^\\/channels\\/[^/]+\\/(?:website|telegram|discord|slack)\\/webhook$/.test(pathname) ||
1100
1133
  /^\\/channels\\/[^/]+\\/whatsapp\\/(?:zapster|meta)\\/webhook$/.test(pathname)
1101
1134
  );
1102
1135
  }
@@ -1269,7 +1302,7 @@ async function handleTool(request, env, toolName) {
1269
1302
  }
1270
1303
  }
1271
1304
 
1272
- async function handleChat(request, env) {
1305
+ async function handleChat(request, env, ctx) {
1273
1306
  let body;
1274
1307
 
1275
1308
  try {
@@ -1306,13 +1339,26 @@ async function handleChat(request, env) {
1306
1339
 
1307
1340
  const previousMessages = await loadConversationMessages(env, conversationId).catch(() => []);
1308
1341
  let content;
1342
+ let providerResult;
1309
1343
  const toolCalls = [];
1310
1344
 
1311
1345
  try {
1312
- const providerResult = await callHostedProvider(env, [...previousMessages, { role: "user", content: message }], request.signal, toolCalls);
1346
+ providerResult = await callHostedProvider(env, [...previousMessages, { role: "user", content: message }], request.signal, toolCalls);
1313
1347
  content = providerResult.content;
1314
1348
  } catch (error) {
1315
- await persistFailure(env, conversationId, message, error, toolCalls);
1349
+ const persisted = await persistFailure(env, conversationId, message, error, toolCalls);
1350
+ queueRuntimeUsage(ctx, sendRuntimeUsage(env, {
1351
+ kind: "chat_run",
1352
+ status: "failed",
1353
+ conversationId,
1354
+ runId: persisted?.runId,
1355
+ provider: manifest.provider.name,
1356
+ model: manifest.provider.model,
1357
+ metadata: {
1358
+ error_code: error && typeof error === "object" && "code" in error ? String(error.code) : "runtime_error",
1359
+ tool_calls: toolCalls.length,
1360
+ },
1361
+ }));
1316
1362
  return hostedErrorResponse(error);
1317
1363
  }
1318
1364
 
@@ -1320,8 +1366,21 @@ async function handleChat(request, env) {
1320
1366
  conversationId,
1321
1367
  userMessage: message,
1322
1368
  assistantMessage: content,
1369
+ usage: providerResult.usage,
1323
1370
  toolCalls,
1324
1371
  });
1372
+ queueRuntimeUsage(ctx, sendRuntimeUsage(env, {
1373
+ kind: "chat_run",
1374
+ status: "succeeded",
1375
+ conversationId: persisted.conversationId ?? conversationId,
1376
+ runId: persisted.runId,
1377
+ provider: manifest.provider.name,
1378
+ model: manifest.provider.model,
1379
+ usage: providerResult.usage,
1380
+ metadata: {
1381
+ tool_calls: toolCalls.length,
1382
+ },
1383
+ }));
1325
1384
 
1326
1385
  return Response.json({
1327
1386
  message: {
@@ -1409,6 +1468,7 @@ async function callHostedProvider(env, messages, signal, toolCalls = []) {
1409
1468
 
1410
1469
  return {
1411
1470
  content: result.message.content,
1471
+ usage: result.usage,
1412
1472
  };
1413
1473
  }
1414
1474
 
@@ -1836,7 +1896,7 @@ async function hostedFakeEmbedding(input, dimensions) {
1836
1896
  }
1837
1897
 
1838
1898
  async function persistFailure(env, conversationId, userMessage, error, toolCalls = []) {
1839
- await persist(env, {
1899
+ return persist(env, {
1840
1900
  conversationId,
1841
1901
  userMessage,
1842
1902
  status: "failed",
@@ -2530,6 +2590,73 @@ async function persist(env, input) {
2530
2590
  return response.json();
2531
2591
  }
2532
2592
 
2593
+ function queueRuntimeUsage(ctx, promise) {
2594
+ const safePromise = promise.catch(() => undefined);
2595
+
2596
+ if (ctx && typeof ctx.waitUntil === "function") {
2597
+ ctx.waitUntil(safePromise);
2598
+ return;
2599
+ }
2600
+
2601
+ void safePromise;
2602
+ }
2603
+
2604
+ async function sendRuntimeUsage(env, input) {
2605
+ if (!env.AGENTKIT_CLOUD_API_URL || !env.AGENTKIT_DEPLOY_ID || !env.AGENTKIT_RUNTIME_GATE_TOKEN) {
2606
+ return;
2607
+ }
2608
+
2609
+ const usage = normalizeRuntimeUsage(input.usage);
2610
+ const metadata = isRecord(input.metadata) ? input.metadata : {};
2611
+ const body = {
2612
+ kind: input.kind || "chat_run",
2613
+ status: input.status || "succeeded",
2614
+ ...(input.runId ? { run_id: String(input.runId) } : {}),
2615
+ ...(input.conversationId ? { conversation_id: String(input.conversationId) } : {}),
2616
+ ...(input.provider ? { provider: String(input.provider) } : {}),
2617
+ ...(input.model ? { model: String(input.model) } : {}),
2618
+ ...(usage.inputTokens !== undefined ? { input_tokens: usage.inputTokens } : {}),
2619
+ ...(usage.outputTokens !== undefined ? { output_tokens: usage.outputTokens } : {}),
2620
+ ...(usage.totalTokens !== undefined ? { total_tokens: usage.totalTokens } : {}),
2621
+ ...(usage.cost && Object.keys(usage.cost).length > 0 ? { cost: usage.cost } : {}),
2622
+ ...(usage.cost?.total !== undefined ? { cost_usd: usage.cost.total } : {}),
2623
+ metadata: sanitizeRuntimeUsageMetadata(metadata),
2624
+ };
2625
+
2626
+ const url = new URL(
2627
+ "/v1/runtime/deploys/" + encodeURIComponent(String(env.AGENTKIT_DEPLOY_ID)) + "/usage",
2628
+ env.AGENTKIT_CLOUD_API_URL,
2629
+ );
2630
+ await fetch(url.href, {
2631
+ method: "POST",
2632
+ headers: {
2633
+ Authorization: "Bearer " + env.AGENTKIT_RUNTIME_GATE_TOKEN,
2634
+ "Content-Type": "application/json",
2635
+ },
2636
+ body: JSON.stringify(body),
2637
+ });
2638
+ }
2639
+
2640
+ function sanitizeRuntimeUsageMetadata(value) {
2641
+ const metadata = {};
2642
+
2643
+ for (const [key, raw] of Object.entries(value)) {
2644
+ if (!/^[A-Za-z0-9_.:-]{1,80}$/.test(key)) {
2645
+ continue;
2646
+ }
2647
+
2648
+ if (typeof raw === "string") {
2649
+ metadata[key] = raw.length > 160 ? raw.slice(0, 160) : raw;
2650
+ } else if (typeof raw === "number" && Number.isFinite(raw)) {
2651
+ metadata[key] = raw;
2652
+ } else if (typeof raw === "boolean") {
2653
+ metadata[key] = raw;
2654
+ }
2655
+ }
2656
+
2657
+ return metadata;
2658
+ }
2659
+
2533
2660
  async function loadConversationMessages(env, conversationId) {
2534
2661
  if (!env.AGENTKIT_CONVERSATIONS || !conversationId) {
2535
2662
  return [];
@@ -2863,6 +2990,68 @@ function titleFromMessage(content) {
2863
2990
  return compact.length <= 80 ? compact : \`\${compact.slice(0, 77)}...\`;
2864
2991
  }
2865
2992
 
2993
+ function readStoredUsage(row) {
2994
+ const usage = {};
2995
+ const inputTokens = normalizeRuntimeInteger(row.input_tokens);
2996
+ const outputTokens = normalizeRuntimeInteger(row.output_tokens);
2997
+ const totalTokens = normalizeRuntimeInteger(row.total_tokens);
2998
+ const cost = normalizeRuntimeCost(parseStoredJson(row.cost_json, {}));
2999
+
3000
+ if (inputTokens !== undefined) usage.inputTokens = inputTokens;
3001
+ if (outputTokens !== undefined) usage.outputTokens = outputTokens;
3002
+ if (totalTokens !== undefined) usage.totalTokens = totalTokens;
3003
+ if (Object.keys(cost).length > 0) usage.cost = cost;
3004
+
3005
+ return usage;
3006
+ }
3007
+
3008
+ function normalizeRuntimeUsage(value) {
3009
+ if (!isRecord(value)) {
3010
+ return {};
3011
+ }
3012
+
3013
+ const usage = {};
3014
+ const inputTokens = normalizeRuntimeInteger(value.inputTokens ?? value.input_tokens);
3015
+ const outputTokens = normalizeRuntimeInteger(value.outputTokens ?? value.output_tokens);
3016
+ const totalTokens = normalizeRuntimeInteger(value.totalTokens ?? value.total_tokens);
3017
+ const cost = normalizeRuntimeCost(value.cost);
3018
+
3019
+ if (inputTokens !== undefined) usage.inputTokens = inputTokens;
3020
+ if (outputTokens !== undefined) usage.outputTokens = outputTokens;
3021
+ if (totalTokens !== undefined) usage.totalTokens = totalTokens;
3022
+ if (Object.keys(cost).length > 0) usage.cost = cost;
3023
+
3024
+ return usage;
3025
+ }
3026
+
3027
+ function normalizeRuntimeCost(value) {
3028
+ if (!isRecord(value)) {
3029
+ return {};
3030
+ }
3031
+
3032
+ const cost = {};
3033
+
3034
+ for (const field of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
3035
+ const normalized = normalizeRuntimeNumber(value[field]);
3036
+
3037
+ if (normalized !== undefined) {
3038
+ cost[field] = normalized;
3039
+ }
3040
+ }
3041
+
3042
+ return cost;
3043
+ }
3044
+
3045
+ function normalizeRuntimeInteger(value) {
3046
+ const normalized = normalizeRuntimeNumber(value);
3047
+ return normalized !== undefined && Number.isInteger(normalized) ? normalized : undefined;
3048
+ }
3049
+
3050
+ function normalizeRuntimeNumber(value) {
3051
+ const parsed = typeof value === "string" && value.trim() ? Number(value) : value;
3052
+ return typeof parsed === "number" && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
3053
+ }
3054
+
2866
3055
  function parseStoredJson(value, fallback) {
2867
3056
  if (typeof value !== "string") {
2868
3057
  return fallback;
@@ -2944,6 +3133,10 @@ CREATE TABLE IF NOT EXISTS runs (
2944
3133
  status TEXT NOT NULL,
2945
3134
  error_code TEXT,
2946
3135
  error_message TEXT,
3136
+ input_tokens INTEGER,
3137
+ output_tokens INTEGER,
3138
+ total_tokens INTEGER,
3139
+ cost_json TEXT,
2947
3140
  created_at TEXT NOT NULL,
2948
3141
  completed_at TEXT
2949
3142
  );
@@ -27,7 +27,7 @@ Use this first inside an AgentKit Agent Capsule.
27
27
  - Improve from hosted or local production evidence: `skills/agentkit-improve/SKILL.md`
28
28
  - Prepare hosted deploy: `skills/agentkit-deploy/SKILL.md`
29
29
  - Work with secrets, external APIs, public access, channels, or real data: `skills/agentkit-security/SKILL.md`
30
- - Add or debug website, Telegram, WhatsApp, or Discord channels: `skills/agentkit-channels/SKILL.md`
30
+ - Add or debug website, Telegram, WhatsApp, Discord, or Slack channels: `skills/agentkit-channels/SKILL.md`
31
31
  - Investigate command failures: `skills/agentkit-troubleshooting/SKILL.md`
32
32
 
33
33
  For a compact docs map, read `references/docs-router.md`.
@@ -6,9 +6,10 @@ Prefer the narrowest source that covers the task.
6
6
  - Tools, schemas, secrets, and direct tool tests: `docs/guides/add-tool.md`
7
7
  - Knowledge sources, indexing, search, and hosted sync: `docs/guides/add-knowledge.md`
8
8
  - Evals and deterministic side-effect guards: `docs/guides/run-evals.md`
9
+ - Safe AgentKit product feedback drafts/submission: `docs/guides/send-feedback.md`
9
10
  - Real provider setup: `docs/guides/use-provider.md`
10
11
  - Deploy readiness, managed secrets, smoke checks, hosted UI: `docs/guides/prepare-deploy.md`
11
- - Channels: `docs/guides/add-channel.md`, `connect-discord.md`, `connect-telegram.md`, `connect-whatsapp-zapster.md`, `debug-channel.md`
12
+ - Channels: `docs/guides/add-channel.md`, `connect-discord.md`, `connect-slack.md`, `connect-telegram.md`, `connect-whatsapp-zapster.md`, `debug-channel.md`
12
13
  - Security: `docs/guides/security-rules.md`
13
14
 
14
15
  Use `npm run agentkit -- docs full` only for a complete-contract audit, framework internals, or a behavior not covered by the task guide.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agentkit-channels
3
- description: Use when adding, connecting, testing, buffering, transcribing audio, or debugging AgentKit website, Telegram, WhatsApp, or Discord channels, including channel config helpers, provider secrets, webhook setup, channel tests, delivery logs, burst-message buffers, and transcription provider secrets.
3
+ description: Use when adding, connecting, testing, buffering, transcribing audio, or debugging AgentKit website, Telegram, WhatsApp, Discord, or Slack channels, including channel config helpers, provider secrets, webhook setup, channel tests, delivery logs, burst-message buffers, and transcription provider secrets.
4
4
  ---
5
5
 
6
6
  # AgentKit Channels
@@ -82,6 +82,7 @@ npm run agentkit -- channels connect telegram support-telegram
82
82
  npm run agentkit -- channels add whatsapp support-whatsapp --provider zapster
83
83
  npm run agentkit -- channels connect discord support-discord
84
84
  npm run agentkit -- channels connect discord server-discord --mode bot
85
+ npm run agentkit -- channels connect slack support-slack
85
86
  npm run agentkit -- channels doctor support-telegram
86
87
  npm run agentkit -- channels test support-telegram --message "hello"
87
88
  npm run agentkit -- channels test-audio support-telegram --fixture voice-note
@@ -92,6 +93,7 @@ npm run agentkit -- channels deliveries list support-telegram
92
93
  ## References
93
94
 
94
95
  - `references/discord.md`
96
+ - `references/slack.md`
95
97
  - `references/telegram.md`
96
98
  - `references/whatsapp-zapster.md`
97
99
  - `references/channel-buffering.md`
@@ -0,0 +1,56 @@
1
+ # Slack Channel
2
+
3
+ Slack uses Events API webhooks.
4
+
5
+ V1 supports:
6
+
7
+ - `app_mention` in channels.
8
+ - `message.im` direct messages.
9
+ - Outbound replies through `chat.postMessage`.
10
+
11
+ V1 does not support Socket Mode, files, audio transcription, app-management automation, or all-channel message listening.
12
+
13
+ ## Required Secrets
14
+
15
+ ```txt
16
+ SLACK_BOT_TOKEN
17
+ SLACK_SIGNING_SECRET
18
+ ```
19
+
20
+ ## Config
21
+
22
+ ```ts
23
+ slackChannel({
24
+ name: "support-slack",
25
+ })
26
+ ```
27
+
28
+ ## Commands
29
+
30
+ ```sh
31
+ agentkit deploy
32
+ agentkit secret set SLACK_BOT_TOKEN --stdin
33
+ agentkit secret set SLACK_SIGNING_SECRET --stdin
34
+ agentkit channels connect slack support-slack
35
+ agentkit channels test support-slack --message "hello"
36
+ agentkit channels deliveries list support-slack
37
+ ```
38
+
39
+ ## Slack Setup
40
+
41
+ In Slack app configuration:
42
+
43
+ 1. Basic Information: copy Signing Secret into `SLACK_SIGNING_SECRET`.
44
+ 2. OAuth & Permissions: add bot scopes `app_mentions:read`, `im:history`, and `chat:write`.
45
+ 3. Install or reinstall the app to the workspace.
46
+ 4. Copy the Bot User OAuth Token into `SLACK_BOT_TOKEN`.
47
+ 5. Event Subscriptions: enable events and paste the AgentKit webhook URL.
48
+ 6. Subscribe to bot events `app_mention` and `message.im`.
49
+
50
+ ## Runtime Behavior
51
+
52
+ AgentKit verifies `X-Slack-Signature` and `X-Slack-Request-Timestamp` against the raw body before processing events. It rejects stale timestamps, answers `url_verification` with the literal challenge, skips bot/subtype messages, maps channel mentions to Slack threads, maps DMs to the Slack DM channel and user, and replies with `chat.postMessage`.
53
+
54
+ Outbound Slack text escapes Slack control mentions and disables link-name expansion and unfurls.
55
+
56
+ Slack audio is not supported in V1; do not configure `audio` on `slackChannel`.
@@ -13,6 +13,7 @@ Use this when something fails.
13
13
  2. Run the narrow inspect command before guessing.
14
14
  3. Route to a task skill when the failure points to config, tools, database, provider, evals, deploy, Knowledge, or channels.
15
15
  4. Load `llms-full.txt` only when the narrow skill and guide do not explain the behavior.
16
+ 5. If the failure appears to be an AgentKit bug, missing docs, or unclear recovery path, create a local feedback draft after diagnosis.
16
17
 
17
18
  ## First Commands
18
19
 
@@ -42,6 +43,12 @@ For deploy issues:
42
43
  npm run agentkit -- deploy doctor
43
44
  ```
44
45
 
46
+ For AgentKit product feedback:
47
+
48
+ ```sh
49
+ npm run agentkit -- feedback create --about last-run --kind bug --summary "short concrete summary"
50
+ ```
51
+
45
52
  For production behavior issues:
46
53
 
47
54
  ```sh
@@ -60,3 +67,10 @@ npm run agentkit -- replay .agentkit/improve/<run> --against local
60
67
  - channel secret missing: set hosted managed secret, not source files;
61
68
  - production behavior drift: collect an improve bundle and convert it to regression evals before patching;
62
69
  - local AgentKit skills are stale: run `npm run agentkit -- skills sync`.
70
+
71
+ ## Feedback Rules
72
+
73
+ - `feedback create` writes a local draft only; it does not send anything.
74
+ - Review the draft before `feedback send`.
75
+ - Sending requires `agentkit login --token agk_user_...`.
76
+ - Do not paste `.env` values, provider keys, cookies, client PII, or full private transcripts into feedback.