## 设计规则:Agent 之间不自动转发
自动转发存在的理由是「人不该等模型记得调 send_mail」—— 收件方是人时这是
纯收益。**收件方是另一个 Agent 时这个理由不成立,而且有害**:双方的插件都
会自动回一封,于是两个模型都以为「我只要把话说完就行」,实际在持续互相唤醒。
生产实测 pi 与 dsh 客套 6 轮直到撞上连续 relay 跳数上限。
规则现在写死在共用模块 `lib/relay-policy.js`(三平台逐字节相同):
- `autoRelayDecision` — 插件该不该替模型开口
- `replyInstruction` — 提示词怎么跟模型说(人类 vs Agent 各一套措辞)
- `inboundHeadline` — 进来的是新活、回复、还是补投
`from_human` 缺失时保守按 Agent 处理:宁可让模型多调一次 send_mail,
也不能承诺一个不会发生的自动回信让发件方白等。
## Gateway 侧:`in_reply_to` + `from_human`
- `notify.Mail` 新增 `ParentMailID`(非空 = 这是对收件方某封信的回复)
- `notify.Mail` 新增 `FromHuman`(走 `repo.IsHumanUser`)
- SSE payload 里叫 `in_reply_to` / `from_human`
- 四个调用点全部传入:handler/mail(转发后产出的邮件,parentMailID 从
resolveTarget 取)、handler/me(同理)、handler/forward(传空串,
因为对收件方而言那封原邮件不在它的线索里)、scheduler/calendar(传空串)
- `ListInbox` 的 SELECT 加 `EXISTS (SELECT 1 FROM users u WHERE u.username = m.from_name)`
→ `models.Mail.FromHuman`,让补拉路径也有这个信号
## 提示词分流
三种处境各一套标题:
- 新活(人类):「你收到一封新邮件」+ 「回信不用你自己发:…」
- 新活(Agent):「你收到一封新邮件(对方是一个 Agent)」+ 「插件不会替你
回信。需要回复时你必须自己调 send_mail…请先判断是否真的需要回复」
- 回复到了:「你上一封信的回复到了。**这不是新任务**。」
- 补投:在标题里说明「离线期间积压」
## homeagent 特殊处理
Go 插件不能直接 `import('../lib/relay-policy.js')`,因此新增 `relay_policy.go`
(Go 对应物)+ `relay_policy_test.go`(11 例,逐条对齐 Node 侧判据)。
`sseLoop` / `catchUp` 两条路径都接上。
## `mailEvent` 命名类型
homeagent 的 SSE 事件解析 / handleNewMail / handlePermissionDecision 三处
原来各写一遍匿名 struct(字段列表几乎相同),加 `from_human` / `in_reply_to`
时漏改一处 → 编译报错但错误信息是两串几乎相同的字段列表,极难定位。
提成 `mailEvent` 命名类型:一处改、三处跟着走。
## 测试
- `lib/relay-policy.test.mjs`(Node)16 例:含「replyInstruction 与
autoRelayDecision 不得互相矛盾」「Agent 来信的标题要点名且回复要明确反对」
- `relay_policy_test.go`(Go)11 例:逐条对齐 Node 侧
- `turn.test.mjs` +3 例:from_human 缺失时按 Agent 处理 / Agent 来信时改口 /
回复到了说「不是新任务」;删掉两条旧的「必定自动转发」断言
- 共用脚本 `check-shared-libs.sh` +1 个文件(relay-policy)
- pi 288 / dsh 241 / opencode 217 / homeagent 14 / gateway 8 包全绿
43 lines
1.8 KiB
Bash
Executable File
43 lines
1.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 共用模块必须逐字节相同 —— 见 docs/PLUGIN-CONTRACT.md 第六节。
|
||
#
|
||
# 一侧改了另一侧没改,几个平台的行为就会悄悄分叉:同一封邮件在 opencode 那边
|
||
# 标了已读、在 DSH 那边没标,而两处代码看起来都"对"。
|
||
#
|
||
# 三方比对以 opencode 为基准逐个对比,而不是两两对比:后者在三方都不同时
|
||
# 会打出三条互相矛盾的差异,读的人无从判断谁是对的。
|
||
set -euo pipefail
|
||
BASE=plugins/opencode-mail-bridge
|
||
PEERS=(plugins/dsh-mail-bridge plugins/pi-mail-bridge)
|
||
fail=0
|
||
|
||
for peer in "${PEERS[@]}"; do
|
||
for f in relay-dedup relay-policy inbox-format session-snapshot workspace model-scope catchup addressing discovery rename-proposal permission-grants adopt; do
|
||
if [[ ! -f "$peer/lib/$f.js" ]]; then
|
||
echo "共用模块缺失:$peer/lib/$f.js" >&2
|
||
fail=1
|
||
continue
|
||
fi
|
||
if ! diff -q "$BASE/lib/$f.js" "$peer/lib/$f.js" >/dev/null 2>&1; then
|
||
echo "共用模块已分叉:lib/$f.js($BASE vs $peer)" >&2
|
||
diff "$BASE/lib/$f.js" "$peer/lib/$f.js" | head -20 >&2
|
||
fail=1
|
||
fi
|
||
done
|
||
# 测试同样要同源:共用模块的行为约定写在测试里,
|
||
# 只同步实现不同步测试,等于允许一侧偷偷放宽约定。
|
||
for f in relay-policy inbox-format session-snapshot workspace model-scope catchup addressing discovery rename-proposal permission-grants adopt; do
|
||
if [[ ! -f "$peer/test/$f.test.mjs" ]]; then
|
||
echo "共用测试缺失:$peer/test/$f.test.mjs" >&2
|
||
fail=1
|
||
continue
|
||
fi
|
||
if ! diff -q "$BASE/test/$f.test.mjs" "$peer/test/$f.test.mjs" >/dev/null 2>&1; then
|
||
echo "共用测试已分叉:test/$f.test.mjs($BASE vs $peer)" >&2
|
||
fail=1
|
||
fi
|
||
done
|
||
done
|
||
|
||
[[ $fail -eq 0 ]] && echo " 共用模块三方同源(opencode / dsh / pi)" || exit 1
|