1use lellm_core::{ChatResponse, LlmError, Message};
17use lellm_graph::{
18 SK_ITERATIONS, SK_MESSAGES, SK_OUTPUT_TOKENS, SK_REASONING_TOKENS, SK_TOTAL_TOOL_CALLS, State,
19 StateKeyExt,
20};
21use lellm_provider::ResolvedModel;
22use std::sync::Arc;
23
24use super::config::{ToolUseConfig, ToolUseDeps, build_request_messages_inner, empty_response};
25use super::context::{
26 AgentExecutionContext, CompactionResult, ContextBudget, ContextCompactor, LocalCompactor,
27 estimate_reasoning_block, estimate_text, estimate_tokens,
28};
29use super::event::{AgentEvent, AgentStream, StopReason};
30use super::fallback::{FallbackAction, FallbackContext};
31use super::iteration::{StreamIterResult, do_stream_iteration, emit};
32use super::tools::{ToolExecutor, ToolSnapshot};
33
34#[derive(Clone)]
40pub struct ResolvedRound {
41 pub snapshot: Arc<ToolSnapshot>,
43 pub definitions: Vec<lellm_core::ToolDefinition>,
45}
46
47impl ResolvedRound {
48 pub fn new(snapshot: Arc<ToolSnapshot>) -> Self {
49 Self {
50 definitions: snapshot.definitions().to_vec(),
51 snapshot,
52 }
53 }
54}
55
56pub(crate) fn create_initial_state(messages: &[Message]) -> State {
60 let mut state = State::new();
61 let messages_json: Vec<serde_json::Value> = messages
62 .iter()
63 .filter_map(|m| serde_json::to_value(m).ok())
64 .collect();
65 state.set_sk(&SK_MESSAGES, messages_json);
66 state.set_sk(&SK_ITERATIONS, 0u32);
67 state.set_sk(&SK_TOTAL_TOOL_CALLS, 0usize);
68 state.set_sk(&SK_OUTPUT_TOKENS, 0usize);
69 state.set_sk(&SK_REASONING_TOKENS, 0usize);
70 state
71}
72
73pub(crate) fn get_messages(state: &State) -> Vec<Message> {
75 state
76 .get_sk::<Vec<serde_json::Value>>(&SK_MESSAGES)
77 .unwrap_or_default()
78 .into_iter()
79 .filter_map(|v| serde_json::from_value(v).ok())
80 .collect()
81}
82
83pub(crate) fn get_iterations(state: &State) -> u32 {
85 state.get_sk::<u32>(&SK_ITERATIONS).unwrap_or(0)
86}
87
88pub(crate) fn get_tool_calls(state: &State) -> usize {
90 state.get_sk::<usize>(&SK_TOTAL_TOOL_CALLS).unwrap_or(0)
91}
92
93pub(crate) fn get_output_tokens(state: &State) -> usize {
95 state.get_sk::<usize>(&SK_OUTPUT_TOKENS).unwrap_or(0)
96}
97
98pub(crate) fn get_reasoning_tokens(state: &State) -> usize {
100 state.get_sk::<usize>(&SK_REASONING_TOKENS).unwrap_or(0)
101}
102
103pub(crate) fn state_push_assistant(
105 state: &mut State,
106 ctx: &mut AgentExecutionContext,
107 content: Vec<lellm_core::ContentBlock>,
108) {
109 let msg = Message::Assistant {
110 content: content.clone(),
111 };
112 let tokens = estimate_tokens(&[msg]);
113 ctx.add_tokens(tokens);
114 let mut messages_json: Vec<serde_json::Value> = state.get_sk(&SK_MESSAGES).unwrap_or_default();
115 messages_json.push(serde_json::to_value(Message::Assistant { content }).unwrap_or_default());
116 state.set_sk(&SK_MESSAGES, messages_json);
117}
118
119pub(crate) fn state_push_tool_results(
121 state: &mut State,
122 ctx: &mut AgentExecutionContext,
123 results: Vec<Message>,
124 budget: &ContextBudget,
125) {
126 let results: Vec<Message> = results
127 .into_iter()
128 .map(|m| {
129 if let Message::ToolResult {
130 ref tool_call_id,
131 is_error: false,
132 ref content,
133 } = m
134 {
135 let truncated = budget.truncate_tool_result_blocks(content);
136 if truncated != *content {
137 return Message::ToolResult {
138 tool_call_id: tool_call_id.clone(),
139 is_error: false,
140 content: truncated,
141 };
142 }
143 }
144 m
145 })
146 .collect();
147 let tokens = estimate_tokens(&results);
148 ctx.add_tokens(tokens);
149 let mut messages_json: Vec<serde_json::Value> = state.get_sk(&SK_MESSAGES).unwrap_or_default();
150 for msg in results {
151 messages_json.push(serde_json::to_value(msg).unwrap_or_default());
152 }
153 state.set_sk(&SK_MESSAGES, messages_json);
154}
155
156pub(crate) fn state_add_tool_calls(state: &mut State, count: usize) {
158 let current = state.get_sk::<usize>(&SK_TOTAL_TOOL_CALLS).unwrap_or(0);
159 state.set_sk(&SK_TOTAL_TOOL_CALLS, current + count);
160}
161
162pub(crate) fn state_add_output_from_content(
164 state: &mut State,
165 ctx: &mut AgentExecutionContext,
166 content: &[lellm_core::ContentBlock],
167) {
168 let mut output_tokens: usize = 0;
169 let mut reasoning_tokens: usize = 0;
170 for b in content {
171 match b {
172 lellm_core::ContentBlock::Text(t) => output_tokens += estimate_text(&t.text),
173 lellm_core::ContentBlock::Thinking(th) => {
174 reasoning_tokens += estimate_reasoning_block(th)
175 }
176 lellm_core::ContentBlock::Image { .. } | lellm_core::ContentBlock::ToolCall(_) => {}
177 }
178 }
179 let current_output = state.get_sk::<usize>(&SK_OUTPUT_TOKENS).unwrap_or(0);
180 state.set_sk(&SK_OUTPUT_TOKENS, current_output + output_tokens);
181 let current_reasoning = state.get_sk::<usize>(&SK_REASONING_TOKENS).unwrap_or(0);
182 state.set_sk(&SK_REASONING_TOKENS, current_reasoning + reasoning_tokens);
183 ctx.add_tokens(output_tokens + reasoning_tokens);
184}
185
186pub(crate) fn state_next_iteration(state: &mut State) {
188 let current = state.get_sk::<u32>(&SK_ITERATIONS).unwrap_or(0);
189 state.set_sk(&SK_ITERATIONS, current + 1);
190}
191
192pub(crate) fn state_reached_max(state: &State, max_iterations: usize) -> bool {
194 get_iterations(state) >= max_iterations as u32
195}
196
197pub(crate) fn state_compact(
199 state: &mut State,
200 ctx: &mut AgentExecutionContext,
201 budget: &ContextBudget,
202 compactor: &dyn ContextCompactor,
203) -> Option<CompactionResult> {
204 if !budget.should_compact(ctx.cached_token_count) {
205 return None;
206 }
207 let messages = get_messages(state);
208 let result = compactor.compact(&messages, budget);
209 let messages_json: Vec<serde_json::Value> = result
210 .messages
211 .iter()
212 .filter_map(|m| serde_json::to_value(m).ok())
213 .collect();
214 state.set_sk(&SK_MESSAGES, messages_json);
215 ctx.cached_token_count = result.after_tokens;
216 Some(result)
217}
218
219pub(crate) fn state_exceeded_total_output(state: &State, max: Option<u32>) -> bool {
221 match max {
222 Some(limit) => get_output_tokens(state) >= limit as usize,
223 None => false,
224 }
225}
226
227pub(crate) fn state_exceeded_total_reasoning(state: &State, max: Option<u32>) -> bool {
229 match max {
230 Some(limit) => get_reasoning_tokens(state) >= limit as usize,
231 None => false,
232 }
233}
234
235pub(crate) fn build_result(
237 state: &State,
238 stop_reason: StopReason,
239 response: ChatResponse,
240) -> ToolUseResult {
241 ToolUseResult {
242 stop_reason,
243 response,
244 messages: get_messages(state),
245 iterations: get_iterations(state) as usize,
246 tool_calls_executed: get_tool_calls(state),
247 }
248}
249
250#[derive(Debug, Clone)]
254pub struct ToolUseResult {
255 pub stop_reason: StopReason,
256 pub response: ChatResponse,
257 pub messages: Vec<Message>,
258 pub iterations: usize,
259 pub tool_calls_executed: usize,
260}
261
262impl ToolUseResult {
263 pub fn is_success(&self) -> bool {
264 matches!(self.stop_reason, StopReason::Complete)
265 }
266}
267
268#[derive(Clone)]
274pub struct ToolUseLoop {
275 model: ResolvedModel,
276 executor: ToolExecutor,
277 config: ToolUseConfig,
278 deps: ToolUseDeps,
279}
280
281impl ToolUseLoop {
282 pub fn new(
283 model: ResolvedModel,
284 executor: ToolExecutor,
285 config: ToolUseConfig,
286 deps: ToolUseDeps,
287 ) -> Self {
288 if config.stream_thinking {
289 let caps = model.provider.capabilities_for(&model.model);
290 if !caps.supports_stream_thinking {
291 tracing::warn!(
292 provider = %model.provider.provider_id(),
293 model = %model.model,
294 "stream_thinking=true but provider does not support thinking deltas; \
295 reasoning content will only be available in the final response"
296 );
297 }
298 }
299
300 Self {
301 model,
302 executor,
303 config,
304 deps,
305 }
306 }
307
308 pub fn simple(model: ResolvedModel, executor: ToolExecutor) -> Self {
310 Self::new(
311 model,
312 executor,
313 ToolUseConfig::default(),
314 ToolUseDeps::default(),
315 )
316 }
317
318 pub fn model(&self) -> &ResolvedModel {
320 &self.model
321 }
322
323 pub fn executor(&self) -> &ToolExecutor {
325 &self.executor
326 }
327
328 pub fn config(&self) -> &ToolUseConfig {
330 &self.config
331 }
332
333 pub fn deps(&self) -> &ToolUseDeps {
335 &self.deps
336 }
337
338 pub async fn execute(&self, messages: Vec<Message>) -> Result<ToolUseResult, LlmError> {
343 let initial_messages = build_request_messages_inner(&self.config, &messages)?;
344
345 let llm_node = super::react::LLMNode::new(
347 "llm",
348 self.model.clone(),
349 self.executor.clone(),
350 self.config.clone(),
351 self.deps.clone(),
352 );
353 let tool_node =
354 super::react::ToolNode::new("tool", self.executor.clone(), self.config.clone());
355 let compactor_node = super::react::CompactorNode::new(
356 "compactor",
357 Arc::new(LocalCompactor::new()),
358 self.config.context_budget.clone(),
359 );
360 let graph = super::react::build_react_graph(llm_node, tool_node, compactor_node);
361 let max_steps = self.config.max_iterations * 2 + 1;
362
363 let mut agent_state = super::typed_state::AgentState::from_messages(initial_messages);
365
366 let mut branch = lellm_graph::BranchState::empty();
368 let mut agent_ctx = lellm_graph::NodeContext::<super::typed_state::AgentState>::new(
369 &mut agent_state,
370 &mut branch,
371 None,
372 );
373
374 graph
375 .run_inline(&mut agent_ctx, max_steps)
376 .await
377 .map_err(|e| lellm_core::LlmError::Provider {
378 provider: "react_graph".into(),
379 status: None,
380 code: None,
381 message: e.to_string(),
382 })?;
383
384 let stop_reason = agent_state.stop_reason.unwrap_or(StopReason::Complete);
385 let last_response = agent_state.last_response.unwrap_or_else(empty_response);
386
387 Ok(ToolUseResult {
388 stop_reason,
389 response: last_response,
390 messages: agent_state.messages,
391 iterations: agent_state.iterations,
392 tool_calls_executed: agent_state.total_tool_calls,
393 })
394 }
395
396 pub fn execute_stream(&self, messages: Vec<Message>) -> AgentStream {
404 let (tx, rx) = tokio::sync::mpsc::channel(32);
405 let model = self.model.clone();
406 let executor = self.executor.clone();
407 let config = self.config.clone();
408 let deps = self.deps.clone();
409
410 tokio::spawn(async move {
411 let initial_messages = match build_request_messages_inner(&config, &messages) {
412 Ok(m) => m,
413 Err(e) => {
414 let _ = tokio::sync::mpsc::Sender::send(
415 &tx,
416 AgentEvent::LoopError {
417 error: e,
418 iterations: 0,
419 },
420 )
421 .await;
422 return;
423 }
424 };
425 let mut state = create_initial_state(&initial_messages);
426 let mut ctx = AgentExecutionContext::new(&initial_messages);
427 let mut last_response: Option<ChatResponse> = None;
428 let compactor: Box<dyn ContextCompactor> = Box::new(LocalCompactor::new());
429
430 loop {
431 if state_reached_max(&state, config.max_iterations) {
432 let _ = emit(
433 &tx,
434 AgentEvent::LoopEnd {
435 result: build_result(
436 &state,
437 StopReason::MaxIterationsReached,
438 last_response.unwrap_or_else(empty_response),
439 ),
440 },
441 )
442 .await;
443 return;
444 }
445
446 state_next_iteration(&mut state);
447 if let Some(compact_result) =
448 state_compact(&mut state, &mut ctx, &config.context_budget, &*compactor)
449 {
450 let _ = emit(
451 &tx,
452 AgentEvent::ContextCompacted {
453 before_tokens: compact_result.before_tokens,
454 after_tokens: compact_result.after_tokens,
455 removed_messages: compact_result.removed_messages,
456 },
457 )
458 .await;
459 }
460
461 let round = ResolvedRound::new(executor.snapshot().await);
462
463 let req = super::config::build_request_inner_with_round(
464 &model,
465 &get_messages(&state),
466 config.max_output_tokens,
467 &config.request_options,
468 get_iterations(&state) as usize,
469 &round.definitions,
470 );
471
472 let iteration = get_iterations(&state) as usize;
474 let attempt_state = state.clone();
475 let attempt_ctx = ctx.clone();
476 let mut attempt: usize = 1;
477
478 let result = loop {
479 let iter_result = do_stream_iteration(
480 model.clone(),
481 tx.clone(),
482 executor.clone(),
483 attempt_state.clone(),
484 attempt_ctx.clone(),
485 req.clone(),
486 config.context_budget.clone(),
487 config.max_output_tokens,
488 config.stream_thinking,
489 round.clone(),
490 )
491 .await;
492
493 match iter_result.result {
494 Ok((r, s)) => break Ok((r, s, iter_result.ctx)),
495 Err(ref err) => {
496 tracing::warn!(
497 attempt = attempt,
498 error = %err,
499 stream_started = iter_result.stream_started,
500 "stream iteration failed, fallback handling"
501 );
502
503 if iter_result.stream_started {
504 let e: LlmError = err.clone();
505 break Err(e);
506 }
507
508 let messages = get_messages(&attempt_state);
509 let fallback_ctx = FallbackContext {
510 error: err,
511 attempt,
512 iterations: iteration,
513 conversation: std::sync::Arc::from(messages.as_slice()),
514 };
515 match deps.fallback.handle(&fallback_ctx).await {
516 FallbackAction::Retry => {
517 attempt += 1;
518 }
519 FallbackAction::Abort => {
520 break Err(err.clone());
521 }
522 }
523 }
524 }
525 };
526
527 let result = match result {
529 Ok((r, s, updated_ctx)) => {
530 state = s;
531 ctx = updated_ctx;
532 Ok(r)
533 }
534 Err(e) => Err(e),
535 };
536
537 if state_exceeded_total_output(&state, config.max_total_output_tokens) {
539 let _ = emit(
540 &tx,
541 AgentEvent::LoopEnd {
542 result: build_result(
543 &state,
544 StopReason::OutputBudgetExceeded,
545 last_response.unwrap_or_else(empty_response),
546 ),
547 },
548 )
549 .await;
550 return;
551 }
552
553 if state_exceeded_total_reasoning(&state, config.max_total_reasoning_tokens) {
554 let _ = emit(
555 &tx,
556 AgentEvent::LoopEnd {
557 result: build_result(
558 &state,
559 StopReason::ReasoningBudgetExceeded,
560 last_response.unwrap_or_else(empty_response),
561 ),
562 },
563 )
564 .await;
565 return;
566 }
567
568 match result {
569 Ok(StreamIterResult::Continue { response }) => {
570 last_response = Some(response);
571 }
572 Ok(StreamIterResult::Complete { response }) => {
573 let _ = emit(
574 &tx,
575 AgentEvent::LoopEnd {
576 result: build_result(&state, StopReason::Complete, response),
577 },
578 )
579 .await;
580 return;
581 }
582 Ok(StreamIterResult::Cancelled { response }) => {
583 let resp = response
584 .or(last_response.take())
585 .unwrap_or_else(empty_response);
586 let _ = emit(
587 &tx,
588 AgentEvent::LoopEnd {
589 result: build_result(&state, StopReason::Cancelled, resp),
590 },
591 )
592 .await;
593 return;
594 }
595 Ok(StreamIterResult::OutputBudgetExceeded { response }) => {
596 tracing::warn!(
597 total_output_tokens = get_output_tokens(&state),
598 "single-round output budget exceeded, stopping agent"
599 );
600 let _ = emit(
601 &tx,
602 AgentEvent::LoopEnd {
603 result: build_result(
604 &state,
605 StopReason::OutputBudgetExceeded,
606 response,
607 ),
608 },
609 )
610 .await;
611 return;
612 }
613 Ok(StreamIterResult::ReasoningBudgetExceeded { response }) => {
614 tracing::warn!(
615 total_reasoning_tokens = get_reasoning_tokens(&state),
616 "single-round reasoning budget exceeded, stopping agent"
617 );
618 let _ = emit(
619 &tx,
620 AgentEvent::LoopEnd {
621 result: build_result(
622 &state,
623 StopReason::ReasoningBudgetExceeded,
624 response,
625 ),
626 },
627 )
628 .await;
629 return;
630 }
631 Err(e) => {
632 let _ = emit(
633 &tx,
634 AgentEvent::LoopError {
635 error: e,
636 iterations: get_iterations(&state) as usize,
637 },
638 )
639 .await;
640 return;
641 }
642 }
643 }
644 });
645
646 rx
647 }
648}