atomcode_core/ctx/
default.rs1use super::CtxBuilder;
9use crate::config::provider::ProviderConfig;
10use crate::conversation::message::Message;
11use crate::conversation::{ContextStats, Conversation};
12use crate::tool::ToolResult;
13
14#[derive(Debug, Clone)]
16pub struct DefaultCtx {
17 pub ctx_window: usize,
21
22 model_id: String,
28}
29
30impl DefaultCtx {
31 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}