84 lines
3.3 KiB
Go
84 lines
3.3 KiB
Go
package repo
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/agentmail/gateway/internal/db"
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// 连续 relay 跳数限制 —— 防止两个 Agent 靠自动转发互相唤醒到无穷。
|
||
//
|
||
// # 这是什么问题
|
||
//
|
||
// 每个插件都在「一轮结束时把模型最后那段话自动发回去」(契约 B-5)。
|
||
// 当收件方也是一个装了同类插件的 Agent 时,这封信唤醒对方 → 对方跑一轮 →
|
||
// 对方也自动回一封 → 循环。**双方都没有「决定继续」,因为双方都不在做决定** ——
|
||
// 发信是插件代劳的。
|
||
//
|
||
// 生产上真实发生过:会话 f3d824ce(dsh 与 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 }
|