@andreprado/agentkit 0.1.0-alpha.7 → 0.1.0-alpha.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andreprado/agentkit",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "repository": {
@@ -2,10 +2,11 @@ import { readFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
 
4
4
  import type { ParsedArgs } from "../args";
5
- import { cloudFetch, cloudGet, cloudPost } from "../cloud-client";
5
+ import { cloudFetch, cloudGet, cloudPost, readCloudError } from "../cloud-client";
6
6
  import { parseCloudApiUrl } from "../flags";
7
7
  import { loadAgentCapsule } from "../../runtime/config";
8
8
  import { readLocalDeployStateIfExists, type LocalDeployState } from "../../runtime/core/deploy-state";
9
+ import { loadCapsuleEnv } from "../../runtime/env";
9
10
  import type { AgentChannel } from "../../index";
10
11
 
11
12
  type DeployState = LocalDeployState;
@@ -30,9 +31,51 @@ type CloudDelivery = {
30
31
  event_id?: string;
31
32
  };
32
33
  received_at: string;
34
+ updated_at?: string;
35
+ error?: {
36
+ code: string;
37
+ message: string;
38
+ };
39
+ events?: Array<{
40
+ state: string;
41
+ message: string;
42
+ created_at: string;
43
+ }>;
44
+ };
45
+
46
+ type ChannelSetupResponse = {
47
+ channel: CloudChannel;
48
+ setup?: {
49
+ provider: "telegram";
50
+ webhook_url: string;
51
+ status: "configured";
52
+ bot_username?: string;
53
+ get_webhook_info?: {
54
+ url: string;
55
+ pending_update_count: number;
56
+ };
57
+ };
58
+ };
59
+
60
+ type ChannelDoctorReport = {
61
+ channel: CloudChannel;
62
+ checks: Array<{
63
+ name: string;
64
+ status: "ok" | "warn" | "fail" | "unknown";
65
+ message: string;
66
+ fix?: string;
67
+ }>;
68
+ last_delivery: {
69
+ inbound: CloudDelivery | null;
70
+ outbound: CloudDelivery | null;
71
+ };
72
+ probable_error: string | null;
73
+ suggested_fix: string | null;
33
74
  };
34
75
 
35
76
  export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
77
+ await loadLocalEnvIntoProcess(process.cwd());
78
+
36
79
  const [subcommand, first, second] = args.positional;
37
80
 
38
81
  if (subcommand === "list") {
@@ -70,23 +113,55 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
70
113
  throw new Error("Usage: agentkit channels add <website|telegram|whatsapp> <name> [--provider zapster|meta]");
71
114
  }
72
115
 
