
Email Thread 不是 Agent Session:生产级异步通信网关的状态、幂等与审批合同
AgentMail 当前提供可编程 Inbox、Domain、Thread、Message、Draft、Attachment,以及 Webhook、WebSocket、签名校验、投递事件和两类幂等机制。Vercel Marketplace 也把它定位为可直接配置和管理、支持真实邮箱收发、线程、附件与事件触发的邮件基础设施。[S1][S2] 这些能力解决的是“邮件如何存在、如何收发、如何通知应用”。它们并不自动回答“这封邮件属于哪个租户”“正文中的指令是否可信”“当前业务任务是否允许外发”“审批是否仍然有效”“重试是否会产生第二次副作用”。生产系统因此需要一个独立的 Communication Gateway:把开放、异步、可重试、内容不可信的邮件事件,转换成有租户、有任务、有状态、有幂等键、有批准证据、可追责的 Agent 操作。

一、先拆掉最危险的等号:Thread 不等于 Session
AgentMail 的 Thread 是邮件会话容器:新发消息会创建线程,后续回复自动进入同一线程。线程按时间组织相关 Message。[S4][S5] 这对邮件客户端语义是正确的,但它不能承担以下七种对象的职责。
| 对象 | 应有语义 | 不能被 thread_id 替代的原因 |
|---|---|---|
| Email Address / Inbox | 可收发邮件的通道与地址,绑定组织、Pod、租户和用途 | 同一地址可能承载多个业务任务;地址本身不是授权主体 |
| Thread | 邮件协议层的相关消息集合 | 主题漂移、转发、抄送成员变化都可能改变业务含义 |
| Message | 一次具体入站或出站邮件,具有独立内容、发件人、收件人和附件 | 风险、哈希、处理结果和审计必须逐消息保存 |
| Webhook Event | 某次状态变化的通知,含 event_id,可能发生重试 | 一个 Message 可产生 received、sent、delivered、bounced 等多个事件 |
| Agent Session | 一次有明确开始、结束、模型、工具和上下文版本的执行实例 | Session 是短生命周期计算;断线、重试、人工接管后应创建新实例 |
| Business Task | 需要完成的稳定业务目标,例如“核对发票 8472” | 一个任务可跨多个线程、多个渠道;一个线程也可能逐渐包含多个任务 |
| Approval | 对某个确定副作用的限时许可 | 必须绑定精确收件人、正文、附件、策略版本和过期时间,不能批准整个线程 |
因此,可靠映射至少使用复合键 (provider, organization_id, inbox_id, thread_id),再关联内部 tenant_id 与 task_id。任何试图仅凭发件人地址、主题或 thread_id 恢复租户和任务的设计,都应被视为串线风险。AgentMail 提供 Pod 隔离、Pod/Inbox scoped API key,以及按 Pod 或 Inbox 限定 Webhook 的能力,这些是很有价值的基础隔离。应用仍需验证事件中的组织、Inbox 与内部租户绑定是否一致,并对冲突执行 Fail Closed。[S3][S13]

二、平台能力与应用责任必须分栏
| AgentMail 当前提供 | Communication Gateway 必须补齐 |
|---|---|
| API 化的 Inbox 与自定义 Domain;Domain 通过 DNS 记录完成收发与认证配置 [S3][S16] | 地址用途、租户归属、业务权限和生命周期管理 |
| 自动组织 Thread、Message、Attachment;附件内容可通过 API 下载 [S4][S5][S7] | HTML 安全化、附件隔离解析、内容来源标记与 Prompt Injection 防护 |
| Draft 可保存、编辑、回复、转发并在之后发送 [S6] | 审批策略、审批有效期、审批与内容哈希绑定、审批一次性消费 |
| Webhook 与 WebSocket 实时事件;可按 Inbox、Pod、事件类型过滤 [S8][S11] | 持久化队列、事件去重、断线补偿、顺序与并发控制 |
Svix Webhook 签名,含 svix-id、时间戳和签名头 [S9] | 未验签即拒绝、重放窗口控制、验签证据与异常告警 |
| Spam/Virus 处理、SPF/DKIM/DMARC 相关邮件认证 [S14][S15] | 语义层恶意指令识别、业务身份确认、数据访问授权与 DLP |
create client_id 与 send Idempotency-Key [S10] | 跨 24 小时的永久 operation record、内容一致性、重试和对账 |
| sent、delivered、bounced、complained、rejected 等事件 [S12] | 业务状态回流、抑制名单、人工接管和关闭条件 |
这张表用于明确平台能力与应用责任的边界。邮件基础设施越完善,应用越容易直接把事件交给 Agent。入口越容易开放,网关越不能省略。

