deep agent 子 agent 完成工作后 agent_tool.InvokableRun 永久阻塞 — runnable.Stream() 不返回
- Dominant language
- Go
- Stars
- 13k
- Forks
- 1.1k
- Avg merge
- 4h 6m
- Merged PRs (30d)
- 41
Description
Environment
- github.com/cloudwego/eino v0.9.12
- github.com/cloudwego/eino-ext/adk/backend/local v0.2.6
- github.com/cloudwego/eino-ext/components/model/openai v0.1.13
- Go 1.23, Linux amd64
Architecture
deep agent (orchestrator, MaxIter=256)
├─ subagent: recon (TypedChatModelAgent, MaxIter=256, EnableStreaming=true)
├─ subagent: exploit (TypedChatModelAgent, MaxIter=256, EnableStreaming=true) ← 卡住
└─ subagent: researcher (TypedChatModelAgent, MaxIter=256, EnableStreaming=true)
配置:
- WithoutGeneralSubAgent: true
- EmitInternalEvents: false (主 agent + 所有子 agent)
- EnableStreaming: true (Runner 创建时设置)
现象
Orchestrator 并行调用 4 个 task() tool(3×exploit + 1×researcher)。其中 3 个正常完成并返回,1 个 exploit 子 agent 完成全部工作后 卡住:
1. 子 agent 最后一轮 LLM 调用返回了最终消息(无 tool_calls,是完整报告)。我们的 ChatModelAgentMiddleware.AfterModelRewriteState 正常执行,成功写入 assistant_log(DB+SSE+文件)。
2. 此后不再有任何 tool execution 或 assistant log。
3. Subtask 状态始终是 in_progress(因为 TaskToolMiddleware.WrapInvokableToolCall 在 line 97 调用 endpoint() 等待 task tool 返回,还未走到 UpdateSubtaskStatus)。
4. Orchestrator 因等待所有并行 tool call 结果而被阻塞。
可观测时间线(UTC):
┌───────────────────┬────────────────────────────────────────────────────┐
│ 时间 │ 事件 │
├───────────────────┼────────────────────────────────────────────────────┤
│ 13:26:35 │ Flow 创建 │
├───────────────────┼────────────────────────────────────────────────────┤
│ 13:31:23 │ 4 个 task() 并行派发 │
├───────────────────┼────────────────────────────────────────────────────┤
│ 13:35:30–13:36:10 │ exploit 子 agent 最后 5 次 shell_execute │
├───────────────────┼────────────────────────────────────────────────────┤
│ 13:36:20 │ 最后一条 assistant_log(完整攻击报告,2014 chars) │
├───────────────────┼────────────────────────────────────────────────────┤
│ 13:36:20 → +∞ │ 无任何活动 │
└───────────────────┴────────────────────────────────────────────────────┘
代码路径分析
deep/task_tool.go:174 typedTaskTool.InvokableRun()
→ agent_tool.go:197 typedAgentTool.InvokableRun()
→ runner.Run(ctx, input, ...) → goroutine:
chatmodel.go:1124 buildMessageReActRunFunc 的 run func
→ runnable.Stream(ctx, in, runOpts...) ← 阻塞在这里
→ agent_tool.go:218 iter.Next() ← 阻塞等待 goroutine 关闭 channel
在 buildMessageReActRunFunc 中(chatmodel.go:1124):
// line 1208-1212
if mp.input.EnableStreaming {
msgStream, err_ = runnable.Stream(ctx, in, runOpts...)
} else {
msg, err_ = runnable.Invoke(ctx, in, runOpts...)
}
runnable.Stream() 内部调用 ReAct graph,graph 的 toolCallCheck 分支(react.go:496-512):
toolCallCheck := func(ctx context.Context, sMsg MessageStream) (string, error) {
defer sMsg.Close()
for {
chunk, err_ := sMsg.Recv()
if err_ != nil {
if err_ == io.EOF {
return terminalNode, nil // 无 tool_calls → 走 END
}
return "", err_
}
if len(chunk.ToolCalls) > 0 {
return cancelCheckNode_, nil // 有 tool_calls → 走 Tools
}
}
}
推测根因
子 agent 最后一轮 LLM 流式响应的所有 chunk 已被消费(AfterModelRewriteState 拿到了完整消息),但底层 model stream 未正确发送 io.EOF。
toolCallCheck 的 sMsg.Recv() 在 EOF 到达之前会一直阻塞。如果再没有新 chunk(因为 LLM 已结束),这个调用就永久 hang。
runnable.Stream() 未返回 → goroutine 不退出 → generator.Close() 不被调用(chatmodel.go:1486)→ agent_tool.go 的 iter.Next() 永远阻塞。
可能的触发条件:EmitInternalEvents: false + EnableStreaming: true 的组合。model stream 的事件通知路径中可能存在未被消费的事件通道,导致背压或 stream 关闭信号丢失。这只是推测,需 eino 团队在内部确认。
影响
- Flow 永久"运行中"僵尸,无法自愈(context 无 timeout)
- 已完成的子 agent 工作结果无法被 orchestrator 使用
- 只能手动 cancel flow
Questions
1. toolCallCheck 中 sMsg.Recv() 阻塞时,是否有任何超时/取消机制能中断它?我们使用了 context.Background() 但可以通过 cancel context 干预。
2. EmitInternalEvents: false 时 model stream 的 EOF 信号是否可能被内部事件通道截获而丢失?
3. eino 是否有计划对 agent tool 的执行增加总超时机制?
Contributor guide
Research direction
Start by reproducing the reported configuration in deep/task_tool.go and agent_tool.go, then trace runner.Run through chatmodel.go:1124 and the runnable.Stream call. Inspect react.go:496-512, especially toolCallCheck and MessageStream.Close, while observing the EmitInternalEvents and EnableStreaming combination. Done means the final no-tool-call response closes the stream, returns from runnable.Stream, and lets the task and orchestrator complete.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- ai, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100