use async_trait::async_trait;
use lellm_graph::node::StreamNodeResult;
use lellm_graph::{FlowEvent, FlowNode, GraphError, NextStep, NodeOutput};
use lellm_graph::{GraphEvent, SpanId, State, StateDelta, TerminalError};
use crate::hook::{AgentHook, AgentHookContext, AgentHookSnapshot};
use crate::runtime::{AgentEvent, ToolUseLoop, ToolUseResult};
#[derive(Clone)]
pub struct AgentFlowNode {
name: String,
loop_: ToolUseLoop,
message_key: String,
stream_events: bool,
hooks: Vec<std::sync::Arc<dyn AgentHook>>,
}
impl AgentFlowNode {
pub fn new(name: impl Into<String>, loop_: ToolUseLoop) -> Self {
Self {
name: name.into(),
loop_,
message_key: "messages".to_string(),
stream_events: true,
hooks: Vec::new(),
}
}
pub fn message_key(mut self, key: impl Into<String>) -> Self {
self.message_key = key.into();
self
}
pub fn stream_events(mut self, enabled: bool) -> Self {
self.stream_events = enabled;
self
}
pub fn hook(mut self, hook: impl AgentHook + 'static) -> Self {
self.hooks.push(std::sync::Arc::new(hook));
self
}
fn extract_messages(&self, state: &State) -> Vec<lellm_core::Message> {
if let Some(value) = state.get(&self.message_key) {
if let Some(arr) = value.as_array() {
let mut messages = Vec::new();
for v in arr {
if let Ok(msg) = serde_json::from_value::<lellm_core::Message>(v.clone()) {
messages.push(msg);
}
}
messages
} else if let Ok(msg) = serde_json::from_value::<lellm_core::Message>(value.clone()) {
vec![msg]
} else {
Vec::new()
}
} else {
Vec::new()
}
}
fn collect_deltas(&self, result: &ToolUseResult) -> Vec<StateDelta> {
let messages: Vec<serde_json::Value> = result
.messages
.iter()
.filter_map(|m| serde_json::to_value(m).ok())
.collect();
vec![
StateDelta::put(&self.message_key, serde_json::json!(messages)),
StateDelta::put(
format!("{}_stop_reason", self.name),
serde_json::json!(format!("{:?}", result.stop_reason)),
),
StateDelta::put(
format!("{}_iterations", self.name),
serde_json::json!(result.iterations),
),
StateDelta::put(
format!("{}_tool_calls", self.name),
serde_json::json!(result.tool_calls_executed),
),
]
}
}
#[async_trait]
impl FlowNode for AgentFlowNode {
async fn execute(&self, state: &State) -> Result<NodeOutput, GraphError> {
let messages = self.extract_messages(state);
if messages.is_empty() {
tracing::debug!(
agent = %self.name,
"no input messages found in state key '{}'",
self.message_key
);
}
let ctx = AgentHookContext {
node_name: self.name.clone(),
input_message_count: messages.len(),
};
let mut hook_deltas: Vec<StateDelta> = self
.hooks
.iter()
.flat_map(|h| h.before_agent(&ctx))
.collect();
let result = self.loop_.execute(messages).await.map_err(|e| {
GraphError::Terminal(TerminalError::NodeExecutionFailed {
node: self.name.clone(),
source: e.into(),
})
})?;
let snapshot = AgentHookSnapshot {
result: result.clone(),
events: Vec::new(), };
hook_deltas.extend(self.hooks.iter().flat_map(|h| h.after_agent(&snapshot)));
let mut deltas = self.collect_deltas(&result);
deltas.extend(hook_deltas);
tracing::debug!(
agent = %self.name,
iterations = result.iterations,
tool_calls = result.tool_calls_executed,
stop_reason = ?result.stop_reason,
"agent execution completed"
);
Ok(NodeOutput {
deltas,
next: NextStep::GoToNext,
metadata: None,
})
}
async fn execute_stream(
&self,
state: &State,
sink: &tokio::sync::mpsc::Sender<GraphEvent>,
span_id: SpanId,
) -> Result<StreamNodeResult, GraphError> {
let messages = self.extract_messages(state);
let ctx = AgentHookContext {
node_name: self.name.clone(),
input_message_count: messages.len(),
};
let hook_deltas: Vec<StateDelta> = self
.hooks
.iter()
.flat_map(|h| h.before_agent(&ctx))
.collect();
let _ = sink
.send(GraphEvent::Node {
span_id,
node_name: self.name.clone(),
event: FlowEvent::NodeStarted {
node_id: self.name.clone(),
span_id,
},
})
.await;
let mut agent_stream = self.loop_.execute_stream(messages);
let mut final_result: Option<ToolUseResult> = None;
let mut error_delta: Option<StateDelta> = None;
let mut events: Vec<AgentEvent> = Vec::new();
while let Some(agent_event) = agent_stream.recv().await {
let is_terminal = matches!(
&agent_event,
AgentEvent::LoopEnd { .. } | AgentEvent::LoopError { .. }
);
events.push(agent_event.clone());
if self.stream_events {
let _ = sink
.send(GraphEvent::Node {
span_id,
node_name: self.name.clone(),
event: FlowEvent::Custom {
node_id: self.name.clone(),
payload: Box::new(agent_event.clone()),
},
})
.await;
}
if is_terminal {
match &agent_event {
AgentEvent::LoopEnd { result } => {
final_result = Some(result.clone());
}
AgentEvent::LoopError { error, .. } => {
error_delta = Some(StateDelta::put(
format!("{}_error", self.name),
serde_json::json!(error.to_string()),
));
}
_ => {}
}
}
}
if let Some(err_delta) = error_delta {
return Ok(StreamNodeResult::Fallback {
deltas: hook_deltas
.into_iter()
.chain(std::iter::once(err_delta))
.collect(),
reason: format!("agent loop error in '{}'", self.name),
node_name: self.name.clone(),
});
}
if let Some(result) = final_result {
let snapshot = AgentHookSnapshot {
result: result.clone(),
events,
};
let after_deltas: Vec<StateDelta> = self
.hooks
.iter()
.flat_map(|h| h.after_agent(&snapshot))
.collect();
let mut deltas = self.collect_deltas(&result);
deltas.extend(hook_deltas);
deltas.extend(after_deltas);
let _ = sink
.send(GraphEvent::Node {
span_id,
node_name: self.name.clone(),
event: FlowEvent::NodeCompleted {
node_id: self.name.clone(),
span_id,
duration: std::time::Duration::ZERO, },
})
.await;
return Ok(StreamNodeResult::Continue {
deltas,
next: NextStep::GoToNext,
span_id,
observed: None,
metadata: None,
});
}
Ok(StreamNodeResult::Fallback {
deltas: hook_deltas,
reason: "agent stream ended without terminal event".into(),
node_name: self.name.clone(),
})
}
}