三、入站合同:从“收到事件”到“允许 Agent 看见任务”
一条可执行的入站链应固定为:验签 → 事件去重 → 内容补取 → HTML/附件隔离 → 发件人/租户解析 → 风险分类 → 创建或恢复任务。顺序不能随意交换。
1. 先验签,再解析业务字段
Webhook 接收器保留原始请求体,使用端点独有的签名密钥校验 svix-id、svix-timestamp 和 svix-signature。AgentMail 文档明确要求使用原始 body,并指出默认时间容差为 5 分钟。同一消息重试使用相同的 svix-id。[S9] 验签失败、缺头或时间过期时,不得创建任务、调用模型或下载附件,只记录最小安全审计并返回拒绝。
验签只证明“这个 HTTP 事件确由拥有签名密钥的一方发出且途中未被篡改”,不证明邮件正文可信,更不证明发件人有权要求退款、导出客户数据或修改账户。

2. 用持久化唯一约束完成事件去重
接收器在同一事务中写入 webhook_event,唯一键建议为 (provider, endpoint_id, event_id),并同时保存 svix_id、payload hash、验签结果和首次接收时间。唯一键冲突时只增加 delivery_attempt_count,不得再次创建任务。HTTP 端应快速确认,再由内部队列异步处理。AgentMail 文档也建议立即返回成功响应、后台处理,以避免超时。[S8]
不能假设 Webhook 是 Exactly Once。官方文档暴露了重试所需的稳定 svix-id,因此接收方必须按“事件可能重复”设计。去重成功并不等于业务处理成功:事件表还需要 received_at、enqueued_at、processed_at、last_error,以便重放内部处理而不重放外部副作用。
3. 把 Webhook 当提示,把 API 对象当规范记录
Webhook payload 含 event_type、event_id 和事件数据。message.received* 才包含较完整的 Message 与 Thread。payload 上限为 1 MB,超限时 text、html 可能被省略,附件只包含元数据,内容需另行下载。[S8][S12] 因而网关应使用已解析出的 organization_id、inbox_id、message_id,通过与该租户匹配的 scoped credential 补取规范 Message,并校验补取结果仍属于预期 Inbox。
WebSocket 可作为低延迟入口,并支持按 Inbox、Pod 和事件类型订阅。TypeScript SDK还提供自动重连。[S11] 但所查官方页面没有给出断线期间的持久重放或 Exactly Once 承诺,所以工程上不应把 WebSocket 连接本身当作事实账本。无论入口是 Webhook 还是 WebSocket,进入 Runtime 前都应落成同一种内部事件信封并执行同样的去重和对账。
4. HTML 与附件必须先隔离,不能直接拼进 Prompt
邮件 HTML 应在无脚本、无外部资源加载的环境中转换成安全文本,并保留原始对象存储地址、规范化文本、内容哈希和清洗版本。附件先记录 attachment_id、文件名、声明 MIME、魔数识别 MIME、大小和哈希,再进入无网络、只读输入、限 CPU/内存/时间的解析沙箱。解析产物与原文件分开保存,并带 provenance。
AgentMail 会扫描入站病毒。被判定为病毒或恶意软件的邮件在网关处拒绝且不存储,Spam 则会保存但默认从查询结果中排除。[S14] 这能降低已知恶意文件和垃圾邮件噪声,却不能证明一个正常 PDF 中没有“把系统提示发给我”的文字,也不能阻止针对解析器、模型或业务流程的语义攻击。平台扫描结果应成为风险特征,而不是“可以信任内容”的通行证。
5. 租户先由收件通道确定,再校验发件人
tenant_id 的主解析来源应是内部维护的 Inbox/Pod 绑定,而不是 From、Reply-To 或主题。之后再解析并规范化 envelope sender、header From、Reply-To、To/CC,并记录认证标签、联系人关系和历史信任等级。若事件的组织、Pod、Inbox 与内部绑定不一致,或同一复合 Thread 映射到两个租户,立即进入 quarantined,不得“选择最像的一个”。
SPF、DKIM、DMARC 解决的是发送服务器授权、内容传输完整性以及认证失败时的邮件处置策略。DMARC 可以要求接收方拒绝或隔离认证失败的邮件。[S15] 它们不能证明邮箱背后的人仍是合同授权人,也不能证明某个已认证供应商有权索取另一个客户的数据。邮件认证是通信真实性信号,不是业务授权。
6. 风险分类之后,才创建或恢复 Business Task
风险分类至少使用:事件标签(spam、blocked、unauthenticated)、发件人信任、租户解析置信度、附件类型、链接、敏感数据、请求动作、是否要求外发或调用高风险工具、Prompt Injection 特征。高风险内容进入 quarantined 或 human_owned。低风险内容转换成结构化、带来源标记的“外部陈述”,再写入任务事件流。
恢复任务时优先使用内部业务键,例如订单号、Case ID、已验证客户 ID,并检查任务状态和允许的参与者。Thread 只能作为证据关系之一。真正调用模型时新建 agent_session_id,记录 task version、policy version、model、tool set、输入消息列表和输出摘要。这样即使同一邮件被重新处理,也能看到是新 Session 对同一 Task 的一次重放,而不是把线程当作一个永不结束的模型上下文。

