Skip to main content

lellm_agent/runtime/
config.rs

1//! ToolUseLoop 配置与请求构建辅助。
2//!
3//! - `ToolUseConfig` — 纯参数,Clone + Send + Sync
4//! - `ToolUseDeps` — 策略服务,Arc 包裹
5//! - `build_request_*` — 请求构建辅助函数
6
7use lellm_core::{ChatRequest, LlmError, Message, ToolDefinition};
8use lellm_provider::ResolvedModel;
9
10use super::context::ContextBudget;
11use super::fallback::FallbackStrategy;
12use super::request_opts::RequestOptions;
13use super::retry::RetryPolicy;
14use std::sync::Arc;
15
16// ─── 配置(纯参数)──────────────────────────────────────────────
17
18/// ToolUseLoop 纯参数配置。
19///
20/// - `Clone` + `Send` + `Sync` — 可安全跨线程共享
21/// - 仅包含数据字段,不含行为逻辑
22/// - 未来可扩展为 `Serialize` / `Deserialize`
23#[derive(Debug, Clone)]
24pub struct ToolUseConfig {
25    /// 系统提示(运行时注入,不修改 messages)
26    pub system_prompt: Option<String>,
27    /// 最大迭代轮次(默认 10)
28    pub max_iterations: usize,
29    /// 每次 LLM 请求的最大输出 token 数(默认 4k)
30    ///
31    /// 控制单次 Provider 调用的响应长度上限,防止模型输出过长。
32    /// 工具调用轮次通常只需几百 token,但模型 thinking 会消耗额外空间,
33    /// 4k 在"够用"和"不浪费"之间取得平衡。
34    /// 若需要长文本生成,可通过 Builder 调大。
35    /// 会自动注入到 `ChatRequest.max_tokens`。
36    pub max_output_tokens: u32,
37    /// 整个 Agent Run 的最大输出 token 总数(可选,默认无限制)。
38    ///
39    /// 即使每轮的 `max_output_tokens` 设置合理,多轮工具调用仍可能导致
40    /// 总输出巨大(如 10 轮 × 4k = 40k)。此字段提供聚合层面的保险丝,
41    /// 防止因工具循环或 Provider 忽略 max_tokens 而导致的成本失控。
42    ///
43    /// 统计范围:Assistant Text(不含 Thinking,不含 Tool Call 结构开销)。
44    /// 在流式模式下边接收边检查,达到阈值立即停止。
45    pub max_total_output_tokens: Option<u32>,
46    /// 整个 Agent Run 的最大推理 token 总数(可选,默认无限制)。
47    ///
48    /// 与 `max_total_output_tokens` 分离:thinking 是模型内部推理,不计入输出预算。
49    /// 双层设计:
50    /// - 单轮:`RequestOptions.max_reasoning_tokens` → 透传给 Provider
51    /// - 总计:`max_total_reasoning_tokens` → Agent 层累计检查
52    pub max_total_reasoning_tokens: Option<u32>,
53    /// 上下文预算管理(默认开启)
54    ///
55    /// **v0.1**: 默认 `ContextBudget::default()`(max_tokens = 128,000)
56    /// **v0.2**: 从 `ResolvedModel.context_window` 自动推导(window * 0.8)
57    ///
58    /// 若要关闭限制,设置 `max_tokens = usize::MAX`。
59    pub context_budget: ContextBudget,
60    /// 每轮 LLM 调用的生成参数覆盖。
61    ///
62    /// 独立字段定义,不与 `ChatRequest` 耦合。
63    /// `apply()` 方法将非默认值(temperature、top_p、reasoning 等)
64    /// 覆盖到 Agent 层构建的基础 `ChatRequest` 上。
65    ///
66    /// `model`、`messages`、`tools` 由 Agent 层注入,不会被覆盖。
67    pub request_options: RequestOptions,
68    /// 是否向消费者流式输出推理过程(ThinkingDelta 事件)。
69    ///
70    /// `false`(默认)= 模型可推理,但不向消费者发射 ThinkingDelta 事件
71    /// `true` = 将推理内容以 ThinkingDelta 事件流式输出
72    ///
73    /// **重要:** 此字段控制框架行为(Event 管道),不属于协议参数。
74    /// 不应出现在 `ChatRequest` 中(Codec 不应看到此字段)。
75    pub stream_thinking: bool,
76    /// 工具重试策略
77    pub retry_policy: RetryPolicy,
78}
79
80impl Default for ToolUseConfig {
81    fn default() -> Self {
82        Self {
83            system_prompt: None,
84            max_iterations: 10,
85            max_output_tokens: 4_000,
86            max_total_output_tokens: None,
87            max_total_reasoning_tokens: None,
88            context_budget: ContextBudget::default(),
89            request_options: RequestOptions::default(),
90            stream_thinking: false,
91            retry_policy: RetryPolicy::default(),
92        }
93    }
94}
95
96// ─── 依赖(策略服务)────────────────────────────────────────────
97
98/// ToolUseLoop 策略依赖。
99///
100/// 包含有行为逻辑的服务对象(Arc 包裹),与纯参数 Config 分离。
101#[derive(Clone)]
102pub struct ToolUseDeps {
103    /// Provider 降级策略
104    pub fallback: Arc<dyn FallbackStrategy>,
105}
106
107impl Default for ToolUseDeps {
108    fn default() -> Self {
109        Self {
110            fallback: Arc::new(super::fallback::DefaultFallback::default()),
111        }
112    }
113}
114
115// ─── 辅助函数 ───────────────────────────────────────────────────
116
117/// 检查消息列表中是否已存在 System 消息。
118pub(super) fn has_system_message(messages: &[Message]) -> bool {
119    messages.iter().any(|m| matches!(m, Message::System { .. }))
120}
121
122/// 构建有效的请求消息列表(用于 spawned task,无法使用 &self)
123pub(super) fn build_request_messages_inner(
124    config: &ToolUseConfig,
125    messages: &[Message],
126) -> Result<Vec<Message>, LlmError> {
127    if let Some(ref sp) = config.system_prompt {
128        if has_system_message(messages) {
129            return Err(LlmError::DuplicateSystemPrompt);
130        }
131        let mut result = vec![Message::System {
132            content: lellm_core::text_block(sp.clone()),
133        }];
134        result.extend(messages.iter().cloned());
135        Ok(result)
136    } else {
137        Ok(messages.to_vec())
138    }
139}
140
141/// 构建 ChatRequest(用于 spawned task)
142///
143/// 先构建基础请求(Agent 层注入 model/messages/tools/max_tokens),
144/// 再应用 RequestOptions 非默认值覆盖。
145///
146/// `definitions` — 预解析的工具定义列表(从 ResolvedRound 获取)。
147pub(super) fn build_request_inner(
148    model: &ResolvedModel,
149    messages: &[Message],
150    max_output_tokens: u32,
151    request_options: &RequestOptions,
152    definitions: &[ToolDefinition],
153) -> ChatRequest {
154    let tools = if definitions.is_empty() {
155        None
156    } else {
157        Some(definitions.to_vec())
158    };
159
160    let mut req = ChatRequest {
161        model: model.model.clone(),
162        messages: messages.to_vec(),
163        tools,
164        max_tokens: Some(max_output_tokens),
165        temperature: None,
166        top_p: None,
167        seed: None,
168        tool_choice: None,
169        stop_sequences: None,
170        prefill: None,
171        reasoning: None,
172        max_reasoning_tokens: None,
173        extra: None,
174    };
175
176    // 应用 RequestOptions 非默认值覆盖
177    request_options.apply(&mut req);
178
179    req
180}
181
182/// 构建首轮 ChatRequest,支持强制指定工具(仅第一轮生效)。
183///
184/// 当 `RequestOptions` 设置了 `tool_choice` 时,仅在第一轮注入;
185/// 后续轮次由 LLM 自主决定是否调用工具。
186///
187/// `definitions` — 预解析的工具定义列表(从 ResolvedRound 获取)。
188pub(super) fn build_request_inner_with_round(
189    model: &ResolvedModel,
190    messages: &[Message],
191    max_output_tokens: u32,
192    request_options: &RequestOptions,
193    iteration: usize,
194    definitions: &[ToolDefinition],
195) -> ChatRequest {
196    let mut req = build_request_inner(
197        model,
198        messages,
199        max_output_tokens,
200        request_options,
201        definitions,
202    );
203
204    // 如果 RequestOptions 设置了 tool_choice 且不是第一轮,清除它
205    // 让 LLM 在工具调用后自主选择
206    if iteration > 0 && request_options.tool_choice.is_some() {
207        req.tool_choice = None;
208    }
209
210    req
211}
212
213/// 构建空的 ChatResponse(边界情况兜底)
214pub(super) fn empty_response() -> lellm_core::ChatResponse {
215    lellm_core::ChatResponse::new(
216        lellm_core::text_block(String::new()),
217        lellm_core::TokenUsage::default(),
218        serde_json::Value::Null,
219    )
220}