73
- const deployState = await readDeployStateRequired(process.cwd());
74
- const provider = channelProviderForCli(type, args.flags.provider);
75
- const localChannel = await readLocalChannelConfig(process.cwd(), type, provider, name);
76
- const payload = {
77
- name,
78
- type,
79
- provider,
80
- secrets: localChannel?.secrets ?? defaultChannelSecretsForCli(type, provider),
81
- ...(localChannel?.limits ? { limits: channelLimitsForApi(localChannel.limits) } : {}),
82
- ...(localChannel?.buffer ? { buffer: localChannel.buffer } : {}),
83
- };
84
- const response = await cloudPost<{ channel: CloudChannel }>(
85
- parseCloudApiUrl(args.flags.api),
86
- `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
87
- payload,
88
- );
116
+ const response = await createHostedChannel(type, name, args.flags.provider, args.flags.api);
89
117
  printChannelStatus(response.channel);
118
+ printSecretRepairHint(response.channel);
119
+ return;
120
+ }
121
+
122
+ if (subcommand === "connect") {
123
+ const type = first;
124
+ const name = second;
125
+
126
+ if (!type || !name || (type !== "website" && type !== "telegram" && type !== "whatsapp")) {
127
+ throw new Error("Usage: agentkit channels connect <website|telegram|whatsapp> <name> [--provider zapster|meta]");
128
+ }
129
+
130
+ const created = await createHostedChannel(type, name, args.flags.provider, args.flags.api);
131
+ let channel = created.channel;
132
+ printChannelStatus(channel);
133
+ printSecretRepairHint(channel);
134
+
135
+ if (hasMissingRequiredSecrets(channel)) {
136
+ console.log("");
137
+ console.log("Connect blocked: set the missing managed secrets, then rerun this same command.");
138
+ console.log("Run: npm run agentkit -- secret sync --from-local");
139
+ process.exitCode = 1;
140
+ return;
141
+ }
142
+
143
+ if (channel.type === "telegram") {
144
+ const setup = await applyTelegramWebhook(channel, args.flags.api);
145
+ channel = setup.channel ?? channel;
146
+ console.log("Telegram webhook confirmed.");
147
+ if (setup.setup?.get_webhook_info) {
148
+ console.log(`Telegram pending updates: ${setup.setup.get_webhook_info.pending_update_count}`);
149
+ }
150
+
151
+ const smoke = await runChannelSmoke(channel, args.flags.api, args.flags.message);
152
+ console.log(`Smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
153
+ printTelegramHumanNextStep(channel.name, setup.setup?.bot_username);
154
+ return;
155
+ }
156
+
157
+ if (channel.type === "whatsapp") {
158
+ console.log("Smoke: skipped until the provider webhook is configured.");
159
+ console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
160
+ } else {
161
+ const smoke = await runChannelSmoke(channel, args.flags.api, args.flags.message);
162
+ console.log(`Smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
163
+ console.log(`Next human step: use ${channel.webhook_url} from your website client.`);
164
+ }
90
165
  return;
91
166
  }
92
167
 
@@ -99,8 +174,10 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
99
174
  printChannelStatus(channel);
100
175
  if (channel.type === "telegram") {
101
176
  if (args.flags.apply) {
102
- await applyTelegramWebhook(channel, args.flags.api);
103
- console.log("Telegram webhook updated.");
177
+ const setup = await applyTelegramWebhook(channel, args.flags.api);
178
+ console.log("Telegram webhook confirmed.");
179
+ printChannelStatus(setup.channel ?? channel);
180
+ printTelegramHumanNextStep(channel.name, setup.setup?.bot_username);
104
181
  } else {
105
182
  console.log(`Setup: call Telegram setWebhook with ${channel.webhook_url}`);
106
183
  console.log("Use --apply to mutate Telegram webhook settings.");
@@ -125,6 +202,24 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
125
202
  return;
126
203
  }
127
204
 
205
+ if (subcommand === "doctor") {
206
+ if (!first) {
207
+ throw new Error("Usage: agentkit channels doctor <name>");
208
+ }
209
+
210
+ const channel = await resolveCloudChannelByName(first, args.flags.api);
211
+ const report = await cloudGet<{ doctor: ChannelDoctorReport }>(
212
+ parseCloudApiUrl(args.flags.api),
213
+ `/v1/channels/${encodeURIComponent(channel.id)}/doctor`,
214
+ );
215
+ printChannelDoctor(report.doctor);
216
+
217
+ if (report.doctor.checks.some((check) => check.status === "fail")) {
218
+ process.exitCode = 1;
219
+ }
220
+ return;
221
+ }
222
+
128
223
  if (subcommand === "test") {
129
224
  if (!first) {
130
225
  throw new Error('Usage: agentkit channels test <name> [--message "hello"] [--fixture <path>]');
@@ -133,14 +228,9 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
133
228
  const channel = await resolveCloudChannelByName(first, args.flags.api);
134
229
  const apiUrl = parseCloudApiUrl(args.flags.api);
135
230
  const body = await channelTestBody(channel, args.flags.message, args.flags.fixture);
136
- const path = new URL(channel.webhook_url).pathname;
137
- const headers = channelTestHeaders(channel);
138
- const response = await cloudFetch(apiUrl, path, {
139
- method: channel.type === "whatsapp" && channel.provider === "meta" ? "POST" : "POST",
140
- headers,
141
- body,
142
- });
143
- const payload = await response.json();
231
+ const signed = await runServerSignedChannelTest(channel, apiUrl, body);
232
+ const response = signed.response;
233
+ const payload = signed.payload;
144
234
 
145
235
  console.log(JSON.stringify(payload, null, 2));
146
236
  if (!response.ok) {
@@ -197,21 +287,113 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
197
287
  }
198
288
 
199
289
  throw new Error(
200
- "Usage: agentkit channels list | add | setup | status | test | deliveries list | deliveries show",
290
+ "Usage: agentkit channels list | add | connect | setup | status | doctor | test | deliveries list | deliveries show",
291
+ );
292
+ }
293
+
294
+ async function createHostedChannel(
295
+ type: "website" | "telegram" | "whatsapp",
296
+ name: string,
297
+ providerFlag: string | boolean | undefined,
298
+ apiFlag: string | boolean | undefined,
299
+ ): Promise<{ channel: CloudChannel }> {
300
+ const deployState = await readDeployStateRequired(process.cwd());
301
+ const provider = channelProviderForCli(type, providerFlag);
302
+ const localChannel = await readLocalChannelConfig(process.cwd(), type, provider, name);
303
+ const payload = {
304
+ name,
305
+ type,
306
+ provider,
307
+ secrets: localChannel?.secrets ?? defaultChannelSecretsForCli(type, provider),
308
+ ...(localChannel?.limits ? { limits: channelLimitsForApi(localChannel.limits) } : {}),
309
+ ...(localChannel?.buffer ? { buffer: localChannel.buffer } : {}),
310
+ };
311
+
312
+ return cloudPost<{ channel: CloudChannel }>(
313
+ parseCloudApiUrl(apiFlag),
314
+ `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
315
+ payload,
316
+ );
317
+ }
318
+
319
+ async function runChannelSmoke(
320
+ channel: CloudChannel,
321
+ apiFlag: string | boolean | undefined,
322
+ messageFlag: string | boolean | undefined,
323
+ ): Promise<{ status: string; delivery_id?: string }> {
324
+ const body = await channelTestBody(channel, messageFlag, undefined);
325
+ const { response, payload } = await runServerSignedChannelTest(channel, parseCloudApiUrl(apiFlag), body);
326
+
327
+ if (!response.ok) {
328
+ throw new Error(readCloudError(payload, response.status));
329
+ }
330
+
331
+ if (isRecord(payload) && typeof payload.status === "string") {
332
+ return {
333
+ status: payload.status,
334
+ ...(typeof payload.delivery_id === "string" ? { delivery_id: payload.delivery_id } : {}),
335
+ };
336
+ }
337
+
338
+ return { status: "unknown" };
339
+ }
340
+
341
+ async function runServerSignedChannelTest(
342
+ channel: CloudChannel,
343
+ apiUrl: string,
344
+ rawBody: string,
345
+ ): Promise<{ response: Response; payload: unknown }> {
346
+ const response = await cloudFetch(apiUrl, `/v1/channels/${encodeURIComponent(channel.id)}/test`, {
347
+ method: "POST",
348
+ headers: { "Content-Type": "application/json" },
349
+ body: JSON.stringify({ raw_body: rawBody }),
350
+ });
351
+ const payload = await response.json().catch(() => null);
352
+
353
+ if (!response.ok && isUnsupportedChannelTestRoute(payload)) {
354
+ return runLocallySignedChannelTest(channel, apiUrl, rawBody);
355
+ }
356
+
357
+ return { response, payload };
358
+ }
359
+
360
+ async function runLocallySignedChannelTest(
361
+ channel: CloudChannel,
362
+ apiUrl: string,
363
+ rawBody: string,
364
+ ): Promise<{ response: Response; payload: unknown }> {
365
+ const path = new URL(channel.webhook_url).pathname;
366
+ const headers = channelTestHeaders(channel);
367
+ const response = await cloudFetch(apiUrl, path, {
368
+ method: "POST",
369
+ headers,
370
+ body: rawBody,
371
+ });
372
+ const payload = await response.json().catch(() => null);
373
+
374
+ return { response, payload };
375
+ }
376
+
377
+ function isUnsupportedChannelTestRoute(payload: unknown): boolean {
378
+ return (
379
+ isRecord(payload) &&
380
+ isRecord(payload.error) &&
381
+ (payload.error.code === "not_found" ||
382
+ payload.error.code === "channel_store_not_configured" ||
383
+ payload.error.code === "channel_delivery_not_found")
201
384
  );
202
385
  }