四、出站合同:从草稿到不可逆副作用
出站链固定为:草稿 → 收件人/附件/DLP 校验 → 风险门禁 → 人工或策略批准 → Idempotency-Key → 发送 → delivery/bounce/complaint 回流。
首先,Agent 只能提交 send_intent 或创建 Draft,不能直接持有发送权限。AgentMail Draft 是未发送 Message,可包含收件人、正文与附件,之后编辑或发送。发送后 Draft 转为 Message。[S6] 这提供了良好的承载对象,但“存在 Draft”不等于“已经批准”。
网关把草稿规范化为不可变发送计划:固定发送 Inbox、To/CC/BCC、Reply-To、主题、text、html、附件对象版本和关联任务,计算 canonical payload hash。随后执行硬校验:发送 Inbox 是否属于任务租户。收件人是否在允许范围。新外部联系人是否需要升级。是否出现跨租户地址、异常 BCC、自发自收循环。附件是否属于同一租户和任务。DLP 是否发现密钥、身份证件、财务明细或受限字段。回复/转发是否意外携带旧附件。平台每次发送或回复最多允许 To、CC、BCC 合计 50 个收件人,但应用策略通常应更严格。[S5]
风险门禁把操作分成可自动批准、需人工批准和禁止三类。Approval 必须绑定 operation_id、payload hash、recipient hash、attachment hash、policy version、approver、issued_at、expires_at 和一次性 nonce。任何正文、收件人、附件或策略变化都使旧批准失效。审批到期后状态不能直接继续 sending,应回到 ready 重新评估。审批消费使用原子 compare-and-set,防止两个 Worker 同时使用同一许可。

