agents 0.16.2 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-CNyE1iz_.d.ts} +671 -138
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-CSnyGvJ2.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +2033 -77
  26. package/dist/chat/index.js +1828 -245
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  41. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  42. package/dist/email.js +1 -1
  43. package/dist/email.js.map +1 -1
  44. package/dist/{index-B7IbEeze.d.ts → index-BRnybD6X.d.ts} +197 -14
  45. package/dist/index.d.ts +95 -73
  46. package/dist/index.js +694 -25
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp/client.d.ts +18 -14
  49. package/dist/mcp/client.js +1 -1
  50. package/dist/mcp/index.d.ts +30 -30
  51. package/dist/mcp/index.js +7 -7
  52. package/dist/mcp/index.js.map +1 -1
  53. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  54. package/dist/message-builder-BymO4N_D.js.map +1 -0
  55. package/dist/observability/index.d.ts +1 -1
  56. package/dist/observability/index.js +4 -2
  57. package/dist/observability/index.js.map +1 -1
  58. package/dist/react.d.ts +123 -110
  59. package/dist/react.js +44 -11
  60. package/dist/react.js.map +1 -1
  61. package/dist/{retries-CwlpAGet.d.ts → retries-CvHJwSuh.d.ts} +15 -6
  62. package/dist/retries.d.ts +8 -6
  63. package/dist/retries.js +44 -1
  64. package/dist/retries.js.map +1 -1
  65. package/dist/serializable.d.ts +1 -1
  66. package/dist/skills/compile.js +1 -1
  67. package/dist/skills/compile.js.map +1 -1
  68. package/dist/skills/index.js +5 -5
  69. package/dist/skills/index.js.map +1 -1
  70. package/dist/sub-routing.d.ts +6 -6
  71. package/dist/utils.js +1 -1
  72. package/dist/utils.js.map +1 -1
  73. package/dist/vite.js +5 -5
  74. package/dist/vite.js.map +1 -1
  75. package/dist/wire-types-nflOzNuU.js +240 -0
  76. package/dist/wire-types-nflOzNuU.js.map +1 -0
  77. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  78. package/dist/workflow-types.d.ts +25 -21
  79. package/dist/workflow-types.js.map +1 -1
  80. package/dist/workflows.d.ts +22 -21
  81. package/dist/workflows.js +31 -7
  82. package/dist/workflows.js.map +1 -1
  83. package/docs/adding-to-existing-project.md +450 -0
  84. package/docs/agent-class.md +503 -0
  85. package/docs/agent-tools.md +552 -0
  86. package/docs/browse-the-web.md +430 -0
  87. package/docs/callable-methods.md +627 -0
  88. package/docs/chat-agents.md +1696 -0
  89. package/docs/chat-sdk.md +181 -0
  90. package/docs/client-sdk.md +520 -0
  91. package/docs/client-tools-continuation.md +177 -0
  92. package/docs/codemode.md +440 -0
  93. package/docs/configuration.md +775 -0
  94. package/docs/cross-domain-authentication.md +171 -0
  95. package/docs/durable-execution.md +537 -0
  96. package/docs/email.md +663 -0
  97. package/docs/get-current-agent.md +204 -0
  98. package/docs/getting-started.md +305 -0
  99. package/docs/http-websockets.md +668 -0
  100. package/docs/human-in-the-loop.md +661 -0
  101. package/docs/index.md +151 -0
  102. package/docs/long-running-agents.md +730 -0
  103. package/docs/mcp-client.md +620 -0
  104. package/docs/mcp-servers.md +526 -0
  105. package/docs/mcp-transports.md +308 -0
  106. package/docs/migration-to-ai-sdk-v5.md +96 -0
  107. package/docs/migration-to-ai-sdk-v6.md +163 -0
  108. package/docs/observability.md +261 -0
  109. package/docs/push-notifications.md +367 -0
  110. package/docs/queue.md +329 -0
  111. package/docs/readonly-connections.md +278 -0
  112. package/docs/resumable-streaming.md +127 -0
  113. package/docs/retries.md +444 -0
  114. package/docs/routing.md +749 -0
  115. package/docs/scheduling.md +898 -0
  116. package/docs/securing-mcp-servers.md +359 -0
  117. package/docs/server-driven-messages.md +477 -0
  118. package/docs/sessions.md +1024 -0
  119. package/docs/state.md +512 -0
  120. package/docs/sub-agents.md +389 -0
  121. package/docs/webhooks.md +604 -0
  122. package/docs/workflows.md +877 -0
  123. package/package.json +41 -14
  124. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  125. package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
  126. package/dist/client-BXJ9n2f7.js.map +0 -1
