1use 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#[derive(Clone)]
48pub struct AgentFlowNode {
49 name: String,
51 loop_: ToolUseLoop,
53 message_key: String,
55 use_react_graph: bool,
57 hooks: Vec<std::sync::Arc<dyn AgentHook>>,
59}
60
61impl AgentFlowNode {
62 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 pub fn message_key(mut self, key: impl Into<String>) -> Self {
75 self.message_key = key.into();
76 self
77 }
78
79 pub fn use_react_graph(mut self, enabled: bool) -> Self {
81 self.use_react_graph = enabled;
82 self
83 }
84
85 pub fn hook(mut self, hook: impl AgentHook + 'static) -> Self {
89 self.hooks.push(std::sync::Arc::new(hook));
90 self
91 }
92
93 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 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 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 async fn execute(&self, ctx: &mut NodeContext<'_>) -> Result<(), GraphError> {
188 let messages = self.extract_messages(ctx);
189
190 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 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 self.execute_with_react_graph(ctx, messages).await
211 } else {
212 self.execute_with_tool_use_loop(ctx, messages).await
214 }
215 }
216}
217
218impl AgentFlowNode {
219 async fn execute_with_tool_use_loop(
221 &self,
222 ctx: &mut NodeContext<'_>,
223 messages: Vec<lellm_core::Message>,
224 ) -> Result<(), GraphError> {
225 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 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 if let Some(err) = error {
267 return Err(GraphError::Terminal(TerminalError::NodeExecutionFailed {
268 node: self.name.clone(),
269 source: err,
270 }));
271 }
272
273 if let Some(result) = final_result {
275 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 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 let mut agent_state = super::typed_state::AgentState::from_messages(messages);
318
319 let graph = self.build_react_graph();
321 let max_steps = self.loop_.config().max_iterations * 5 + 3;
324
325 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 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 events: Vec::new(),
355 };
356 for hook in &self.hooks {
357 hook.after_agent(&snapshot);
358 }
359
360 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 fn write_agent_result(
375 &self,
376 ctx: &mut NodeContext<'_>,
377 state: &super::typed_state::AgentState,
378 ) {
379 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}