mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
refactor(core)!: N0 无状态化 —— 删除 Agent.currentOutputChannel,通道只跟输入事件/帧走
驻留式子 agent 设计(docs/zh/resident-subagent-design.md)的里程碑 N0。 ## 问题 `a.currentOutputChannel` 是 **agent 级可变字段**,只在 prepare 段写入,而被打断任务 恢复时**不重新 prepare**(resumeTask 只 rebase 前缀)。于是中断任务 prepare 时把它 覆盖成自己的通道,被恢复的任务再把回复发到**中断任务的通道**上——两个任务串台。 后果不只是标签错:工具提示词里那句"当前输入来源通道是 X,对应输出门工具是 output_send__X"会诱导模型**把回复主动发到错误的通道**。 ## 两处一起改(用户指出的两件事) 1. **内核不应持有"当前通道"**:通道是随输入事件带进来的,路由发生在**进内核之前**, 输出是 agent 的**主动调用**。删除该字段,改为一律从输入事件推导 (`outputChannelOf(evt)`)或读本任务的帧(`f.OutputChannel`)。 2. **提示词不应预设 outputch**:删掉"当前输入来源通道是 X → 用 output_send__X"那两行, 改为"不要假设当前通道是固定值;先看消息本身与上下文的来源信息,不确定时先调 output_list_channels"。 ## 改动面(把通道一路显式传下去,而不是读共享状态) - `agent.go`:删字段 - `task.go`:新增 `outputChannelOf` / `isCriticalChannel`;帧记录通道; 安全点与 setCritical 用帧/事件推导;步骤内事件标签改用 `f.OutputChannel`; `executeToolCall(f.CurTool, f.OutputChannel)`;`callLLMWithFallback(..., f.OutputChannel)` - `process.go`:`chatStreamWithFallback` / `accumulateStream` 增加 channel 参数 (增量事件的 channel 标签由此而来) - `stage.go`:`runStage` 从 `ctx.Extra["output_channel"]` 读(发起方写入) - `eventloop.go`:`emitResponse` 用 `outputChannelOf(evt)`;stageCtx 带上通道 - `spawn.go` / `toolcall.go`:`executeSpawnChild` 的 parentChannel 由调用方(帧)传入 (子任务完成通知要回到**发起这次 spawn 的那个任务**的通道) - `distill.go`:删掉 consolidation 路径里的赋值 - `tooldefs.go`:删掉提示词里的通道预设 ## 验收 - `scheduler_channel_routing_test.go`(N0 守卫):中断任务跑过之后,被恢复任务的 输出通道仍是它自己的(改前实测为 cli,期望 qq) - `TestCriticalSection_ConsolidationMarked`:补上推导链 「输入事件 → 通道 → isCriticalChannel → scheduler.critical」的集成断言 - 全仓 `go test ./...` 37 包 ok / 0 FAIL;`-race ./internal/agent/...` 干净 - 残留 `currentOutputChannel` 引用为 0(只剩描述历史的注释)
This commit is contained in:
@ -126,6 +126,14 @@ type TaskFrame struct {
|
||||
IsInterrupt bool
|
||||
StartedAt time.Time
|
||||
Terminal taskTerminal
|
||||
// OutputChannel 是本任务的输出通道(来源通道的稳定副本)。
|
||||
//
|
||||
// 这是本任务通道的**唯一**来源:内核不持有"当前通道"可变状态(N0 已删除
|
||||
// Agent.currentOutputChannel)。那类字段会被后来的任务覆盖,而被打断任务
|
||||
// 恢复时不重新 prepare(resumeTask 只 rebase 前缀),于是两任务串台——
|
||||
// 被打断任务的回复发到中断任务的通道上(见
|
||||
// TestPreempt_ResumeKeepsOwnOutputChannel)。
|
||||
OutputChannel string
|
||||
|
||||
// PrefixLen 是 stepPrepare 构建的**基础前缀**长度(system + timeline + 用户输入)。
|
||||
// 恢复时用它把「本任务自己的现场」接回重建后的前缀之上(见 rebaseFramePrefix)。
|
||||
@ -134,6 +142,28 @@ type TaskFrame struct {
|
||||
InputBlocks []agentAPI.ContentBlock
|
||||
}
|
||||
|
||||
// outputChannelOf 从**输入事件**推导本次输出应走的通道。
|
||||
//
|
||||
// 内核不持有"当前通道"可变状态:那类字段会被后来的任务(中断任务)覆盖,
|
||||
// 使被打断任务恢复后的提示词/事件标签串台。通道只跟着事件与帧走。
|
||||
func outputChannelOf(evt *agentIO.InputEvent) string {
|
||||
if evt == nil {
|
||||
return ""
|
||||
}
|
||||
if evt.OutputChannel != "" {
|
||||
return evt.OutputChannel
|
||||
}
|
||||
return evt.Source
|
||||
}
|
||||
|
||||
// isCriticalChannel 报告某个通道是否是**整任务不可抢占**的临界区。
|
||||
//
|
||||
// 目前只有 `_consolidation_`(记忆整理直接改图库)。工具执行/ONNX/CAS 属于
|
||||
// **单步**临界区,由"只在 step 之间检查让位"天然保护,不在这里列。
|
||||
func isCriticalChannel(channel string) bool {
|
||||
return channel == channelConsolidation
|
||||
}
|
||||
|
||||
func (a *Agent) newTaskFrame(input string, stageCtx *sdk.StageContext) *TaskFrame {
|
||||
return &TaskFrame{Input: input, StageCtx: stageCtx, Step: StepPrepare}
|
||||
}
|
||||
@ -148,7 +178,7 @@ func (a *Agent) runTaskSteps(f *TaskFrame) stepOutcome {
|
||||
for i := 0; i < maxSteps; i++ {
|
||||
// 安全点:只在 step 之间检查让位。临界区(StepToolExec)不在此列,
|
||||
// 因为让位信号由 interruptLoop 置位、而本循环是唯一读帧者。
|
||||
if !a.inCriticalSection() && a.sched.preemptGrantedFor() && a.sched.canSuspend() {
|
||||
if !isCriticalChannel(f.OutputChannel) && a.sched.preemptGrantedFor() && a.sched.canSuspend() {
|
||||
return outcomeSuspended
|
||||
}
|
||||
// 工具轮次硬上限(设计文档 D6):在发起下一轮 LLM 前收尾。
|
||||
@ -283,13 +313,11 @@ func (a *Agent) prepareInputTask(evt *agentIO.InputEvent) (*TaskFrame, taskTermi
|
||||
return nil, terminalSkipped
|
||||
}
|
||||
|
||||
a.currentOutputChannel = evt.OutputChannel
|
||||
if a.currentOutputChannel == "" {
|
||||
a.currentOutputChannel = evt.Source
|
||||
}
|
||||
// 通道只从**输入事件**推导,内核不持有"当前通道"可变状态
|
||||
// (见 outputChannelOf;这消除了中断任务覆盖它导致被打断任务串台的整类问题)。
|
||||
// 进入本任务的临界区属性(记忆整理整任务不可抢占)。
|
||||
// 必须在 processConsolidation 之前设置——它就在下面同步执行。
|
||||
a.sched.setCritical(a.inCriticalSection())
|
||||
a.sched.setCritical(isCriticalChannel(outputChannelOf(evt)))
|
||||
|
||||
if evt.OutputChannel == channelConsolidation {
|
||||
a.processConsolidation(evt, in.text)
|
||||
@ -381,6 +409,8 @@ func (a *Agent) prepareInputTask(evt *agentIO.InputEvent) (*TaskFrame, taskTermi
|
||||
f.CleanInput = cleanInput
|
||||
f.IsInterrupt = isInterrupt
|
||||
f.StartedAt = start
|
||||
// 通道记进帧:恢复时用它把 agent 级字段改回来(见 TaskFrame.OutputChannel)。
|
||||
f.OutputChannel = outputChannelOf(evt)
|
||||
return f, terminalNone
|
||||
}
|
||||
|
||||
@ -525,11 +555,11 @@ func (a *Agent) stepLLM(f *TaskFrame) stepOutcome {
|
||||
}
|
||||
|
||||
providers := a.resolveProviders(req)
|
||||
resp, llmErr := a.callLLMWithFallback(req, providers)
|
||||
resp, llmErr := a.callLLMWithFallback(req, providers, f.OutputChannel)
|
||||
|
||||
if llmErr != nil {
|
||||
if errors.Is(llmErr, context.Canceled) && a.ctx.Err() == nil {
|
||||
if a.currentOutputChannel == "_consolidation_" {
|
||||
if f.OutputChannel == channelConsolidation {
|
||||
f.Err = fmt.Errorf("interrupted by user input")
|
||||
return outcomeFailed
|
||||
}
|
||||
@ -579,7 +609,7 @@ func (a *Agent) stepLLM(f *TaskFrame) stepOutcome {
|
||||
if resp.ReasoningContent != "" {
|
||||
a.publishEvent(events.EventReasoning, map[string]interface{}{
|
||||
"content": resp.ReasoningContent,
|
||||
"channel": a.currentOutputChannel,
|
||||
"channel": f.OutputChannel,
|
||||
})
|
||||
}
|
||||
|
||||
@ -633,7 +663,7 @@ func (a *Agent) stepToolBegin(f *TaskFrame) stepOutcome {
|
||||
"args": tc.Arguments,
|
||||
"result": result,
|
||||
"status": "denied",
|
||||
"channel": a.currentOutputChannel,
|
||||
"channel": f.OutputChannel,
|
||||
})
|
||||
f.ToolIdx++
|
||||
return outcomeContinue
|
||||
@ -657,7 +687,7 @@ func (a *Agent) stepToolBegin(f *TaskFrame) stepOutcome {
|
||||
|
||||
// stepToolExec 执行工具。**临界区**:见设计文档 §4.3。
|
||||
func (a *Agent) stepToolExec(f *TaskFrame) stepOutcome {
|
||||
result := a.executeToolCall(f.CurTool)
|
||||
result := a.executeToolCall(f.CurTool, f.OutputChannel)
|
||||
f.CurResult = result
|
||||
f.ToolResults = append(f.ToolResults, ToolResultItem{Name: f.CurTool.Name, Output: result})
|
||||
log.Printf("[agent] tool %s result: %s", f.CurTool.Name, truncateStr(result, 100))
|
||||
@ -768,7 +798,7 @@ func (a *Agent) stepToolAfter(f *TaskFrame) stepOutcome {
|
||||
"args": tc.Arguments,
|
||||
"result": result,
|
||||
"status": "ok",
|
||||
"channel": a.currentOutputChannel,
|
||||
"channel": f.OutputChannel,
|
||||
})
|
||||
|
||||
f.ToolIdx++
|
||||
@ -810,7 +840,7 @@ func (a *Agent) resolveProviders(req *agentAPI.CompletionRequest) []agentAPI.Pro
|
||||
|
||||
// callLLMWithFallback 在候选 provider 间回退,并把同源瞬时错误重试一次。
|
||||
// 逐行等价于原 process() 内的双层循环。
|
||||
func (a *Agent) callLLMWithFallback(req *agentAPI.CompletionRequest, providers []agentAPI.Provider) (*agentAPI.CompletionResponse, error) {
|
||||
func (a *Agent) callLLMWithFallback(req *agentAPI.CompletionRequest, providers []agentAPI.Provider, channel string) (*agentAPI.CompletionResponse, error) {
|
||||
var resp *agentAPI.CompletionResponse
|
||||
var llmErr error
|
||||
|
||||
@ -843,7 +873,7 @@ func (a *Agent) callLLMWithFallback(req *agentAPI.CompletionRequest, providers [
|
||||
a.cancelLLM = fCancel
|
||||
a.llmMu.Unlock()
|
||||
|
||||
resp, llmErr = chatStreamWithFallback(fCtx, fbProvider, req, a)
|
||||
resp, llmErr = chatStreamWithFallback(fCtx, fbProvider, req, a, channel)
|
||||
|
||||
a.llmMu.Lock()
|
||||
a.cancelLLM = nil
|
||||
|
||||
Reference in New Issue
Block a user