mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
feat(resident): 分诊助手定位 + 残余任务由父显式决定
用户澄清(重要定性):这不是「内核替父决定」,而是**及时反馈** —— 主 agent 忙时不该让用户干等十几分钟。子 agent 是**分诊助手**: 简单的直接处理并回复,需要主 agent 的立刻回「忙碌中,请稍候」、不勉强作答。 三处补齐: 1. 分诊助手的职责提示词(之前完全没给 ⇒ 子不知道自己为什么存在): 两条路(直接办 / 报忙碌)、拿不准时报忙碌、必须 output_send 到原通道。 2. 驻留子继承父的 SystemPrompt(之前没传 ⇒ 子只用一句兜底文案, 拿不到「异步通道必须显式 output_send,否则回复被静默丢弃」这条铁律。 webui 这类同步通道能回是因为走 ResponseCh,掩盖了这个缺陷)。 3. 残余任务由父显式决定(用户要求):reclaim/destroy 时子手头未处理的消息 不再由内核悄悄处置 —— 内核只负责列清楚,父用 residual=keep/drop 决定。 之前 pendingEvents 只收带 ResponseCh 的,异步(qq)残余任务完全不在内, 被销毁时静默消失、用户零反馈且日志无痕。 配套: - scheduler.takeAllPendingEvents:取走全部未执行事件(不筛通道) - ApplyResidual(keep|drop):keep 转回父队列(保留 ResponseCh), drop 逐条记日志 + 给同步调用方补终态(否则 cli/a2a 永久挂起) - 状态面暴露 offload_owned,让父分清「我建的子」与「内核临时拉的助手」 - 工具 schema 加 residual 参数并说明 drop 的代价 这也是用户观察到的「机制很自然」的落点:分诊助手就在同一张登记表里, 父能 inspect/send/compress/reclaim/destroy,控制面 6 动作按 id 生效不区分来源。 测试 +7(残余 keep 转回且保留 ResponseCh / drop 通知同步调用方 / 空残余如实报告 / 分诊提示词 / 继承 SystemPrompt), 其中 drop 那条已实测「对着静默丢弃的旧实现会失败」。全套绿。
This commit is contained in:
@ -77,12 +77,16 @@ func (o OffloadOptions) normalized() OffloadOptions {
|
||||
//
|
||||
// 它必须**自己说清是系统做的**:用户看到队列里出现一条没人发过的消息时,
|
||||
// 唯一能解释这件事的就是这句话本身。
|
||||
//
|
||||
// 同时要说清"不必重复处理":那些消息已由子 agent 回复(或已回复"忙碌中"),
|
||||
// 主 agent 再处理一遍会让用户收到重复回复。
|
||||
func offloadNotice(count int, residentID string) string {
|
||||
return fmt.Sprintf(
|
||||
"[系统] %d 条积压任务已转投给驻留子 agent %s 处理(主 agent 正忙于长任务,"+
|
||||
"内核为它们拉起了独立 agent 并行执行)。它们的回复会由 %s 直接发到对应通道;"+
|
||||
"本提示仅用于说明「那几条消息不会再由你处理」,无需为它们采取任何行动。",
|
||||
count, residentID, residentID)
|
||||
"[系统] %d 条积压消息已在主 agent 忙期间交由临时助手 %s 先行分诊"+
|
||||
"(简单的已直接处理并回复,需要你的那些已告知用户「忙碌中,请稍候」)。"+
|
||||
"它们**不需要你再处理**了;若其中有需要你后续跟进的,请查看上述通道的会话记录。"+
|
||||
"本提示仅用于说明情况,无需回复。",
|
||||
count, residentID)
|
||||
}
|
||||
|
||||
// offloadCandidate 是一条可被转投的排队任务。
|
||||
@ -283,13 +287,14 @@ func (a *Agent) ensureOffloadResident(opts OffloadOptions) (string, error) {
|
||||
info, err := a.SpawnResident(ResidentOptions{
|
||||
ID: id,
|
||||
// 不配 inputch:它是内核的**干活** agent,不接收任何插件的用户输入
|
||||
// (用户要求"不配输入通道")。它只由父经 sub/<id> 投喂任务。
|
||||
// (用户要求"不配输入通道")。它只由父经转投拿到任务。
|
||||
InputChs: nil,
|
||||
// 全部输出通道:它要能把结果发回 qq/webui 等正确通道
|
||||
// (用户要求"持有全部输出通道")。nil = 完整授权。
|
||||
AllowedOutputs: nil,
|
||||
TempPath: a.residentTempPath(id),
|
||||
OffloadOwned: true,
|
||||
TaskPrompt: offloadTaskPrompt(),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -297,6 +302,42 @@ func (a *Agent) ensureOffloadResident(opts OffloadOptions) (string, error) {
|
||||
return info.ID, nil
|
||||
}
|
||||
|
||||
// offloadTaskPrompt 是转投专用驻留子的**分诊职责**说明。
|
||||
//
|
||||
// 为什么必须给:不给的话子完全不知道自己为什么存在(只知道自己是"小宅"),
|
||||
// 拿到一条转投消息时不知道它是"用户正在等回复的请求",
|
||||
// 也不知道自己只有两条路可走(直接办 / 报忙碌)。
|
||||
//
|
||||
// 用户的定位(2026-09-19 明确):这不是"内核替父决定",而是**及时反馈** ——
|
||||
// 主 agent 忙时不该让用户干等(实测有 13 分钟的现场)。
|
||||
// 子的职责是**分诊**(triage):
|
||||
// - 简单、不需主 agent 介入的 → 直接办完并回复;
|
||||
// - 需要主 agent 介入的 → 立刻回「忙碌中,请稍候」,**不要勉强做**。
|
||||
func offloadTaskPrompt() string {
|
||||
return `你是主 agent 的临时助手,负责在主 agent 忙不过来时**分诊**它的积压消息。
|
||||
|
||||
背景:主 agent 正在执行一个长任务,短时间无法处理新消息。你被临时拉起,
|
||||
专门承接这些积压的请求,**避免用户干等**(此前用户可能要等十几分钟)。
|
||||
|
||||
对每一条消息,你只有两条路:
|
||||
|
||||
1. 【直接办】如果这件事简单、明确、不需要主 agent 的全局上下文或长期规划
|
||||
(例如:查个信息、跑个小命令、读个文件、简单问答)——
|
||||
**直接做完,并把结果发回原通道**。
|
||||
|
||||
2. 【报忙碌】如果这件事需要主 agent 介入(需要它的长期记忆、正在进行的任务上下文、
|
||||
需要它做多步决策,或你无法确定怎么做)——
|
||||
**不要勉强尝试**。立刻回复用户:主 agent 当前忙碌中,请稍候。
|
||||
|
||||
重要约束:
|
||||
- **必须把回复发到用户原本的通道**。面向 qq、wechat 等异步通道时,
|
||||
纯文本返回会被丢弃 —— 必须显式调用 output_send__{通道名},否则用户收不到,
|
||||
而你会以为已经回过了。
|
||||
- 不要向用户暴露"我是被临时拉起的助手"这类内部细节,用主 agent 的口吻回复。
|
||||
- 拿不准属于哪一类时,选【报忙碌】。宁可让用户稍后得到准确答复,
|
||||
也不要给出错误的直接回答。`
|
||||
}
|
||||
|
||||
// residentTempPath 计算某个驻留子的 temp 图记忆路径(与既有约定一致)。
|
||||
func (a *Agent) residentTempPath(id string) string {
|
||||
return strings.TrimRight(a.dataDir, "/") + "/residents/" + id + "/graph.db"
|
||||
|
||||
Reference in New Issue
Block a user