@andreprado/agentkit 0.1.0-alpha.12 → 0.1.0-alpha.14

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.
@@ -21,6 +21,7 @@ agentkit channels setup support-telegram
21
21
  agentkit channels status support-telegram
22
22
  agentkit channels test support-telegram --message "hello"
23
23
  agentkit channels deliveries list support-telegram
24
+ agentkit channels buffers list support-telegram
24
25
  ```
25
26
 
26
27
  Use `--api <url>` with hosted commands when testing against a non-default AgentKit Cloud API.
@@ -76,6 +77,16 @@ Buffering is scoped to one channel conversation. AgentKit still validates and de
76
77
 
77
78
  Use `buffer: { mode: "off" }` or omit `buffer` to process each inbound message as its own agent run.
78
79
 
80
+ Buffer controls:
81
+
82
+ ```sh
83
+ agentkit channels buffers list support-whatsapp
84
+ agentkit channels buffers show support-whatsapp <conversation-id>
85
+ agentkit channels buffers flush support-whatsapp <conversation-id>
86
+ agentkit channels buffers clear support-whatsapp <conversation-id>
87
+ agentkit channels buffers retry support-whatsapp <conversation-id>
88
+ ```
89
+
79
90
  ## Safety Rules
80
91
 
81
92
  - Never put provider token values in `agentkit.config.ts`.
@@ -93,9 +104,11 @@ agentkit inspect
93
104
  agentkit channels list
94
105
  agentkit channels test support-telegram --message "hello"
95
106
  agentkit channels deliveries list support-telegram
107
+ agentkit channels buffers list support-telegram
96
108
  ```
97
109
 
98
110
  Expected hosted status includes `NAME`, `TYPE`, `PROVIDER`, `STATUS`, and `LAST_EVENT`. Secret output is name plus `set` or `missing`, never the value.
111
+ Outbound provider success is `provider_sent`. Explicit dry-run mode is `adapter_stubbed`, which means the provider was not called.
99
112
 
100
113
  ## Troubleshooting
101
114
 
@@ -351,6 +351,7 @@ Do not ship these into user capsules by default:
351
351
  - account grant/revoke;
352
352
  - backend contract implementation;
353
353
  - Cloudflare/Turso/R2 provisioning internals;
354
+ - release-lane ownership;
354
355
  - package publishing.
355
356
 
356
357
  Those workflows belong in maintainer skills inside the AgentKit repository, not in generated user capsules.
@@ -154,7 +154,7 @@ Add:
154
154
  - `GET /v1/deploys/{deploy_id}/channels`;
155
155
  - `GET /v1/channels/{channel_id}`;
156
156
  - `DELETE /v1/channels/{channel_id}`;
157
- - public webhook routes for `/channels/{channel_id}/{type}/{provider}/webhook`;
157
+ - public webhook routes for `/channels/{channel_name}/{type}/{provider}/webhook`, with old `chn_*` routes accepted for compatibility;
158
158
  - delivery inspection routes for CLI use.
159
159
 
160
160
  The local `npm run agentkit:operator -- serve` path can use an in-memory or local SQLite fake store first. Production Postgres support can follow once the contract is tested. In both stores, persist only secret names and statuses, never secret values.
@@ -192,7 +192,7 @@ Use this boundary for V1 local cloud tests:
192
192
  | --- | --- | --- |
193
193
  | Channel resource metadata | Yes: `channels` table or equivalent fake store keyed by `chn_...`. | Read-only manifest input after deploy. |
194
194
  | Channel secret references | Yes: names and `set`/`missing` status only. | Runtime receives injected secret values by name; no readback. |
195
- | Channel endpoint URL | Yes: generated from deploy URL plus stable channel ID. | Worker routes requests by channel ID, type, and provider. |
195
+ | Channel endpoint URL | Yes: generated from deploy URL plus stable channel name. | Worker forwards deploy ID and routes requests by channel name, type, and provider; old `chn_*` URLs remain compatibility routes. |
196
196
  | Channel delivery records | Yes: `deliveries` table keyed by `del_...` for CLI inspection. | Worker/consumer reports state transitions back to control plane or durable storage. |
197
197
  | Channel event audit records | Yes: compact event records keyed by `chevt_...`, with redacted provider metadata. | Ingress creates or reports event records after provider validation. |
198
198
  | External identity mappings | Fake DO-compatible store for local tests, keyed by `chid_...`. | Durable Object owns strongly consistent identity to conversation mapping. |
