cloudwego / cloudwego/eino

deep agent 子 agent 完成工作后 agent_tool.InvokableRun 永久阻塞 — runnable.Stream() 不返回

Open
#1,137 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.