Skip to main content

atomcode_core/ctx/
default.rs

1//! [`DefaultCtx`] — the fallback [`CtxBuilder`] implementation.
2//!
3//! Thin wrapper around [`crate::ctx::render`]: every method delegates
4//! to the default render/compression policy, parameterized only by
5//! `ctx_window`. Any model not matched by a rule in
6//! [`super::for_provider`] lands here.
7
8use super::CtxBuilder;
9use crate::config::provider::ProviderConfig;
10use crate::conversation::message::Message;
11use crate::conversation::{ContextStats, Conversation};
12use crate::tool::ToolResult;
13
14/// Fallback strategy — matches legacy behavior byte-for-byte.
15#[derive(Debug, Clone)]
16pub struct DefaultCtx {
17    /// Token budget for this provider (from `ProviderConfig.context_window`,
18    /// clamped to a defensive minimum of 8000 to avoid divide-by-zero
19    /// and thrashing in pathological configs).
20    pub ctx_window: usize,
21
22    /// Lowercased model id. Used by
23    /// [`crate::ctx::render::apply_model_directives`] to decide whether
24    /// to append CJK language lock / MiniMax thinking discipline to the
25    /// system prompt. Captured at construction time so `build_messages`
26    /// stays `&self`.
27    model_id: String,
28}
29
30impl DefaultCtx {
31    /// Construct a `DefaultCtx` from a provider config.
32    pub fn new(provider: &ProviderConfig) -> Self {
33        Self {
34            ctx_window: provider.context_window.max(8000),
35            model_id: provider.model.to_lowercase(),
36        }
37    }
38}
39
40impl CtxBuilder for DefaultCtx {
41    fn build_messages(
42        &self,
43        conv: &Conversation,
44        system_prompt: &str,
45        turn_reminder: &str,
46    ) -> (Vec<Message>, ContextStats) {
47        let sys = crate::ctx::render::apply_model_directives(system_prompt, &self.model_id);
48        crate::ctx::render::build_messages(conv, &sys, self.ctx_window, turn_reminder)
49    }
50
51    fn needs_compression(&self, conv: &Conversation, system_tokens: usize) -> bool {
52        crate::ctx::render::needs_compression(conv, system_tokens, self.ctx_window)
53    }
54
55    fn compression_plan(&self, conv: &Conversation) -> Option<(String, usize)> {
56        let (content, n) = crate::ctx::render::build_compression_content(conv);
57        if content.is_empty() || n == 0 {
58            None
59        } else {
60            Some((content, n))
61        }
62    }
63
64    fn truncate_tool_output(&self, result: &mut ToolResult, tool_name: &str) {
65        crate::ctx::truncate::truncate_output(result, tool_name, self.ctx_window);
66    }
67
68    fn ctx_window(&self) -> usize {
69        self.ctx_window
70    }
71
72    fn name(&self) -> &'static str {
73        "default"
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::config::provider::ProviderConfig;
81
82    fn test_provider(ctx: usize) -> ProviderConfig {
83        ProviderConfig {
84            provider_type: "test".into(),
85            api_key: None,
86            model: "test-model".into(),
87            base_url: None,
88            system_prompt: None,
89            user_agent: None,
90            context_window: ctx,
91            max_tokens: None,
92            thinking_type: None,
93            thinking_keep: None,
94            reasoning_history: None,
95            thinking_enabled: None,
96            thinking_budget: None,
97            skip_tls_verify: false,
98            ephemeral: false,
99
100}
101    }
102
103    #[test]
104    fn name_is_default() {
105        let d = DefaultCtx::new(&test_provider(128_000));
106        assert_eq!(d.name(), "default");
107    }
108
109    #[test]
110    fn ctx_window_clamped_to_8k_minimum() {
111        let d = DefaultCtx::new(&test_provider(0));
112        assert_eq!(d.ctx_window, 8000);
113        let d = DefaultCtx::new(&test_provider(4_000));
114        assert_eq!(d.ctx_window, 8000);
115        let d = DefaultCtx::new(&test_provider(32_000));
116        assert_eq!(d.ctx_window, 32_000);
117    }
118
119    #[test]
120    fn build_messages_empty_conv_returns_system_only() {
121        let d = DefaultCtx::new(&test_provider(128_000));
122        let conv = Conversation::new();
123        let (msgs, _stats) = d.build_messages(&conv, "SYS", "");
124        assert_eq!(msgs.len(), 1);
125        assert!(matches!(
126            msgs[0].role,
127            crate::conversation::message::Role::System
128        ));
129    }
130
131    #[test]
132    fn compression_plan_none_below_threshold() {
133        let d = DefaultCtx::new(&test_provider(128_000));
134        let conv = Conversation::new();
135        assert!(d.compression_plan(&conv).is_none());
136    }
137}