@@ -44,7 +44,7 @@ META_WHATSAPP_VERIFY_TOKEN
44
44
  agentkit channels add telegram support-telegram
45
45
  agentkit channels add whatsapp support-whatsapp --provider zapster
46
46
  agentkit channels setup support-telegram --apply
47
- agentkit channels setup support-whatsapp
47
+ agentkit channels connect whatsapp support-whatsapp --provider zapster
48
48
  ```
49
49
 
50
50
  5. Run smoke checks:
@@ -55,6 +55,21 @@ agentkit channels test support-telegram --message "hello"
55
55
  agentkit channels status support-whatsapp
56
56
  agentkit channels test support-whatsapp --message "hello"
57
57
  agentkit channels deliveries list support-telegram --since 1h
58
+ agentkit channels deliveries list support-whatsapp --since 1h
59
+ agentkit channels buffers list support-whatsapp
60
+ ```
61
+
62
+ After deploy, record this exact handoff block:
63
+
64
+ ```txt
65
+ Deploy URL: https://<deploy-host>
66
+ Telegram channel URL: https://<deploy-host>/channels/support-telegram/telegram/telegram/webhook
67
+ Zapster channel URL: https://<deploy-host>/channels/support-whatsapp/whatsapp/zapster/webhook
68
+ Required provider webhook settings: Telegram setWebhook URL/secret token; Zapster webhook URL plus optional ZAPSTER_WEBHOOK_TOKEN query parameter
69
+ Required managed secrets: TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET, ZAPSTER_API_KEY, ZAPSTER_INSTANCE_ID, ZAPSTER_WEBHOOK_ID
70
+ Smoke commands: agentkit channels doctor <name>; agentkit channels test <name> --message "hello"; agentkit channels deliveries list <name> --since 1h
71
+ Rollback command: curl -X DELETE "$AGENTKIT_CLOUD_API_URL/v1/channels/<channel-id>" -H "Authorization: Bearer $AGENTKIT_CLOUD_API_TOKEN"; then remove provider webhook registration before rolling back the Worker
72
+ Known unverified items: any channel with no real provider inbound, no provider_sent outbound, stale buffers, or AGENTKIT_CHANNEL_SEND_DRY_RUN=1
58
73
  ```
59
74
 
60
75
  ## Local Fake Versus Real Provider Gates
@@ -82,6 +97,7 @@ Zapster smoke requires `ZAPSTER_API_KEY`, `ZAPSTER_INSTANCE_ID`, and `AGENTKIT_Z
82
97
  - Invalid Telegram signature or Zapster origin headers return `401` and create a failed delivery.
83
98
  - Duplicate provider event returns `200` with `duplicate` and creates no second queue job.
84
99
  - Accepted inbound text creates one queue job and one outbound delivery.
100
+ - Real outbound success is `provider_sent` and includes a provider message ID. `adapter_stubbed` means dry-run mode built a request but did not call the provider.
85
101
  - Buffered inbound bursts stay in `buffered` state, then flush into one queue job and one outbound delivery.
86
102
  - Retryable failures dead-letter after the configured retry ceiling.
87
103
  - `channel_limit_exceeded` does not create queue backlog.
@@ -95,7 +95,7 @@ agentkit channels deliveries show <delivery-id>
95
95
  Expected webhook URL shape:
96
96
 
97
97
  ```txt