203
386
 
204
387
  async function applyTelegramWebhook(
205
388
  channel: CloudChannel,
206
389
  apiFlag: string | boolean | undefined,
207
- ): Promise<void> {
390
+ ): Promise<ChannelSetupResponse> {
208
391
  try {
209
- await cloudPost(
392
+ return await cloudPost<ChannelSetupResponse>(
210
393
  parseCloudApiUrl(apiFlag),
211
394
  `/v1/channels/${encodeURIComponent(channel.id)}/setup`,
212
395
  {},
213
396
  );
214
- return;
215
397
  } catch (error) {
216
398
  if (!shouldFallbackToLocalTelegramSetup(error)) {
217
399
  throw error;
@@ -226,9 +408,10 @@ async function applyTelegramWebhook(
226
408
  }
227
409
 
228
410
  let response: Response;
411
+ const apiBaseUrl = telegramApiBaseUrlFromEnv();
229
412
 
230
413
  try {
231
- response = await fetch(`https://api.telegram.org/bot${botToken}/setWebhook`, {
414
+ response = await fetch(new URL(`/bot${botToken}/setWebhook`, apiBaseUrl).href, {
232
415
  method: "POST",
233
416
  headers: { "Content-Type": "application/json" },
234
417
  body: JSON.stringify({
@@ -246,6 +429,40 @@ async function applyTelegramWebhook(
246
429
  if (!response.ok || !isTelegramOk(payload)) {
247
430
  throw new Error(readTelegramSetupError(payload, response.status));
248
431
  }
432
+
433
+ const webhookInfo = await fetch(new URL(`/bot${botToken}/getWebhookInfo`, apiBaseUrl).href)
434
+ .then((telegramResponse) => telegramResponse.json())
435
+ .catch(() => null);
436
+ const webhookUrl =
437
+ webhookInfo &&
438
+ typeof webhookInfo === "object" &&
439
+ "result" in webhookInfo &&
440
+ webhookInfo.result &&
441
+ typeof webhookInfo.result === "object" &&
442
+ "url" in webhookInfo.result &&
443
+ typeof webhookInfo.result.url === "string"
444
+ ? webhookInfo.result.url
445
+ : "";
446
+
447
+ if (webhookUrl !== channel.webhook_url) {
448
+ throw new Error(`Telegram getWebhookInfo returned ${webhookUrl || "no webhook URL"} instead of ${channel.webhook_url}.`);
449
+ }
450
+
451
+ return {
452
+ channel: {
453
+ ...channel,
454
+ status: "connected",
455
+ },
456
+ setup: {
457
+ provider: "telegram",
458
+ webhook_url: channel.webhook_url,
459
+ status: "configured",
460
+ get_webhook_info: {
461
+ url: webhookUrl,
462
+ pending_update_count: 0,
463
+ },
464
+ },
465
+ };
249
466
  }
250
467
 
251
468
  function shouldFallbackToLocalTelegramSetup(error: unknown): boolean {
@@ -270,6 +487,33 @@ function readTelegramSetupError(payload: unknown, status: number): string {
270
487
  return `Telegram setWebhook failed with HTTP ${status}.`;
271
488
  }
272
489
 
490
+ function telegramApiBaseUrlFromEnv(): string {
491
+ return process.env.AGENTKIT_TELEGRAM_API_BASE_URL ?? "https://api.telegram.org";
492
+ }
493
+
494
+ async function loadLocalEnvIntoProcess(cwd: string): Promise<void> {
495
+ const env = await loadCapsuleEnv(cwd, process.env);
496
+
497
+ for (const [key, value] of Object.entries(env)) {
498
+ if (value !== undefined && process.env[key] === undefined) {
499
+ process.env[key] = value;
500
+ }
501
+ }
502
+ }
503
+
504
+ function printTelegramHumanNextStep(channelName: string, botUsername: string | undefined): void {
505
+ console.log("");
506
+ console.log("Next human step:");
507
+ console.log(` Open ${botUsername ? `@${botUsername}` : "your Telegram bot"} in Telegram`);
508
+ console.log(" Send /start");
509
+ console.log(' Send "hello"');
510
+ console.log(` Then run: npm run agentkit -- channels deliveries list ${channelName}`);
511
+ }
512
+
513
+ function isRecord(value: unknown): value is Record<string, unknown> {
514
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
515
+ }
516
+
273
517
  async function readDeployStateIfExists(cwd: string): Promise<DeployState | null> {
274
518
  return readLocalDeployStateIfExists(cwd);
275
519
  }
@@ -308,8 +552,14 @@ function printChannelStatus(channel: CloudChannel): void {
308
552
  console.log(`Webhook URL: ${channel.webhook_url}`);
309
553
  }
310
554
  console.log("Required secrets:");
311
- for (const secret of channel.required_secrets) {
312
- console.log(` ${secret.name}: ${secret.status}`);
555
+ if (channel.required_secrets.length === 0 && defaultChannelSecretsForCli(channel.type, channel.provider).length > 0) {
556
+ console.log(
557
+ ` control-plane channel metadata bug: required_secrets is empty for ${channel.type}/${channel.provider}`,
558
+ );
559
+ } else {
560
+ for (const secret of channel.required_secrets) {
561
+ console.log(` ${secret.name}: ${secret.status}`);
562
+ }
313
563
  }
314
564
  if (channel.buffer) {
315
565
  if (channel.buffer.mode === "off") {
@@ -322,6 +572,26 @@ function printChannelStatus(channel: CloudChannel): void {
322
572
  }
323
573
  }
324
574
 
575
+ function printSecretRepairHint(channel: CloudChannel): void {
576
+ if (channel.required_secrets.length > 0 || defaultChannelSecretsForCli(channel.type, channel.provider).length === 0) {
577
+ return;
578
+ }
579
+
580
+ console.log(
581
+ "Hint: control-plane channel metadata bug. Rerun add/connect against the updated control-plane to repair required_secrets.",
582
+ );
583
+ }
584
+
585
+ function hasMissingRequiredSecrets(channel: CloudChannel): boolean {
586
+ const expected = defaultChannelSecretsForCli(channel.type, channel.provider);
587
+
588
+ if (channel.required_secrets.length === 0 && expected.length > 0) {
589
+ return true;
590
+ }
591
+
592
+ return channel.required_secrets.some((secret) => secret.status !== "set");
593
+ }
594
+
325
595
  async function printChannelDeliverySummary(
326
596
  channel: CloudChannel,
327
597
  apiFlag: string | boolean | undefined,
@@ -339,6 +609,51 @@ async function printChannelDeliverySummary(
339
609
  console.log("Provider health: not_checked");
340
610
  }
341
611
 
612
+ function printChannelDoctor(report: ChannelDoctorReport): void {
613
+ console.log(`Channel doctor: ${report.channel.name}`);
614
+ console.log("");
615
+ console.log("Checks:");
616
+ for (const check of report.checks) {
617
+ console.log(` ${formatCheckStatus(check.status)} ${check.name}: ${check.message}`);
618
+ if (check.fix) {
619
+ console.log(` Fix: ${check.fix}`);
620
+ }
621
+ }
622
+
623
+ console.log("");
624
+ console.log("Last delivery:");
625
+ console.log(
626
+ ` Inbound: ${report.last_delivery.inbound ? `${report.last_delivery.inbound.status} ${report.last_delivery.inbound.updated_at ?? report.last_delivery.inbound.received_at}` : "none"}`,
627
+ );
628
+ console.log(
629
+ ` Outbound: ${report.last_delivery.outbound ? `${report.last_delivery.outbound.status} ${report.last_delivery.outbound.updated_at ?? report.last_delivery.outbound.received_at}` : "none"}`,
630
+ );
631
+
632
+ if (report.probable_error) {
633
+ console.log("");
634
+ console.log(`Probable error: ${report.probable_error}`);
635
+ }
636
+ if (report.suggested_fix) {
637
+ console.log(`Suggested fix: ${report.suggested_fix}`);
638
+ }
639
+ }
640
+
641
+ function formatCheckStatus(status: ChannelDoctorReport["checks"][number]["status"]): string {
642
+ if (status === "ok") {
643
+ return "OK";
644
+ }
645
+
646
+ if (status === "warn") {
647
+ return "WARN";
648
+ }
649
+
650
+ if (status === "fail") {
651
+ return "FAIL";
652
+ }
653
+
654
+ return "UNKNOWN";
655
+ }
656
+
342
657
  async function resolveCloudChannelByName(name: string, apiFlag: string | boolean | undefined): Promise<CloudChannel> {
343
658
  const deployState = await readDeployStateRequired(process.cwd());
344
659
  const response = await cloudGet<{ channels: CloudChannel[] }>(
package/src/cli/help.ts CHANGED
@@ -77,8 +77,10 @@ Usage:
77
77
  agentkit conversations trace <conversation-id>
78
78
  agentkit channels list
79
79
  agentkit channels add <website|telegram|whatsapp> <name> [--provider zapster|meta] [--api <url>]
80
+ agentkit channels connect <website|telegram|whatsapp> <name> [--provider zapster|meta] [--api <url>]
80
81
  agentkit channels setup <name> [--apply] [--api <url>]
81
82
  agentkit channels status <name> [--api <url>]
83
+ agentkit channels doctor <name> [--api <url>]
82
84
  agentkit channels test <name> [--message <text>] [--fixture <path>] [--api <url>]
83
85
  agentkit channels deliveries list <name> [--api <url>]
84
86
  agentkit channels deliveries show <delivery-id> [--api <url>]
@@ -112,18 +112,7 @@ export const telegramChannelAdapter: ChannelAdapter = {
112
112
  ];
113
113
  },
114
114
  async sendMessage(input) {
115
- const request = buildTelegramSendMessageRequest(input);
116
-
117
- return {
118
- ok: true,
119
- status: "sent",
120
- providerRequestId: request.idempotencyKey,
121
- providerMetadata: {
122
- method: request.method,
123
- url: request.url,
124
- body: request.body,
125
- },
126
- };
115
+ return sendTelegramMessage(input);
127
116
  },
128
117
  getStatus(input) {
129
118
  if (!input.secrets.TELEGRAM_BOT_TOKEN) {
@@ -152,7 +141,130 @@ export type TelegramSendMessageRequest = {
152
141
  idempotencyKey: string;
153
142
  };
154
143
 
144
+ type TelegramFetch = (url: string, init: RequestInit) => Promise<Response>;
145
+
155
146
  export function buildTelegramSendMessageRequest(input: ChannelSendInput): TelegramSendMessageRequest {
147
+ return buildTelegramSendMessageRequestInternal(input, "<redacted>");
148
+ }
149
+
150
+ export async function sendTelegramMessage(
151
+ input: ChannelSendInput,
152
+ fetcher: TelegramFetch = fetch,
153
+ ): Promise<Awaited<ReturnType<ChannelAdapter["sendMessage"]>>> {
154
+ if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
155
+ const request = buildTelegramSendMessageRequest(input);
156
+
157
+ return {
158
+ ok: true,
159
+ status: "sent",
160
+ providerRequestId: request.idempotencyKey,
161
+ providerMetadata: {
162
+ method: request.method,
163
+ url: request.url,
164
+ body: request.body,
165
+ },
166
+ };
167
+ }
168
+
169
+ const token = input.secrets.TELEGRAM_BOT_TOKEN;
170
+
171
+ if (!token) {
172
+ return {
173
+ ok: false,
174
+ retryable: false,
175
+ code: "channel_secret_missing",
176
+ message: "Secret TELEGRAM_BOT_TOKEN is not set for this channel.",
177
+ };
178
+ }
179
+
180
+ let request: TelegramSendMessageRequest;
181
+ let actualRequest: TelegramSendMessageRequest;
182
+
183
+ try {
184
+ request = buildTelegramSendMessageRequest(input);
185
+ actualRequest = buildTelegramSendMessageRequestInternal(input, token);
186
+ } catch (error) {
187
+ return {
188
+ ok: false,
189
+ retryable: false,
190
+ code: "channel_send_failed",
191
+ message: error instanceof Error ? error.message : String(error),
192
+ };
193
+ }
194
+
195
+ let response: Response;
196
+
197
+ try {
198
+ response = await fetcher(actualRequest.url, {
199
+ method: actualRequest.method,
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ },
203
+ body: JSON.stringify(actualRequest.body),
204
+ });
205
+ } catch (error) {
206
+ const errorMessage = (error instanceof Error ? error.message : String(error)).replace(token, "<redacted>");
207
+
208
+ return {
209
+ ok: false,
210
+ retryable: true,
211
+ code: "channel_provider_unavailable",
212
+ message: `Telegram sendMessage request failed before a response: ${errorMessage}`,
213
+ providerRequestId: request.idempotencyKey,
214
+ providerMetadata: {
215
+ method: request.method,
216
+ url: request.url,
217
+ body: request.body,
218
+ },
219
+ };
220
+ }
221
+
222
+ const payload = await response.json().catch(() => null);
223
+
224
+ if (!response.ok || !isRecord(payload) || payload.ok !== true) {
225
+ const description =
226
+ isRecord(payload) && typeof payload.description === "string"
227
+ ? payload.description
228
+ : `Telegram sendMessage returned HTTP ${response.status}.`;
229
+ const syntheticExpectedFailure = isSyntheticTelegramChatId(request.body.chat_id) && /chat not found/i.test(description);
230
+
231
+ return {
232
+ ok: false,
233
+ retryable: syntheticExpectedFailure ? false : response.status === 429 || response.status >= 500,
234
+ code: syntheticExpectedFailure ? "synthetic_expected_failure" : "channel_send_failed",
235
+ message: syntheticExpectedFailure
236
+ ? "Synthetic Telegram smoke reached AgentKit. Telegram correctly rejected the fake chat ID."
237
+ : description,
238
+ providerRequestId: request.idempotencyKey,
239
+ providerMetadata: {
240
+ method: request.method,
241
+ url: request.url,
242
+ body: request.body,
243
+ },
244
+ };
245
+ }
246
+
247
+ const result = isRecord(payload.result) ? payload.result : undefined;
248
+ const messageId = readNumberishString(result ?? {}, "message_id");
249
+
250
+ return {
251
+ ok: true,
252
+ status: "sent",
253
+ providerRequestId: request.idempotencyKey,
254
+ ...(messageId ? { providerMessageId: messageId } : {}),
255
+ providerMetadata: {
256
+ method: request.method,
257
+ url: request.url,
258
+ body: request.body,
259
+ },
260
+ };
261
+ }
262
+
263
+ function isSyntheticTelegramChatId(chatId: string): boolean {
264
+ return chatId === "456" || chatId.startsWith("agentkit-synthetic-");
265
+ }
266
+
267
+ function buildTelegramSendMessageRequestInternal(input: ChannelSendInput, token: string): TelegramSendMessageRequest {
156
268
  const chatId = input.externalIdentity.key.match(/:chat:([^:]+)$/)?.[1];
157
269
 
158
270
  if (!chatId) {
@@ -161,7 +273,7 @@ export function buildTelegramSendMessageRequest(input: ChannelSendInput): Telegr
161
273
 
162
274
  return {
163
275
  method: "POST",
164
- url: "https://api.telegram.org/bot<redacted>/sendMessage",
276
+ url: `https://api.telegram.org/bot${token}/sendMessage`,
165
277
  body: {
166
278
  chat_id: chatId,
167
279
  text: input.message.content,
@@ -5,12 +5,17 @@ export type ChannelMessageContentType = "text";
5
5
  export type ChannelEventKind = "message" | "delivery_status" | "unsupported";
6
6
  export type ChannelDeliveryDirection = "inbound" | "outbound";
7
7
  export type ChannelDeliveryState =
8
+ | "webhook_received"
8
9
  | "received"
9
10
  | "validated"
10
11
  | "duplicate"
11
12
  | "buffered"
12
13
  | "queued"
13
14
  | "running"
15
+ | "agent_completed"
16
+ | "outbound_sent"
17
+ | "provider_failed"
18
+ | "synthetic_expected_failure"
14
19
  | "sent"
15
20
  | "delivered"
16
21
  | "failed"
@@ -90,7 +95,7 @@ export type ChannelSendResult =
90
95
  | {
91
96
  ok: false;
92
97
  retryable: boolean;
93
- code: "channel_secret_missing" | "channel_provider_unavailable" | "channel_send_failed";
98
+ code: "channel_secret_missing" | "channel_provider_unavailable" | "channel_send_failed" | "synthetic_expected_failure";
94
99
  message: string;
95
100
  providerRequestId?: string;
96
101
  providerMetadata?: Record<string, unknown>;
@@ -12,9 +12,9 @@ Channels receive user messages. Tools let the agent call external systems. Keep
12
12
  1. Add channel helpers in `agentkit.config.ts`.
13
13
  2. Keep `runtime: "edge"` and `storage.driver: "agentkit"`.
14
14
  3. Deploy before hosted channel creation.
15
- 4. Add channel resources through the CLI.
16
- 5. Configure provider secrets as managed secrets.
17
- 6. Test and inspect delivery logs.
15
+ 4. Configure provider secrets as managed secrets.
16
+ 5. Connect channel resources through the CLI.
17
+ 6. Test, doctor, and inspect delivery logs.
18
18
 
19
19
  ## Buffering
20
20
 
@@ -43,9 +43,9 @@ npm run agentkit -- inspect
43
43
  npm run agentkit -- deploy
44
44
  npm run agentkit -- channels list
45
45
  npm run agentkit -- channels add website website-chat
46
- npm run agentkit -- channels add telegram support-telegram
46
+ npm run agentkit -- channels connect telegram support-telegram
47
47
  npm run agentkit -- channels add whatsapp support-whatsapp --provider zapster
48
- npm run agentkit -- channels setup support-telegram
48
+ npm run agentkit -- channels doctor support-telegram
49
49
  npm run agentkit -- channels test support-telegram --message "hello"
50
50
  npm run agentkit -- channels deliveries list support-telegram
51
51
  ```
@@ -53,6 +53,6 @@ Expected delivery states:
53
53
  buffered
54
54
  queued
55
55
  running
56
- sent
56
+ agent_completed
57
+ outbound_sent
57
58
  ```
58
-
@@ -5,6 +5,7 @@ Start with:
5
5
  ```sh
6
6
  agentkit channels list
7
7
  agentkit channels status <name>
8
+ agentkit channels doctor <name>
8
9
  agentkit channels test <name> --message "hello"
9
10
  agentkit channels deliveries list <name> --since 24h
10
11
  agentkit channels deliveries show <delivery-id>
@@ -13,15 +14,17 @@ agentkit channels deliveries show <delivery-id>
13
14
  Common states:
14
15
 
15
16
  ```txt
16
- received
17
+ webhook_received
17
18
  validated
18
19
  duplicate
19
20
  buffered
20
21
  queued
21
22
  running
22
- sent
23
+ agent_completed
24
+ outbound_sent
23
25
  delivered
24
- failed
26
+ provider_failed
27
+ synthetic_expected_failure
25
28
  dead_lettered
26
29
  skipped
27
30
  ```
@@ -34,4 +37,5 @@ Common errors:
34
37
  - `channel_payload_invalid`: malformed or unsupported provider payload.
35
38
  - `channel_event_duplicate`: provider retry; do not create a second run.
36
39
  - `channel_limit_exceeded`: backpressure skipped the message.
40
+ - `synthetic_expected_failure`: a synthetic test reached AgentKit, but the provider correctly rejected a fake test recipient.
37
41
  - `buffered` delivery state: message is waiting for the channel quiet window or max wait before one coalesced agent run is queued.
@@ -11,15 +11,16 @@ Commands:
11
11
 
12
12
  ```sh
13
13
  agentkit deploy
14
- agentkit channels add telegram support-telegram
15
- agentkit channels setup support-telegram
16
- agentkit channels setup support-telegram --apply
14
+ agentkit secret set TELEGRAM_BOT_TOKEN --stdin
15
+ agentkit secret set TELEGRAM_WEBHOOK_SECRET --stdin
16
+ agentkit channels connect telegram support-telegram
17
+ agentkit channels doctor support-telegram
17
18
  agentkit channels status support-telegram
18
19
  agentkit channels test support-telegram --message "hello"
19
20
  agentkit channels deliveries list support-telegram
20
21
  ```
21
22
 
22
- `setup --apply` calls Telegram `setWebhook`. Use it only when real Telegram secrets are present.
23
+ `connect` creates or reuses the hosted channel, validates managed secrets, calls Telegram `setWebhook`, confirms `getWebhookInfo`, runs a synthetic smoke, and prints the human Telegram steps. Use `setup --apply` only when you need to repeat webhook registration without running smoke.
23
24
 
24
25
  Buffer rapid Telegram messages:
25
26