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}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct SystemInstructionSection {
111 pub content: String,
113 pub title: String,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct CustomSystemInstructions {
120 pub text: String,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct AppendedSystemInstructions {
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub custom_identity: Option<String>,
130 #[serde(default)]
132 pub appended_sections: Vec<SystemInstructionSection>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(untagged)]
138pub enum SystemInstructions {
139 Custom(CustomSystemInstructions),
141 Appended(AppendedSystemInstructions),
143}
144
145#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
147pub enum BuiltinTools {
148 #[serde(rename = "CREATE_FILE")]
150 CreateFile,
151 #[serde(rename = "EDIT_FILE")]
153 EditFile,
154 #[serde(rename = "FIND_FILE")]
156 FindFile,
157 #[serde(rename = "LIST_DIR")]
159 ListDir,
160 #[serde(rename = "RUN_COMMAND")]
162 RunCommand,
163 #[serde(rename = "SEARCH_DIR")]
165 SearchDir,
166 #[serde(rename = "VIEW_FILE")]
168 ViewFile,
169 #[serde(rename = "START_SUBAGENT")]
171 StartSubagent,
172 #[serde(rename = "GENERATE_IMAGE")]
174 GenerateImage,
175 #[serde(rename = "FINISH")]
177 Finish,
178}
179
180impl BuiltinTools {
181 pub const fn as_str(&self) -> &'static str {
183 match self {
184 Self::CreateFile => "CREATE_FILE",
185 Self::EditFile => "EDIT_FILE",
186 Self::FindFile => "FIND_FILE",
187 Self::ListDir => "LIST_DIR",
188 Self::RunCommand => "RUN_COMMAND",
189 Self::SearchDir => "SEARCH_DIR",
190 Self::ViewFile => "VIEW_FILE",
191 Self::StartSubagent => "START_SUBAGENT",
192 Self::GenerateImage => "GENERATE_IMAGE",
193 Self::Finish => "FINISH",
194 }
195 }
196
197 pub fn read_only() -> Vec<Self> {
199 vec![
200 Self::FindFile,
201 Self::ListDir,
202 Self::ViewFile,
203 Self::SearchDir,
204 ]
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize, Default)]
210pub struct CapabilitiesConfig {
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub enabled_tools: Option<Vec<BuiltinTools>>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub disabled_tools: Option<Vec<BuiltinTools>>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub compaction_threshold: Option<u32>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub finish_tool_schema_json: Option<String>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub image_model: Option<String>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230#[serde(tag = "type")]
231pub enum McpServerConfig {
232 #[serde(rename = "stdio")]
234 Stdio {
235 command: String,
237 args: Vec<String>,
239 },
240 #[serde(rename = "sse")]
242 Sse {
243 url: String,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 headers: Option<HashMap<String, String>>,
248 },
249 #[serde(rename = "http")]
251 Http {
252 url: String,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 headers: Option<HashMap<String, String>>,
257 #[serde(default = "default_mcp_timeout")]
259 timeout: f64,
260 #[serde(default = "default_mcp_sse_timeout")]
262 sse_read_timeout: f64,
263 #[serde(default = "default_true")]
265 terminate_on_close: bool,
266 },
267}
268
269const fn default_mcp_timeout() -> f64 {
270 30.0
271}
272const fn default_mcp_sse_timeout() -> f64 {
273 300.0
274}
275const fn default_true() -> bool {
276 true
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct ToolCall {
282 pub id: String,
284 pub name: String,
286 pub args: Value,
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub canonical_path: Option<String>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct ToolResult {
296 pub name: String,
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub id: Option<String>,
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub result: Option<Value>,
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub error: Option<String>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize, Default)]
311pub struct UsageMetadata {
312 pub prompt_token_count: i32,
314 pub candidates_token_count: i32,
316 pub total_token_count: i32,
318 #[serde(default)]
320 pub cached_content_token_count: i32,
321 #[serde(default)]
323 pub thoughts_token_count: i32,
324}
325
326#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
328pub enum StepType {
329 #[serde(rename = "TEXT_RESPONSE")]
331 TextResponse,
332 #[serde(rename = "TOOL_CALL")]
334 ToolCall,
335 #[serde(rename = "SYSTEM_MESSAGE")]
337 SystemMessage,
338 #[serde(rename = "COMPACTION")]
340 Compaction,
341 #[serde(rename = "FINISH")]
343 Finish,
344 #[serde(rename = "UNKNOWN")]
346 Unknown,
347}
348
349#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
351pub enum StepSource {
352 #[serde(rename = "SYSTEM")]
354 System,
355 #[serde(rename = "USER")]
357 User,
358 #[serde(rename = "MODEL")]
360 Model,
361 #[serde(rename = "UNKNOWN")]
363 Unknown,
364}
365
366#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
368pub enum StepTarget {
369 #[serde(rename = "TARGET_USER")]
371 User,
372 #[serde(rename = "TARGET_ENVIRONMENT")]
374 Environment,
375 #[serde(rename = "TARGET_UNSPECIFIED")]
377 Unspecified,
378 #[serde(rename = "UNKNOWN")]
380 Unknown,
381}
382
383#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
385pub enum StepStatus {
386 #[serde(rename = "ACTIVE")]
388 Active,
389 #[serde(rename = "DONE")]
391 Done,
392 #[serde(rename = "WAITING_FOR_USER")]
394 WaitingForUser,
395 #[serde(rename = "ERROR")]
397 Error,
398 #[serde(rename = "CANCELED")]
400 Canceled,
401 #[serde(rename = "UNKNOWN")]
403 Unknown,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct Step {
409 pub id: String,
411 pub step_index: u32,
413 pub r#type: StepType,
415 pub source: StepSource,
417 pub target: StepTarget,
419 pub status: StepStatus,
421 pub content: String,
423 pub content_delta: String,
425 pub thinking: String,
427 pub thinking_delta: String,
429 pub tool_calls: Vec<ToolCall>,
431 pub error: String,
433 pub is_complete_response: Option<bool>,
435 pub structured_output: Option<Value>,
437 pub usage_metadata: Option<UsageMetadata>,
439 #[serde(default)]
441 pub cascade_id: String,
442 #[serde(default)]
444 pub trajectory_id: String,
445 #[serde(default)]
447 pub http_code: u32,
448}
449
450impl Default for Step {
451 fn default() -> Self {
452 Self {
453 id: String::new(),
454 step_index: 0,
455 r#type: StepType::Unknown,
456 source: StepSource::Unknown,
457 target: StepTarget::Unknown,
458 status: StepStatus::Unknown,
459 content: String::new(),
460 content_delta: String::new(),
461 thinking: String::new(),
462 thinking_delta: String::new(),
463 tool_calls: Vec::new(),
464 error: String::new(),
465 is_complete_response: None,
466 structured_output: None,
467 usage_metadata: None,
468 cascade_id: String::new(),
469 trajectory_id: String::new(),
470 http_code: 0,
471 }
472 }
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize, Default)]
477pub struct HookResult {
478 pub allow: bool,
480 #[serde(default)]
482 pub message: String,
483}
484
485#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct QuestionResponse {
488 pub selected_option_ids: Option<Vec<String>>,
490 #[serde(default)]
492 pub freeform_response: String,
493 #[serde(default)]
495 pub skipped: bool,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct QuestionHookResult {
501 pub responses: Vec<QuestionResponse>,
503 #[serde(default)]
505 pub cancelled: bool,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct AskQuestionOption {
511 pub id: String,
513 pub text: String,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct AskQuestionEntry {
520 pub question: String,
522 pub options: Vec<AskQuestionOption>,
524 #[serde(default)]
526 pub is_multi_select: bool,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct ChatResponse {
532 pub text: String,
534 pub thinking: String,
536 pub steps: Vec<Step>,
538 pub usage_metadata: UsageMetadata,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(tag = "chunk_type")]
545pub enum StreamChunk {
546 Thought {
548 step_index: u32,
550 text: String,
552 },
553 Text {
555 step_index: u32,
557 text: String,
559 },
560 ToolCall(ToolCall),
562}
563
564#[cfg(test)]
565mod tests {
566 #![allow(
567 clippy::unwrap_used,
568 clippy::expect_used,
569 clippy::panic,
570 clippy::field_reassign_with_default
571 )]
572 use super::*;
573 use serde_json::json;
574
575 #[test]
576 fn test_tool_call_construction() {
577 let tc = ToolCall {
578 id: "call_1".to_string(),
579 name: "read_file".to_string(),
580 args: json!({"path": "/tmp/foo"}),
581 canonical_path: None,
582 };
583 assert_eq!(tc.name, "read_file");
584 assert_eq!(tc.args["path"], "/tmp/foo");
585 assert_eq!(tc.id, "call_1");
586 assert_eq!(tc.canonical_path, None);
587 }
588
589 #[test]
590 fn test_tool_call_serialization() {
591 let json_data = r#"{"id":"call_1","name":"read_file","args":{"path":"/tmp/foo"}}"#;
592 let tc: ToolCall = serde_json::from_str(json_data).unwrap();
593 assert_eq!(tc.name, "read_file");
594 assert_eq!(tc.args["path"], "/tmp/foo");
595 assert_eq!(tc.id, "call_1");
596 assert_eq!(tc.canonical_path, None);
597 }
598
599 #[test]
600 fn test_tool_result_success() {
601 let tr = ToolResult {
602 name: "sum_tool".to_string(),
603 id: Some("call_1".to_string()),
604 result: Some(json!(42)),
605 error: None,
606 };
607 assert_eq!(tr.name, "sum_tool");
608 assert_eq!(tr.result.unwrap(), 42);
609 assert!(tr.error.is_none());
610 assert_eq!(tr.id.unwrap(), "call_1");
611 }
612
613 #[test]
614 fn test_tool_result_error() {
615 let tr = ToolResult {
616 name: "bad_tool".to_string(),
617 id: None,
618 result: None,
619 error: Some("kaboom".to_string()),
620 };
621 assert_eq!(tr.name, "bad_tool");
622 assert!(tr.result.is_none());
623 assert_eq!(tr.error.unwrap(), "kaboom");
624 assert!(tr.id.is_none());
625 }
626
627 #[test]
628 fn test_tool_result_mutability() {
629 let mut tr = ToolResult {
630 name: "tool".to_string(),
631 id: None,
632 result: None,
633 error: None,
634 };
635 tr.result = Some(json!("updated"));
636 assert_eq!(tr.result.unwrap(), "updated");
637 }
638
639 #[test]
640 fn test_step_defaults() {
641 let step = Step::default();
642 assert_eq!(step.id, "");
643 assert_eq!(step.step_index, 0);
644 assert!(matches!(step.r#type, StepType::Unknown));
645 assert!(matches!(step.status, StepStatus::Unknown));
646 assert!(matches!(step.source, StepSource::Unknown));
647 assert_eq!(step.content, "");
648 assert!(step.tool_calls.is_empty());
649 assert_eq!(step.error, "");
650 }
651
652 #[test]
653 fn test_step_mutability() {
654 let mut step = Step::default();
655 step.content = "goodbye".to_string();
656 assert_eq!(step.content, "goodbye");
657 }
658
659 #[test]
660 fn test_hook_result_defaults() {
661 let hr = HookResult::default();
662 assert!(!hr.allow); assert_eq!(hr.message, "");
664 }
665
666 #[test]
667 fn test_question_response_defaults() {
668 let qr = QuestionResponse {
669 selected_option_ids: None,
670 freeform_response: String::new(),
671 skipped: false,
672 };
673 assert!(qr.selected_option_ids.is_none());
674 assert_eq!(qr.freeform_response, "");
675 assert!(!qr.skipped);
676 }
677
678 #[test]
679 fn test_question_response_skipped() {
680 let qr = QuestionResponse {
681 selected_option_ids: None,
682 freeform_response: String::new(),
683 skipped: true,
684 };
685 assert!(qr.skipped);
686 }
687
688 #[test]
689 fn test_gemini_config_defaults() {
690 let config = GeminiConfig::default();
691 assert!(config.api_key.is_none());
692 assert_eq!(config.models.default.name, DEFAULT_MODEL);
693 assert!(config.models.default.generation.thinking_level.is_none());
694 }
695
696 #[test]
697 fn test_thinking_level_serialization() {
698 let level = ThinkingLevel::Low;
699 let json_str = serde_json::to_string(&level).unwrap();
700 assert_eq!(json_str, "\"low\"");
701 }
702
703 #[test]
704 fn test_capabilities_config_defaults() {
705 let config = CapabilitiesConfig::default();
706 assert!(config.enabled_tools.is_none());
707 assert!(config.disabled_tools.is_none());
708 assert!(config.compaction_threshold.is_none());
709 }
710}