98
- https://<deploy-host>/channels/chn_<id>/telegram/webhook
98
+ https://<deploy-host>/channels/support-telegram/telegram/telegram/webhook
99
99
  ```
100
100
 
101
101
  ## Troubleshooting
@@ -12,11 +12,11 @@ Use this after `whatsappChannel({ name: "support-whatsapp", provider: "zapster"
12
12
 
13
13
  ```sh
14
14
  agentkit deploy
15
- agentkit channels add whatsapp support-whatsapp --provider zapster
16
- agentkit channels setup support-whatsapp
15
+ agentkit channels connect whatsapp support-whatsapp --provider zapster
17
16
  agentkit channels status support-whatsapp
18
17
  agentkit channels test support-whatsapp --message "hello"
19
18
  agentkit channels deliveries list support-whatsapp --since 24h
19
+ agentkit channels buffers list support-whatsapp
20
20
  ```
21
21
 
22
22
  Required secrets:
@@ -80,13 +80,13 @@ whatsappChannel({
80
80
  Expected webhook URL shape:
81
81
 
82
82
  ```txt
83
- https://<deploy-host>/channels/chn_<id>/whatsapp/zapster/webhook
83
+ https://<deploy-host>/channels/support-whatsapp/whatsapp/zapster/webhook
84
84
  ```
85
85
 
86
86
  If the channel declares `ZAPSTER_WEBHOOK_TOKEN`, register the Zapster URL with the token as a query parameter:
87
87
 
88
88
  ```txt
89
- https://<deploy-host>/channels/chn_<id>/whatsapp/zapster/webhook?token=<ZAPSTER_WEBHOOK_TOKEN>
89
+ https://<deploy-host>/channels/support-whatsapp/whatsapp/zapster/webhook?token=<ZAPSTER_WEBHOOK_TOKEN>
90
90
  ```
91
91
 
92
92
  ## Safety Rules
@@ -98,7 +98,9 @@ https://<deploy-host>/channels/chn_<id>/whatsapp/zapster/webhook?token=<ZAPSTER_
98
98
  - Use optional `ZAPSTER_WEBHOOK_TOKEN` in the webhook URL when the endpoint should require an extra secret known only to AgentKit and Zapster.
99
99
  - Unsupported media should be logged as skipped/unsupported without creating an agent run.
100
100
  - AgentKit handles Zapster `message.received` envelopes with event id at `id`, message text at `data.content.text`, and contact identity at `data.sender.id`.
101
- - Outbound replies call `POST https://api.zapsterapi.com/v1/wa/messages` with bearer auth and a JSON body containing `recipient`, `text`, and `instance_id`. Only set `AGENTKIT_CHANNEL_SEND_DRY_RUN=1` in tests when Zapster should not receive a real message.
101
+ - Outbound replies call `POST https://api.zapsterapi.com/v1/wa/messages` with bearer auth and a JSON body containing `recipient`, `text`, and `instance_id`.
102
+ - Real provider success is recorded as `provider_sent` only when Zapster returns a provider message ID.
103
+ - Only set `AGENTKIT_CHANNEL_SEND_DRY_RUN=1` in tests when Zapster should not receive a real message. Dry-run deliveries are recorded as `adapter_stubbed`, not sent.
102
104
 
103
105
  ## Verification
104
106
 
@@ -107,6 +109,10 @@ agentkit channels status support-whatsapp
107
109
  agentkit channels test support-whatsapp --message "hello"
108
110
  agentkit channels deliveries list support-whatsapp
109
111
  agentkit channels deliveries show <delivery-id>
112
+ agentkit channels buffers list support-whatsapp
113
+ agentkit channels buffers flush support-whatsapp <conversation-id>
114
+ agentkit channels buffers clear support-whatsapp <conversation-id>
115
+ agentkit channels buffers retry support-whatsapp <conversation-id>
110
116
  ```
111
117
 
112
118
  ## Troubleshooting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andreprado/agentkit",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "repository": {
@@ -20,7 +20,6 @@
20
20
  "docs",
21
21
  "!docs/guides/backend-contracts.md",
22
22
  "!docs/guides/debug-channel.md",
23
- "!docs/guides/deploy-agentkit-cloud-hostinger.md",
24
23
  "README.md"
25
24
  ],
26
25
  "publishConfig": {
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -97,6 +97,7 @@ export async function readCloudAuth(): Promise<CloudAuthConfig | null> {
97
97
  export async function writeCloudAuth(config: CloudAuthConfig): Promise<void> {
98
98
  await mkdir(join(homedir(), ".agentkit"), { recursive: true });
99
99
  await writeFile(cloudAuthPath(), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
100
+ await chmod(cloudAuthPath(), 0o600);
100
101
  }
101
102
 
102
103
  export async function clearCloudAuth(): Promise<void> {
@@ -178,13 +179,17 @@ function cloudAuthMatchesApiUrl(auth: CloudAuthConfig, apiUrl: string): boolean
178
179
  export async function cloudApiRequest(apiUrl: string, path: string, init: RequestInit): Promise<unknown> {
179
180
  const auth = await readCloudAuth();
180
181
  const headers = new Headers(init.headers);
182
+ const apiToken =
183
+ auth?.source === "file" && !cloudAuthMatchesApiUrl(auth, apiUrl)
184
+ ? undefined
185
+ : auth?.token;
181
186
 
182
187
  if (!headers.has("Content-Type") && init.body !== undefined) {
183
188
  headers.set("Content-Type", "application/json");
184
189
  }
185
190
 
186
- if (auth?.token && !headers.has("Authorization")) {
187
- headers.set("Authorization", `Bearer ${auth.token}`);
191
+ if (apiToken && !headers.has("Authorization")) {
192
+ headers.set("Authorization", `Bearer ${apiToken}`);
188
193
  }
189
194
 
190
195
  const response = await fetch(new URL(path, parseCloudApiUrl(apiUrl)).href, {
@@ -43,6 +43,35 @@ type CloudDelivery = {
43
43
  }>;
44
44
  };
45
45
 
46
+ type CloudChannelBuffer = {
47
+ channel_id: string;
48
+ deploy_id: string;
49
+ conversation_id: string;
50
+ job_id?: string;
51
+ delivery_id?: string;
52
+ buffered_delivery_ids: string[];
53
+ buffered_message_count: number;
54
+ buffered_char_count: number;
55
+ status: string;
56
+ first_received_at: string;
57
+ last_received_at: string;
58
+ available_at?: string;
59
+ age_ms: number;
60
+ stale: boolean;
61
+ message_preview: string;
62
+ };
63
+
64
+ type ChannelBufferActionResponse = {
65
+ buffer: CloudChannelBuffer | null;
66
+ job: {
67
+ id: string;
68
+ status: string;
69
+ conversation_id: string;
70
+ } | null;
71
+ cleared?: boolean;
72
+ retried?: boolean;
73
+ };
74
+
46
75
  type ChannelSetupResponse = {
47
76
  channel: CloudChannel;
48
77
  setup?: {
@@ -65,6 +94,7 @@ type ChannelDoctorReport = {
65
94
  message: string;
66
95
  fix?: string;
67
96
  }>;
97
+ buffers: CloudChannelBuffer[];
68
98
  last_delivery: {
69
99
  inbound: CloudDelivery | null;
70
100
  outbound: CloudDelivery | null;
@@ -76,7 +106,7 @@ type ChannelDoctorReport = {
76
106
  export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
77
107
  await loadLocalEnvIntoProcess(process.cwd());
78
108
 
79
- const [subcommand, first, second] = args.positional;
109
+ const [subcommand, first, second, third] = args.positional;
80
110
 
81
111
  if (subcommand === "list") {
82
112
  const deployState = await readDeployStateIfExists(process.cwd());
@@ -154,7 +184,11 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
154
184
  return;
155
185
  }
156
186
 
157
- if (channel.type === "whatsapp") {
187
+ if (channel.type === "whatsapp" && channel.provider === "zapster") {
188
+ const smoke = await runChannelSmoke(channel, args.flags.api, args.flags.message);
189
+ console.log(`Webhook smoke: ${smoke.status}${smoke.delivery_id ? ` (${smoke.delivery_id})` : ""}`);
190
+ console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
191
+ } else if (channel.type === "whatsapp") {
158
192
  console.log("Smoke: skipped until the provider webhook is configured.");
159
193
  console.log(`Next human step: paste ${channel.webhook_url} into ${channel.provider}'s webhook settings.`);
160
194
  } else {
@@ -286,8 +320,53 @@ export async function handleChannelsCommand(args: ParsedArgs): Promise<void> {
286
320
  }
287
321
  }
288
322
 
323
+ if (subcommand === "buffers") {
324
+ if (first === "list") {
325
+ if (!second) {
326
+ throw new Error("Usage: agentkit channels buffers list <name>");
327
+ }
328
+
329
+ const channel = await resolveCloudChannelByName(second, args.flags.api);
330
+ const response = await cloudGet<{ buffers: CloudChannelBuffer[] }>(
331
+ parseCloudApiUrl(args.flags.api),
332
+ `/v1/channels/${encodeURIComponent(channel.id)}/buffers`,
333
+ );
334
+ printChannelBuffers(response.buffers);
335
+ return;
336
+ }
337
+
338
+ if (first === "show") {
339
+ if (!second) {
340
+ throw new Error("Usage: agentkit channels buffers show [<name>] <conversation-id>");
341
+ }
342
+
343
+ const target = await resolveChannelBufferTarget(second, third, args.flags.api);
344
+ const response = await cloudGet<{ buffer: CloudChannelBuffer }>(
345
+ parseCloudApiUrl(args.flags.api),
346
+ `/v1/channels/${encodeURIComponent(target.channel.id)}/buffers/${encodeURIComponent(target.conversationId)}`,
347
+ );
348
+ console.log(JSON.stringify(response.buffer, null, 2));
349
+ return;
350
+ }
351
+
352
+ if (first === "flush" || first === "clear" || first === "retry") {
353
+ if (!second) {
354
+ throw new Error(`Usage: agentkit channels buffers ${first} [<name>] <conversation-id>`);
355
+ }
356
+
357
+ const target = await resolveChannelBufferTarget(second, third, args.flags.api);
358
+ const response = await cloudPost<ChannelBufferActionResponse>(
359
+ parseCloudApiUrl(args.flags.api),
360
+ `/v1/channels/${encodeURIComponent(target.channel.id)}/buffers/${encodeURIComponent(target.conversationId)}/${first}`,
361
+ {},
362
+ );
363
+ printChannelBufferAction(first, response);
364
+ return;
365
+ }
366
+ }
367
+
289
368
  throw new Error(
290
- "Usage: agentkit channels list | add | connect | setup | status | doctor | test | deliveries list | deliveries show",
369
+ "Usage: agentkit channels list | add | connect | setup | status | doctor | test | deliveries list | deliveries show | buffers list | buffers show | buffers flush | buffers clear | buffers retry",
291
370
  );
292
371
  }
293
372
 
@@ -635,6 +714,16 @@ function printChannelDoctor(report: ChannelDoctorReport): void {
635
714
  ` Outbound: ${report.last_delivery.outbound ? `${report.last_delivery.outbound.status} ${report.last_delivery.outbound.updated_at ?? report.last_delivery.outbound.received_at}` : "none"}`,
636
715
  );
637
716
 
717
+ if (report.buffers.length > 0) {
718
+ console.log("");
719
+ console.log("Buffers:");
720
+ for (const buffer of report.buffers) {
721
+ console.log(
722
+ ` ${buffer.conversation_id}: ${buffer.status} messages=${buffer.buffered_message_count} stale=${buffer.stale ? "yes" : "no"}`,
723
+ );
724
+ }
725
+ }
726
+
638
727
  if (report.probable_error) {
639
728
  console.log("");
640
729
  console.log(`Probable error: ${report.probable_error}`);
@@ -660,19 +749,119 @@ function formatCheckStatus(status: ChannelDoctorReport["checks"][number]["status
660
749
  return "UNKNOWN";
661
750
  }
662
751
 
752
+ function printChannelBuffers(buffers: CloudChannelBuffer[]): void {
753
+ if (buffers.length === 0) {
754
+ console.log("No channel buffers found.");
755
+ return;
756
+ }
757
+
758
+ console.log("CONVERSATION\tSTATUS\tMESSAGES\tCHARS\tSTALE\tAVAILABLE_AT\tJOB");
759
+ for (const buffer of buffers) {
760
+ console.log(
761
+ [
762
+ buffer.conversation_id,
763
+ buffer.status,
764
+ String(buffer.buffered_message_count),
765
+ String(buffer.buffered_char_count),
766
+ buffer.stale ? "yes" : "no",
767
+ buffer.available_at ?? "",
768
+ buffer.job_id ?? "",
769
+ ].join("\t"),
770
+ );
771
+ }
772
+ }
773
+
774
+ function printChannelBufferAction(action: string, response: ChannelBufferActionResponse): void {
775
+ const conversationId = response.buffer?.conversation_id ?? response.job?.conversation_id ?? "unknown";
776
+
777
+ if (action === "flush") {
778
+ console.log(
779
+ response.job
780
+ ? `Flushed buffer ${conversationId}; queued job ${response.job.id}.`
781
+ : `No queued buffer was flushed for ${conversationId}.`,
782
+ );
783
+ return;
784
+ }
785
+
786
+ if (action === "clear") {
787
+ console.log(
788
+ response.cleared
789
+ ? `Cleared buffer ${conversationId}.`
790
+ : `No queued buffer was cleared for ${conversationId}.`,
791
+ );
792
+ return;
793
+ }
794
+
795
+ console.log(
796
+ response.retried && response.job
797
+ ? `Retried channel job ${response.job.id} for ${conversationId}.`
798
+ : `No failed or dead-lettered job was retried for ${conversationId}.`,
799
+ );
800
+ }
801
+
663
802
  async function resolveCloudChannelByName(name: string, apiFlag: string | boolean | undefined): Promise<CloudChannel> {
803
+ const channels = await listCloudChannelsForCurrentDeploy(apiFlag);
804
+ const channel = channels.find((candidate) => candidate.name === name);
805
+
806
+ if (!channel) {
807
+ const deployState = await readDeployStateRequired(process.cwd());
808
+ throw new Error(`Channel "${name}" was not found for deploy ${deployState.deploy_id}.`);
809
+ }
810
+
811
+ return channel;
812
+ }
813
+
814
+ async function listCloudChannelsForCurrentDeploy(apiFlag: string | boolean | undefined): Promise<CloudChannel[]> {
664
815
  const deployState = await readDeployStateRequired(process.cwd());
665
816
  const response = await cloudGet<{ channels: CloudChannel[] }>(
666
817
  parseCloudApiUrl(apiFlag),
667
818
  `/v1/deploys/${encodeURIComponent(deployState.deploy_id)}/channels`,
668
819
  );
669
- const channel = response.channels.find((candidate) => candidate.name === name);
670
820
 
671
- if (!channel) {
672
- throw new Error(`Channel "${name}" was not found for deploy ${deployState.deploy_id}.`);
821
+ return response.channels;
822
+ }
823
+
824
+ async function resolveChannelBufferTarget(
825
+ first: string,
826
+ second: string | undefined,
827
+ apiFlag: string | boolean | undefined,
828
+ ): Promise<{ channel: CloudChannel; conversationId: string }> {
829
+ if (second) {
830
+ return {
831
+ channel: await resolveCloudChannelByName(first, apiFlag),
832
+ conversationId: second,
833
+ };
673
834
  }
674
835
 
675
- return channel;
836
+ const conversationId = first;
837
+ const channels = await listCloudChannelsForCurrentDeploy(apiFlag);
838
+ const matches: CloudChannel[] = [];
839
+
840
+ for (const channel of channels) {
841
+ const response = await cloudGet<{ buffers: CloudChannelBuffer[] }>(
842
+ parseCloudApiUrl(apiFlag),
843
+ `/v1/channels/${encodeURIComponent(channel.id)}/buffers`,
844
+ );
845
+
846
+ if (response.buffers.some((buffer) => buffer.conversation_id === conversationId)) {
847
+ matches.push(channel);
848
+ }
849
+ }
850
+
851
+ if (matches.length === 1) {
852
+ return {
853
+ channel: matches[0],
854
+ conversationId,
855
+ };
856
+ }
857
+
858
+ if (matches.length > 1) {
859
+ throw new Error(
860
+ `Conversation ${conversationId} has buffers in multiple channels. Use: agentkit channels buffers show <name> ${conversationId}`,
861
+ );
862
+ }
863
+
864
+ throw new Error(`No channel buffer found for conversation ${conversationId}.`);
676
865
  }
677
866
 
678
867
  function channelProviderForCli(type: string, providerFlag: string | boolean | undefined): string {
package/src/cli/help.ts CHANGED
@@ -84,6 +84,11 @@ Usage:
84
84
  agentkit channels test <name> [--message <text>] [--fixture <path>] [--api <url>]
85
85
  agentkit channels deliveries list <name> [--api <url>]
86
86
  agentkit channels deliveries show <delivery-id> [--api <url>]
87
+ agentkit channels buffers list <name> [--api <url>]
88
+ agentkit channels buffers show [<name>] <conversation-id> [--api <url>]
89
+ agentkit channels buffers flush [<name>] <conversation-id> [--api <url>]
90
+ agentkit channels buffers clear [<name>] <conversation-id> [--api <url>]
91
+ agentkit channels buffers retry [<name>] <conversation-id> [--api <url>]
87
92
  agentkit inspect
88
93
  agentkit build [--target cloudflare|container]
89
94
  agentkit login --token <token> [--api <url>]
@@ -156,7 +156,7 @@ export async function sendTelegramMessage(
156
156
 
157
157
  return {
158
158
  ok: true,
159
- status: "sent",
159
+ status: "stubbed",
160
160
  providerRequestId: request.idempotencyKey,
161
161
  providerMetadata: {
162
162
  method: request.method,
@@ -247,11 +247,26 @@ export async function sendTelegramMessage(
247
247
  const result = isRecord(payload.result) ? payload.result : undefined;
248
248
  const messageId = readNumberishString(result ?? {}, "message_id");
249
249
 
250
+ if (!messageId) {
251
+ return {
252
+ ok: false,
253
+ retryable: false,
254
+ code: "channel_send_failed",
255
+ message: "Telegram sendMessage did not return result.message_id.",
256
+ providerRequestId: request.idempotencyKey,
257
+ providerMetadata: {
258
+ method: request.method,
259
+ url: request.url,
260
+ body: request.body,
261
+ },
262
+ };
263
+ }
264
+
250
265
  return {
251
266
  ok: true,
252
267
  status: "sent",
253
268
  providerRequestId: request.idempotencyKey,
254
- ...(messageId ? { providerMessageId: messageId } : {}),
269
+ providerMessageId: messageId,
255
270
  providerMetadata: {
256
271
  method: request.method,
257
272
  url: request.url,
@@ -263,7 +263,7 @@ export async function sendZapsterMessage(
263
263
 
264
264
  return {
265
265
  ok: true,
266
- status: "sent",
266
+ status: "stubbed",
267
267
  providerRequestId: request.idempotencyKey,
268
268
  providerMetadata: {
269
269
  method: request.method,
@@ -351,11 +351,27 @@ export async function sendZapsterMessage(
351
351
  };
352
352
  }
353
353
 
354
+ if (!messageId) {
355
+ return {
356
+ ok: false,
357
+ retryable: false,
358
+ code: "channel_send_failed",
359
+ message: "Zapster sendMessage did not return message_id or id.",
360
+ providerRequestId: request.idempotencyKey,
361
+ providerMetadata: {
362
+ method: request.method,
363
+ url: request.url,
364
+ status: response.status,
365
+ body: redactedPayload,
366
+ },
367
+ };
368
+ }
369
+
354
370
  return {
355
371
  ok: true,
356
372
  status: "sent",
357
- providerRequestId: messageId ?? request.idempotencyKey,
358
- ...(messageId ? { providerMessageId: messageId } : {}),
373
+ providerRequestId: request.idempotencyKey,
374
+ providerMessageId: messageId,
359
375
  providerMetadata: {
360
376
  method: request.method,
361
377
  url: request.url,
@@ -13,6 +13,9 @@ export type ChannelDeliveryState =
13
13
  | "queued"
14
14
  | "running"
15
15
  | "agent_completed"
16
+ | "adapter_stubbed"
17
+ | "provider_request_built"
18
+ | "provider_sent"
16
19
  | "outbound_sent"
17
20
  | "provider_failed"
18
21
  | "synthetic_expected_failure"
@@ -89,7 +92,7 @@ export type ChannelSendResult =
89
92
  ok: true;
90
93
  providerRequestId?: string;
91
94
  providerMessageId?: string;
92
- status: "sent" | "delivered";
95
+ status: "stubbed" | "sent" | "delivered";
93
96
  providerMetadata?: Record<string, unknown>;
94
97
  }
95
98
  | {
@@ -32,6 +32,7 @@ const KNOWLEDGE_EMBEDDING_PROVIDERS = new Set<AgentKnowledgeEmbeddingProvider>([
32
32
  const CHANNEL_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
33
33
  const SECRET_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
34
34
  const moduleDir = dirname(fileURLToPath(import.meta.url));
35
+ let configImportNonce = 0;
35
36
 
36
37
  export type LoadedAgentCapsule = {
37
38
  root: string;
@@ -108,15 +109,13 @@ function resolveLocalStoragePath(root: string, config: AgentConfig): string | un
108
109
  }
109
110
 
110
111
  async function loadConfigModule(configPath: string): Promise<unknown> {
111
- const configUrl = pathToFileURL(configPath);
112
- configUrl.searchParams.set("t", String(Date.now()));
112
+ const source = await readFile(configPath, "utf8");
113
113
 
114
114
  try {
115
- const module = (await import(configUrl.href)) as { default?: unknown };
116
- return module.default;
115
+ return await importConfigSource(configPath, source);
117
116
  } catch (error) {
118
117
  if (isMissingAgentKitSelfImport(error)) {
119
- return loadConfigModuleWithLocalSelfImport(configPath, error);
118
+ return loadConfigModuleWithLocalSelfImport(configPath, source, error);
120
119
  }
121
120
 
122
121
  throw new AgentKitError(
@@ -127,8 +126,11 @@ async function loadConfigModule(configPath: string): Promise<unknown> {
127
126
  }
128
127
  }
129
128
 
130
- async function loadConfigModuleWithLocalSelfImport(configPath: string, originalError: unknown): Promise<unknown> {
131
- const source = await readFile(configPath, "utf8");
129
+ async function loadConfigModuleWithLocalSelfImport(
130
+ configPath: string,
131
+ source: string,
132
+ originalError: unknown,
133
+ ): Promise<unknown> {
132
134
  const localAgentKitUrl = pathToFileURL(resolve(moduleDir, "../index.ts")).href;
133
135
  const patchedSource = source.replace(
134
136
  /from\s+["']@andreprado\/agentkit["']/g,
@@ -143,15 +145,20 @@ async function loadConfigModuleWithLocalSelfImport(configPath: string, originalE
143
145
  );
144
146
  }
145
147
 
148
+ return importConfigSource(configPath, patchedSource);
149
+ }
150
+
151
+ async function importConfigSource(configPath: string, source: string): Promise<unknown> {
152
+ const nonce = nextConfigImportNonce();
146
153
  const tempConfigPath = join(
147
154
  dirname(configPath),
148
- `.agentkit.config.loader-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.ts`,
155
+ `.agentkit.config.loader-${process.pid}-${nonce}-${Math.random().toString(16).slice(2)}.ts`,
149
156
  );
150
157
 
151
158
  try {
152
- await writeFile(tempConfigPath, patchedSource);
159
+ await writeFile(tempConfigPath, source);
153
160
  const configUrl = pathToFileURL(tempConfigPath);
154
- configUrl.searchParams.set("t", String(Date.now()));
161
+ configUrl.searchParams.set("t", nonce);
155
162
  const module = (await import(configUrl.href)) as { default?: unknown };
156
163
  return module.default;
157
164
  } catch (error) {
@@ -165,6 +172,11 @@ async function loadConfigModuleWithLocalSelfImport(configPath: string, originalE
165
172
  }
166
173
  }
167
174
 
175
+ function nextConfigImportNonce(): string {
176
+ configImportNonce += 1;
177
+ return `${Date.now()}-${configImportNonce}`;
178
+ }
179
+
168
180
  function isMissingAgentKitSelfImport(error: unknown): boolean {
169
181
  const message = error instanceof Error ? error.message : String(error);
170
182
  return message.includes("@andreprado/agentkit");
@@ -781,6 +781,7 @@ export default {
781
781
 
782
782
  function isHostedChannelWebhookPath(pathname) {
783
783
  return (
784
+ /^\\/channels\\/[^/]+\\/(?:website\\/agentkit|telegram\\/telegram|whatsapp\\/(?:zapster|meta))\\/webhook$/.test(pathname) ||
784
785
  /^\\/channels\\/[^/]+\\/(?:website|telegram)\\/webhook$/.test(pathname) ||
785
786
  /^\\/channels\\/[^/]+\\/whatsapp\\/(?:zapster|meta)\\/webhook$/.test(pathname)
786
787
  );
@@ -43,6 +43,11 @@ Behavior:
43
43
  Debug:
44
44
 
45
45
  ```sh
46
+ agentkit channels buffers list <name>
47
+ agentkit channels buffers show <conversation-id>
48
+ agentkit channels buffers flush <conversation-id>
49
+ agentkit channels buffers clear <conversation-id>
50
+ agentkit channels buffers retry <conversation-id>
46
51
  agentkit channels deliveries list <name>
47
52
  agentkit channels deliveries show <delivery-id>
48
53
  ```
@@ -54,5 +59,7 @@ buffered
54
59
  queued
55
60
  running
56
61
  agent_completed
57
- outbound_sent
62
+ provider_request_built
63
+ provider_sent
64
+ adapter_stubbed
58
65
  ```
@@ -9,6 +9,11 @@ agentkit channels doctor <name>
9
9
  agentkit channels test <name> --message "hello"
10
10
  agentkit channels deliveries list <name> --since 24h
11
11
  agentkit channels deliveries show <delivery-id>
12
+ agentkit channels buffers list <name>
13
+ agentkit channels buffers show <conversation-id>
14
+ agentkit channels buffers flush <conversation-id>
15
+ agentkit channels buffers clear <conversation-id>
16
+ agentkit channels buffers retry <conversation-id>
12
17
  ```
13
18
 
14
19
  Common states:
@@ -21,7 +26,9 @@ buffered
21
26
  queued
22
27
  running
23
28
  agent_completed
24
- outbound_sent
29
+ provider_request_built
30
+ provider_sent
31
+ adapter_stubbed
25
32
  delivered
26
33
  provider_failed
27
34
  synthetic_expected_failure
@@ -39,3 +46,5 @@ Common errors:
39
46
  - `channel_limit_exceeded`: backpressure skipped the message.
40
47
  - `synthetic_expected_failure`: a synthetic test reached AgentKit, but the provider correctly rejected a fake test recipient.
41
48
  - `buffered` delivery state: message is waiting for the channel quiet window or max wait before one coalesced agent run is queued.
49
+ - `adapter_stubbed`: explicit dry-run mode built a provider request but did not call the provider.
50
+ - `provider_sent`: the provider accepted the outbound send and returned a provider message ID.