1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10pub const DEFAULT_MODEL: &str = "gemini-3.5-flash";
12
13pub const DEFAULT_IMAGE_GENERATION_MODEL: &str = "gemini-3.1-flash-image-preview";
15
16#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "lowercase")]
19pub enum ThinkingLevel {
20 Minimal,
22 Low,
24 Medium,
26 High,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32pub struct GenerationConfig {
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub thinking_level: Option<ThinkingLevel>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ModelEntry {
41 pub name: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub api_key: Option<String>,
46 #[serde(default)]
48 pub generation: GenerationConfig,
49}
50
51impl Default for ModelEntry {
52 fn default() -> Self {
53 Self {
54 name: DEFAULT_MODEL.to_string(),
55 api_key: None,
56 generation: GenerationConfig::default(),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ModelConfig {
64 #[serde(default = "default_model_entry")]
66 pub default: ModelEntry,
67 #[serde(default = "default_image_generation_entry")]
69 pub image_generation: ModelEntry,
70}
71
72fn default_model_entry() -> ModelEntry {
73 ModelEntry {
74 name: DEFAULT_MODEL.to_string(),
75 api_key: None,
76 generation: GenerationConfig::default(),
77 }
78}
79
80fn default_image_generation_entry() -> ModelEntry {
81 ModelEntry {
82 name: DEFAULT_IMAGE_GENERATION_MODEL.to_string(),
83 api_key: None,
84 generation: GenerationConfig::default(),
85 }
86}
87
88impl Default for ModelConfig {
89 fn default() -> Self {
90 Self {
91 default: default_model_entry(),
92 image_generation: default_image_generation_entry(),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, Default)]
99pub struct GeminiConfig {
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub api_key: Option<String>,
103 #[serde(default)]
105 pub models: ModelConfig,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub enable_google_search: Option<bool>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub enable_url_context: Option<bool>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct SystemInstructionSection {
117 pub content: String,
119 pub title: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct CustomSystemInstructions {
126 pub text: String,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct AppendedSystemInstructions {
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub custom_identity: Option<String>,
136 #[serde(default)]
138 pub appended_sections: Vec<SystemInstructionSection>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(untagged)]
144pub enum SystemInstructions {
145 Custom(CustomSystemInstructions),
147 Appended(AppendedSystemInstructions),
149}
150
151#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
153pub enum BuiltinTools {
154 #[serde(rename = "CREATE_FILE")]
156 CreateFile,
157 #[serde(rename = "EDIT_FILE")]
159 EditFile,
160 #[serde(rename = "FIND_FILE")]
162 FindFile,
163 #[serde(rename = "LIST_DIR")]
165 ListDir,
166 #[serde(rename = "RUN_COMMAND")]
168 RunCommand,
169 #[serde(rename = "SEARCH_DIR")]
171 SearchDir,
172 #[serde(rename = "VIEW_FILE")]
174 ViewFile,
175 #[serde(rename = "START_SUBAGENT")]
177 StartSubagent,
178 #[serde(rename = "GENERATE_IMAGE")]
180 GenerateImage,
181 #[serde(rename = "FINISH")]
183 Finish,
184}
185
186impl BuiltinTools {
187 pub const fn as_str(&self) -> &'static str {
189 match self {
190 Self::CreateFile => "CREATE_FILE",
191 Self::EditFile => "EDIT_FILE",
192 Self::FindFile => "FIND_FILE",
193 Self::ListDir => "LIST_DIR",
194 Self::RunCommand => "RUN_COMMAND",
195 Self::SearchDir => "SEARCH_DIR",
196 Self::ViewFile => "VIEW_FILE",
197 Self::StartSubagent => "START_SUBAGENT",
198 Self::GenerateImage => "GENERATE_IMAGE",
199 Self::Finish => "FINISH",
200 }
201 }
202
203 pub fn read_only() -> Vec<Self> {
205 vec![
206 Self::FindFile,
207 Self::ListDir,
208 Self::ViewFile,
209 Self::SearchDir,
210 ]
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, Default)]
216pub struct CapabilitiesConfig {
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub enabled_tools: Option<Vec<BuiltinTools>>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub disabled_tools: Option<Vec<BuiltinTools>>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub compaction_threshold: Option<u32>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub finish_tool_schema_json: Option<String>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub image_model: Option<String>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236#[serde(tag = "type")]
237pub enum McpServerConfig {
238 #[serde(rename = "stdio")]
240 Stdio {
241 command: String,
243 args: Vec<String>,
245 },
246 #[serde(rename = "sse")]
248 Sse {
249 url: String,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 headers: Option<HashMap<String, String>>,
254 },
255 #[serde(rename = "http")]
257 Http {
258 url: String,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 headers: Option<HashMap<String, String>>,
263 #[serde(default = "default_mcp_timeout")]
265 timeout: f64,
266 #[serde(default = "default_mcp_sse_timeout")]
268 sse_read_timeout: f64,
269 #[serde(default = "default_true")]
271 terminate_on_close: bool,
272 },
273}
274
275const fn default_mcp_timeout() -> f64 {
276 30.0
277}
278const fn default_mcp_sse_timeout() -> f64 {
279 300.0
280}
281const fn default_true() -> bool {
282 true
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct ToolCall {
288 pub id: String,
290 pub name: String,
292 pub args: Value,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub canonical_path: Option<String>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ToolResult {
302 pub name: String,
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub id: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub result: Option<Value>,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub error: Option<String>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, Default)]
317pub struct UsageMetadata {
318 pub prompt_token_count: i32,
320 pub candidates_token_count: i32,
322 pub total_token_count: i32,
324 #[serde(default)]
326 pub cached_content_token_count: i32,
327 #[serde(default)]
329 pub thoughts_token_count: i32,
330}
331
332#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
334pub enum StepType {
335 #[serde(rename = "TEXT_RESPONSE")]
337 TextResponse,
338 #[serde(rename = "TOOL_CALL")]
340 ToolCall,
341 #[serde(rename = "SYSTEM_MESSAGE")]
343 SystemMessage,
344 #[serde(rename = "COMPACTION")]
346 Compaction,
347 #[serde(rename = "FINISH")]
349 Finish,
350 #[serde(rename = "UNKNOWN")]
352 Unknown,
353}
354
355#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
357pub enum StepSource {
358 #[serde(rename = "SYSTEM")]
360 System,
361 #[serde(rename = "USER")]
363 User,
364 #[serde(rename = "MODEL")]
366 Model,
367 #[serde(rename = "UNKNOWN")]
369 Unknown,
370}
371
372#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
374pub enum StepTarget {
375 #[serde(rename = "TARGET_USER")]
377 User,
378 #[serde(rename = "TARGET_ENVIRONMENT")]
380 Environment,
381 #[serde(rename = "TARGET_UNSPECIFIED")]
383 Unspecified,
384 #[serde(rename = "UNKNOWN")]
386 Unknown,
387}
388
389#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
391pub enum StepStatus {
392 #[serde(rename = "ACTIVE")]
394 Active,
395 #[serde(rename = "DONE")]
397 Done,
398 #[serde(rename = "WAITING_FOR_USER")]
400 WaitingForUser,
401 #[serde(rename = "ERROR")]
403 Error,
404 #[serde(rename = "CANCELED")]
406 Canceled,
407 #[serde(rename = "UNKNOWN")]
409 Unknown,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct Step {
415 pub id: String,
417 pub step_index: u32,
419 pub r#type: StepType,
421 pub source: StepSource,
423 pub target: StepTarget,
425 pub status: StepStatus,
427 pub content: String,
429 pub content_delta: String,
431 pub thinking: String,
433 pub thinking_delta: String,
435 pub tool_calls: Vec<ToolCall>,
437 pub error: String,
439 pub is_complete_response: Option<bool>,
441 pub structured_output: Option<Value>,
443 pub usage_metadata: Option<UsageMetadata>,
445 #[serde(default)]
447 pub cascade_id: String,
448 #[serde(default)]
450 pub trajectory_id: String,
451 #[serde(default)]
453 pub http_code: u32,
454}
455
456impl Default for Step {
457 fn default() -> Self {
458 Self {
459 id: String::new(),
460 step_index: 0,
461 r#type: StepType::Unknown,
462 source: StepSource::Unknown,
463 target: StepTarget::Unknown,
464 status: StepStatus::Unknown,
465 content: String::new(),
466 content_delta: String::new(),
467 thinking: String::new(),
468 thinking_delta: String::new(),
469 tool_calls: Vec::new(),
470 error: String::new(),
471 is_complete_response: None,
472 structured_output: None,
473 usage_metadata: None,
474 cascade_id: String::new(),
475 trajectory_id: String::new(),
476 http_code: 0,
477 }
478 }
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize, Default)]
483pub struct HookResult {
484 pub allow: bool,
486 #[serde(default)]
488 pub message: String,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct QuestionResponse {
494 pub selected_option_ids: Option<Vec<String>>,
496 #[serde(default)]
498 pub freeform_response: String,
499 #[serde(default)]
501 pub skipped: bool,
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct QuestionHookResult {
507 pub responses: Vec<QuestionResponse>,
509 #[serde(default)]
511 pub cancelled: bool,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct AskQuestionOption {
517 pub id: String,
519 pub text: String,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct AskQuestionEntry {
526 pub question: String,
528 pub options: Vec<AskQuestionOption>,
530 #[serde(default)]
532 pub is_multi_select: bool,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct ChatResponse {
538 pub text: String,
540 pub thinking: String,
542 pub steps: Vec<Step>,
544 pub usage_metadata: UsageMetadata,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
550#[serde(tag = "chunk_type")]
551pub enum StreamChunk {
552 Thought {
554 step_index: u32,
556 text: String,
558 },
559 Text {
561 step_index: u32,
563 text: String,
565 },
566 ToolCall(ToolCall),
568}
569
570#[cfg(test)]
571mod tests {
572 #![allow(
573 clippy::unwrap_used,
574 clippy::expect_used,
575 clippy::panic,
576 clippy::field_reassign_with_default
577 )]
578 use super::*;
579 use serde_json::json;
580
581 #[test]
582 fn test_tool_call_construction() {
583 let tc = ToolCall {
584 id: "call_1".to_string(),
585 name: "read_file".to_string(),
586 args: json!({"path": "/tmp/foo"}),
587 canonical_path: None,
588 };
589 assert_eq!(tc.name, "read_file");
590 assert_eq!(tc.args["path"], "/tmp/foo");
591 assert_eq!(tc.id, "call_1");
592 assert_eq!(tc.canonical_path, None);
593 }
594
595 #[test]
596 fn test_tool_call_serialization() {
597 let json_data = r#"{"id":"call_1","name":"read_file","args":{"path":"/tmp/foo"}}"#;
598 let tc: ToolCall = serde_json::from_str(json_data).unwrap();
599 assert_eq!(tc.name, "read_file");
600 assert_eq!(tc.args["path"], "/tmp/foo");
601 assert_eq!(tc.id, "call_1");
602 assert_eq!(tc.canonical_path, None);
603 }
604
605 #[test]
606 fn test_tool_result_success() {
607 let tr = ToolResult {
608 name: "sum_tool".to_string(),
609 id: Some("call_1".to_string()),
610 result: Some(json!(42)),
611 error: None,
612 };
613 assert_eq!(tr.name, "sum_tool");
614 assert_eq!(tr.result.unwrap(), 42);
615 assert!(tr.error.is_none());
616 assert_eq!(tr.id.unwrap(), "call_1");
617 }
618
619 #[test]
620 fn test_tool_result_error() {
621 let tr = ToolResult {
622 name: "bad_tool".to_string(),
623 id: None,
624 result: None,
625 error: Some("kaboom".to_string()),
626 };
627 assert_eq!(tr.name, "bad_tool");
628 assert!(tr.result.is_none());
629 assert_eq!(tr.error.unwrap(), "kaboom");
630 assert!(tr.id.is_none());
631 }
632
633 #[test]
634 fn test_tool_result_mutability() {
635 let mut tr = ToolResult {
636 name: "tool".to_string(),
637 id: None,
638 result: None,
639 error: None,
640 };
641 tr.result = Some(json!("updated"));
642 assert_eq!(tr.result.unwrap(), "updated");
643 }
644
645 #[test]
646 fn test_step_defaults() {
647 let step = Step::default();
648 assert_eq!(step.id, "");
649 assert_eq!(step.step_index, 0);
650 assert!(matches!(step.r#type, StepType::Unknown));
651 assert!(matches!(step.status, StepStatus::Unknown));
652 assert!(matches!(step.source, StepSource::Unknown));
653 assert_eq!(step.content, "");
654 assert!(step.tool_calls.is_empty());
655 assert_eq!(step.error, "");
656 }
657
658 #[test]
659 fn test_step_mutability() {
660 let mut step = Step::default();
661 step.content = "goodbye".to_string();
662 assert_eq!(step.content, "goodbye");
663 }
664
665 #[test]
666 fn test_hook_result_defaults() {
667 let hr = HookResult::default();
668 assert!(!hr.allow); assert_eq!(hr.message, "");
670 }
671
672 #[test]
673 fn test_question_response_defaults() {
674 let qr = QuestionResponse {
675 selected_option_ids: None,
676 freeform_response: String::new(),
677 skipped: false,
678 };
679 assert!(qr.selected_option_ids.is_none());
680 assert_eq!(qr.freeform_response, "");
681 assert!(!qr.skipped);
682 }
683
684 #[test]
685 fn test_question_response_skipped() {
686 let qr = QuestionResponse {
687 selected_option_ids: None,
688 freeform_response: String::new(),
689 skipped: true,
690 };
691 assert!(qr.skipped);
692 }
693
694 #[test]
695 fn test_gemini_config_defaults() {
696 let config = GeminiConfig::default();
697 assert!(config.api_key.is_none());
698 assert_eq!(config.models.default.name, DEFAULT_MODEL);
699 assert!(config.models.default.generation.thinking_level.is_none());
700 assert!(config.enable_google_search.is_none());
701 assert!(config.enable_url_context.is_none());
702 }
703
704 #[test]
705 fn test_thinking_level_serialization() {
706 let level = ThinkingLevel::Low;
707 let json_str = serde_json::to_string(&level).unwrap();
708 assert_eq!(json_str, "\"low\"");
709 }
710
711 #[test]
712 fn test_capabilities_config_defaults() {
713 let config = CapabilitiesConfig::default();
714 assert!(config.enabled_tools.is_none());
715 assert!(config.disabled_tools.is_none());
716 assert!(config.compaction_threshold.is_none());
717 }
718}