lellm_agent/runtime/react/
llm_node.rs1use std::sync::Arc;
17
18use async_trait::async_trait;
19use futures_util::StreamExt;
20
21use lellm_core::{CacheControl, ChatResponse, ContentBlock, Message, TextBlock, ThinkingBlock};
22use lellm_graph::{GraphError, LeafContext, LeafNode, TerminalError};
23
24use super::super::config::{ToolUseConfig, build_request_inner_with_round};
25use super::super::context::{estimate_reasoning_block, estimate_text};
26use super::super::invoker::LlmInvoker;
27use super::super::runtime::ResolvedRound;
28use super::super::stream_translation::{TranslationResult, translate_provider_event};
29use super::super::tools::ToolExecutor;
30use super::super::typed_state::{AgentMutation, AgentState};
31
32fn split_output_tokens(content: &[lellm_core::ContentBlock]) -> (usize, usize) {
34 let mut output_tokens: usize = 0;
35 let mut reasoning_tokens: usize = 0;
36 for b in content {
37 match b {
38 lellm_core::ContentBlock::Text(t) => output_tokens += estimate_text(&t.text),
39 lellm_core::ContentBlock::Thinking(th) => {
40 reasoning_tokens += estimate_reasoning_block(th)
41 }
42 lellm_core::ContentBlock::Image { .. } | lellm_core::ContentBlock::ToolCall(_) => {}
43 }
44 }
45 (output_tokens, reasoning_tokens)
46}
47
48#[derive(Clone)]
54pub struct LLMNode {
55 pub name: String,
56 pub invoker: Arc<LlmInvoker>,
58 pub executor: ToolExecutor,
60 pub config: ToolUseConfig,
62}
63
64impl LLMNode {
65 pub fn new(
66 name: impl Into<String>,
67 invoker: Arc<LlmInvoker>,
68 executor: ToolExecutor,
69 config: ToolUseConfig,
70 ) -> Self {
71 Self {
72 name: name.into(),
73 invoker,
74 executor,
75 config,
76 }
77 }
78}
79
80#[async_trait]
81impl LeafNode<AgentState> for LLMNode {
82 async fn execute(&self, ctx: &mut LeafContext<'_, AgentState>) -> Result<(), GraphError> {
83 let state = ctx.state().clone();
85
86 ctx.record(AgentMutation::IncrementIteration);
88
89 let round = ResolvedRound::new(self.executor.snapshot().await);
91
92 let req = build_request_inner_with_round(
93 self.invoker.model(),
94 &state.messages,
95 self.config.max_output_tokens,
96 &self.config.request_options,
97 state.iterations + 1,
98 &round.definitions,
99 self.config.tool_cache_policy,
100 );
101
102 let mut stream = self
104 .invoker
105 .invoke_stream(&req, &state.messages, state.iterations)
106 .await
107 .map_err(|e| {
108 GraphError::Terminal(TerminalError::NodeExecutionFailed {
109 node: self.name.clone(),
110 source: e.into(),
111 })
112 })?;
113
114 let mut content_blocks: Vec<ContentBlock> = Vec::new();
116 let mut current_text = String::new();
117 let mut current_thinking = String::new();
118 let mut tool_calls_count: usize = 0;
119 let mut usage: Option<lellm_core::TokenUsage> = None;
120
121 while let Some(event) = stream.next().await {
122 match event {
123 Ok(provider_event) => {
124 if let lellm_provider::ProviderEvent::ResponseComplete {
126 tool_calls,
127 usage: u,
128 } = &provider_event
129 {
130 for tc in tool_calls {
131 content_blocks.push(ContentBlock::ToolCall(tc.clone()));
132 }
133 tool_calls_count = content_blocks
134 .iter()
135 .filter(|b| matches!(b, ContentBlock::ToolCall(_)))
136 .count();
137 usage = u.clone();
138 continue;
139 }
140
141 match translate_provider_event(&provider_event) {
142 TranslationResult::EmitWithText { chunk, delta } => {
143 ctx.emit(chunk);
144 current_text.push_str(&delta);
145 }
146 TranslationResult::EmitWithThinking { chunk, delta, .. } => {
147 ctx.emit(chunk);
148 current_thinking.push_str(&delta);
149 }
150 TranslationResult::Emit(chunk) => ctx.emit(chunk),
151 _ => {}
152 }
153 }
154 Err(e) => {
155 return Err(GraphError::Terminal(TerminalError::NodeExecutionFailed {
156 node: self.name.clone(),
157 source: e.into(),
158 }));
159 }
160 }
161 }
162
163 if !current_thinking.is_empty() {
165 content_blocks.push(ContentBlock::Thinking(ThinkingBlock {
166 thinking: current_thinking,
167 redacted: None,
168 }));
169 }
170 if !current_text.is_empty() {
171 content_blocks.push(ContentBlock::Text(TextBlock {
172 text: current_text,
173 cache_control: Some(CacheControl::Breakpoint),
176 }));
177 }
178
179 let response = ChatResponse {
180 content: content_blocks,
181 usage: usage.unwrap_or_default(),
182 raw: serde_json::json!(null),
183 };
184
185 let (output_tokens, reasoning_tokens) = split_output_tokens(&response.content);
187 ctx.record(AgentMutation::AddOutputTokens(output_tokens));
188 ctx.record(AgentMutation::AddReasoningTokens(reasoning_tokens));
189
190 let content = response.content.clone();
192 let msg = Message::Assistant { content };
193 ctx.record(AgentMutation::AppendMessage(msg));
194
195 let has_tools = response.has_tool_calls();
197 if has_tools {
198 ctx.record(AgentMutation::AddToolCalls(tool_calls_count));
199 }
200
201 ctx.record(AgentMutation::SetLastResponse(response));
203
204 tracing::debug!(
206 iteration = state.iterations + 1,
207 has_tool_calls = has_tools,
208 "LLM call completed"
209 );
210
211 Ok(())
212 }
213}