Skip to main content

lellm_agent/runtime/
runtime.rs

1//! Agent Loop — LLM ↔ 工具调用闭环。
2//!
3//! 负责 LLM 返回 tool_calls → 执行工具 → 结果注入 → 再次调用 LLM 的循环,
4//! 直到 LLM 返回纯文本或达到最大轮次。
5//!
6//! # 架构分层
7//!
8//! ```text
9//! ToolUseLoop
10//! ├── model:       ResolvedModel     (Provider + model name)
11//! ├── executor:    ToolExecutor      (ToolCatalog + 执行引擎)
12//! ├── config:      ToolUseConfig     (纯参数, Clone + Send + Sync)
13//! └── deps:        ToolUseDeps       (策略服务, Arc 包裹)
14//! ```
15
16use 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// ─── 本轮解析数据 ────────────────────────────────────────────────
35
36/// 本轮对话锁定的快照 + 定义。
37///
38/// 一旦创建,内容不再变化。充当单轮的"真理之源"。
39#[derive(Clone)]
40pub struct ResolvedRound {
41    /// 本轮对话锁定的快照
42    pub snapshot: Arc<ToolSnapshot>,
43    /// 为当前 LLM 供给的工具定义(已在前置阶段从快照中提取并平铺)
44    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
56// ─── State 辅助函数 ─────────────────────────────────────────────
57
58/// 创建初始 State,写入消息列表和计数器。
59pub(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
73/// 从 State 读取消息列表(反序列化)。
74pub(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
83/// 从 State 读取迭代轮次。
84pub(crate) fn get_iterations(state: &State) -> u32 {
85    state.get_sk::<u32>(&SK_ITERATIONS).unwrap_or(0)
86}
87
88/// 从 State 读取累计工具调用数。
89pub(crate) fn get_tool_calls(state: &State) -> usize {
90    state.get_sk::<usize>(&SK_TOTAL_TOOL_CALLS).unwrap_or(0)
91}
92
93/// 从 State 读取累计输出 Token 数。
94pub(crate) fn get_output_tokens(state: &State) -> usize {
95    state.get_sk::<usize>(&SK_OUTPUT_TOKENS).unwrap_or(0)
96}
97
98/// 从 State 读取累计推理 Token 数。
99pub(crate) fn get_reasoning_tokens(state: &State) -> usize {
100    state.get_sk::<usize>(&SK_REASONING_TOKENS).unwrap_or(0)
101}
102
103/// 追加 Assistant 响应到消息历史。
104pub(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
119/// 追加工具执行结果到消息历史。
120pub(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
156/// 记录本轮工具调用数量。
157pub(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
162/// 从 ContentBlock 分离估算 Output 和 Reasoning Token。
163pub(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
186/// 进入下一轮迭代。
187pub(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
192/// 判断是否已达到最大轮次。
193pub(crate) fn state_reached_max(state: &State, max_iterations: usize) -> bool {
194    get_iterations(state) >= max_iterations as u32
195}
196
197/// 对消息历史执行压缩。
198pub(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
219/// 检查是否超过总输出预算。
220pub(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
227/// 检查是否超过总推理预算。
228pub(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
235/// 构建最终执行结果。
236pub(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// ─── 执行结果 ───────────────────────────────────────────────────
251
252/// ToolUseLoop 执行结果
253#[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// ─── ToolUseLoop ────────────────────────────────────────────────
269
270/// 管理 LLM 与工具调用闭环。
271///
272/// 内部全为 Arc/Clone,clone 为 O(1),支持并发 execute。
273#[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    /// 便捷构造 — 使用默认配置和依赖。
309    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    /// 获取模型引用。
319    pub fn model(&self) -> &ResolvedModel {
320        &self.model
321    }
322
323    /// 获取执行器引用。
324    pub fn executor(&self) -> &ToolExecutor {
325        &self.executor
326    }
327
328    /// 获取配置引用。
329    pub fn config(&self) -> &ToolUseConfig {
330        &self.config
331    }
332
333    /// 获取依赖引用。
334    pub fn deps(&self) -> &ToolUseDeps {
335        &self.deps
336    }
337
338    /// 非流式执行
339    ///
340    /// v0.4+: AgentState 驱动 — 使用 `Graph<AgentState>.run_inline()` 执行 ReAct 循环。
341    /// 零序列化,编译期类型安全。
342    pub async fn execute(&self, messages: Vec<Message>) -> Result<ToolUseResult, LlmError> {
343        let initial_messages = build_request_messages_inner(&self.config, &messages)?;
344
345        // 构建 ReAct Graph (Graph<AgentState, AgentStateMerge>)
346        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        // 初始化 AgentState
364        let mut agent_state = super::typed_state::AgentState::from_messages(initial_messages);
365
366        // 创建 NodeContext<AgentState> 并调用 run_inline
367        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    /// 流式执行,返回事件接收器。
397    ///
398    /// ⚠️ TODO (v04 #3): 迁移到 ReAct Graph 驱动。
399    /// 当前仍使用手写 while 循环。完整迁移需要:
400    /// 1. LLMNode 支持 streaming API (`provider.stream()`)
401    /// 2. `Graph::run_inline_stream()` 方法
402    /// 3. AgentEvent ↔ StreamChunk 桥接
403    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                // 内联 fallback 重试循环
473                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                // 成功时合并 state
528                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                // 总预算检查
538                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}