8bd8271f49
Revert "fix(agent): output_send 缺收件人时从输入事件自动补"
...
This reverts commit 6b74862 .
## 为什么撤
方案本身是错的,三点:
1. **假设 meta 的收件人字段跨通道通用。** 只实现了 qq(user_id/group_id),
而 wechat、群聊各有各的字段名。逐通道补全会变成一张靠猜的映射表,
每加一个通道就得重猜一遍。
2. **假设「回复来源 = 收件人」。** 线上实测直接推翻(2026-09-26 19:05):
一次请求从 webui 会话发起、却要发到 QQ(input source=webui,
output channel=qq)。收件人与来源根本不是一回事 —— 用户在
WebUI 里说"发个 QQ 给我",收件人只能来自上下文或用户明说。
按 channel 名补,等于用错误的假设去覆盖真实场景。
3. **没解决问题,反而引入新风险。** 19:05:46 那次照样报同样的错
("meta 中需要 group_id 或 user_id"),补全逻辑对跨通道场景无效。
而一旦补错,消息会发给错的人 —— 那比报错坏得多。
## 保留的部分
现象描述与排查结论留在这个 revert 的说明里,便于后续重新设计时
不再重复踩:各通道 meta 结构差异极大,要让插件自己声明收件人来源
(qq 插件知道自己的 user_id 从哪来),内核只转发不猜。
不采用"把格式写进工具描述"这类方案:那只缓解症状,
且各通道格式仍需逐个核实。
2026-09-26 19:11:51 +08:00
6b74862118
fix(agent): output_send 缺收件人时从输入事件自动补
...
## 现象(线上 2026-09-26 17:57)
用户从 QQ 私聊发来消息,agent 生成了回复也调了 output_send__qq,
但**没填 meta**:
17:57:45 qq_get_message → {user_id: 2198972886, message_type: private}
17:57:46 output_send__qq → 失败:meta 中需要 group_id 或 user_id 字段
17:58:14 output_send__qq_help → 查格式
17:58:14 output_send__qq → ok ← 靠重试成功,整轮耗时 74s
信息内核**本来就有**(输入事件里带着 user_id/group_id),却要模型从
qq_get_message 的返回里手抄进 meta。抄错就失败,失败才去查 _help。
而"回复"这件事的收件人是确定的(= 消息来源),本不该由模型负责。
运气差就不重试:同日 17:15 / 17:21 两次 `tools=[]` —— 模型压根没调
output_send,回复生成了但没发出去,日志连一行告警都没有。
## 改法
meta 缺收件人时,内核从**本轮输入事件**推导后补上。模型只需给内容。
## 边界(都刻意收窄:宁可不补,也不能补错)
- 显式传了 meta ⇒ 原样返回。主动 DM 别人等场景必须保持原行为。
- meta 里已有 group_id/user_id ⇒ 不覆盖。
- meta 是坏 JSON ⇒ 原样返回。让下游报"格式错",而不是被静默替换成
一个模型没要求过的收件人 —— 那比报错更坏:消息会发给错的人。
- 非 qq 通道(如 webui)⇒ 不补。webui 走 ResponseCh,不过 output_send。
其余异步通道(wechat 等)不猜:猜错等于发错人。
- 输入事件里没有收件人信息 ⇒ 留空,让下游按原逻辑报"需要 user_id"。
宁可报错让模型重试,也不要编一个收件人。
- 群消息里 user_id 是**发送者**不是收件人 ⇒ group_id 非 "0" 时优先用
group_id,否则会把消息发回给群成员本人。
数字型 user_id 要按整数格式化:JSON 反序列化成 float64 时
fmt.Sprint 会得到 "2.198972886e+09"。
## 判据:7 条
私聊补 user_id / 群聊补 group_id 且不补 user_id / 显式 meta 不改写 /
已有收件人不覆盖 / 坏 JSON 不静默替换 / webui 不补 / 无信息留空(含 nil evt)。
★ 实现时我先自己写了个 payloadString,编译报错才发现包内已有更完整的
版本(memorypass.go:176,含 float64/int64/int/json.Number 分支),
直接复用 —— 不重复造轮子。
全量 42 包绿。
2026-09-26 18:29:23 +08:00
1d4f2beeea
fix(prompt): 去掉"每轮只能发一次 output_send"的凭空限制;type 缺省即 text
...
用户现场指出:**qq 插件的输出通道判据太严了**(那条判据在插件侧,已单独修:
`output_send__qq` 不再受"当前会话身份"限制)。同时内核提示词里还有一条**同类的凭空限制**:
「每轮对话**通常只需调用一次** output_send__{通道名} 即可完成回复。
仅在内容确实超过单条消息长度上限(如 >4000 字)时才拆分为多条」
可设计上输出是 agent 的**主动调用**:收到一次输入后,可以往**任意(已授权的)通道**
发**任意多次**(分段播报、先回执后结论、同时通知多个通道都合法)。这句话会让模型
自己收起合理的多次输出 —— 而且它不是任何机制的要求,只是当初为压 output-loop 写的
措辞(真正的防环机制是"回执只回 ok、不回传富结果",那条保留)。
改法:
- 提示词改为明确授权:**输出次数与目标通道由你自己决定**,没有「一轮只能发一次」的限制;
只保留两条真话:单条长度上限(超长拆完整段落)、别反复重发**完全相同**的内容。
- `output_send__*` 的 `type` 参数改为**可选**(缺省 text):判据该拦的是"不知道发什么",
不是"没写众所周知的默认值"——此前缺 type 会直接失败并让模型重试一次。
判据 3 条(新增 `output_rules_test.go`):提示词不得含输出次数限制且必须显式授权 /
省略 type 时按 text 发送成功且 schema 的 required 只有 payload / 空 payload 仍被拦。
(cherry picked from commit 17ea7fd5f0 )
2026-09-13 16:04:16 +08:00
f7c3a4e81d
feat(channel): N1b —— 输出通道授权集合(三处过滤一致)+ 输出通道→目标 agent 的 inputch 解析
...
设计:docs/zh/resident-subagent-design.md §4.5(里程碑 N1b)。
## 输出通道授权集合(默认完整授权,父可收窄)
`AgentConfig.AllowedOutputs`(nil/空 = 完整授权)。三处过滤点必须一致,
否则会出现「列表里看不到、按名字还能调」的裂缝:
1. **工具表**:不为未授权的通道生成 output_send__X(模型看不到就不会调)
2. **列表工具**:output_list_channels 只列授权的
3. **调用点**:凭名字直调未授权的输出门必须被拒(纵深防御)
## 输出通道 → 目标 agent 的 inputch 解析
`ChannelRegistry.BindOutputTarget / ResolveOutputTarget`:
把输出通道解析成「目标 agent + 目标 inputch」,这是"输出可寻址到具体 agent"
(子→父、父→指定子)的**数据面**;真正的跨 agent 投递在里程碑 N4。
未登记的输出通道 ok=false —— 表示由传输层 device 自行处理(qq/webui 这类)。
已登记目标的输出通道,在 output_list_channels 里会标出「目标: <agent> / inputch <名字>」。
## 验收
`internal/agent/core/output_grant_test.go`(3 项):
- 默认完整授权:全部输出门生成 + 列表含全部
- 白名单收窄:三个过滤点同时生效(工具表 / 列表 / 直调被拒)
- 目标解析:绑定/解析、未登记由传输层处理、列表标出目标、空名报错
全仓 go test ./... 37 包 ok / 0 FAIL。
2026-09-13 09:13:19 +08:00
e218d0f100
fix(agent): 工具循环占位不再驱动重复发送,输出回执/子任务结果幂等
...
生产现象:单轮内 output_send__qq 被调用 34 次、持续 514 秒,直到 QQ 插件
自己的循环保险拒绝发送才停下(problem.md)。根因是多环节叠加,核心侧修四处:
1. 工具轮补位文案(process.go)
通用占位「请根据以上工具结果继续。」对纯输出通道调用是错的:异步通道
(qq/wechat)的回复只能经 output_send__* 交付,所以模型「已完成回复」的
表达形式就是一个工具调用,紧随其后的「请继续」会被读成「还要再做一步」,
而能做的「一步」恰好还是再发一条消息。
改为按上一批工具的性质选文案:全部是 output_send__* 时补
「若你的回复已完成,直接返回纯文本即可结束本轮,无需再调用任何工具。」
同时每轮先移除旧占位再补一条,避免占位在 prompt 前缀里线性累积。
(该占位是 zen 网关「最后一条必须是 user」的传输层附加物,HEAD 版本是
无条件内联追加、从不移除。)
2. 输出成功回执(output.go)
「已通过 [qq] 通道发送: map[status:sent]」这类富回执会被读成「这步成功,
继续下一步」。成功改为只回极简标记。
3. proc 桥标量透传(internal/plugin/proc/plugin.go)
插件返回 "ok" 时不再伪造 {status:sent} 覆盖插件真实返回值,否则只改
output.go 不生效。
4. 子任务结果幂等(spawn.go)
child_result 原先读到即删,而完成通知长期留在持久上下文里
(formatMergedTimeline 每轮重新注入),第二次查询必然得到
「不存在或已过期」这个永久失败信号,模型据此认为任务未完成而反复重试。
改为保留结果 + delivered 标记,重复查询返回明确提示;结果按上限有界淘汰。
顺带:agent.go 去掉文档层显式向量器注入(TF-IDF 已内置为 fallback),
cmd/homed/main.go 同步 document.NewStore 的 tokenizer 参数。
测试:internal/agent/core/tooloop_test.go(5 例)、spawn_test.go(3 例)。
2026-09-10 20:36:39 +08:00
b74ee15321
fix(cabi): output_send 等待真实发送结果,消除假成功(plan 11.1 / Part 0.1)
...
根因:CORE_REGISTER_OUTPUT_CH handler 无条件返回 {status:queued}+err=nil,
模型永远收到「已发送」,实际失败(如 meta 缺 user_id)只写日志,模型无法感知不会重试。
现网近 7 天成功 44 次、失败 2 次全部谎报成功。
改动:
- loader.go: 新增 awaitOutputResult(+可注入版 awaitOutputResultWith)+ outputSendTimeout=10s
goroutine 执行 cgo 发送 + 带超时 channel 等结果 → sent / error / unconfirmed 三态
handler 由 executeOutputSendTool 从 Go 侧调起,非 cgo 栈,不构成 cgo 嵌套
- output.go: executeOutputSendTool 识别 unconfirmed|queued,回报「发送结果未确认」而非「已发送」
- output_test.go: Success/Failure/Timeout 三用例
验证: go build exit 0; go test ./internal/plugin/... ./internal/agent/... 全绿
接口冻结: git diff third_party/homeagent-sdk/sdk/ 为空
2026-08-31 12:16:39 +08:00
bd0f84c1f7
fix: deduplicate assistant content in multi-tool turns to prevent premature loop exit
...
- process.go: only emit resp.Content on the first tool call per batch,
subsequent assistant messages use empty content (serialized as null)
- provider.go: MarshalJSON outputs null content when empty with tool_calls
to comply with DeepSeek/OpenAI expected format
- output.go: update parameter interface (payload/type/meta) to match
tool definitions (uncommitted from previous refactor)
2026-07-22 17:05:13 +08:00
85992902d9
refactor: split agent.go into 12 files + add mcp_restart_server tool
2026-07-22 15:40:27 +08:00