@andreprado/agentkit 0.1.0-alpha.11 → 0.1.0-alpha.13
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/docs/guides/add-channel.md +13 -0
- package/docs/guides/agentkit-skills-architecture.md +1 -0
- package/docs/guides/channel-security.md +1 -1
- package/docs/guides/channels-implementation-map.md +2 -2
- package/docs/guides/channels-production-handoff.md +18 -2
- package/docs/guides/connect-telegram.md +1 -1
- package/docs/guides/connect-whatsapp-zapster.md +11 -4
- package/package.json +1 -2
- package/src/cli/commands/channels.ts +196 -7
- package/src/cli/help.ts +5 -0
- package/src/runtime/channels/telegram.ts +17 -2
- package/src/runtime/channels/whatsapp-zapster.ts +214 -22
- package/src/runtime/channels.ts +4 -1
- package/src/runtime/targets/cloudflare/build.ts +1 -0
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +10 -1
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +2 -0
|
@@ -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.
|
|
@@ -27,7 +27,7 @@ AGENTKIT_RUN_ZAPSTER_CHANNEL_TESTS=1 bun test
|
|
|
27
27
|
|
|
28
28
|
Telegram smoke also requires `TELEGRAM_BOT_TOKEN`, `TELEGRAM_WEBHOOK_SECRET`, and `AGENTKIT_TELEGRAM_WEBHOOK_URL`.
|
|
29
29
|
|
|
30
|
-
Zapster smoke also requires `ZAPSTER_API_KEY`, `
|
|
30
|
+
Zapster smoke also requires `ZAPSTER_API_KEY`, `ZAPSTER_INSTANCE_ID`, and `AGENTKIT_ZAPSTER_TO`. `AGENTKIT_ZAPSTER_SEND_URL` is optional and defaults to `https://api.zapsterapi.com/v1/wa/messages`.
|
|
31
31
|
|
|
32
32
|
## Files Created Or Edited
|
|
33
33
|
|
|
@@ -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/{
|
|
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
|
|
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
|
|
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
|
|
@@ -74,7 +89,7 @@ AGENTKIT_RUN_ZAPSTER_CHANNEL_TESTS=1 bun test
|
|
|
74
89
|
|
|
75
90
|
Telegram smoke requires `TELEGRAM_BOT_TOKEN`, `TELEGRAM_WEBHOOK_SECRET`, and `AGENTKIT_TELEGRAM_WEBHOOK_URL`.
|
|
76
91
|
|
|
77
|
-
Zapster smoke requires `ZAPSTER_API_KEY`, `
|
|
92
|
+
Zapster smoke requires `ZAPSTER_API_KEY`, `ZAPSTER_INSTANCE_ID`, and `AGENTKIT_ZAPSTER_TO`. `AGENTKIT_ZAPSTER_SEND_URL` is optional and defaults to `https://api.zapsterapi.com/v1/wa/messages`.
|
|
78
93
|
|
|
79
94
|
## Smoke Checks
|
|
80
95
|
|
|
@@ -82,6 +97,7 @@ Zapster smoke requires `ZAPSTER_API_KEY`, `AGENTKIT_ZAPSTER_SEND_URL`, and `AGEN
|
|
|
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/
|
|
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
|
|
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/
|
|
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/
|
|
89
|
+
https://<deploy-host>/channels/support-whatsapp/whatsapp/zapster/webhook?token=<ZAPSTER_WEBHOOK_TOKEN>
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
## Safety Rules
|
|
@@ -98,6 +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`.
|
|
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.
|
|
101
104
|
|
|
102
105
|
## Verification
|
|
103
106
|
|
|
@@ -106,6 +109,10 @@ agentkit channels status support-whatsapp
|
|
|
106
109
|
agentkit channels test support-whatsapp --message "hello"
|
|
107
110
|
agentkit channels deliveries list support-whatsapp
|
|
108
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>
|
|
109
116
|
```
|
|
110
117
|
|
|
111
118
|
## Troubleshooting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andreprado/agentkit",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.13",
|
|
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": {
|
|
@@ -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
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
269
|
+
providerMessageId: messageId,
|
|
255
270
|
providerMetadata: {
|
|
256
271
|
method: request.method,
|
|
257
272
|
url: request.url,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ChannelAdapter,
|
|
3
3
|
ChannelSendInput,
|
|
4
|
+
ChannelSendResult,
|
|
4
5
|
RawWebhookEvent,
|
|
5
6
|
WebhookVerificationInput,
|
|
6
7
|
WebhookVerificationResult,
|
|
@@ -140,17 +141,7 @@ export const zapsterWhatsappChannelAdapter: ChannelAdapter = {
|
|
|
140
141
|
];
|
|
141
142
|
},
|
|
142
143
|
async sendMessage(input) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
ok: true,
|
|
147
|
-
status: "sent",
|
|
148
|
-
providerRequestId: request.idempotencyKey,
|
|
149
|
-
providerMetadata: {
|
|
150
|
-
method: request.method,
|
|
151
|
-
url: request.url,
|
|
152
|
-
},
|
|
153
|
-
};
|
|
144
|
+
return sendZapsterMessage(input);
|
|
154
145
|
},
|
|
155
146
|
getStatus(input) {
|
|
156
147
|
const missingSecrets = ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"].filter(
|
|
@@ -248,16 +239,152 @@ export type ZapsterSendMessageRequest = {
|
|
|
248
239
|
method: "POST";
|
|
249
240
|
url: string;
|
|
250
241
|
body: {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
body: string;
|
|
255
|
-
};
|
|
242
|
+
recipient: string;
|
|
243
|
+
text: string;
|
|
244
|
+
instance_id: string;
|
|
256
245
|
};
|
|
257
246
|
idempotencyKey: string;
|
|
258
247
|
};
|
|
259
248
|
|
|
249
|
+
type ZapsterFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
250
|
+
|
|
251
|
+
const ZAPSTER_SEND_MESSAGE_URL = "https://api.zapsterapi.com/v1/wa/messages";
|
|
252
|
+
|
|
260
253
|
export function buildZapsterSendMessageRequest(input: ChannelSendInput): ZapsterSendMessageRequest {
|
|
254
|
+
return buildZapsterSendMessageRequestInternal(input, "<redacted>");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function sendZapsterMessage(
|
|
258
|
+
input: ChannelSendInput,
|
|
259
|
+
fetcher: ZapsterFetch = fetch,
|
|
260
|
+
): Promise<ChannelSendResult> {
|
|
261
|
+
if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
|
|
262
|
+
const request = buildZapsterSendMessageRequest(input);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
status: "stubbed",
|
|
267
|
+
providerRequestId: request.idempotencyKey,
|
|
268
|
+
providerMetadata: {
|
|
269
|
+
method: request.method,
|
|
270
|
+
url: request.url,
|
|
271
|
+
body: request.body,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const apiKey = input.secrets.ZAPSTER_API_KEY;
|
|
277
|
+
const instanceId = input.secrets.ZAPSTER_INSTANCE_ID;
|
|
278
|
+
|
|
279
|
+
if (!apiKey || !instanceId) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
retryable: false,
|
|
283
|
+
code: "channel_secret_missing",
|
|
284
|
+
message: "Secret ZAPSTER_API_KEY or ZAPSTER_INSTANCE_ID is missing.",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let request: ZapsterSendMessageRequest;
|
|
289
|
+
let actualRequest: ZapsterSendMessageRequest;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
request = buildZapsterSendMessageRequest(input);
|
|
293
|
+
actualRequest = buildZapsterSendMessageRequestInternal(input, instanceId);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
retryable: false,
|
|
298
|
+
code: "channel_send_failed",
|
|
299
|
+
message: error instanceof Error ? error.message : String(error),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let response: Response;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
response = await fetcher(actualRequest.url, {
|
|
307
|
+
method: actualRequest.method,
|
|
308
|
+
headers: {
|
|
309
|
+
Authorization: `Bearer ${apiKey}`,
|
|
310
|
+
"Content-Type": "application/json",
|
|
311
|
+
Accept: "application/json",
|
|
312
|
+
},
|
|
313
|
+
body: JSON.stringify(actualRequest.body),
|
|
314
|
+
});
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const errorMessage = redactString(error instanceof Error ? error.message : String(error), [apiKey, instanceId]);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
retryable: true,
|
|
321
|
+
code: "channel_provider_unavailable",
|
|
322
|
+
message: `Zapster sendMessage request failed before a response: ${errorMessage}`,
|
|
323
|
+
providerRequestId: request.idempotencyKey,
|
|
324
|
+
providerMetadata: {
|
|
325
|
+
method: request.method,
|
|
326
|
+
url: request.url,
|
|
327
|
+
body: request.body,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const payload = await readZapsterResponseBody(response);
|
|
333
|
+
const redactedPayload = redactValue(payload, [apiKey, instanceId]);
|
|
334
|
+
const messageId = isRecord(payload)
|
|
335
|
+
? readString(payload, "message_id") ?? readString(payload, "id")
|
|
336
|
+
: undefined;
|
|
337
|
+
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
342
|
+
code: "channel_send_failed",
|
|
343
|
+
message: redactString(readZapsterErrorMessage(payload, response.status), [apiKey, instanceId]),
|
|
344
|
+
providerRequestId: request.idempotencyKey,
|
|
345
|
+
providerMetadata: {
|
|
346
|
+
method: request.method,
|
|
347
|
+
url: request.url,
|
|
348
|
+
status: response.status,
|
|
349
|
+
body: redactedPayload,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
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
|
+
|
|
370
|
+
return {
|
|
371
|
+
ok: true,
|
|
372
|
+
status: "sent",
|
|
373
|
+
providerRequestId: request.idempotencyKey,
|
|
374
|
+
providerMessageId: messageId,
|
|
375
|
+
providerMetadata: {
|
|
376
|
+
method: request.method,
|
|
377
|
+
url: request.url,
|
|
378
|
+
status: response.status,
|
|
379
|
+
body: redactedPayload,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function buildZapsterSendMessageRequestInternal(
|
|
385
|
+
input: ChannelSendInput,
|
|
386
|
+
instanceId: string,
|
|
387
|
+
): ZapsterSendMessageRequest {
|
|
261
388
|
const to = input.externalIdentity.key.match(/contact:([^:]+)$/)?.[1];
|
|
262
389
|
|
|
263
390
|
if (!to) {
|
|
@@ -266,18 +393,83 @@ export function buildZapsterSendMessageRequest(input: ChannelSendInput): Zapster
|
|
|
266
393
|
|
|
267
394
|
return {
|
|
268
395
|
method: "POST",
|
|
269
|
-
url:
|
|
396
|
+
url: ZAPSTER_SEND_MESSAGE_URL,
|
|
270
397
|
body: {
|
|
271
|
-
to,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
body: input.message.content,
|
|
275
|
-
},
|
|
398
|
+
recipient: to,
|
|
399
|
+
text: input.message.content,
|
|
400
|
+
instance_id: instanceId,
|
|
276
401
|
},
|
|
277
402
|
idempotencyKey: `whatsapp:zapster:${input.channelId}:${input.conversationId}:${redactPhone(to)}`,
|
|
278
403
|
};
|
|
279
404
|
}
|
|
280
405
|
|
|
406
|
+
async function readZapsterResponseBody(response: Response): Promise<unknown> {
|
|
407
|
+
const text = await response.text().catch(() => "");
|
|
408
|
+
|
|
409
|
+
if (!text) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
return JSON.parse(text) as unknown;
|
|
415
|
+
} catch {
|
|
416
|
+
return text;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function readZapsterErrorMessage(payload: unknown, status: number): string {
|
|
421
|
+
if (typeof payload === "string" && payload.length > 0) {
|
|
422
|
+
return payload;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (isRecord(payload)) {
|
|
426
|
+
const message = readString(payload, "message") ?? readString(payload, "error") ?? readString(payload, "detail");
|
|
427
|
+
|
|
428
|
+
if (message) {
|
|
429
|
+
return message;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const error = readRecord(payload, "error");
|
|
433
|
+
const nestedMessage = readString(error, "message") ?? readString(error, "detail");
|
|
434
|
+
|
|
435
|
+
if (nestedMessage) {
|
|
436
|
+
return nestedMessage;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return `Zapster sendMessage returned HTTP ${status}.`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function redactValue(value: unknown, secrets: string[]): unknown {
|
|
444
|
+
if (typeof value === "string") {
|
|
445
|
+
return redactString(value, secrets);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!isRecord(value) && !Array.isArray(value)) {
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const serialized = redactString(JSON.stringify(value), secrets);
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
return JSON.parse(serialized) as unknown;
|
|
456
|
+
} catch {
|
|
457
|
+
return serialized;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function redactString(value: string, secrets: string[]): string {
|
|
462
|
+
let redacted = value;
|
|
463
|
+
|
|
464
|
+
for (const secret of secrets) {
|
|
465
|
+
if (secret) {
|
|
466
|
+
redacted = redacted.split(secret).join("<redacted>");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return redacted;
|
|
471
|
+
}
|
|
472
|
+
|
|
281
473
|
function parseJsonObject(input: RawWebhookEvent): Record<string, unknown> {
|
|
282
474
|
try {
|
|
283
475
|
const parsed = JSON.parse(input.rawBody) as unknown;
|
package/src/runtime/channels.ts
CHANGED
|
@@ -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
|
| {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -29,6 +29,8 @@ Paste the stable AgentKit webhook URL into Zapster settings. If the channel decl
|
|
|
29
29
|
|
|
30
30
|
AgentKit handles Zapster `message.received` envelopes with event id at `id`, message text at `data.content.text`, and contact identity at `data.sender.id`. Unsupported media should be logged as skipped/unsupported without creating an agent run.
|
|
31
31
|
|
|
32
|
+
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.
|
|
33
|
+
|
|
32
34
|
Buffer rapid WhatsApp messages:
|
|
33
35
|
|
|
34
36
|
```ts
|