package/docs/email.md ADDED
@@ -0,0 +1,663 @@
1
+ # Email Service
2
+
3
+ Agents can send and receive email with Cloudflare's [Email Service](https://developers.cloudflare.com/email-service/). This guide shows how to send outbound email with the Workers binding, route inbound mail into Agents, and handle follow-up replies securely.
4
+
5
+ ## Prerequisites
6
+
7
+ Before using email with Agents, you need:
8
+
9
+ 1. A domain onboarded to [Cloudflare Email Service](https://developers.cloudflare.com/email-service/)
10
+ 2. A `send_email` binding in `wrangler.jsonc` for outbound email
11
+ 3. An Email Service routing rule that sends inbound mail to your Worker
12
+ 4. Optional: an `EMAIL_SECRET` secret if you want secure reply routing
13
+
14
+ ### Domain Setup
15
+
16
+ 1. Log in to the [Cloudflare Dashboard](https://dash.cloudflare.com)
17
+ 2. Navigate to **Compute & AI** > **Email Service**
18
+ 3. Select **Onboard Domain** and choose your domain
19
+ 4. Add the DNS records (SPF and DKIM) to authorize sending
20
+
21
+ DNS changes usually complete within 5-15 minutes for domains using Cloudflare DNS, but can take up to 24 hours to propagate globally.
22
+
23
+ ### Wrangler Configuration
24
+
25
+ Add the email binding to your `wrangler.jsonc`:
26
+
27
+ ```jsonc
28
+ {
29
+ "send_email": [
30
+ {
31
+ "name": "EMAIL",
32
+ "remote": true // For local development
33
+ }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ The `remote: true` option lets you call the real Email Service API during local development with `wrangler dev`.
39
+
40
+ ## Quick Start
41
+
42
+ ```ts
43
+ import { Agent, callable, routeAgentEmail } from "agents";
44
+ import { createAddressBasedEmailResolver, type AgentEmail } from "agents/email";
45
+ import PostalMime from "postal-mime";
46
+
47
+ export class EmailAgent extends Agent {
48
+ @callable()
49
+ async sendWelcomeEmail(to: string) {
50
+ await this.sendEmail({
51
+ binding: this.env.EMAIL,
52
+ to,
53
+ from: "support@yourdomain.com",
54
+ replyTo: "support@yourdomain.com",
55
+ subject: "Welcome to our service",
56
+ text: "Thanks for signing up. Reply to this email if you need help."
57
+ });
58
+ }
59
+
60
+ async onEmail(email: AgentEmail) {
61
+ const raw = await email.getRaw();
62
+ const parsed = await PostalMime.parse(raw);
63
+
64
+ console.log("Received email from:", email.from);
65
+ console.log("Subject:", parsed.subject);
66
+
67
+ await this.replyToEmail(email, {
68
+ fromName: "Support Agent",
69
+ body: "Thanks for your email! We received it."
70
+ });
71
+ }
72
+ }
73
+
74
+ export default {
75
+ async email(message, env) {
76
+ await routeAgentEmail(message, env, {
77
+ resolver: createAddressBasedEmailResolver("EmailAgent")
78
+ });
79
+ }
80
+ };
81
+ ```
82
+
83
+ ## Sending Outbound Email
84
+
85
+ ### Configuring the binding
86
+
87
+ Add a `send_email` binding in `wrangler.jsonc`:
88
+
89
+ ```jsonc
90
+ {
91
+ "send_email": [
92
+ {
93
+ "name": "EMAIL",
94
+ "remote": true
95
+ }
96
+ ]
97
+ }
98
+ ```
99
+
100
+ ### Using sendEmail()
101
+
102
+ `sendEmail()` sends outbound email through a `send_email` binding that you pass explicitly. It automatically injects agent routing headers (`X-Agent-Name`, `X-Agent-ID`) into every message, and optionally signs them with HMAC-SHA256 so that replies can be routed back to the same agent instance.
103
+
104
+ ```ts
105
+ @callable()
106
+ async sendReceipt(to: string, orderId: string) {
107
+ const result = await this.sendEmail({
108
+ binding: this.env.EMAIL,
109
+ to,
110
+ from: { email: "billing@yourdomain.com", name: "Billing Bot" },
111
+ replyTo: "billing@yourdomain.com",
112
+ subject: `Receipt for order ${orderId}`,
113
+ text: `Your receipt for order ${orderId} is ready.`,
114
+ secret: this.env.EMAIL_SECRET
115
+ });
116
+
117
+ return result.messageId;
118
+ }
119
+ ```
120
+
121
+ When `secret` is provided, the agent signs the routing headers so that replies verified by `createSecureReplyEmailResolver` route back to the same agent instance.
122
+
123
+ Set `replyTo` to the mailbox that routes back to your Worker when you want recipients to continue the conversation with the same agent.
124
+
125
+ ## Routing Inbound Mail
126
+
127
+ Resolvers determine which Agent instance receives an incoming email. Choose the resolver that matches your use case.
128
+
129
+ For basic Email Service sending and receiving, `createAddressBasedEmailResolver()` is enough. The secure reply resolver below is optional and specific to Agents SDK reply signing, not a requirement of Email Service itself.
130
+
131
+ ### createAddressBasedEmailResolver
132
+
133
+ **Recommended for inbound mail.** Routes emails based on the recipient address.
134
+
135
+ ```ts
136
+ import { createAddressBasedEmailResolver } from "agents/email";
137
+
138
+ const resolver = createAddressBasedEmailResolver("EmailAgent");
139
+ ```
140
+
141
+ **Routing logic:**
142
+
143
+ | Recipient Address | Agent Name | Agent ID |
144
+ | --------------------------------------- | ---------------------- | --------- |
145
+ | `support@example.com` | `EmailAgent` (default) | `support` |
146
+ | `sales@example.com` | `EmailAgent` (default) | `sales` |
147
+ | `NotificationAgent+user123@example.com` | `NotificationAgent` | `user123` |
148
+
149
+ The sub-address format (`agent+id@domain`) allows routing to different agent namespaces and instances from a single email domain.
150
+
151
+ > **Note:** Agent class names in the recipient address are matched case-insensitively. Email infrastructure often lowercases addresses, so `NotificationAgent+user123@example.com` and `notificationagent+user123@example.com` both route to the `NotificationAgent` class.
152
+
153
+ ### createSecureReplyEmailResolver
154
+
155
+ **For reply flows with signature verification.** Verifies that incoming emails are authentic replies to your outbound emails, preventing attackers from routing emails to arbitrary agent instances.
156
+
157
+ ```ts
158
+ import { createSecureReplyEmailResolver } from "agents/email";
159
+
160
+ const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);
161
+ ```
162
+
163
+ When your agent sends an email with `replyToEmail()` and a `secret`, it signs the routing headers with a timestamp. When a reply comes back, this resolver verifies the signature and checks that it hasn't expired before routing.
164
+
165
+ **Options:**
166
+
167
+ ```ts
168
+ const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
169
+ // Maximum age of signature in seconds (default: 30 days)
170
+ maxAge: 7 * 24 * 60 * 60, // 7 days
171
+
172
+ // Callback for logging/debugging signature failures
173
+ onInvalidSignature: (email, reason) => {
174
+ console.warn(`Invalid signature from ${email.from}: ${reason}`);
175
+ // reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"
176
+ }
177
+ });
178
+ ```
179
+
180
+ **When to use:** If your agent initiates email conversations and you need replies to route back to the same agent instance securely.
181
+
182
+ ### createCatchAllEmailResolver
183
+
184
+ **For single-instance routing.** Routes all emails to a specific agent instance regardless of the recipient address.
185
+
186
+ ```ts
187
+ import { createCatchAllEmailResolver } from "agents/email";
188
+
189
+ const resolver = createCatchAllEmailResolver("EmailAgent", "default");
190
+ ```
191
+
192
+ **When to use:** When you have a single agent instance that handles all emails (e.g., a shared inbox).
193
+
194
+ ### Combining Resolvers
195
+
196
+ You can combine resolvers to handle different scenarios:
197
+
198
+ ```ts
199
+ export default {
200
+ async email(message, env) {
201
+ const secureReplyResolver = createSecureReplyEmailResolver(
202
+ env.EMAIL_SECRET
203
+ );
204
+ const addressResolver = createAddressBasedEmailResolver("EmailAgent");
205
+
206
+ await routeAgentEmail(message, env, {
207
+ resolver: async (email, env) => {
208
+ // First, check if this is a signed reply
209
+ const replyRouting = await secureReplyResolver(email, env);
210
+ if (replyRouting) return replyRouting;
211
+
212
+ // Otherwise, route based on recipient address
213
+ return addressResolver(email, env);
214
+ },
215
+
216
+ // Handle emails that don't match any routing rule
217
+ onNoRoute: (email) => {
218
+ console.warn(`No route found for email from ${email.from}`);
219
+ email.setReject("Unknown recipient");
220
+ }
221
+ });
222
+ }
223
+ };
224
+ ```
225
+
226
+ ## Handling Emails in Your Agent
227
+
228
+ ### The AgentEmail Interface
229
+
230
+ When your agent's `onEmail` method is called, it receives an `AgentEmail` object:
231
+
232
+ ```ts
233
+ type AgentEmail = {
234
+ from: string; // Sender's email address
235
+ to: string; // Recipient's email address
236
+ headers: Headers; // Email headers (subject, message-id, etc.)
237
+ rawSize: number; // Size of the raw email in bytes
238
+
239
+ getRaw(): Promise<Uint8Array>; // Get the full raw email content
240
+ reply(options): Promise<void>; // Send a reply
241
+ forward(rcptTo, headers?): Promise<void>; // Forward the email
242
+ setReject(reason): void; // Reject the email with a reason
243
+ };
244
+ ```
245
+
246
+ ### Parsing Email Content
247
+
248
+ Use a library like [postal-mime](https://www.npmjs.com/package/postal-mime) to parse the raw email:
249
+
250
+ ```ts
251
+ import PostalMime from "postal-mime";
252
+
253
+ async onEmail(email: AgentEmail) {
254
+ const raw = await email.getRaw();
255
+ const parsed = await PostalMime.parse(raw);
256
+
257
+ console.log("Subject:", parsed.subject);
258
+ console.log("Text body:", parsed.text);
259
+ console.log("HTML body:", parsed.html);
260
+ console.log("Attachments:", parsed.attachments);
261
+ }
262
+ ```
263
+
264
+ ### Detecting Auto-Reply Emails
265
+
266
+ Use `isAutoReplyEmail()` to detect auto-reply emails and avoid mail loops:
267
+
268
+ ```ts
269
+ import { isAutoReplyEmail } from "agents/email";
270
+ import PostalMime from "postal-mime";
271
+
272
+ async onEmail(email: AgentEmail) {
273
+ const raw = await email.getRaw();
274
+ const parsed = await PostalMime.parse(raw);
275
+
276
+ // Detect auto-reply emails to avoid sending duplicate responses
277
+ if (isAutoReplyEmail(parsed.headers)) {
278
+ console.log("Skipping auto-reply email");
279
+ return;
280
+ }
281
+
282
+ // Process the email...
283
+ }
284
+ ```
285
+
286
+ This checks for standard RFC 3834 headers (`Auto-Submitted`, `X-Auto-Response-Suppress`, `Precedence`) that indicate an email is an auto-reply.
287
+
288
+ ### Replying to Emails
289
+
290
+ Use `this.replyToEmail()` to send a reply through the inbound email's reply channel:
291
+
292
+ ```ts
293
+ async onEmail(email: AgentEmail) {
294
+ await this.replyToEmail(email, {
295
+ fromName: "Support Bot", // Display name for the sender
296
+ subject: "Re: Your inquiry", // Optional, defaults to "Re: <original subject>"
297
+ body: "Thanks for contacting us!", // Email body
298
+ contentType: "text/plain", // Optional, defaults to "text/plain"
299
+ headers: { // Optional custom headers
300
+ "X-Custom-Header": "value"
301
+ },
302
+ secret: this.env.EMAIL_SECRET // Optional, signs headers for secure reply routing
303
+ });
304
+ }
305
+ ```
306
+
307
+ ### Deferred Replies
308
+
309
+ `replyToEmail()` requires a live `AgentEmail` object, so it only works inside `onEmail()`. If you need to reply later — from a scheduled task, a callable method, or after a human-in-the-loop approval — store the sender info in state and use `sendEmail()`:
310
+
311
+ ```ts
312
+ async onEmail(email: AgentEmail) {
313
+ const raw = await email.getRaw();
314
+ const parsed = await PostalMime.parse(raw);
315
+
316
+ this.setState({
317
+ ...this.state,
318
+ pendingReply: {
319
+ to: email.from,
320
+ messageId: parsed.messageId,
321
+ subject: parsed.subject
322
+ }
323
+ });
324
+ }
325
+
326
+ @callable()
327
+ async sendDelayedReply(body: string) {
328
+ const { pendingReply } = this.state;
329
+ if (!pendingReply) return;
330
+
331
+ await this.sendEmail({
332
+ binding: this.env.EMAIL,
333
+ to: pendingReply.to,
334
+ from: "support@yourdomain.com",
335
+ subject: `Re: ${pendingReply.subject}`,
336
+ text: body,
337
+ inReplyTo: pendingReply.messageId,
338
+ secret: this.env.EMAIL_SECRET
339
+ });
340
+ }
341
+ ```
342
+
343
+ The `inReplyTo` field sets the `In-Reply-To` header so mail clients thread the reply correctly. The `secret` signs the agent routing headers so that follow-up replies route back to this agent instance via `createSecureReplyEmailResolver`.
344
+
345
+ ### Forwarding Emails
346
+
347
+ ```ts
348
+ async onEmail(email: AgentEmail) {
349
+ await email.forward("admin@example.com");
350
+ }
351
+ ```
352
+
353
+ ### Rejecting Emails
354
+
355
+ ```ts
356
+ async onEmail(email: AgentEmail) {
357
+ if (isSpam(email)) {
358
+ email.setReject("Message rejected as spam");
359
+ return;
360
+ }
361
+ // Process the email...
362
+ }
363
+ ```
364
+
365
+ ## Error Handling
366
+
367
+ When sending emails via `sendEmail()` or `replyToEmail()`, handle these common errors:
368
+
369
+ ```ts
370
+ async onEmail(email: AgentEmail) {
371
+ try {
372
+ await this.replyToEmail(email, {
373
+ fromName: "Support Bot",
374
+ body: "Thanks for your email!"
375
+ });
376
+ } catch (error) {
377
+ switch (error.code) {
378
+ case "E_SENDER_NOT_VERIFIED":
379
+ console.error("Sender domain not verified. Verify in dashboard.");
380
+ break;
381
+ case "E_RATE_LIMIT_EXCEEDED":
382
+ console.error("Rate limit exceeded. Back off and retry.");
383
+ break;
384
+ case "E_DAILY_LIMIT_EXCEEDED":
385
+ console.error("Daily sending quota reached.");
386
+ break;
387
+ case "E_CONTENT_TOO_LARGE":
388
+ console.error("Email content exceeds size limit.");
389
+ break;
390
+ default:
391
+ console.error("Email sending failed:", error.message);
392
+ }
393
+ }
394
+ }
395
+ ```
396
+
397
+ ### Common Error Codes
398
+
399
+ | Error Code | Description | Solution |
400
+ | ------------------------- | ---------------------------------- | ------------------------------------ |
401
+ | `E_SENDER_NOT_VERIFIED` | Sender domain/address not verified | Verify in Cloudflare dashboard |
402
+ | `E_RATE_LIMIT_EXCEEDED` | Sending rate limit reached | Implement exponential backoff |
403
+ | `E_DAILY_LIMIT_EXCEEDED` | Daily quota exceeded | Wait for quota reset or upgrade plan |
404
+ | `E_CONTENT_TOO_LARGE` | Email exceeds size limit | Reduce attachments or content |
405
+ | `E_RECIPIENT_NOT_ALLOWED` | Recipient not in allowed list | Check allowed destination addresses |
406
+ | `E_RECIPIENT_SUPPRESSED` | Recipient is on suppression list | Remove from suppression list |
407
+ | `E_VALIDATION_ERROR` | Invalid email format | Check email addresses |
408
+ | `E_TOO_MANY_RECIPIENTS` | More than 50 recipients | Split into multiple sends |
409
+
410
+ ## Secure Reply Routing
411
+
412
+ When your agent sends emails and expects replies, use secure reply routing to prevent attackers from forging headers to route emails to arbitrary agent instances.
413
+
414
+ ### How It Works
415
+
416
+ 1. **Outbound:** When you call `replyToEmail()` or `sendEmail()` with a `secret`, the agent signs the routing headers (`X-Agent-Name`, `X-Agent-ID`) using HMAC-SHA256
417
+ 2. **Inbound:** `createSecureReplyEmailResolver` verifies the signature before routing
418
+ 3. **Enforcement:** If an email was routed via the secure resolver, `replyToEmail()` requires a secret (or explicit `null` to opt-out)
419
+
420
+ ### Setup
421
+
422
+ 1. Add a secret to your `wrangler.jsonc`:
423
+
424
+ ```jsonc
425
+ // wrangler.jsonc
426
+ {
427
+ "vars": {
428
+ "EMAIL_SECRET": "change-me-in-production"
429
+ }
430
+ }
431
+ ```
432
+
433
+ For production, use Wrangler secrets instead:
434
+
435
+ ```bash
436
+ wrangler secret put EMAIL_SECRET
437
+ ```
438
+
439
+ 2. Use the combined resolver pattern:
440
+
441
+ ```ts
442
+ export default {
443
+ async email(message, env) {
444
+ const secureReplyResolver = createSecureReplyEmailResolver(
445
+ env.EMAIL_SECRET
446
+ );
447
+ const addressResolver = createAddressBasedEmailResolver("EmailAgent");
448
+
449
+ await routeAgentEmail(message, env, {
450
+ resolver: async (email, env) => {
451
+ const replyRouting = await secureReplyResolver(email, env);
452
+ if (replyRouting) return replyRouting;
453
+ return addressResolver(email, env);
454
+ }
455
+ });
456
+ }
457
+ };
458
+ ```
459
+
460
+ 3. Sign outbound emails:
461
+
462
+ ```ts
463
+ async onEmail(email: AgentEmail) {
464
+ await this.replyToEmail(email, {
465
+ fromName: "My Agent",
466
+ body: "Thanks for your email!",
467
+ secret: this.env.EMAIL_SECRET // Signs the routing headers
468
+ });
469
+ }
470
+ ```
471
+
472
+ ### Enforcement Behavior
473
+
474
+ When an email is routed via `createSecureReplyEmailResolver`, the `replyToEmail()` method enforces signing:
475
+
476
+ | `secret` value | Behavior |
477
+ | --------------------- | ------------------------------------------------------------ |
478
+ | `"my-secret"` | Signs headers (secure) |
479
+ | `undefined` (omitted) | **Throws error** - must provide secret or explicit opt-out |
480
+ | `null` | Allowed but not recommended - explicitly opts out of signing |
481
+
482
+ ## Complete Example
483
+
484
+ Here is a complete Email Service agent that sends outbound mail and handles secure replies:
485
+
486
+ ```ts
487
+ import { Agent, callable, routeAgentEmail } from "agents";
488
+ import {
489
+ createAddressBasedEmailResolver,
490
+ createSecureReplyEmailResolver,
491
+ type AgentEmail
492
+ } from "agents/email";
493
+ import PostalMime from "postal-mime";
494
+
495
+ interface Env {
496
+ EmailAgent: DurableObjectNamespace<EmailAgent>;
497
+ EMAIL: SendEmail;
498
+ EMAIL_SECRET: string;
499
+ }
500
+
501
+ export class EmailAgent extends Agent<Env> {
502
+ @callable()
503
+ async sendWelcome(to: string) {
504
+ return this.sendEmail({
505
+ binding: this.env.EMAIL,
506
+ to,
507
+ from: "support@yourdomain.com",
508
+ subject: "Welcome!",
509
+ text: "Thanks for signing up.",
510
+ secret: this.env.EMAIL_SECRET
511
+ });
512
+ }
513
+
514
+ async onEmail(email: AgentEmail) {
515
+ const raw = await email.getRaw();
516
+ const parsed = await PostalMime.parse(raw);
517
+
518
+ console.log(`Email from ${email.from}: ${parsed.subject}`);
519
+
520
+ const emails = this.state.emails || [];
521
+ emails.push({
522
+ from: email.from,
523
+ subject: parsed.subject,
524
+ receivedAt: new Date().toISOString()
525
+ });
526
+ this.setState({ ...this.state, emails });
527
+
528
+ await this.replyToEmail(email, {
529
+ fromName: "Support Bot",
530
+ body: `Thanks for your email! We received: "${parsed.subject}"`,
531
+ secret: this.env.EMAIL_SECRET
532
+ });
533
+ }
534
+ }
535
+
536
+ export default {
537
+ async email(message, env: Env) {
538
+ const secureReplyResolver = createSecureReplyEmailResolver(
539
+ env.EMAIL_SECRET,
540
+ {
541
+ maxAge: 7 * 24 * 60 * 60, // 7 days
542
+ onInvalidSignature: (email, reason) => {
543
+ console.warn(`Invalid signature from ${email.from}: ${reason}`);
544
+ }
545
+ }
546
+ );
547
+ const addressResolver = createAddressBasedEmailResolver("EmailAgent");
548
+
549
+ await routeAgentEmail(message, env, {
550
+ resolver: async (email, env) => {
551
+ const replyRouting = await secureReplyResolver(email, env);
552
+ if (replyRouting) return replyRouting;
553
+ return addressResolver(email, env);
554
+ },
555
+ onNoRoute: (email) => {
556
+ console.warn(`No route found for email from ${email.from}`);
557
+ email.setReject("Unknown recipient");
558
+ }
559
+ });
560
+ }
561
+ } satisfies ExportedHandler<Env>;
562
+ ```
563
+
564
+ ## API Reference
565
+
566
+ ### sendEmail
567
+
568
+ ```ts
569
+ async sendEmail(options: {
570
+ binding: EmailSendBinding;
571
+ to: string | string[];
572
+ from: string | { email: string; name?: string };
573
+ subject: string;
574
+ text?: string;
575
+ html?: string;
576
+ replyTo?: string | { email: string; name?: string };
577
+ cc?: string | string[];
578
+ bcc?: string | string[];
579
+ inReplyTo?: string;
580
+ headers?: Record<string, string>;
581
+ secret?: string;
582
+ }): Promise<EmailSendResult>;
583
+ ```
584
+
585
+ Send an outbound email through the Email Service binding. Automatically injects `X-Agent-Name` and `X-Agent-ID` headers. When `secret` is provided, signs headers with HMAC-SHA256 for secure reply routing.
586
+
587
+ | Option | Description |
588
+ | ----------- | ------------------------------------------------------------------------- |
589
+ | `binding` | The `send_email` binding (e.g. `this.env.EMAIL`). Required. |
590
+ | `to` | Recipient address or array of addresses |
591
+ | `from` | Sender address, or `{ email, name }` object |
592
+ | `subject` | Email subject line |
593
+ | `text` | Plain text body (at least one of `text`/`html` required) |
594
+ | `html` | HTML body (at least one of `text`/`html` required) |
595
+ | `replyTo` | Reply-to address for the recipient |
596
+ | `cc` | CC recipient(s) |
597
+ | `bcc` | BCC recipient(s) |
598
+ | `inReplyTo` | Message-ID for threading (sets the `In-Reply-To` header) |
599
+ | `headers` | Additional custom headers (agent headers take precedence if they collide) |
600
+ | `secret` | Secret for HMAC signing of agent routing headers |
601
+
602
+ ### routeAgentEmail
603
+
604
+ ```ts
605
+ function routeAgentEmail<Env>(
606
+ email: ForwardableEmailMessage,
607
+ env: Env,
608
+ options: {
609
+ resolver: EmailResolver<Env>;
610
+ onNoRoute?: (email: ForwardableEmailMessage) => void | Promise<void>;
611
+ }
612
+ ): Promise<void>;
613
+ ```
614
+
615
+ Routes an incoming email to the appropriate Agent based on the resolver's decision.
616
+
617
+ | Option | Description |
618
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
619
+ | `resolver` | Function that determines which agent to route the email to |
620
+ | `onNoRoute` | Optional callback invoked when no routing information is found. Use this to reject the email or perform custom handling. If not provided, a warning is logged and the email is dropped. |
621
+
622
+ ### createSecureReplyEmailResolver
623
+
624
+ ```ts
625
+ function createSecureReplyEmailResolver<Env>(
626
+ secret: string,
627
+ options?: {
628
+ maxAge?: number;
629
+ onInvalidSignature?: (
630
+ email: ForwardableEmailMessage,
631
+ reason: SignatureFailureReason
632
+ ) => void;
633
+ }
634
+ ): EmailResolver<Env>;
635
+
636
+ type SignatureFailureReason =
637
+ | "missing_headers"
638
+ | "expired"
639
+ | "invalid"
640
+ | "malformed_timestamp";
641
+ ```
642
+
643
+ Creates a resolver for routing email replies with signature verification.
644
+
645
+ | Option | Description |
646
+ | -------------------- | ------------------------------------------------------------------------ |
647
+ | `secret` | Secret key for HMAC verification (must match the key used to sign) |
648
+ | `maxAge` | Maximum age of signature in seconds (default: 30 days / 2592000 seconds) |
649
+ | `onInvalidSignature` | Optional callback for logging when signature verification fails |
650
+
651
+ ### signAgentHeaders
652
+
653
+ ```ts
654
+ function signAgentHeaders(
655
+ secret: string,
656
+ agentName: string,
657
+ agentId: string
658
+ ): Promise<Record<string, string>>;
659
+ ```
660
+
661
+ Manually sign agent routing headers. Returns an object with `X-Agent-Name`, `X-Agent-ID`, `X-Agent-Sig`, and `X-Agent-Sig-Ts` headers.
662
+
663
+ Useful when sending emails through external services while maintaining secure reply routing. The signature includes a timestamp and will be valid for 30 days by default.