状态:待审批 日期:2026-03-17
当前 SubAgent 是一次性执行模式:spawnSubAgent() 创建 SubAgent → Run() 执行 → 记忆整理 → 返回结果 → 上下文销毁。每次调用都是全新对话,只能通过持久化记忆系统(core memory + archival memory)继承之前学到的信息。
增加 Interactive Mode,使 SubAgent 可以:
- 多轮对话:parent agent 可以多次向同一个 SubAgent 发送消息,SubAgent 保持上下文
- 手动卸载:parent agent 使用
UnloadSubAgent工具主动结束 SubAgent 会话 - 自动清理:parent agent 退出时(
Run()结束),所有活跃的 SubAgent 自动卸载并整理记忆
- 不持久化 session 到 SQLite:SubAgent 的会话历史只存在于内存中,退出时通过 memorize 将学习写入记忆系统(与当前设计一致)
- 不做超时回收:SubAgent 的生命周期完全由 parent agent 控制(显式 unload 或 parent 退出)
Agent 结构体新增:
activeSubAgents sync.Map // key: conversationID → *activeSubAgent
activeSubAgent 结构体:
conversationID string // "parentAgentID/roleName"
role string // 角色名
parentAgentID string // 父 Agent ID
cfg RunConfig // 预构建的配置(记忆、工具等)
messages []llm.ChatMessage // 对话历史(内存中)
mu sync.Mutex // 保护 messages
cancel context.CancelFunc // 用于强制终止
conversationID = parentAgentID + "/" + roleName
唯一性保证:
- 同一 parent agent 同时只能有一个某 role 的 interactive SubAgent
- 如果已有活跃会话,后续
SubAgent工具调用自动路由到现有会话(多轮对话) - 不同 parent agent 的相同 role 有各自独立的会话
创建(SubAgent 工具,interactive=true):
1. 检查 activeSubAgents 是否已有该 conversationID
2. 如有 → 获取会话,追加新 user 消息,继续 Run()
3. 如无 → 创建新会话,构建 RunConfig,Run()
多轮对话(SubAgent 工具,interactive=true,已有会话):
1. 获取 activeSubAgent
2. 追加 user 消息到 messages
3. 用现有 messages 继续调用 Run()
4. SubAgent 返回后,messages 更新(追加 assistant 回复)
5. SubAgent 退出 Run() 时,RunOutput.WaitingForInput = true
6. Run() 返回给 parent agent,parent agent 可以继续发送
手动卸载(UnloadSubAgent 工具):
1. 获取 activeSubAgent
2. 执行 consolidateSubAgentMemory()
3. 从 activeSubAgents 删除
4. 调用 cancel() 终止 SubAgent
自动清理(parent Run() 结束):
1. 遍历 activeSubAgents,找到属于当前 parentAgentID 的所有会话
2. 对每个会话执行 consolidateSubAgentMemory()
3. 从 activeSubAgents 删除
4. 调用 cancel()
| 退出方式 | 记忆整理 | 资源回收 |
|---|---|---|
| UnloadSubAgent(手动) | ✅ memorize | ✅ 删除会话 |
| parent Run() 结束(自动) | ✅ memorize | ✅ 删除会话 |
| SubAgent 自行结束(无更多工具调用) | ❌ 不整理(等待 parent 卸载) | ❌ 保持活跃 |
关键设计:SubAgent 在 Run() 中正常退出(无更多工具调用)时,不自动整理记忆、不删除会话。它只是暂停,等待 parent agent 下次发送消息。只有 parent agent 显式卸载或 parent 退出时,才触发记忆整理和资源回收。
当前 Run() 在没有更多工具调用时返回 final content 并结束。Interactive SubAgent 需要一个"暂停等待"状态:
type RunOutput struct {
*bus.OutboundMessage
Messages []llm.ChatMessage
WaitingForInput bool // Interactive mode: SubAgent 暂停,等待 parent 继续对话
}
在 RunConfig 中新增:
type RunConfig struct {
// ... 现有字段 ...
// Interactive 交互模式
// true 时,Run() 在 SubAgent 无工具调用时返回 WaitingForInput=true
// 而不是返回 final content 并结束
Interactive bool
}
当 cfg.Interactive == true 且 LLM 返回无工具调用的内容时:
当前行为:
return buildOutput(&bus.OutboundMessage{Content: cleanContent, ...})
Interactive 行为:
// 将 assistant 回复追加到 messages(供下次继续)
messages = append(messages, llm.NewAssistantMessage(cleanContent))
// 返回,标记为等待输入
return buildOutput(&bus.OutboundMessage{
Content: cleanContent, // parent agent 看到的回复
WaitingForInput: true, // 告诉 spawn 逻辑:不要整理记忆
})
在现有 SubAgentTool 参数中增加 interactive 字段:
func (t *SubAgentTool) Parameters() []llm.ToolParam {
return []llm.ToolParam{
{Name: "task", Type: "string", Description: "The task description", Required: true},
{Name: "role", Type: "string", Description: "Predefined role name", Required: true},
{Name: "interactive", Type: "boolean", Description: "If true, keep the sub-agent alive for multi-turn conversation. Use UnloadSubAgent to end it."},
}
}
交互模式下 SubAgentTool.Execute() 的流程变化:
- 解析
interactive参数 - 如果
interactive=true,调用ctx.Manager.InteractiveSubAgent()而非RunSubAgent() - 返回 SubAgent 的回复
type UnloadSubAgentTool struct{}
func (t *UnloadSubAgentTool) Name() string { return "UnloadSubAgent" }
func (t *UnloadSubAgentTool) Description() string {
return `End an interactive sub-agent session and consolidate its memory.
The sub-agent's conversation learnings will be persisted to its memory system.
Parameters (JSON):
- role: string (required), the role name of the sub-agent to unload
Example: {"role": "code-reviewer"}`
}
func (t *UnloadSubAgentTool) Parameters() []llm.ToolParam {
return []llm.ToolParam{
{Name: "role", Type: "string", Description: "The role name of the interactive sub-agent to end", Required: true},
}
}
func (t *UnloadSubAgentTool) Execute(ctx *ToolContext, input string) (*ToolResult, error) {
// 解析 role 参数
// 调用 ctx.Manager.UnloadSubAgent(ctx, role)
// 返回确认信息
}
type SubAgentManager interface {
RunSubAgent(parentCtx *ToolContext, task string, systemPrompt string, allowedTools []string, caps SubAgentCapabilities, roleName string) (string, error)
// 新增:交互式 SubAgent
InteractiveSubAgent(parentCtx *ToolContext, task string, systemPrompt string, allowedTools []string, caps SubAgentCapabilities, roleName string) (string, error)
// 新增:卸载交互式 SubAgent
UnloadSubAgent(parentCtx *ToolContext, roleName string) (string, error)
}
- 文件:
agent/subagent_interactive.go(新增) - 内容:
type activeSubAgent struct { conversationID string role string parentAgentID string cfg RunConfig messages []llm.ChatMessage // 对话历史 mu sync.Mutex cancel context.CancelFunc } - 存储:
Agent.activeSubAgents sync.Map(key: conversationID)
- 文件:
agent/subagent_interactive.go - 流程:
- 计算
conversationID = parentAgentID + "/" + roleName - 查找
activeSubAgents中是否已有会话 - 已有会话(多轮):
- 锁定 mu
- 追加
llm.NewUserMessage(task)到 messages - 构建 cfg(复用已有 cfg 的 Memory、ToolContextExtras 等,替换 messages)
- 设置
cfg.Interactive = true cfg.Messages = subAgent.messagesRun(ctx, cfg)→ 获取回复- 将 assistant 回复追加到 messages
- 返回回复给 parent agent
- 新会话:
- 调用
buildSubAgentRunConfig()构建完整 cfg - 设置
cfg.Interactive = true Run(ctx, cfg)→ 获取回复- 将 assistant 回复追加到 messages
- 创建
activeSubAgent存入activeSubAgents - 返回回复给 parent agent
- 调用
- 计算
- 文件:
agent/subagent_interactive.go - 流程:
- 计算 conversationID
- 从
activeSubAgents获取并删除 - 执行
consolidateSubAgentMemory() - 调用
cancel()(如果 SubAgent 正在运行) - 返回确认信息
- 文件:
agent/subagent_interactive.go - 流程:
- 遍历
activeSubAgents,找到属于指定 parentAgentID 的所有会话 - 对每个执行
consolidateSubAgentMemory()+cancel() - 从 map 中删除
- 遍历
- 调用时机:
spawnSubAgent()返回前(或buildMainRunConfig中注册 cleanup callback)
- 文件:
agent/engine.go - 改动:
RunConfig新增Interactive bool
- 文件:
agent/engine.go - 改动: 在
Run()中,当 LLM 无工具调用时:if !response.HasToolCalls() { if cfg.Interactive { // 交互模式:追加 assistant 回复到 messages,标记等待输入 messages = append(messages, llm.NewAssistantMessage(cleanContent)) out := buildOutput(&bus.OutboundMessage{ Channel: cfg.Channel, ChatID: cfg.ChatID, Content: cleanContent, ToolsUsed: toolsUsed, WaitingForInput: true, }) return out } // 非交互模式:原有逻辑 return buildOutput(...) }
- 文件:
tools/subagent.go - 改动: Parameters() 增加
interactive字段,Execute() 根据 interactive 参数选择调用路径
- 文件:
tools/subagent.go(或新建tools/unload_subagent.go) - 内容: UnloadSubAgent 工具实现
- 文件:
tools/interface.go - 改动: 接口新增
InteractiveSubAgent()和UnloadSubAgent()方法
- 文件:
agent/engine.go - 改动:
spawnAgentAdapter实现新增的两个接口方法
- 文件:
agent/engine_wire.go - 改动: 在
spawnSubAgent()中,当Run()返回后,检查是否有当前 parent agent 的活跃 SubAgent 会话,执行清理 - 更好的方案: 使用
defer在Run()的调用点(processMessage中Run(ctx, cfg)之后)触发清理
- 文件:
tools/interface.go(DefaultRegistry) - 改动:
RegisterCore(&UnloadSubAgentTool{})
activeSubAgent的创建、查找、删除- conversationID 的唯一性
- Interactive 模式:创建 → 多轮对话 → 卸载
- 自动清理:parent 退出时 SubAgent 被清理
- 非 interactive 模式不受影响(回归)
SubAgent 的交互是短暂的(parent agent 一次 Run() 调用内),不需要跨进程持久化。使用 sync.Map 简单高效,退出时通过 memorize 持久化学习。
在 activeSubAgent.messages 中(内存)。下次 parent 发送消息时,这些 messages 被传入新的 Run() 调用继续对话。
复用现有的 AutoCompress 机制。在 buildSubAgentRunConfig 中已经为 SubAgent 配置了 AutoCompress,Interactive 模式下同样生效。
每个 parent agent 有独立的 conversationID(parentAgentID/roleName),互不影响。同一 parent agent 同时只能有一个某 role 的 Interactive SubAgent(第二次调用路由到已有会话)。
- Run() 退出后,LLM 连接、工具等由 GC 回收
- 只有
activeSubAgent结构(含 messages 切片)保留在内存中 - UnloadSubAgent 或 parent 退出时,
activeSubAgent被删除,messages 可被 GC
| 文件 | 改动类型 | 说明 |
|---|---|---|
agent/subagent_interactive.go | 新增 | activeSubAgent 结构、InteractiveSubAgent、UnloadSubAgent、cleanupSubAgents |
agent/engine.go | 修改 | RunConfig.Interactive、Run() 交互模式退出逻辑、RunOutput.WaitingForInput |
agent/agent.go | 修改 | Agent 新增 activeSubAgents sync.Map 字段 |
agent/engine_wire.go | 修改 | spawnSubAgent 退出清理、buildSubAgentRunConfig 可能微调 |
tools/subagent.go | 修改 | SubAgentTool 增加 interactive 参数 |
tools/interface.go | 修改 | SubAgentManager 接口扩展、DefaultRegistry 注册 UnloadSubAgentTool |
tools/unload_subagent.go | 新增(可选) | UnloadSubAgentTool 实现 |
| 风险 | 缓解措施 |
|---|---|
| Interactive SubAgent messages 膨胀 | AutoCompress 自动压缩 |
| parent 异常退出未清理 | Agent.Close() 中遍历清理所有 activeSubAgents |
| 并发访问 activeSubAgent | sync.Mutex 保护 messages,sync.Map 原子操作 |
| UnloadSubAgent 时 SubAgent 正在 Run() | cancel context 中断 Run(),consolidate 超时保护 |