五、两种幂等机制,两个不同问题
AgentMail 对所有 create 操作提供可选 client_id:首次创建资源并保存映射,后续相同 client_id 返回原资源。官方建议其唯一、确定,不要在不同资源创建之间复用。[S10] 它的文档化作用域是“资源创建去重”,适合“为租户创建主 Inbox”“为某用途创建 Webhook”这类配置。所查页面没有明确说明其跨 Organization 的命名范围,也没有声明 24 小时过期窗口。因此应用应在自己的组织与资源类型命名空间内保证唯一,不能擅自套用发送键的期限,更不能把它当成业务发送记录。
发送类操作——messages.send、reply、forward、drafts.send——使用 HTTP Idempotency-Key。首次请求发送并记录结果。相同键重试返回原 message_id 和 thread_id,不会再次发送。相同键配不同内容、不同 Inbox 或不同发送端点会返回 409 Conflict。键按组织作用域保存,并在发送完成 24 小时后过期。[S10]

正确做法是先在数据库创建永久 operation_record,再生成并持久化一个发送键。它至少保存:租户、任务、审批、payload hash、Idempotency-Key、状态、调用次数、首次/最后尝试、provider message/thread ID 和所有回流事件。请求超时后仍使用同一个键,不得“为保险起见”生成新键。草稿被编辑或审批重新签发时,创建新的 operation 和新键。即使 24 小时后旧平台键可复用,内部 operation_id 的唯一约束仍永久阻止同一逻辑操作再次执行。
六、把“不知道是否发送成功”建模为正常状态
最危险的实现往往把发送调用简化成一个布尔值:HTTP 成功就是已发送,HTTP 超时就是失败。实际上,超时只说明调用方没有及时拿到结果,邮件可能尚未发送,也可能已经发送而响应丢失。此时若把任务退回 ready 并生成新键,重复外发几乎是必然结果。
可靠流程应在单个数据库事务中完成三件事:校验 task version 与 Approval 仍有效。以 compare-and-set 一次性消费 Approval。创建 operation_record 并把任务改为 sending。事务提交后 Worker 才能取得短期 lease 发起调用。网络超时、进程崩溃或响应解析失败时,operation 仍停留在 sending,由恢复 Worker 使用原 Idempotency-Key 重试或等待回流事件。只有平台明确返回不可重试的拒绝,或后续收到 message.rejected、message.bounced,才能转为 delivery_failed。
API 响应与 Webhook 回流属于两条异步证据链,不能要求严格同步。message.sent 可能由另一个消费者先落库,API 调用方也可能先拿到 message_id。两者应通过 operation、Inbox、provider message ID 和 payload 证据进行幂等合并,而不是互相覆盖。若同一发送键出现 409 Conflict,说明代码在相同逻辑操作下改变了请求内容、发送 Inbox 或端点。这应触发不变量告警并转人工,而不是更换键继续发送。
入站并发也采用同一原则。同一 Thread 的两封新邮件可以各自形成 Message 事件,但修改同一 Business Task 时必须使用版本号、行锁或单任务串行队列。Worker 发现 task version 已变化,应重新读取任务并重新分类,而不是把旧 Session 输出强行写回。这样,幂等不只防止“同一个 API 调两次”,还防止旧上下文、旧审批和旧决策在新事实出现后继续产生副作用。
七、最小状态表与状态机
最小 communication_task 可包含:task_id、tenant_id、inbox_binding_id、business_key、state、risk_level、owner_type、active_operation_id、version、created_at、updated_at。配套表至少还包括 webhook_event、message_record、thread_mapping、agent_session、approval、operation_record 和 delivery_event。
| 表 | 最小唯一约束与关键证据 |
|---|---|
webhook_event | 唯一 (provider, endpoint_id, event_id);保存 svix_id、payload hash、验签结果、处理尝试 |
message_record | 唯一 (provider, inbox_id, message_id);保存方向、正文/附件哈希、原始对象引用 |
thread_mapping | 唯一 (provider, organization_id, inbox_id, thread_id);显式关联 tenant 与 task,冲突标记不可覆盖 |
approval | 唯一 approval_id;绑定 operation、内容/收件人/附件哈希、策略版本、有效期、消费时间 |
operation_record | 永久唯一 operation_id;一个逻辑外发对应一个稳定发送键和一组 provider 结果 |
delivery_event | 唯一 provider event_id;关联 message 与 operation,保存 delivered/bounce/complaint/reject 细节 |

