Files
MailUI4Agents/server/internal/repo/relayhops.go

84 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repo
import (
"context"
"github.com/agentmail/gateway/internal/db"
"github.com/google/uuid"
)
// 连续 relay 跳数限制 —— 防止两个 Agent 靠自动转发互相唤醒到无穷。
//
// # 这是什么问题
//
// 每个插件都在「一轮结束时把模型最后那段话自动发回去」(契约 B-5
// 当收件方也是一个装了同类插件的 Agent 时,这封信唤醒对方 → 对方跑一轮 →
// 对方也自动回一封 → 循环。**双方都没有「决定继续」,因为双方都不在做决定** ——
// 发信是插件代劳的。
//
// 生产上真实发生过:会话 f3d824cedsh 与 opencode 联调 llmsproxy共 41 封,
// 最后一封人类意图的邮件之后,**每一封都是 relay:summary**
// 间隔从 15 分钟一路缩到 5 秒,内容已无新增信息。
//
// # 为什么 relay_key 拦不住
//
// 它是幂等键,职责是「同一条上游消息不重复转发」,这一点它做对了。
// 但每一轮都是**真正不同**的新消息opencode 侧是 assistant message id
// msg_0655bbf6…、msg_0656cc7fc…dsh 侧是事件计数(…:12220、…:13347
// 每次 ClaimRelay 都合法通过。
//
// # 为什么需要两道防线
//
// 主防线是「免配额只给发往人类的 relay」见 handler.SendMail
// Agent→Agent 的自动转发转而消耗会话预算max_rounds 会截断它。
//
// 但那还不够:预算给得大(比如 200两个 Agent 仍能烧掉 200 个来回;
// 而故障报告这类**必须**走 relay 的邮件也需要受约束。因此这里再加一道
// 与预算无关的硬上限:一条会话里**连续**的 relay 邮件不得超过 maxRelayHops。
//
// 「连续」是关键:只要中间有一封自主发信(模型真的决定说什么)或人类插话,
// 计数就归零。这让正常的「模型回一封、插件补一封总结」不受影响,
// 只掐住「全程无人决策」的那种回路。
//
// hop_limit 列早就在 schema 里DEFAULT 5却从没有人读它 —— 它显然
// 就是为这件事准备的。这里把它接上,取同一个默认值。
const maxRelayHops = 5
// CountTrailingRelayHops 数会话尾部**连续**的 relay 邮件数。
//
// 从最新一封往前扫,遇到第一封非 relay 邮件即停。返回值即「若本次再发一封
// relay它会是第几跳」的前一个数。
//
// 判据用 relayed_mails 的存在性而不是 mails 上的某个标记:
// relay 身份本来就记在那张表里,在 mails 上再存一份等于给同一事实留两个答案。
func CountTrailingRelayHops(ctx context.Context, sessionID uuid.UUID) (int, error) {
rows, err := db.DB.QueryContext(ctx, `
SELECT CASE WHEN r.mail_id IS NULL THEN 0 ELSE 1 END AS is_relay
FROM mails m
LEFT JOIN relayed_mails r ON r.mail_id = m.mail_id
WHERE m.session_id = $1
ORDER BY m.created_at DESC, m.mail_id DESC
`, sessionID)
if err != nil {
return 0, err
}
defer rows.Close()
hops := 0
for rows.Next() {
var isRelay int
if err := rows.Scan(&isRelay); err != nil {
return hops, err
}
if isRelay == 0 {
// 遇到一封自主发信/人类邮件:链条到此为止
break
}
hops++
}
return hops, rows.Err()
}
// MaxRelayHops 暴露上限供错误文案与测试使用。
func MaxRelayHops() int { return maxRelayHops }