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::{CacheControl, ChatRequest, LlmError, Message, Prompt, 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    ///
27    /// 支持 `Prompt` 类型,统一了简单文本与分层缓存两种模式。
28    /// - 简单文本:通过 `From<String>` 自动转换
29    /// - 分层缓存:`Prompt::builder().layer_cached(...).build()`
30    pub system: Option<Prompt>,
31    /// 最大迭代轮次(默认 10)
32    pub max_iterations: usize,
33    /// 每次 LLM 请求的最大输出 token 数(默认 4k)
34    ///
35    /// 控制单次 Provider 调用的响应长度上限,防止模型输出过长。
36    /// 工具调用轮次通常只需几百 token,但模型 thinking 会消耗额外空间,
37    /// 4k 在"够用"和"不浪费"之间取得平衡。
38    /// 若需要长文本生成,可通过 Builder 调大。
39    /// 会自动注入到 `ChatRequest.max_tokens`。
40    pub max_output_tokens: u32,
41    /// 整个 Agent Run 的最大输出 token 总数(可选,默认无限制)。
42    ///
43    /// 即使每轮的 `max_output_tokens` 设置合理,多轮工具调用仍可能导致
44    /// 总输出巨大(如 10 轮 × 4k = 40k)。此字段提供聚合层面的保险丝,
45    /// 防止因工具循环或 Provider 忽略 max_tokens 而导致的成本失控。
46    ///
47    /// 统计范围:Assistant Text(不含 Thinking,不含 Tool Call 结构开销)。
48    /// 在流式模式下边接收边检查,达到阈值立即停止。
49    pub max_total_output_tokens: Option<u32>,
50    /// 整个 Agent Run 的最大推理 token 总数(可选,默认无限制)。
51    ///
52    /// 与 `max_total_output_tokens` 分离:thinking 是模型内部推理,不计入输出预算。
53    /// 双层设计:
54    /// - 单轮:`RequestOptions.max_reasoning_tokens` → 透传给 Provider
55    /// - 总计:`max_total_reasoning_tokens` → Agent 层累计检查
56    pub max_total_reasoning_tokens: Option<u32>,
57    /// 上下文预算管理(默认开启)
58    ///
59    /// **v0.1**: 默认 `ContextBudget::default()`(max_tokens = 128,000)
60    /// **v0.2**: 从 `ResolvedModel.context_window` 自动推导(window * 0.8)
61    ///
62    /// 若要关闭限制,设置 `max_tokens = usize::MAX`。
63    pub context_budget: ContextBudget,
64    /// 每轮 LLM 调用的生成参数覆盖。
65    ///
66    /// 独立字段定义,不与 `ChatRequest` 耦合。
67    /// `apply()` 方法将非默认值(temperature、top_p、reasoning 等)
68    /// 覆盖到 Agent 层构建的基础 `ChatRequest` 上。
69    ///
70    /// `model`、`messages`、`tools` 由 Agent 层注入,不会被覆盖。
71    pub request_options: RequestOptions,
72    /// 是否向消费者流式输出推理过程(ThinkingDelta 事件)。
73    ///
74    /// `false`(默认)= 模型可推理,但不向消费者发射 ThinkingDelta 事件
75    /// `true` = 将推理内容以 ThinkingDelta 事件流式输出
76    ///
77    /// **重要:** 此字段控制框架行为(Event 管道),不属于协议参数。
78    /// 不应出现在 `ChatRequest` 中(Codec 不应看到此字段)。
79    pub stream_thinking: bool,
80    /// 工具缓存策略(默认 `Auto`)。
81    ///
82    /// 控制框架如何为 Tool Definitions 添加 `cache_control` 标记:
83    /// - `Auto`(默认):为未设置 `cache_control` 的工具自动添加 `Breakpoint`
84    /// - `Preserve`:不修改用户设置的 `cache_control`
85    /// - `Disabled`:显式清除所有工具的 `cache_control`
86    pub tool_cache_policy: ToolCachePolicy,
87    /// 工具重试策略
88    pub retry_policy: RetryPolicy,
89}
90
91/// 工具缓存策略。
92///
93/// 控制框架如何为 Tool Definitions 补充 `cache_control` 标记,
94/// 以最大化前缀缓存命中率。
95#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
96pub enum ToolCachePolicy {
97    /// 自动模式(默认):为未设置 `cache_control` 的工具添加 `Breakpoint`。
98    /// 已显式设置的标记不会被覆盖。
99    #[default]
100    Auto,
101    /// 保留模式:不修改用户设置的 `cache_control`。
102    Preserve,
103    /// 禁用模式:清除所有工具的 `cache_control`。
104    Disabled,
105}
106
107impl Default for ToolUseConfig {
108    fn default() -> Self {
109        Self {
110            system: None,
111            max_iterations: 10,
112            max_output_tokens: 4_000,
113            max_total_output_tokens: None,
114            max_total_reasoning_tokens: None,
115            context_budget: ContextBudget::default(),
116            request_options: RequestOptions::default(),
117            stream_thinking: false,
118            tool_cache_policy: ToolCachePolicy::default(),
119            retry_policy: RetryPolicy::default(),
120        }
121    }
122}
123
124// ─── 依赖(策略服务)────────────────────────────────────────────
125
126/// ToolUseLoop 策略依赖。
127///
128/// 包含有行为逻辑的服务对象(Arc 包裹),与纯参数 Config 分离。
129#[derive(Clone)]
130pub struct ToolUseDeps {
131    /// Provider 降级策略
132    pub fallback: Arc<dyn FallbackStrategy>,
133}
134
135impl Default for ToolUseDeps {
136    fn default() -> Self {
137        Self {
138            fallback: Arc::new(super::fallback::DefaultFallback::default()),
139        }
140    }
141}
142
143// ─── 辅助函数 ───────────────────────────────────────────────────
144
145/// 检查消息列表中是否已存在 System 消息。
146pub(super) fn has_system_message(messages: &[Message]) -> bool {
147    messages.iter().any(|m| matches!(m, Message::System { .. }))
148}
149
150/// 构建有效的请求消息列表(用于 spawned task,无法使用 &self)
151pub(super) fn build_request_messages_inner(
152    config: &ToolUseConfig,
153    messages: &[Message],
154) -> Result<Vec<Message>, LlmError> {
155    if let Some(ref system) = config.system {
156        if has_system_message(messages) {
157            return Err(LlmError::DuplicateSystemPrompt);
158        }
159        let mut result = vec![Message::System {
160            content: system.to_content_blocks(),
161        }];
162        result.extend(messages.iter().cloned());
163        Ok(result)
164    } else {
165        Ok(messages.to_vec())
166    }
167}
168
169/// 构建 ChatRequest(用于 spawned task)
170///
171/// 先构建基础请求(Agent 层注入 model/messages/tools/max_tokens),
172/// 再应用 RequestOptions 非默认值覆盖。
173///
174/// `definitions` — 预解析的工具定义列表(从 ResolvedRound 获取)。
175/// `tool_cache_policy` — 工具缓存策略,控制如何为 tools 添加 cache_control。
176pub(super) fn build_request_inner(
177    model: &ResolvedModel,
178    messages: &[Message],
179    max_output_tokens: u32,
180    request_options: &RequestOptions,
181    definitions: &[ToolDefinition],
182    tool_cache_policy: ToolCachePolicy,
183) -> ChatRequest {
184    let tools = match tool_cache_policy {
185        ToolCachePolicy::Auto => {
186            if definitions.is_empty() {
187                None
188            } else {
189                Some(
190                    definitions
191                        .iter()
192                        .map(|d| {
193                            if d.cache_control.is_none() {
194                                let mut cloned = d.clone();
195                                cloned.cache_control = Some(CacheControl::Breakpoint);
196                                cloned
197                            } else {
198                                d.clone()
199                            }
200                        })
201                        .collect(),
202                )
203            }
204        }
205        ToolCachePolicy::Preserve => {
206            if definitions.is_empty() {
207                None
208            } else {
209                Some(definitions.to_vec())
210            }
211        }
212        ToolCachePolicy::Disabled => {
213            if definitions.is_empty() {
214                None
215            } else {
216                Some(
217                    definitions
218                        .iter()
219                        .map(|d| {
220                            let mut cloned = d.clone();
221                            cloned.cache_control = None;
222                            cloned
223                        })
224                        .collect(),
225                )
226            }
227        }
228    };
229
230    let mut req = ChatRequest {
231        model: model.model.clone(),
232        messages: messages.to_vec(),
233        tools,
234        max_tokens: Some(max_output_tokens),
235        temperature: None,
236        top_p: None,
237        seed: None,
238        tool_choice: None,
239        stop_sequences: None,
240        prefill: None,
241        reasoning: None,
242        max_reasoning_tokens: None,
243        extra: None,
244    };
245
246    // 应用 RequestOptions 非默认值覆盖
247    request_options.apply(&mut req);
248
249    req
250}
251
252/// 构建首轮 ChatRequest,支持强制指定工具(仅第一轮生效)。
253///
254/// 当 `RequestOptions` 设置了 `tool_choice` 时,仅在第一轮注入;
255/// 后续轮次由 LLM 自主决定是否调用工具。
256///
257/// `definitions` — 预解析的工具定义列表(从 ResolvedRound 获取)。
258/// `tool_cache_policy` — 工具缓存策略,控制如何为 tools 添加 cache_control。
259pub(super) fn build_request_inner_with_round(
260    model: &ResolvedModel,
261    messages: &[Message],
262    max_output_tokens: u32,
263    request_options: &RequestOptions,
264    iteration: usize,
265    definitions: &[ToolDefinition],
266    tool_cache_policy: ToolCachePolicy,
267) -> ChatRequest {
268    let mut req = build_request_inner(
269        model,
270        messages,
271        max_output_tokens,
272        request_options,
273        definitions,
274        tool_cache_policy,
275    );
276
277    // 如果 RequestOptions 设置了 tool_choice 且不是第一轮,清除它
278    // 让 LLM 在工具调用后自主选择
279    if iteration > 0 && request_options.tool_choice.is_some() {
280        req.tool_choice = None;
281    }
282
283    req
284}
285
286/// 构建空的 ChatResponse(边界情况兜底)
287pub(super) fn empty_response() -> lellm_core::ChatResponse {
288    lellm_core::ChatResponse::new(
289        lellm_core::text_block(String::new()),
290        lellm_core::TokenUsage::default(),
291        serde_json::Value::Null,
292    )
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use lellm_core::Prompt;
299
300    #[test]
301    fn test_build_request_messages_with_prompt() {
302        let config = ToolUseConfig {
303            system: Some(
304                Prompt::builder()
305                    .layer_cached("核心身份")
306                    .layer_cached("工具指南")
307                    .layer_dynamic("会话上下文")
308                    .build(),
309            ),
310            ..Default::default()
311        };
312
313        let messages: Vec<Message> = vec![Message::user_text("你好")];
314        let result = build_request_messages_inner(&config, &messages).unwrap();
315
316        // Should have system + user
317        assert_eq!(result.len(), 2);
318
319        // Verify system message has 3 content blocks with correct cache markers
320        if let Message::System { content } = &result[0] {
321            assert_eq!(content.len(), 3);
322
323            // Layer 1 — cached
324            if let lellm_core::ContentBlock::Text(t) = &content[0] {
325                assert_eq!(t.text, "核心身份");
326                assert!(t.cache_control.is_some());
327            } else {
328                panic!("expected Text block");
329            }
330
331            // Layer 2 — cached
332            if let lellm_core::ContentBlock::Text(t) = &content[1] {
333                assert_eq!(t.text, "工具指南");
334                assert!(t.cache_control.is_some());
335            } else {
336                panic!("expected Text block");
337            }
338
339            // Layer 3 — dynamic (no cache)
340            if let lellm_core::ContentBlock::Text(t) = &content[2] {
341                assert_eq!(t.text, "会话上下文");
342                assert!(t.cache_control.is_none());
343            } else {
344                panic!("expected Text block");
345            }
346        } else {
347            panic!("expected System message");
348        }
349    }
350
351    #[test]
352    fn test_build_request_messages_with_plain_prompt() {
353        let config = ToolUseConfig {
354            system: Some("简单提示".into()),
355            ..Default::default()
356        };
357
358        let messages: Vec<Message> = vec![Message::user_text("你好")];
359        let result = build_request_messages_inner(&config, &messages).unwrap();
360
361        assert_eq!(result.len(), 2);
362        if let Message::System { content } = &result[0] {
363            assert_eq!(content.len(), 1);
364            assert_eq!(content[0].as_text(), Some("简单提示"));
365        } else {
366            panic!("expected System message");
367        }
368    }
369
370    #[test]
371    fn test_build_request_messages_no_system() {
372        let config = ToolUseConfig::default();
373        let messages: Vec<Message> = vec![Message::user_text("你好")];
374        let result = build_request_messages_inner(&config, &messages).unwrap();
375
376        assert_eq!(result.len(), 1);
377        assert!(matches!(result[0], Message::User { .. }));
378    }
379
380    #[test]
381    fn test_duplicate_system_prompt_error() {
382        let config = ToolUseConfig {
383            system: Some("系统提示".into()),
384            ..Default::default()
385        };
386
387        let messages: Vec<Message> =
388            vec![Message::system_text("已有系统"), Message::user_text("你好")];
389        let result = build_request_messages_inner(&config, &messages);
390
391        assert!(result.is_err());
392    }
393}