claude_code_acp/types/
meta.rs

1//! Meta field parsing for ACP requests
2//!
3//! ACP protocol's `new_session` and `load_session` requests support a `_meta` field
4//! for passing additional configuration.
5
6use serde::{Deserialize, Serialize};
7
8/// System prompt configuration from meta field
9///
10/// Allows clients to customize the system prompt via the `_meta.systemPrompt` field.
11///
12/// # JSON Structure
13///
14/// ```json
15/// {
16///   "_meta": {
17///     "systemPrompt": {
18///       "append": "Additional instructions..."
19///     }
20///   }
21/// }
22/// ```
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct SystemPromptMeta {
25    /// Text to append to the system prompt
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub append: Option<String>,
28
29    /// Text to replace the entire system prompt (higher priority than append)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub replace: Option<String>,
32}
33
34impl SystemPromptMeta {
35    /// Parse from a JSON value (the `_meta` object)
36    pub fn from_meta(meta: &serde_json::Value) -> Option<Self> {
37        meta.get("systemPrompt")
38            .and_then(|v| serde_json::from_value(v.clone()).ok())
39    }
40
41    /// Check if any system prompt modification is configured
42    pub fn is_configured(&self) -> bool {
43        self.append.is_some() || self.replace.is_some()
44    }
45}
46
47/// Claude Code specific options from meta field
48///
49/// # JSON Structure
50///
51/// ```json
52/// {
53///   "_meta": {
54///     "claudeCode": {
55///       "options": {
56///         "resume": "session-uuid-12345",
57///         "maxThinkingTokens": 4096
58///       }
59///     }
60///   }
61/// }
62/// ```
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64pub struct ClaudeCodeOptions {
65    /// Session ID to resume
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub resume: Option<String>,
68
69    /// Maximum tokens for thinking blocks (extended thinking mode)
70    /// Typical values: 4096, 8000, 16000
71    #[serde(skip_serializing_if = "Option::is_none", rename = "maxThinkingTokens")]
72    pub max_thinking_tokens: Option<u32>,
73}
74
75/// Claude Code meta configuration
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77pub struct ClaudeCodeMeta {
78    /// Claude Code options
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub options: Option<ClaudeCodeOptions>,
81}
82
83impl ClaudeCodeMeta {
84    /// Parse from a JSON value (the `_meta` object)
85    pub fn from_meta(meta: &serde_json::Value) -> Option<Self> {
86        meta.get("claudeCode")
87            .and_then(|v| serde_json::from_value(v.clone()).ok())
88    }
89
90    /// Get the session ID to resume, if any
91    pub fn get_resume_session_id(&self) -> Option<&str> {
92        self.options.as_ref()?.resume.as_deref()
93    }
94
95    /// Get the max thinking tokens setting, if any
96    pub fn get_max_thinking_tokens(&self) -> Option<u32> {
97        self.options.as_ref()?.max_thinking_tokens
98    }
99}
100
101/// Combined meta configuration for new session requests
102///
103/// Parses all supported meta fields from ACP request's `_meta` field.
104#[derive(Debug, Clone, Default)]
105pub struct NewSessionMeta {
106    /// System prompt configuration
107    pub system_prompt: Option<SystemPromptMeta>,
108
109    /// Claude Code specific configuration
110    pub claude_code: Option<ClaudeCodeMeta>,
111
112    /// Whether to disable built-in tools
113    pub disable_built_in_tools: bool,
114}
115
116impl NewSessionMeta {
117    /// Create a NewSessionMeta with just the resume option
118    ///
119    /// This is useful for `loadSession` where we want to resume
120    /// from a specific session ID.
121    pub fn with_resume(session_id: &str) -> Self {
122        Self {
123            system_prompt: None,
124            claude_code: Some(ClaudeCodeMeta {
125                options: Some(ClaudeCodeOptions {
126                    resume: Some(session_id.to_string()),
127                    max_thinking_tokens: None,
128                }),
129            }),
130            disable_built_in_tools: false,
131        }
132    }
133
134    /// Parse from ACP request's `_meta` field
135    ///
136    /// # Arguments
137    ///
138    /// * `meta` - The `_meta` field from the ACP request (optional)
139    ///
140    /// # Returns
141    ///
142    /// A `NewSessionMeta` with all parsed fields, or defaults if meta is None
143    pub fn from_request_meta(meta: Option<&serde_json::Value>) -> Self {
144        let Some(meta) = meta else {
145            return Self::default();
146        };
147
148        Self {
149            system_prompt: SystemPromptMeta::from_meta(meta),
150            claude_code: ClaudeCodeMeta::from_meta(meta),
151            disable_built_in_tools: meta
152                .get("disableBuiltInTools")
153                .and_then(|v| v.as_bool())
154                .unwrap_or(false),
155        }
156    }
157
158    /// Get the text to append to the system prompt, if any
159    pub fn get_system_prompt_append(&self) -> Option<&str> {
160        self.system_prompt.as_ref()?.append.as_deref()
161    }
162
163    /// Get the text to replace the system prompt, if any
164    pub fn get_system_prompt_replace(&self) -> Option<&str> {
165        self.system_prompt.as_ref()?.replace.as_deref()
166    }
167
168    /// Get the session ID to resume, if any
169    pub fn get_resume_session_id(&self) -> Option<&str> {
170        self.claude_code.as_ref()?.get_resume_session_id()
171    }
172
173    /// Get the max thinking tokens setting, if any
174    pub fn get_max_thinking_tokens(&self) -> Option<u32> {
175        self.claude_code.as_ref()?.get_max_thinking_tokens()
176    }
177
178    /// Check if this session should resume from a previous session
179    pub fn should_resume(&self) -> bool {
180        self.get_resume_session_id().is_some()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use serde_json::json;
188
189    #[test]
190    fn test_system_prompt_meta_parse() {
191        let meta = json!({
192            "systemPrompt": {
193                "append": "Please respond in Chinese"
194            }
195        });
196
197        let parsed = SystemPromptMeta::from_meta(&meta).unwrap();
198        assert_eq!(parsed.append, Some("Please respond in Chinese".to_string()));
199        assert!(parsed.replace.is_none());
200        assert!(parsed.is_configured());
201    }
202
203    #[test]
204    fn test_claude_code_meta_parse() {
205        let meta = json!({
206            "claudeCode": {
207                "options": {
208                    "resume": "session-uuid-12345"
209                }
210            }
211        });
212
213        let parsed = ClaudeCodeMeta::from_meta(&meta).unwrap();
214        assert_eq!(parsed.get_resume_session_id(), Some("session-uuid-12345"));
215    }
216
217    #[test]
218    fn test_claude_code_meta_with_thinking_tokens() {
219        let meta = json!({
220            "claudeCode": {
221                "options": {
222                    "resume": "session-uuid-12345",
223                    "maxThinkingTokens": 4096
224                }
225            }
226        });
227
228        let parsed = ClaudeCodeMeta::from_meta(&meta).unwrap();
229        assert_eq!(parsed.get_resume_session_id(), Some("session-uuid-12345"));
230        assert_eq!(parsed.get_max_thinking_tokens(), Some(4096));
231    }
232
233    #[test]
234    fn test_new_session_meta_full() {
235        let meta = json!({
236            "systemPrompt": {
237                "append": "Be concise"
238            },
239            "claudeCode": {
240                "options": {
241                    "resume": "abc-123",
242                    "maxThinkingTokens": 8000
243                }
244            },
245            "disableBuiltInTools": true
246        });
247
248        let parsed = NewSessionMeta::from_request_meta(Some(&meta));
249        assert_eq!(parsed.get_system_prompt_append(), Some("Be concise"));
250        assert_eq!(parsed.get_resume_session_id(), Some("abc-123"));
251        assert_eq!(parsed.get_max_thinking_tokens(), Some(8000));
252        assert!(parsed.disable_built_in_tools);
253        assert!(parsed.should_resume());
254    }
255
256    #[test]
257    fn test_new_session_meta_empty() {
258        let parsed = NewSessionMeta::from_request_meta(None);
259        assert!(parsed.system_prompt.is_none());
260        assert!(parsed.claude_code.is_none());
261        assert!(!parsed.disable_built_in_tools);
262        assert!(!parsed.should_resume());
263    }
264
265    #[test]
266    fn test_new_session_meta_partial() {
267        let meta = json!({
268            "systemPrompt": {
269                "replace": "You are a helpful assistant"
270            }
271        });
272
273        let parsed = NewSessionMeta::from_request_meta(Some(&meta));
274        assert_eq!(
275            parsed.get_system_prompt_replace(),
276            Some("You are a helpful assistant")
277        );
278        assert!(parsed.get_system_prompt_append().is_none());
279        assert!(parsed.get_resume_session_id().is_none());
280    }
281
282    #[test]
283    fn test_new_session_meta_with_resume() {
284        let meta = NewSessionMeta::with_resume("session-abc-123");
285        assert_eq!(meta.get_resume_session_id(), Some("session-abc-123"));
286        assert!(meta.should_resume());
287        assert!(meta.system_prompt.is_none());
288        assert!(!meta.disable_built_in_tools);
289    }
290}