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_provider::ResolvedModel;
18use std::sync::Arc;
19
20use super::config::{ToolUseConfig, ToolUseDeps, build_request_messages_inner, empty_response};
21use super::context::{
22    CompactionResult, ContextBudget, ContextCompactor, LocalCompactor, estimate_reasoning_block,
23    estimate_text, estimate_tokens,
24};
25use super::event::{AgentEvent, AgentStream, StopReason};
26use super::fallback::{FallbackAction, FallbackContext};
27use super::iteration::{StreamIterResult, do_stream_iteration, emit, execute_with_fallback};
28use super::tools::{ToolExecutor, ToolSnapshot, execute_batch_with};
29
30// ─── 本轮解析数据 ────────────────────────────────────────────────
31
32/// 本轮对话锁定的快照 + 定义。
33///
34/// 一旦创建,内容不再变化。充当单轮的"真理之源"。
35#[derive(Clone)]
36pub struct ResolvedRound {
37    /// 本轮对话锁定的快照
38    pub snapshot: Arc<ToolSnapshot>,
39    /// 为当前 LLM 供给的工具定义(已在前置阶段从快照中提取并平铺)
40    pub definitions: Vec<lellm_core::ToolDefinition>,
41}
42
43impl ResolvedRound {
44    pub fn new(snapshot: Arc<ToolSnapshot>) -> Self {
45        Self {
46            definitions: snapshot.definitions().to_vec(),
47            snapshot,
48        }
49    }
50}
51
52// ─── 循环状态机 ───────────────────────────────────────────────
53
54/// Agent Loop 共享状态 — 非流式与流式执行模式共用。
55///
56/// `Clone` 用于迭代重试时回滚状态(snapshot → restore)。
57#[derive(Debug, Clone)]
58pub struct LoopState {
59    pub messages: Vec<Message>,
60    /// 消息历史的估算 Token 数(与 messages 同步更新)
61    pub estimated_tokens: usize,
62    pub iterations: usize,
63    pub tool_calls_executed: usize,
64    /// 整个 Agent Run 的累计输出 Token 数(Text,不含 Thinking 和 Tool Call)
65    /// 用于 max_total_output_tokens 预算检查。
66    pub total_output_tokens: usize,
67    /// 整个 Agent Run 的累计推理 Token 数(Thinking,不含 Text 和 Tool Call)
68    /// 用于可观测性;单轮推理预算通过 `ChatRequest.max_reasoning_tokens` 透传给 Provider。
69    pub total_reasoning_tokens: usize,
70}
71
72impl LoopState {
73    pub fn new(messages: Vec<Message>) -> Self {
74        let estimated_tokens = estimate_tokens(&messages);
75        Self {
76            messages,
77            estimated_tokens,
78            iterations: 0,
79            tool_calls_executed: 0,
80            total_output_tokens: 0,
81            total_reasoning_tokens: 0,
82        }
83    }
84
85    /// 累计输出 Token(Text)
86    pub fn add_output_tokens(&mut self, tokens: usize) {
87        self.total_output_tokens += tokens;
88    }
89
90    /// 累计推理 Token(Thinking)
91    pub fn add_reasoning_tokens(&mut self, tokens: usize) {
92        self.total_reasoning_tokens += tokens;
93    }
94
95    /// 检查是否超过总输出预算
96    pub fn exceeded_total_output(&self, max: Option<u32>) -> bool {
97        match max {
98            Some(limit) => self.total_output_tokens >= limit as usize,
99            None => false,
100        }
101    }
102
103    /// 检查是否超过总推理预算
104    pub fn exceeded_total_reasoning(&self, max: Option<u32>) -> bool {
105        match max {
106            Some(limit) => self.total_reasoning_tokens >= limit as usize,
107            None => false,
108        }
109    }
110
111    /// 从 ContentBlock 分离估算 Output(Text)和 Reasoning(Thinking)Token
112    pub fn add_output_from_content(&mut self, content: &[lellm_core::ContentBlock]) {
113        let mut output_tokens: usize = 0;
114        let mut reasoning_tokens: usize = 0;
115        for b in content {
116            match b {
117                lellm_core::ContentBlock::Text(t) => output_tokens += estimate_text(&t.text),
118                lellm_core::ContentBlock::Thinking(th) => {
119                    reasoning_tokens += estimate_reasoning_block(th)
120                }
121                lellm_core::ContentBlock::Image { .. } | lellm_core::ContentBlock::ToolCall(_) => {}
122            }
123        }
124        self.total_output_tokens += output_tokens;
125        self.total_reasoning_tokens += reasoning_tokens;
126    }
127
128    /// 追加 Assistant 响应到历史
129    pub fn push_assistant(&mut self, content: Vec<lellm_core::ContentBlock>) {
130        let msg = Message::Assistant {
131            content: content.clone(),
132        };
133        let tokens = estimate_tokens(&[msg]);
134        self.estimated_tokens += tokens;
135        self.messages.push(Message::Assistant { content });
136    }
137
138    /// 追加工具执行结果到历史。
139    pub fn push_tool_results(&mut self, results: Vec<Message>, budget: &ContextBudget) {
140        let results: Vec<Message> = results
141            .into_iter()
142            .map(|m| {
143                if let Message::ToolResult {
144                    ref tool_call_id,
145                    is_error: false,
146                    ref content,
147                } = m
148                {
149                    let truncated = budget.truncate_tool_result_blocks(content);
150                    if truncated != *content {
151                        return Message::ToolResult {
152                            tool_call_id: tool_call_id.clone(),
153                            is_error: false,
154                            content: truncated,
155                        };
156                    }
157                }
158                m
159            })
160            .collect();
161        let tokens = estimate_tokens(&results);
162        self.estimated_tokens += tokens;
163        self.messages.extend(results);
164    }
165
166    /// 记录本轮工具调用数量
167    pub fn add_tool_calls(&mut self, count: usize) {
168        self.tool_calls_executed += count;
169    }
170
171    /// 进入下一轮迭代
172    pub fn next_iteration(&mut self) {
173        self.iterations += 1;
174    }
175
176    /// 判断是否已达到最大轮次
177    pub fn reached_max(&self, max_iterations: usize) -> bool {
178        self.iterations >= max_iterations
179    }
180
181    /// 对消息历史执行压缩。
182    pub fn compact(
183        &mut self,
184        budget: &ContextBudget,
185        compactor: &dyn ContextCompactor,
186    ) -> Option<CompactionResult> {
187        if !budget.should_compact(self.estimated_tokens) {
188            return None;
189        }
190        let result = compactor.compact(&self.messages, budget);
191        self.messages = result.messages.clone();
192        self.estimated_tokens = result.after_tokens;
193        Some(result)
194    }
195
196    /// 构建最终执行结果
197    pub fn finish(&self, stop_reason: StopReason, response: ChatResponse) -> ToolUseResult {
198        ToolUseResult {
199            stop_reason,
200            response,
201            messages: self.messages.clone(),
202            iterations: self.iterations,
203            tool_calls_executed: self.tool_calls_executed,
204        }
205    }
206
207    pub fn finish_complete(&self, response: ChatResponse) -> ToolUseResult {
208        self.finish(StopReason::Complete, response)
209    }
210
211    pub fn finish_max_iterations(&self, response: ChatResponse) -> ToolUseResult {
212        self.finish(StopReason::MaxIterationsReached, response)
213    }
214
215    pub fn finish_cancelled(&self, response: ChatResponse) -> ToolUseResult {
216        self.finish(StopReason::Cancelled, response)
217    }
218
219    pub fn finish_output_budget(&self, response: ChatResponse) -> ToolUseResult {
220        self.finish(StopReason::OutputBudgetExceeded, response)
221    }
222
223    pub fn finish_reasoning_budget(&self, response: ChatResponse) -> ToolUseResult {
224        self.finish(StopReason::ReasoningBudgetExceeded, response)
225    }
226}
227
228// ─── 执行结果 ───────────────────────────────────────────────────
229
230/// ToolUseLoop 执行结果
231#[derive(Debug, Clone)]
232pub struct ToolUseResult {
233    pub stop_reason: StopReason,
234    pub response: ChatResponse,
235    pub messages: Vec<Message>,
236    pub iterations: usize,
237    pub tool_calls_executed: usize,
238}
239
240impl ToolUseResult {
241    pub fn is_success(&self) -> bool {
242        matches!(self.stop_reason, StopReason::Complete)
243    }
244}
245
246// ─── ToolUseLoop ────────────────────────────────────────────────
247
248/// 管理 LLM 与工具调用闭环。
249///
250/// 内部全为 Arc/Clone,clone 为 O(1),支持并发 execute。
251#[derive(Clone)]
252pub struct ToolUseLoop {
253    model: ResolvedModel,
254    executor: ToolExecutor,
255    config: ToolUseConfig,
256    deps: ToolUseDeps,
257}
258
259impl ToolUseLoop {
260    pub fn new(
261        model: ResolvedModel,
262        executor: ToolExecutor,
263        config: ToolUseConfig,
264        deps: ToolUseDeps,
265    ) -> Self {
266        if config.stream_thinking {
267            let caps = model.provider.capabilities_for(&model.model);
268            if !caps.supports_stream_thinking {
269                tracing::warn!(
270                    provider = %model.provider.provider_id(),
271                    model = %model.model,
272                    "stream_thinking=true but provider does not support thinking deltas; \
273                     reasoning content will only be available in the final response"
274                );
275            }
276        }
277
278        Self {
279            model,
280            executor,
281            config,
282            deps,
283        }
284    }
285
286    /// 便捷构造 — 使用默认配置和依赖。
287    pub fn simple(model: ResolvedModel, executor: ToolExecutor) -> Self {
288        Self::new(
289            model,
290            executor,
291            ToolUseConfig::default(),
292            ToolUseDeps::default(),
293        )
294    }
295
296    /// 非流式执行
297    pub async fn execute(&self, messages: Vec<Message>) -> Result<ToolUseResult, LlmError> {
298        let initial_messages = build_request_messages_inner(&self.config, &messages)?;
299        let mut state = LoopState::new(initial_messages);
300        let mut last_response: Option<ChatResponse> = None;
301        let compactor: Box<dyn ContextCompactor> = Box::new(LocalCompactor::new());
302
303        loop {
304            if state.reached_max(self.config.max_iterations) {
305                return Ok(
306                    state.finish_max_iterations(last_response.unwrap_or_else(empty_response))
307                );
308            }
309            state.next_iteration();
310            state.compact(&self.config.context_budget, &*compactor);
311
312            // 1.5 解析本轮工具快照(每轮一次,固定工具集)
313            let round = ResolvedRound::new(self.executor.snapshot().await);
314
315            // 2. 构建请求
316            let req = super::config::build_request_inner_with_round(
317                &self.model,
318                &state.messages,
319                self.config.max_output_tokens,
320                &self.config.request_options,
321                state.iterations,
322                &round.definitions,
323            );
324
325            // 3. 执行 Provider
326            let iteration = state.iterations;
327            let msg_snapshot = state.messages.clone();
328            let response = execute_with_fallback(
329                &self.deps.fallback,
330                |_| true, // 非流式模式允许重试
331                || self.model.provider.call(&req),
332                iteration,
333                &msg_snapshot,
334            )
335            .await?;
336            last_response = Some(response.clone());
337
338            // 4. 单轮推理预算检查(非流式路径)
339            if let Some(limit) = self.config.request_options.max_reasoning_tokens {
340                let round_reasoning: usize = response
341                    .content
342                    .iter()
343                    .filter_map(|b| match b {
344                        lellm_core::ContentBlock::Thinking(th) => {
345                            Some(estimate_reasoning_block(th))
346                        }
347                        _ => None,
348                    })
349                    .sum();
350                if round_reasoning > limit as usize {
351                    tracing::warn!(
352                        round_reasoning,
353                        max_reasoning_tokens = limit,
354                        "single-round reasoning budget exceeded (non-stream, soft limit)"
355                    );
356                    return Ok(state.finish_reasoning_budget(response));
357                }
358            }
359
360            // 5. 后处理响应
361            state.add_output_from_content(&response.content);
362
363            if state.exceeded_total_output(self.config.max_total_output_tokens) {
364                return Ok(state.finish_output_budget(response));
365            }
366
367            if state.exceeded_total_reasoning(self.config.max_total_reasoning_tokens) {
368                return Ok(state.finish_reasoning_budget(response));
369            }
370
371            if !response.has_tool_calls() {
372                return Ok(state.finish_complete(response));
373            }
374
375            let tool_calls: Vec<_> = response.tool_calls().cloned().collect();
376            state.push_assistant(response.content.clone());
377            state.add_tool_calls(tool_calls.len());
378
379            let batch =
380                execute_batch_with(&tool_calls, &round.snapshot, &self.executor.retry_policy())
381                    .await;
382
383            if batch.panicked {
384                tracing::warn!("tool batch task panicked — error results filled in by executor");
385            }
386
387            state.push_tool_results(batch.results, &self.config.context_budget);
388
389            tracing::debug!(
390                iteration = state.iterations,
391                tool_calls = tool_calls.len(),
392                "tool-use loop iteration"
393            );
394        }
395    }
396
397    /// 流式执行,返回事件接收器
398    pub fn execute_stream(&self, messages: Vec<Message>) -> AgentStream {
399        let (tx, rx) = tokio::sync::mpsc::channel(32);
400        let model = self.model.clone();
401        let executor = self.executor.clone();
402        let config = self.config.clone();
403        let deps = self.deps.clone();
404
405        tokio::spawn(async move {
406            let initial_messages = match build_request_messages_inner(&config, &messages) {
407                Ok(m) => m,
408                Err(e) => {
409                    let _ = tokio::sync::mpsc::Sender::send(
410                        &tx,
411                        AgentEvent::LoopError {
412                            error: e,
413                            iterations: 0,
414                        },
415                    )
416                    .await;
417                    return;
418                }
419            };
420            let mut state = LoopState::new(initial_messages);
421            let mut last_response: Option<ChatResponse> = None;
422            let compactor: Box<dyn ContextCompactor> = Box::new(LocalCompactor::new());
423
424            loop {
425                if state.reached_max(config.max_iterations) {
426                    let _ = emit(
427                        &tx,
428                        AgentEvent::LoopEnd {
429                            result: state.finish_max_iterations(
430                                last_response.unwrap_or_else(empty_response),
431                            ),
432                        },
433                    )
434                    .await;
435                    return;
436                }
437
438                state.next_iteration();
439                if let Some(compact_result) = state.compact(&config.context_budget, &*compactor) {
440                    let _ = emit(
441                        &tx,
442                        AgentEvent::ContextCompacted {
443                            before_tokens: compact_result.before_tokens,
444                            after_tokens: compact_result.after_tokens,
445                            removed_messages: compact_result.removed_messages,
446                        },
447                    )
448                    .await;
449                }
450
451                // 解析本轮工具快照(每轮一次,固定工具集)
452                let round = ResolvedRound::new(executor.snapshot().await);
453
454                let req = super::config::build_request_inner_with_round(
455                    &model,
456                    &state.messages,
457                    config.max_output_tokens,
458                    &config.request_options,
459                    state.iterations,
460                    &round.definitions,
461                );
462
463                // 内联 fallback 重试循环
464                let iteration = state.iterations;
465                let attempt_state = state.clone();
466                let mut attempt: usize = 1;
467
468                let result = loop {
469                    let iter_result = do_stream_iteration(
470                        model.clone(),
471                        tx.clone(),
472                        executor.clone(),
473                        attempt_state.clone(),
474                        req.clone(),
475                        config.context_budget.clone(),
476                        config.max_output_tokens,
477                        config.stream_thinking,
478                        round.clone(),
479                    )
480                    .await;
481
482                    match iter_result.result {
483                        Ok(v) => break Ok(v),
484                        Err(ref err) => {
485                            tracing::warn!(
486                                attempt = attempt,
487                                error = %err,
488                                stream_started = iter_result.stream_started,
489                                "stream iteration failed, fallback handling"
490                            );
491
492                            if iter_result.stream_started {
493                                let e: LlmError = err.clone();
494                                break Err(e);
495                            }
496
497                            let ctx = FallbackContext {
498                                error: err,
499                                attempt,
500                                iterations: iteration,
501                                conversation: std::sync::Arc::from(
502                                    attempt_state.messages.as_slice(),
503                                ),
504                            };
505                            match deps.fallback.handle(&ctx).await {
506                                FallbackAction::Retry => {
507                                    attempt += 1;
508                                }
509                                FallbackAction::Abort => {
510                                    break Err(err.clone());
511                                }
512                            }
513                        }
514                    }
515                };
516
517                // 成功时合并 state
518                let result = match result {
519                    Ok((r, s)) => {
520                        state = s;
521                        Ok(r)
522                    }
523                    Err(e) => Err(e),
524                };
525
526                // 总预算检查
527                if state.exceeded_total_output(config.max_total_output_tokens) {
528                    let _ = emit(
529                        &tx,
530                        AgentEvent::LoopEnd {
531                            result: state
532                                .finish_output_budget(last_response.unwrap_or_else(empty_response)),
533                        },
534                    )
535                    .await;
536                    return;
537                }
538
539                if state.exceeded_total_reasoning(config.max_total_reasoning_tokens) {
540                    let _ = emit(
541                        &tx,
542                        AgentEvent::LoopEnd {
543                            result: state.finish_reasoning_budget(
544                                last_response.unwrap_or_else(empty_response),
545                            ),
546                        },
547                    )
548                    .await;
549                    return;
550                }
551
552                match result {
553                    Ok(StreamIterResult::Continue { response }) => {
554                        last_response = Some(response);
555                    }
556                    Ok(StreamIterResult::Complete { response }) => {
557                        let _ = emit(
558                            &tx,
559                            AgentEvent::LoopEnd {
560                                result: state.finish_complete(response),
561                            },
562                        )
563                        .await;
564                        return;
565                    }
566                    Ok(StreamIterResult::Cancelled { response }) => {
567                        let resp = response
568                            .or(last_response.take())
569                            .unwrap_or_else(empty_response);
570                        let _ = emit(
571                            &tx,
572                            AgentEvent::LoopEnd {
573                                result: state.finish_cancelled(resp),
574                            },
575                        )
576                        .await;
577                        return;
578                    }
579                    Ok(StreamIterResult::OutputBudgetExceeded { response }) => {
580                        tracing::warn!(
581                            total_output_tokens = state.total_output_tokens,
582                            "single-round output budget exceeded, stopping agent"
583                        );
584                        let _ = emit(
585                            &tx,
586                            AgentEvent::LoopEnd {
587                                result: state.finish_output_budget(response),
588                            },
589                        )
590                        .await;
591                        return;
592                    }
593                    Ok(StreamIterResult::ReasoningBudgetExceeded { response }) => {
594                        tracing::warn!(
595                            total_reasoning_tokens = state.total_reasoning_tokens,
596                            "single-round reasoning budget exceeded, stopping agent"
597                        );
598                        let _ = emit(
599                            &tx,
600                            AgentEvent::LoopEnd {
601                                result: state.finish_reasoning_budget(response),
602                            },
603                        )
604                        .await;
605                        return;
606                    }
607                    Err(e) => {
608                        let _ = emit(
609                            &tx,
610                            AgentEvent::LoopError {
611                                error: e,
612                                iterations: state.iterations,
613                            },
614                        )
615                        .await;
616                        return;
617                    }
618                }
619            }
620        });
621
622        rx
623    }
624}