1use 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#[derive(Clone)]
50pub struct AgentFlowNode {
51 name: String,
53 loop_: ToolUseLoop,
55 message_key: String,
57 use_react_graph: bool,
59 hooks: Vec<std::sync::Arc<dyn AgentHook>>,
61}
62
63impl AgentFlowNode {
64 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 pub fn message_key(mut self, key: impl Into<String>) -> Self {
77 self.message_key = key.into();
78 self
79 }
80
81 pub fn use_react_graph(mut self, enabled: bool) -> Self {
83 self.use_react_graph = enabled;
84 self
85 }
86
87 pub fn hook(mut self, hook: impl AgentHook + 'static) -> Self {
91 self.hooks.push(std::sync::Arc::new(hook));
92 self
93 }
94
95 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 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 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 async fn execute(&self, ctx: &mut NodeContext<'_>) -> Result<(), GraphError> {
192 let messages = self.extract_messages(ctx);
193
194 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 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 self.execute_with_react_graph(ctx, messages).await
215 } else {
216 self.execute_with_tool_use_loop(ctx, messages).await
218 }
219 }
220}
221
222impl AgentFlowNode {
223 async fn execute_with_tool_use_loop(
225 &self,
226 ctx: &mut NodeContext<'_>,
227 messages: Vec<lellm_core::Message>,
228 ) -> Result<(), GraphError> {
229 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 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 if let Some(err) = error {
270 return Err(GraphError::Terminal(TerminalError::NodeExecutionFailed {
271 node: self.name.clone(),
272 source: err,
273 }));
274 }
275
276 if let Some(result) = final_result {
278 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 async fn execute_with_react_graph(
313 &self,
314 ctx: &mut NodeContext<'_>,
315 messages: Vec<lellm_core::Message>,
316 ) -> Result<(), GraphError> {
317 let agent_state = super::typed_state::AgentState::from_messages(messages);
319
320 let graph = self.build_react_graph();
322 let max_steps = self.loop_.config().max_iterations * 5 + 3;
325
326 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 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 events: Vec::new(),
358 };
359 for hook in &self.hooks {
360 hook.after_agent(&snapshot);
361 }
362
363 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 fn write_agent_result(
378 &self,
379 ctx: &mut NodeContext<'_>,
380 state: &super::typed_state::AgentState,
381 ) {
382 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}