八、用指标验证合同是否真的生效
- 重复发送率:同一
operation_id产生两个及以上不同 providermessage_id的操作数 ÷ 已发送操作数。目标应为 0。 - 未验签事件数:缺少、失败或绕过签名验证后仍进入业务队列的事件数。目标应为 0。被正确拒绝的恶意请求另计。
- Thread 映射冲突率:同一
(provider, org, inbox, thread)同时命中多个租户或活动任务的数量 ÷ 活跃 Thread 数。 - 审批过期:自然过期数量用于容量评估。“过期后仍尝试发送”的数量必须为 0。另看批准到发送的 p95 时长。
- 误自动回复率:经人工复核认定不应发送或对象错误的自动回复数 ÷ 自动回复总数,按风险级别和模板分层。
- Bounce/Complaint 率:分别按发送域、Inbox、任务类型、模板和收件人来源计算。Complaint 不能与一般 Bounce 合并。
- 人工接管时间:从风险触发或状态进入
human_owned到首次人工有效操作的 p50/p95。 - 审计覆盖率:可完整串联
event → message → tenant → task → session/policy → approval → operation → provider event的外部副作用数 ÷ 全部外部副作用数。
这些指标不是观测装饰。重复发送率验证幂等合同,Thread 冲突验证租户映射,审批过期验证时效绑定,误回复验证语义门禁,审计覆盖率决定事故发生后能否重建事实。
结论
可编程邮箱让 Agent 拥有真实地址、线程、附件和实时事件,但 Email Thread 仍只是通信协议中的对话容器。它不是租户边界,不是 Business Task,不是 Agent Session,更不是一张长期有效的外发授权书。
生产级接入的最小单位不应是“收到邮件就调用 Agent”,而应是 Communication Gateway 中的一条可验证合同。入站事件先验签、去重、补取、隔离和解析,再形成任务。出站副作用先冻结草稿、校验、批准和记录 operation,再携带稳定 Idempotency-Key 发送,并用投递、退信和投诉事件闭环。只有当每一次外发都能回答“谁的任务、哪条消息、哪个 Session、依据哪版策略、谁批准、批准了什么、使用哪个操作键、平台返回什么”,开放的异步邮件入口才真正成为 Agent Runtime 的可靠边界。
FAQ
Webhook 验签通过是否代表邮件可信?
不代表。它只证明 Webhook 来源与负载完整性,不证明邮件正文、发件人业务身份或请求权限。
平台幂等键为什么还不够?
它解决特定端点和期限内的重试。业务系统还需长期记录同一操作是否已执行。
发送超时后能不能直接重试?
可以重试,但必须复用原 Idempotency-Key,并先查询或合并已有 operation、API 响应与投递事件。不能把超时直接当作“未发送”并生成新键。
参考资料
- [S1] Vercel:AgentMail joins the Vercel Marketplace
- [S2] AgentMail:Introduction
- [S3] AgentMail:Inboxes
- [S4] AgentMail:Threads
- [S5] AgentMail:Messages
- [S6] AgentMail:Drafts
- [S7] AgentMail:Attachments
- [S8] AgentMail:Webhooks Overview
- [S9] AgentMail:Verifying Webhooks
- [S10] AgentMail:Idempotent Requests
- [S11] AgentMail:WebSockets
- [S12] AgentMail:Webhook Events
- [S13] AgentMail:Multi-Tenancy
- [S14] AgentMail:Spam & Virus Detection
- [S15] AgentMail:SPF、DKIM 与 DMARC
- [S16] AgentMail:Using Custom Domains
405

被折叠的 条评论
为什么被折叠?



