Skip to main content

lellm_agent/runtime/react/
tool_node.rs

1//! 工具执行节点 — 读取 tool_calls,执行工具,写入 results。
2//!
3//! # Typed State
4//!
5//! 从 ctx 获取 `AgentState`,执行工具,追加结果到消息历史。
6
7use async_trait::async_trait;
8
9use lellm_core::ToolCall;
10use lellm_graph::{GraphError, LeafContext, LeafNode};
11
12use super::super::config::{ToolUseConfig, empty_response};
13use super::super::context::ContextBudget;
14use super::super::runtime::ResolvedRound;
15use super::super::tools::ToolExecutor;
16use super::super::typed_state::{AgentMutation, AgentState};
17
18/// 工具执行节点。
19#[derive(Clone)]
20pub struct ToolNode {
21    pub name: String,
22    pub executor: ToolExecutor,
23    pub config: ToolUseConfig,
24}
25
26impl ToolNode {
27    pub fn new(name: impl Into<String>, executor: ToolExecutor, config: ToolUseConfig) -> Self {
28        Self {
29            name: name.into(),
30            executor,
31            config,
32        }
33    }
34}
35
36#[async_trait]
37impl LeafNode<AgentState> for ToolNode {
38    async fn execute(&self, ctx: &mut LeafContext<'_, AgentState>) -> Result<(), GraphError> {
39        use lellm_graph::{StreamChunk, ToolPhase};
40
41        // 1. 获取工具调用
42        let round = ResolvedRound::new(self.executor.snapshot().await);
43        let state = ctx.state().clone();
44        let last_response = state.last_response.unwrap_or_else(empty_response);
45        let tool_calls: Vec<ToolCall> = last_response.tool_calls().cloned().collect();
46
47        if tool_calls.is_empty() {
48            return Ok(());
49        }
50
51        // 2. Emit Queued + Started for all tools (严格按 ToolCall 顺序)
52        for call in &tool_calls {
53            ctx.emit(StreamChunk::ToolLifecycle {
54                phase: ToolPhase::Queued,
55                call_id: call.id.clone(),
56                tool_name: call.name.clone(),
57            });
58            ctx.emit(StreamChunk::ToolLifecycle {
59                phase: ToolPhase::Started,
60                call_id: call.id.clone(),
61                tool_name: call.name.clone(),
62            });
63        }
64
65        // 3. 并发执行每个工具,完成后立即 emit Finished + ToolOutput
66        let retry_policy = self.executor.retry_policy().clone();
67        let snapshot = round.snapshot.clone();
68        let budget = self.config.context_budget.clone();
69
70        let mut handles = Vec::with_capacity(tool_calls.len());
71        for call in &tool_calls {
72            let entry = snapshot.get(&call.name).cloned();
73            let rp = retry_policy.clone();
74            let call_clone = call.clone();
75            let budget_clone = budget.clone();
76
77            handles.push(tokio::spawn(async move {
78                let start = std::time::Instant::now();
79                let result: lellm_core::ToolResult = match entry {
80                    Some(reg) => {
81                        rp.execute_with_retry(&reg.func, &call_clone.arguments)
82                            .await
83                    }
84                    None => Err(lellm_core::ToolError::not_found(format!(
85                        "unknown tool: {}",
86                        call_clone.name
87                    ))),
88                };
89                let duration = start.elapsed();
90
91                // 应用预算截断
92                let msg = lellm_core::Message::tool_result(&call_clone, &result);
93                let msg = apply_budget_truncate(msg, &budget_clone);
94
95                (msg, duration)
96            }));
97        }
98
99        // 4. 收集结果,join_all 保持顺序,i-th result 对应 i-th tool_call
100        let mut results: Vec<Option<lellm_core::Message>> = vec![None; tool_calls.len()];
101        let mut panicked = false;
102
103        let collect = futures_util::future::join_all(handles).await;
104        for (i, (call, join_result)) in tool_calls.iter().zip(collect).enumerate() {
105            match join_result {
106                Ok((msg, duration)) => {
107                    // Emit Finished
108                    ctx.emit(StreamChunk::ToolLifecycle {
109                        phase: ToolPhase::Finished,
110                        call_id: call.id.clone(),
111                        tool_name: call.name.clone(),
112                    });
113                    // Emit ToolOutput
114                    if let Some(chunk) = tool_output_chunk(&msg, &call.id, &call.name, duration) {
115                        ctx.emit(chunk);
116                    }
117                    results[i] = Some(msg);
118                }
119                Err(join_err) => {
120                    panicked = true;
121                    let err_msg = lellm_core::Message::tool_result(
122                        call,
123                        &Err(lellm_core::ToolError {
124                            kind: lellm_core::ToolErrorKind::Internal,
125                            message: format!("tool task panicked: {join_err}"),
126                        }),
127                    );
128                    ctx.emit(StreamChunk::ToolLifecycle {
129                        phase: ToolPhase::Finished,
130                        call_id: call.id.clone(),
131                        tool_name: call.name.clone(),
132                    });
133                    if let Some(chunk) =
134                        tool_output_chunk(&err_msg, &call.id, &call.name, std::time::Duration::ZERO)
135                    {
136                        ctx.emit(chunk);
137                    }
138                    results[i] = Some(err_msg);
139                }
140            }
141        }
142
143        if panicked {
144            tracing::warn!("tool batch task panicked — error results filled");
145        }
146
147        // 5. Emit 消息追加 Mutation
148        ctx.record(AgentMutation::AppendMessages(
149            results.into_iter().flatten().collect(),
150        ));
151
152        tracing::debug!(tool_calls = tool_calls.len(), "tool execution completed");
153
154        Ok(())
155    }
156}
157
158/// 对单个 ToolResult 应用预算截断。
159fn apply_budget_truncate(msg: lellm_core::Message, budget: &ContextBudget) -> lellm_core::Message {
160    if let lellm_core::Message::ToolResult {
161        ref tool_call_id,
162        is_error: false,
163        ref content,
164    } = msg
165    {
166        let truncated = budget.truncate_tool_result_blocks(content);
167        if truncated != *content {
168            return lellm_core::Message::ToolResult {
169                tool_call_id: tool_call_id.clone(),
170                is_error: false,
171                content: truncated,
172            };
173        }
174    }
175    msg
176}
177
178/// 从 Message::ToolResult 提取内容,构建 ToolOutput chunk。
179fn tool_output_chunk(
180    msg: &lellm_core::Message,
181    call_id: &str,
182    tool_name: &str,
183    duration: std::time::Duration,
184) -> Option<lellm_graph::StreamChunk> {
185    if let lellm_core::Message::ToolResult {
186        content, is_error, ..
187    } = msg
188    {
189        let content_str: String = content
190            .iter()
191            .filter_map(|b| match b {
192                lellm_core::ContentBlock::Text(t) => Some(t.text.clone()),
193                lellm_core::ContentBlock::Image { .. }
194                | lellm_core::ContentBlock::Thinking(_)
195                | lellm_core::ContentBlock::ToolCall(_) => None,
196            })
197            .collect::<Vec<_>>()
198            .join("");
199        Some(lellm_graph::StreamChunk::ToolOutput {
200            call_id: call_id.to_string(),
201            tool_name: tool_name.to_string(),
202            content: content_str,
203            is_error: *is_error,
204            duration,
205        })
206    } else {
207        None
208    }
209}