Skip to main content

lellm_agent/runtime/
flow_node.rs

1//! AgentFlowNode — 将 Agent 包装为 Graph FlowNode。
2//!
3//! 在 Graph 编排中作为节点执行 Agent Loop,读写 State 中的消息。
4//!
5//! v04: 支持两种执行模式:
6//! - **ToolUseLoop 模式**(默认):直接调用 ToolUseLoop,简单高效
7//! - **ReAct Graph 模式**:内部构建 LLM → Condition → Tool → LLM 有环图,架构更清晰
8
9use async_trait::async_trait;
10
11use lellm_graph::{FlowNode, Graph, GraphError, NodeContext, StateMutation, TerminalError};
12
13use crate::hook::{AgentHook, AgentHookContext, AgentHookSnapshot};
14use crate::runtime::{
15    AgentEvent, StopReason, ToolUseLoop, ToolUseResult, TranslationResult, translate_provider_event,
16};
17
18/// Agent 在 Graph 中的节点包装。
19///
20/// 将 `ToolUseLoop` 适配为 `FlowNode`,使其可以作为 Graph 的节点执行。
21///
22/// # State 约定
23///
24/// - 输入: `ctx.get("messages")` → `Vec<serde_json::Value>` 或 `serde_json::Value` 数组
25/// - 输出: `ctx.set("messages")` → 更新后的消息列表
26/// - 自定义 key: 通过 `message_key` 配置
27///
28/// # 执行模式
29///
30/// - **ToolUseLoop 模式**(默认):直接调用 ToolUseLoop,保留所有功能(context budget、compaction、fallback、retry)
31/// - **ReAct Graph 模式**:内部构建 LLM → Condition → Tool → LLM 有环图,架构更清晰
32///
33/// # 示例
34///
35/// ```rust,ignore
36/// use lellm_agent::AgentFlowNode;
37/// use lellm_graph::{GraphBuilder, NodeKind};
38///
39/// // ToolUseLoop 模式(默认)
40/// let agent = AgentFlowNode::new("agent", tool_use_loop.clone());
41/// let mut graph = GraphBuilder::new("my_graph");
42/// graph.node("agent", NodeKind::External(Arc::new(agent)));
43///
44/// // ReAct Graph 模式
45/// let agent = AgentFlowNode::new("agent", tool_use_loop)
46///     .use_react_graph(true);
47/// graph.node("agent_react", NodeKind::External(Arc::new(agent)));
48/// ```
49#[derive(Clone)]
50pub struct AgentFlowNode {
51    /// 节点名称
52    name: String,
53    /// Agent 执行循环
54    loop_: ToolUseLoop,
55    /// State 中消息的 key(默认 "messages")
56    message_key: String,
57    /// 是否使用 ReAct Graph 模式(默认 false)
58    use_react_graph: bool,
59    /// Agent-level hooks(在 agent loop 前后调用)
60    hooks: Vec<std::sync::Arc<dyn AgentHook>>,
61}
62
63impl AgentFlowNode {
64    /// 创建新的 AgentFlowNode。
65    pub fn new(name: impl Into<String>, loop_: ToolUseLoop) -> Self {
66        Self {
67            name: name.into(),
68            loop_,
69            message_key: "messages".to_string(),
70            use_react_graph: false,
71            hooks: Vec::new(),
72        }
73    }
74
75    /// 设置 State 中消息的 key(默认 "messages")。
76    pub fn message_key(mut self, key: impl Into<String>) -> Self {
77        self.message_key = key.into();
78        self
79    }
80
81    /// 使用 ReAct Graph 模式(内部构建 LLM → Condition → Tool → LLM 有环图)。
82    pub fn use_react_graph(mut self, enabled: bool) -> Self {
83        self.use_react_graph = enabled;
84        self
85    }
86
87    /// 添加 Agent-level hook。
88    ///
89    /// Hook 在 agent loop 执行前后调用。
90    pub fn hook(mut self, hook: impl AgentHook + 'static) -> Self {
91        self.hooks.push(std::sync::Arc::new(hook));
92        self
93    }
94
95    /// 从 Typed State 中提取输入消息。
96    fn extract_messages(&self, ctx: &NodeContext<'_>) -> Vec<lellm_core::Message> {
97        if let Some(value) = ctx.state().get(&self.message_key) {
98            if let Some(arr) = value.as_array() {
99                let mut messages = Vec::new();
100                for v in arr {
101                    if let Ok(msg) = serde_json::from_value::<lellm_core::Message>(v.clone()) {
102                        messages.push(msg);
103                    }
104                }
105                messages
106            } else if let Ok(msg) = serde_json::from_value::<lellm_core::Message>(value.clone()) {
107                vec![msg]
108            } else {
109                Vec::new()
110            }
111        } else {
112            Vec::new()
113        }
114    }
115
116    /// 将执行结果以 StateMutation 写入 ctx。
117    fn apply_result(&self, ctx: &mut NodeContext<'_>, result: &ToolUseResult) {
118        let messages: Vec<serde_json::Value> = result
119            .messages
120            .iter()
121            .filter_map(|m| serde_json::to_value(m).ok())
122            .collect();
123
124        ctx.record(StateMutation::Put(
125            self.message_key.clone(),
126            serde_json::json!(messages),
127        ));
128        ctx.record(StateMutation::Put(
129            format!("{}_stop_reason", self.name),
130            serde_json::json!(format!("{:?}", result.stop_reason)),
131        ));
132        ctx.record(StateMutation::Put(
133            format!("{}_iterations", self.name),
134            serde_json::json!(result.iterations as u64),
135        ));
136        ctx.record(StateMutation::Put(
137            format!("{}_tool_calls", self.name),
138            serde_json::json!(result.tool_calls_executed as u64),
139        ));
140    }
141
142    /// 构建内部 ReAct Graph。
143    ///
144    /// ```text
145    /// START → budget_check
146    ///
147    /// budget_check --budget_ok--> [llm]
148    ///          --need_compact--> [compactor] → [llm]
149    ///
150    /// [llm] → [tool_decision]
151    ///    --has_tool_calls--> [tool] → [budget_check] (循环)
152    ///    --no_tool_calls--> [end]
153    /// ```
154    fn build_react_graph(
155        &self,
156    ) -> Graph<super::typed_state::AgentState, super::typed_state::AgentStateMerge> {
157        let config = self.loop_.config().clone();
158        let executor = self.loop_.executor().clone();
159        let invoker = std::sync::Arc::new(crate::runtime::invoker::LlmInvoker::from_config(
160            self.loop_.model().clone(),
161            self.loop_.config(),
162            self.loop_.deps().fallback.clone(),
163        ));
164
165        let llm_node = crate::runtime::react::LLMNode::new(
166            format!("{}_llm", self.name),
167            invoker,
168            executor.clone(),
169            config.clone(),
170        );
171
172        let tool_node = crate::runtime::react::ToolNode::new(
173            format!("{}_tool", self.name),
174            executor.clone(),
175            config.clone(),
176        );
177
178        let compactor_node = crate::runtime::react::CompactorNode::new(
179            format!("{}_compactor", self.name),
180            std::sync::Arc::new(crate::runtime::LocalCompactor::new()),
181            config.context_budget.clone(),
182        );
183
184        crate::runtime::react::build_react_graph(llm_node, tool_node, compactor_node)
185    }
186}
187
188#[async_trait]
189impl FlowNode for AgentFlowNode {
190    /// 执行 — 运行完整的 Agent Loop。
191    async fn execute(&self, ctx: &mut NodeContext<'_>) -> Result<(), GraphError> {
192        let messages = self.extract_messages(ctx);
193
194        // 如果没有消息,发送一个警告但仍继续执行
195        if messages.is_empty() {
196            tracing::debug!(
197                agent = %self.name,
198                "no input messages found in state key '{}'",
199                self.message_key
200            );
201        }
202
203        // 调用 before_agent hooks
204        let hook_ctx = AgentHookContext {
205            node_name: self.name.clone(),
206            input_message_count: messages.len(),
207        };
208        for hook in &self.hooks {
209            hook.before_agent(&hook_ctx);
210        }
211
212        if self.use_react_graph {
213            // ReAct Graph 模式:构建内部有环图并执行
214            self.execute_with_react_graph(ctx, messages).await
215        } else {
216            // ToolUseLoop 模式:直接调用 ToolUseLoop
217            self.execute_with_tool_use_loop(ctx, messages).await
218        }
219    }
220}
221
222impl AgentFlowNode {
223    /// 使用 ToolUseLoop 模式执行。
224    async fn execute_with_tool_use_loop(
225        &self,
226        ctx: &mut NodeContext<'_>,
227        messages: Vec<lellm_core::Message>,
228    ) -> Result<(), GraphError> {
229        // 启动流式 Agent Loop 收集结果
230        let mut agent_stream = self.loop_.execute_stream(messages);
231        let mut final_result: Option<ToolUseResult> = None;
232        let mut error: Option<Box<dyn std::error::Error + Send + Sync>> = None;
233        let mut events: Vec<AgentEvent> = Vec::new();
234
235        while let Some(agent_event) = agent_stream.recv().await {
236            let is_terminal = matches!(
237                &agent_event,
238                AgentEvent::LoopEnd { .. } | AgentEvent::LoopError { .. }
239            );
240
241            events.push(agent_event.clone());
242
243            // 转发流式事件到 ctx.emit()(通过翻译层)
244            if let AgentEvent::Provider(provider_event) = &agent_event {
245                match translate_provider_event(provider_event) {
246                    TranslationResult::Emit(chunk)
247                    | TranslationResult::EmitWithText { chunk, .. }
248                    | TranslationResult::EmitWithThinking { chunk, .. } => ctx.emit(chunk),
249                    TranslationResult::Usage(_)
250                    | TranslationResult::Finished
251                    | TranslationResult::Ignore => {}
252                }
253            }
254
255            if is_terminal {
256                match &agent_event {
257                    AgentEvent::LoopEnd { result } => {
258                        final_result = Some(result.clone());
259                    }
260                    AgentEvent::LoopError { error: err, .. } => {
261                        error = Some(Box::new(err.clone()));
262                    }
263                    _ => {}
264                }
265            }
266        }
267
268        // 处理错误
269        if let Some(err) = error {
270            return Err(GraphError::Terminal(TerminalError::NodeExecutionFailed {
271                node: self.name.clone(),
272                source: err,
273            }));
274        }
275
276        // 写入最终结果
277        if let Some(result) = final_result {
278            // 调用 after_agent hooks
279            let snapshot = AgentHookSnapshot {
280                result: result.clone(),
281                events,
282            };
283            for hook in &self.hooks {
284                hook.after_agent(&snapshot);
285            }
286
287            self.apply_result(ctx, &result);
288
289            tracing::debug!(
290                agent = %self.name,
291                iterations = result.iterations,
292                tool_calls = result.tool_calls_executed,
293                stop_reason = ?result.stop_reason,
294                "agent execution completed (ToolUseLoop mode)"
295            );
296        } else {
297            return Err(GraphError::Terminal(TerminalError::NodeExecutionFailed {
298                node: self.name.clone(),
299                source: "agent stream ended without terminal event".into(),
300            }));
301        }
302
303        Ok(())
304    }
305
306    /// 使用 ReAct Graph 模式执行。
307    ///
308    /// v0.4+ AgentState 驱动:
309    /// 1. 构建 `Graph<AgentState, AgentStateMerge>`
310    /// 2. 调用 `run_inline()` — 自动处理 Mutation 应用 + 控制信号 + 路由
311    /// 3. 将最终结果写入父 ctx
312    async fn execute_with_react_graph(
313        &self,
314        ctx: &mut NodeContext<'_>,
315        messages: Vec<lellm_core::Message>,
316    ) -> Result<(), GraphError> {
317        // 1. 初始化 AgentState
318        let agent_state = super::typed_state::AgentState::from_messages(messages);
319
320        // 2. 构建内部 ReAct Graph (Graph<AgentState, AgentStateMerge>)
321        let graph = self.build_react_graph();
322        // 最坏情况:每次迭代 5 steps(budget_check → compactor → llm → post_llm_check → tool)
323        // 最后 3 steps(budget_check → llm → post_llm_check)
324        let max_steps = self.loop_.config().max_iterations * 5 + 3;
325
326        // 3. 创建 ExecutionContext<AgentState> 并调用 run_inline
327        let mut exec_ctx = lellm_graph::node_context::ExecutionContext::new(
328            agent_state,
329            None,
330            lellm_graph::CancellationToken::new(),
331        );
332
333        graph.run_inline(&mut exec_ctx, max_steps).await?;
334
335        // 4. 调用 after_agent hooks
336        let agent_state = exec_ctx.state();
337        let result = super::ToolUseResult {
338            stop_reason: agent_state
339                .stop_reason
340                .clone()
341                .unwrap_or(StopReason::Complete),
342            response: agent_state.last_response.clone().unwrap_or_else(|| {
343                lellm_core::ChatResponse::new(
344                    lellm_core::text_block(String::new()),
345                    lellm_core::TokenUsage::default(),
346                    serde_json::Value::Null,
347                )
348            }),
349            messages: agent_state.messages.clone(),
350            iterations: agent_state.iterations,
351            tool_calls_executed: agent_state.total_tool_calls,
352        };
353        let snapshot = AgentHookSnapshot {
354            result: result.clone(),
355            // TODO(react-graph): run_inline 不产生 AgentEvent 流,暂传空数组。
356            // 后续需要在 LLMNode/ToolNode 中收集 AgentEvent 并传递。
357            events: Vec::new(),
358        };
359        for hook in &self.hooks {
360            hook.after_agent(&snapshot);
361        }
362
363        // 5. 将最终结果写入父 ctx
364        self.write_agent_result(ctx, agent_state);
365
366        tracing::debug!(
367            agent = %self.name,
368            iterations = agent_state.iterations,
369            tool_calls = agent_state.total_tool_calls,
370            "agent execution completed (ReAct Graph mode, AgentState-driven)"
371        );
372
373        Ok(())
374    }
375
376    /// 将 AgentState 最终结果以 StateMutation 写入 ctx。
377    fn write_agent_result(
378        &self,
379        ctx: &mut NodeContext<'_>,
380        state: &super::typed_state::AgentState,
381    ) {
382        // 写入消息历史(与 apply_result 保持一致)
383        let messages: Vec<serde_json::Value> = state
384            .messages
385            .iter()
386            .filter_map(|m| serde_json::to_value(m).ok())
387            .collect();
388        ctx.record(StateMutation::Put(
389            self.message_key.clone(),
390            serde_json::json!(messages),
391        ));
392
393        if let Some(ref stop_reason) = state.stop_reason {
394            ctx.record(StateMutation::Put(
395                format!("{}_stop_reason", self.name),
396                serde_json::json!(format!("{:?}", stop_reason)),
397            ));
398        }
399        ctx.record(StateMutation::Put(
400            format!("{}_iterations", self.name),
401            serde_json::json!(state.iterations as u64),
402        ));
403        ctx.record(StateMutation::Put(
404            format!("{}_tool_calls", self.name),
405            serde_json::json!(state.total_tool_calls as u64),
406        ));
407    }
408}