Files
HomeAgent/internal/agent/core/inputroute.go
JianFeeeee 886ba78b11 fix(scheduler): inputch 划给子后输入只流向子 —— 补上「进内核之前」的输入路由
用户指出的语义(设计稿 §4.1 早已写明):
**inputch 是可分配资源**,「路由发生在**进内核之前**」—— 划给某个 agent 后,
该通道的输入**只流向那个 agent**;outputch 不同,授权是**非独占**的,
父依旧可以通过它发送内容。

而代码里 inputch 划拨只做了**登记**,没有做**路由**:
- 插件注入输入的 io 是**根 agent 的**(`cmd/homed` 里 `pluginReg.SetIOManager(iom)`);
- 唯一消费输入的是「该 io 自己的调度器」(`scheduler.go` 读 `a.io.InputChan()`);
- `ChannelRegistry.Assign` 只把 Owner 写进登记表,**没有任何转发动作**。

⇒ 现场表现(用户线上联调):子挂 `inputch=[timer]`,**timer 的输入却打在父身上**
(日志 `[agent] interrupt from timer/timer`),子侧 `轮次=0` 永远不动。
登记表里的 Owner 于是沦为标签。

改法(按 §4.1 把路由放回"进内核之前"):
- `IOManager` 增加 `InputRouter`(`SetInputRouter`),并把**五处直接入队**收口到
  `deliverInput`:`InjectInput` / `InjectInputSync` / `InjectInputTo` /
  `InjectInputSyncTo` / `InjectInterrupt`(排队与中断两条路都过路由)。
- 内核注入路由器 `Agent.routeInputByOwner`:查 inputch 的 Owner —— 归自己/未分配 ⇒
  本内核处理;归自己的某个驻留子 ⇒ `DeliverRouted` 交给它(**不再进父的队列**);
  归一个不存在的 agent ⇒ **不吞输入**,父兜底 + 留痕(吞掉输入比多处理一条更糟)。
- `DeliverRouted` 是"已路由"的投递口,不再二次路由(避免成环)。
- 同步输入的 `ResponseCh` 随事件一起走 ⇒ 回答由持有者写回同一回程(§4.3)。

判据(新增 6 条):
- io 层:被接管时排队/中断都**不入本内核队列**(且中断确实经过路由)/ 放行与未设
  路由器时与历史行为一致 / `DeliverRouted` 不再触发路由
- 内核层:划给子的 inputch 输入进**子**(子 Executed>0)且**父 Enqueued 不变** /
  归属到不存在的 agent 时父兜底(不吞)/ 未分配的 inputch 仍归父

(cherry picked from commit 4707b05498)
2026-09-13 15:35:59 +08:00

45 lines
2.1 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 core
import (
"log"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
)
// routeInputByOwner 实现**输入路由**inputch 是最基本的输入路由单位,
// 划给某个 agent 之后,该通道的输入**只流向那个 agent**,本内核看不到它
// docs/zh/resident-subagent-design.md §4.1:「路由发生在进内核之前」)。
//
// 为什么必须在进内核之前做:插件注入输入的收口是**父**的 IOManager
// `cmd/homed` 里 pluginReg 拿到的就是它),而父的内核是该 io 唯一的消费者。
// 如果不按归属路由,登记表里的 Owner 就只是个标签 —— 现场表现正是如此:
// 子挂着 `inputch=[timer]`timer 的输入却打在父身上,子的轮次永远是 0。
//
// 返回 true = 本次注入已被"持有该 inputch 的 agent"接管,本内核不再处理。
//
// 已知边界:路由只在本 agent 的**直接**驻留子里找。若孙辈的 inputch 由子划拨,
// 而插件注入打在根 io 上,根解析不到那个 owner ⇒ 兜底给根处理(有日志)。
// 这一层要等"孙辈 + 根可见的 agent 表"再收口,此处不静默丢输入。
func (a *Agent) routeInputByOwner(evt *agentIO.InputEvent, isInterrupt bool) bool {
if evt == nil || evt.OutputChannel == "" || a.io == nil {
return false
}
entry, ok := a.io.LookupInputChannel(evt.OutputChannel)
if !ok || entry.Owner == "" || entry.Owner == string(a.id) {
return false // 未分配 / 归自己 ⇒ 本内核处理
}
a.residentMu.Lock()
rc := a.residents[entry.Owner]
a.residentMu.Unlock()
if rc == nil || rc.agent == nil || rc.agent.io == nil {
// 归属到一个不存在(或已销毁、登记表尚未归还)的 agent
// **不吞输入** —— 由本内核兜底处理并留痕。吞掉一条输入比多处理一条更糟:
// 用户会看到"消息发出去了却没人理",而日志里什么都没有。
log.Printf("[route] inputch %s 归属 %s 无对应 agent输入由 %s 兜底", evt.OutputChannel, entry.Owner, a.id)
return false
}
rc.agent.io.DeliverRouted(evt, isInterrupt)
return true
}