diff --git a/internal/handler/multi_agent.go b/internal/handler/multi_agent.go index 06d2c161..c2de9083 100644 --- a/internal/handler/multi_agent.go +++ b/internal/handler/multi_agent.go @@ -44,11 +44,20 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) { c.Header("X-Accel-Buffering", "no") + // 用于在 sendEvent 中判断是否为用户主动停止导致的取消。 + // 注意:baseCtx 会在后面创建;该变量用于闭包提前捕获引用。 + var baseCtx context.Context + clientDisconnected := false sendEvent := func(eventType, message string, data interface{}) { if clientDisconnected { return } + // 用户主动停止时,Eino 可能仍会并发上报 eventType=="error"。 + // 为避免 UI 看到“取消错误 + cancelled 文案”两条回复,这里直接丢弃取消对应的 error。 + if eventType == "error" && baseCtx != nil && errors.Is(context.Cause(baseCtx), ErrTaskCancelled) { + return + } select { case <-c.Request.Context().Done(): clientDisconnected = true @@ -135,7 +144,6 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) { ) if runErr != nil { - h.logger.Error("Eino DeepAgent 执行失败", zap.Error(runErr)) cause := context.Cause(baseCtx) if errors.Is(cause, ErrTaskCancelled) { taskStatus = "cancelled" @@ -153,6 +161,7 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) { return } + h.logger.Error("Eino DeepAgent 执行失败", zap.Error(runErr)) taskStatus = "failed" h.tasks.UpdateTaskStatus(conversationID, taskStatus) errMsg := "执行失败: " + runErr.Error() diff --git a/internal/multiagent/no_nested_task.go b/internal/multiagent/no_nested_task.go new file mode 100644 index 00000000..09ad28e9 --- /dev/null +++ b/internal/multiagent/no_nested_task.go @@ -0,0 +1,62 @@ +package multiagent + +import ( + "context" + "strings" + + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/components/tool" +) + +// noNestedTaskMiddleware 禁止在已经处于 task(sub-agent) 执行链中再次调用 task, +// 避免子代理再次委派子代理造成的无限委派/递归。 +// +// 通过在 ctx 中设置临时标记来实现嵌套检测:外层 task 调用会先标记 ctx, +// 子代理内再调用 task 时会命中该标记并拒绝。 +type noNestedTaskMiddleware struct { + adk.BaseChatModelAgentMiddleware +} + +type nestedTaskCtxKey struct{} + +func newNoNestedTaskMiddleware() adk.ChatModelAgentMiddleware { + return &noNestedTaskMiddleware{} +} + +func (m *noNestedTaskMiddleware) WrapInvokableToolCall( + ctx context.Context, + endpoint adk.InvokableToolCallEndpoint, + tCtx *adk.ToolContext, +) (adk.InvokableToolCallEndpoint, error) { + if tCtx == nil || strings.TrimSpace(tCtx.Name) == "" { + return endpoint, nil + } + // Deep 内置 task 工具名固定为 "task";为兼容可能的大小写/空白,仅做不区分大小写匹配。 + if !strings.EqualFold(strings.TrimSpace(tCtx.Name), "task") { + return endpoint, nil + } + + // 已在 task 执行链中:拒绝继续委派,直接报错让上层快速终止。 + if ctx != nil { + if v, ok := ctx.Value(nestedTaskCtxKey{}).(bool); ok && v { + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + // Important: return a tool result text (not an error) to avoid hard-stopping the whole multi-agent run. + // The nested task is still prevented from spawning another sub-agent, so recursion is avoided. + _ = argumentsInJSON + _ = opts + return "Nested task delegation is forbidden (already inside a sub-agent delegation chain) to avoid infinite delegation. Please continue the work using the current agent's tools.", nil + }, nil + } + } + + // 标记当前 task 调用链,确保子代理内的再次 task 调用能检测到嵌套。 + return func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + ctx2 := ctx + if ctx2 == nil { + ctx2 = context.Background() + } + ctx2 = context.WithValue(ctx2, nestedTaskCtxKey{}, true) + return endpoint(ctx2, argumentsInJSON, opts...) + }, nil +} + diff --git a/internal/multiagent/runner.go b/internal/multiagent/runner.go index bf1d7761..3104d3b3 100644 --- a/internal/multiagent/runner.go +++ b/internal/multiagent/runner.go @@ -252,7 +252,11 @@ func RunDeepAgent( WithoutGeneralSubAgent: ma.WithoutGeneralSubAgent, WithoutWriteTodos: ma.WithoutWriteTodos, MaxIteration: deepMaxIter, - Handlers: []adk.ChatModelAgentMiddleware{mainSumMw}, + // 防止 sub-agent 再调用 task(再委派 sub-agent),形成无限委派链。 + Handlers: []adk.ChatModelAgentMiddleware{ + newNoNestedTaskMiddleware(), + mainSumMw, + }, ToolsConfig: adk.ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ Tools: mainTools,