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