feat(scheduler): 主 agent 忙时把积压任务自动转投给驻留子

问题(2026-09-19 线上实测):主 agent 被长任务占住时(现场:12 分 8 秒、69 次
工具调用),后来到达的消息全部以 level insufficient 排进中断队列干等 —— 同级
中断不能抢占同级运行任务(canPreempt),只能等前一个跑完。而内核本有驻留子
(独立 agent + 独立调度器)可并行干活。

行为(用户 2026-09-19 明确要求):
- 触发:运行任务持续 > offload_busy_after(5m) 且积压 >= offload_min_pending(3)
- 拉起/复用「转投专用」驻留子,把积压的纯排队输入转投过去
- 在原队列位置留下说明「[系统] N 条积压任务已转投给驻留子 agent X 处理…」

通道配置(按用户口径,与人工创建的子刻意不同):
- 不配 inputch(内核的干活 agent,不接收插件用户输入)
- 持有全部输出通道(结果要能发回 qq/webui 等正确通道)

三个设计要点(都是实测撞出来的,写进代码注释与设计文档 §7.1):
1. 检查必须在**独立 goroutine**:schedulerLoop 同步执行任务,放它里面在
   「正忙」期间根本回不到循环顶部 ⇒ 永不触发(我第一版就写错了,测试才发现)。
2. 只转投 TaskQueued 纯排队输入:中断任务带级别语义、self 任务与父的记忆面绑定。
3. 转投失败/关闭时必须把任务**放回队列前端**:吞一条输入比多处理一条更糟。

这是设计 §7「决策在父的模型手里」的**刻意例外**(父正忙、物理上无法决策,
而积压任务本来就是空的),已在文档中显式记录,且默认关闭、由部署方显式打开。

测试 11 条:只取排队输入 / 不足量不取 / 放回不丢任务 / 说明自解释 / 默认关闭 /
空闲不触发 / 端到端转投 / 上限不增殖 / 独立 goroutine 确实会触发。
This commit is contained in:
JianFeeeee
2026-09-19 16:59:47 +08:00
parent e273924511
commit 69446a2649
8 changed files with 799 additions and 1 deletions

View File

@ -340,6 +340,40 @@
- **创建/销毁/回收/查看/发送**是**父可调用的原语(工具)****决策**(压还是收、收哪些)
在父的模型手里 —— 内核不替父决定。
### 7.1 例外:积压任务自动转投(内核主动拉起)[已定 · 唯一例外]
**背景2026-09-19 线上实测)**:主 agent 被一条长任务占住时当天现场12 分 8 秒、
69 次工具调用),后来的 QQ 消息全部以 `level insufficient` 排进中断队列干等 ——
同级中断不能抢占同级运行任务(`scheduler.canPreempt`),只能等前一个跑完。
而内核明明有驻留子(独立 agent + 独立调度器 + 共享输出通道视图)可以并行干活。
**行为**:当运行任务已持续超过 `core.agent.offload_busy_after`(默认 5m
**且**排队输入积到 `offload_min_pending`(默认 3条时内核
1. 拉起(或在 `offload_max_residents` 内复用一个)**转投专用驻留子**
2. 把积压的**纯排队输入**转投给它;
3. 在原队列位置留下一条说明:`[系统] N 条积压任务已转投给驻留子 agent X 处理…`
**转投驻留子的通道配置(刻意与人工创建的子不同)**
- **不配 inputch**:它是内核的干活 agent不接收任何插件的用户输入
- **持有全部输出通道**`AllowedOutputs` 为空 = 完整授权):它必须能把结果发回
qq/webui 等正确通道(否则干活结果无处可去)。
**为什么这是对上述原则的例外,且可接受**:父此刻正忙(物理上无法做决策),
而积压任务**本来就是空的** —— 转投只是把「排队干等」换成「有人在做」,
不改变任何已提交决策的语义。若不做例外,这个能力就只能由父的模型发起,
而它恰恰是忙不过来的那个。
**默认关闭**`core.agent.offload_enabled=false`):它改变的是系统行为而非修 bug
按「显式才是特权」(与 `scheduler.DefaultLevel` 同一条理由)由部署方打开。
**不做的事(边界)**
- 只转投 `TaskQueued` 纯排队输入。中断任务带级别语义(转投会打乱中断阶梯)、
self 任务是内核内部记账(与父的记忆面绑定)—— 两者都不动。
- 只对**根 agent** 生效:子再去拉孙子会形成无界增殖,而积压的源头是根那条链。
- 实现上检查跑在**独立 goroutine**`schedulerLoop` 是同步执行的,
放在那里在「正忙」期间根本不会回到循环顶部(等于永不触发)。
- **默认完整授权**[已定]:子默认拿到全部插件与工具(含输出门);
父可在创建时**收窄**(收窄工具子集、收窄可用输出通道集合)。
⚠️ 默认含输出门意味着**子可以直接对用户通道发消息**;若要默认收窄,改一处默认即可。