reasonkit/thinktool/
registry.rs

1//! Protocol Registry
2//!
3//! Manages loading, storing, and retrieving ThinkTool protocols.
4
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use super::protocol::Protocol;
9use super::toml_loader;
10use super::yaml_loader;
11use crate::error::{Error, Result};
12
13/// Registry of available ThinkTool protocols
14#[derive(Debug, Default)]
15pub struct ProtocolRegistry {
16    /// Loaded protocols by ID
17    protocols: HashMap<String, Protocol>,
18
19    /// Protocol search paths
20    search_paths: Vec<PathBuf>,
21}
22
23impl ProtocolRegistry {
24    /// Create a new empty registry
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Create registry with default search paths
30    pub fn with_defaults() -> Self {
31        let mut registry = Self::new();
32
33        // Add default search paths
34        if let Ok(cwd) = std::env::current_dir() {
35            registry.add_search_path(cwd.join("protocols"));
36        }
37
38        // User config directory
39        if let Some(config_dir) = dirs_config_path() {
40            registry.add_search_path(config_dir.join("reasonkit").join("protocols"));
41        }
42
43        registry
44    }
45
46    /// Add a search path for protocol files
47    pub fn add_search_path(&mut self, path: impl Into<PathBuf>) {
48        let path = path.into();
49        if !self.search_paths.contains(&path) {
50            self.search_paths.push(path);
51        }
52    }
53
54    /// Load all protocols from search paths
55    pub fn load_all(&mut self) -> Result<usize> {
56        let mut count = 0;
57
58        for path in &self.search_paths.clone() {
59            if path.exists() && path.is_dir() {
60                count += self.load_from_directory(path)?;
61            }
62        }
63
64        Ok(count)
65    }
66
67    /// Load protocols from a specific directory
68    pub fn load_from_directory(&mut self, dir: &Path) -> Result<usize> {
69        let mut count = 0;
70
71        let entries = std::fs::read_dir(dir).map_err(|e| Error::IoMessage {
72            message: format!("Failed to read directory {}: {}", dir.display(), e),
73        })?;
74
75        for entry in entries.flatten() {
76            let path = entry.path();
77
78            if path.is_file() {
79                let ext = path.extension().and_then(|e| e.to_str());
80
81                match ext {
82                    Some("json") => {
83                        if let Ok(protocol) = self.load_json_file(&path) {
84                            self.register(protocol)?;
85                            count += 1;
86                        }
87                    }
88                    Some("yaml") | Some("yml") => {
89                        // Load YAML protocols using yaml_loader
90                        match yaml_loader::load_from_yaml_file(&path) {
91                            Ok(protocols) => {
92                                for protocol in protocols {
93                                    self.register(protocol)?;
94                                    count += 1;
95                                }
96                                tracing::info!(
97                                    "Loaded {} protocols from YAML: {}",
98                                    count,
99                                    path.display()
100                                );
101                            }
102                            Err(e) => {
103                                tracing::warn!(
104                                    "Failed to load YAML protocol {}: {}",
105                                    path.display(),
106                                    e
107                                );
108                            }
109                        }
110                    }
111                    Some("toml") => {
112                        // Load TOML protocols using toml_loader
113                        match toml_loader::load_from_toml_file(&path) {
114                            Ok(protocols) => {
115                                for protocol in protocols {
116                                    self.register(protocol)?;
117                                    count += 1;
118                                }
119                                tracing::info!(
120                                    "Loaded {} protocols from TOML: {}",
121                                    count,
122                                    path.display()
123                                );
124                            }
125                            Err(e) => {
126                                tracing::warn!(
127                                    "Failed to load TOML protocol {}: {}",
128                                    path.display(),
129                                    e
130                                );
131                            }
132                        }
133                    }
134                    _ => {}
135                }
136            }
137        }
138
139        Ok(count)
140    }
141
142    /// Load a protocol from a JSON file
143    fn load_json_file(&self, path: &Path) -> Result<Protocol> {
144        let content = std::fs::read_to_string(path).map_err(|e| Error::IoMessage {
145            message: format!("Failed to read {}: {}", path.display(), e),
146        })?;
147
148        let protocol: Protocol = serde_json::from_str(&content).map_err(|e| Error::Parse {
149            message: format!("Failed to parse protocol {}: {}", path.display(), e),
150        })?;
151
152        // Validate the protocol
153        protocol.validate().map_err(|errors| {
154            Error::Validation(format!(
155                "Invalid protocol {}: {}",
156                protocol.id,
157                errors.join(", ")
158            ))
159        })?;
160
161        Ok(protocol)
162    }
163
164    /// Load protocols from the standard thinktools.yaml file
165    pub fn load_from_yaml(&mut self, path: &Path) -> Result<usize> {
166        let protocols = yaml_loader::load_from_yaml_file(path)?;
167        let count = protocols.len();
168
169        for protocol in protocols {
170            self.register(protocol)?;
171        }
172
173        Ok(count)
174    }
175
176    /// Register a protocol (by value)
177    pub fn register(&mut self, protocol: Protocol) -> Result<()> {
178        // Validate before registering
179        protocol.validate().map_err(|errors| {
180            Error::Validation(format!(
181                "Invalid protocol {}: {}",
182                protocol.id,
183                errors.join(", ")
184            ))
185        })?;
186
187        let id = protocol.id.clone();
188        self.protocols.insert(id, protocol);
189        Ok(())
190    }
191
192    /// Get a protocol by ID
193    pub fn get(&self, id: &str) -> Option<&Protocol> {
194        self.protocols.get(id)
195    }
196
197    /// Check if a protocol exists
198    pub fn contains(&self, id: &str) -> bool {
199        self.protocols.contains_key(id)
200    }
201
202    /// List all protocol IDs
203    pub fn list_ids(&self) -> Vec<&str> {
204        self.protocols.keys().map(|s| s.as_str()).collect()
205    }
206
207    /// List all protocols
208    pub fn list(&self) -> Vec<&Protocol> {
209        self.protocols.values().collect()
210    }
211
212    /// Get protocol count
213    pub fn len(&self) -> usize {
214        self.protocols.len()
215    }
216
217    /// Check if registry is empty
218    pub fn is_empty(&self) -> bool {
219        self.protocols.is_empty()
220    }
221
222    /// Remove a protocol by ID
223    pub fn remove(&mut self, id: &str) -> Option<Protocol> {
224        self.protocols.remove(id)
225    }
226
227    /// Clear all protocols
228    pub fn clear(&mut self) {
229        self.protocols.clear();
230    }
231
232    /// Register built-in protocols (hardcoded fallback)
233    pub fn register_builtins(&mut self) -> Result<()> {
234        // Try to load from YAML first
235        let mut loaded_from_yaml = false;
236        if let Ok(cwd) = std::env::current_dir() {
237            // Check protocols/thinktools.yaml
238            let yaml_path = cwd.join("protocols").join("thinktools.yaml");
239
240            if yaml_path.exists() {
241                match self.load_from_yaml(&yaml_path) {
242                    Ok(count) => {
243                        tracing::info!("Loaded {} protocols from thinktools.yaml", count);
244                        loaded_from_yaml = true;
245                    }
246                    Err(e) => {
247                        tracing::warn!("Failed to load thinktools.yaml: {}, falling back to hardcoded protocols", e);
248                    }
249                }
250            }
251        }
252
253        // Only use hardcoded fallbacks if we failed to load from YAML
254        if !loaded_from_yaml {
255            tracing::info!("Using hardcoded fallback protocols");
256            self.register(builtin_gigathink())?;
257            self.register(builtin_laserlogic())?;
258            self.register(builtin_bedrock())?;
259            self.register(builtin_proofguard())?;
260            self.register(builtin_brutalhonesty())?;
261        }
262
263        Ok(())
264    }
265}
266
267/// Get config directory path
268fn dirs_config_path() -> Option<PathBuf> {
269    dirs::config_dir()
270}
271
272// ═══════════════════════════════════════════════════════════════════════════
273// BUILT-IN PROTOCOLS (FALLBACK)
274// ═══════════════════════════════════════════════════════════════════════════
275
276use super::protocol::{
277    AggregationType, CritiqueSeverity, InputSpec, OutputSpec, ProtocolMetadata, ProtocolStep,
278    ReasoningStrategy, StepAction, StepOutputFormat,
279};
280
281fn builtin_gigathink() -> Protocol {
282    Protocol {
283        id: "gigathink".to_string(),
284        name: "GigaThink".to_string(),
285        version: "1.0.0".to_string(),
286        description: "Expansive creative thinking - generate 10+ diverse perspectives".to_string(),
287        strategy: ReasoningStrategy::Expansive,
288        input: InputSpec {
289            required: vec!["query".to_string()],
290            optional: vec!["context".to_string(), "constraints".to_string()],
291        },
292        steps: vec![
293            ProtocolStep {
294                id: "identify_dimensions".to_string(),
295                action: StepAction::Generate {
296                    min_count: 5,
297                    max_count: 10,
298                },
299                prompt_template:
300                    r#"Identify 5-10 distinct dimensions or angles to analyze this question:
301
302Question: {{query}}
303{{#if context}}Context: {{context}}{{/if}}
304{{#if constraints}}Constraints: {{constraints}}{{/if}}
305
306For each dimension, provide a brief label. Format as a numbered list."#
307                        .to_string(),
308                output_format: StepOutputFormat::List,
309                min_confidence: 0.7,
310                depends_on: vec![],
311                branch: None,
312            },
313            ProtocolStep {
314                id: "explore_perspectives".to_string(),
315                action: StepAction::Analyze {
316                    criteria: vec![
317                        "novelty".to_string(),
318                        "relevance".to_string(),
319                        "depth".to_string(),
320                    ],
321                },
322                prompt_template: r#"For each dimension identified, provide:
3231. Key insight from this perspective
3242. Supporting evidence or example
3253. Implications or consequences
3264. Confidence score (0.0-1.0)
327
328Dimensions to explore:
329{{identify_dimensions}}
330
331Question: {{query}}"#
332                    .to_string(),
333                output_format: StepOutputFormat::Structured,
334                min_confidence: 0.6,
335                depends_on: vec!["identify_dimensions".to_string()],
336                branch: None,
337            },
338            ProtocolStep {
339                id: "synthesize".to_string(),
340                action: StepAction::Synthesize {
341                    aggregation: AggregationType::ThematicClustering,
342                },
343                prompt_template:
344                    r#"Synthesize the perspectives into key themes and actionable insights:
345
346Perspectives:
347{{explore_perspectives}}
348
349Provide:
3501. Major themes (2-4)
3512. Key insights (3-5)
3523. Recommended actions (if applicable)
3534. Areas of uncertainty"#
354                        .to_string(),
355                output_format: StepOutputFormat::Structured,
356                min_confidence: 0.8,
357                depends_on: vec!["explore_perspectives".to_string()],
358                branch: None,
359            },
360        ],
361        output: OutputSpec {
362            format: "GigaThinkResult".to_string(),
363            fields: vec![
364                "dimensions".to_string(),
365                "perspectives".to_string(),
366                "themes".to_string(),
367                "insights".to_string(),
368                "confidence".to_string(),
369            ],
370        },
371        validation: vec![],
372        metadata: ProtocolMetadata {
373            category: "creative".to_string(),
374            composable_with: vec!["laserlogic".to_string(), "brutalhonesty".to_string()],
375            typical_tokens: 2500,
376            estimated_latency_ms: 5000,
377            ..Default::default()
378        },
379    }
380}
381
382fn builtin_laserlogic() -> Protocol {
383    Protocol {
384        id: "laserlogic".to_string(),
385        name: "LaserLogic".to_string(),
386        version: "1.0.0".to_string(),
387        description: "Precision deductive reasoning with fallacy detection".to_string(),
388        strategy: ReasoningStrategy::Deductive,
389        input: InputSpec {
390            required: vec!["argument".to_string()],
391            optional: vec!["context".to_string()],
392        },
393        steps: vec![
394            ProtocolStep {
395                id: "extract_claims".to_string(),
396                action: StepAction::Analyze {
397                    criteria: vec!["clarity".to_string(), "completeness".to_string()],
398                },
399                prompt_template: r#"Extract the logical structure from this argument:
400
401Argument: {{argument}}
402
403Identify:
4041. Main conclusion
4052. Supporting premises
4063. Implicit assumptions
4074. Causal claims (if any)
408
409Format each as a clear statement."#
410                    .to_string(),
411                output_format: StepOutputFormat::Structured,
412                min_confidence: 0.7,
413                depends_on: vec![],
414                branch: None,
415            },
416            ProtocolStep {
417                id: "check_validity".to_string(),
418                action: StepAction::Validate {
419                    rules: vec![
420                        "logical_consistency".to_string(),
421                        "premise_support".to_string(),
422                    ],
423                },
424                prompt_template: r#"Evaluate the logical validity of this argument analysis:
425
426{{extract_claims}}
427
428Based on the claims identified above, check:
4291. Do the premises logically lead to the conclusion?
4302. Are there gaps in reasoning?
4313. Is the argument valid (structure) vs sound (true premises)?
4324. Rate logical strength (0.0-1.0)"#
433                    .to_string(),
434                output_format: StepOutputFormat::Structured,
435                min_confidence: 0.8,
436                depends_on: vec!["extract_claims".to_string()],
437                branch: None,
438            },
439            ProtocolStep {
440                id: "detect_fallacies".to_string(),
441                action: StepAction::Critique {
442                    severity: CritiqueSeverity::Standard,
443                },
444                prompt_template: r#"Check for logical fallacies in the argument:
445
446Argument structure:
447{{extract_claims}}
448
449Common fallacies to check:
450- Ad hominem, Straw man, False dichotomy
451- Appeal to authority, Circular reasoning
452- Hasty generalization, Post hoc
453- Slippery slope, Red herring
454
455For each fallacy found, explain where and why."#
456                    .to_string(),
457                output_format: StepOutputFormat::List,
458                min_confidence: 0.7,
459                depends_on: vec!["extract_claims".to_string()],
460                branch: None,
461            },
462        ],
463        output: OutputSpec {
464            format: "LaserLogicResult".to_string(),
465            fields: vec![
466                "conclusion".to_string(),
467                "premises".to_string(),
468                "validity".to_string(),
469                "fallacies".to_string(),
470                "confidence".to_string(),
471            ],
472        },
473        validation: vec![],
474        metadata: ProtocolMetadata {
475            category: "analytical".to_string(),
476            composable_with: vec!["gigathink".to_string(), "bedrock".to_string()],
477            typical_tokens: 1800,
478            estimated_latency_ms: 4000,
479            ..Default::default()
480        },
481    }
482}
483
484fn builtin_bedrock() -> Protocol {
485    Protocol {
486        id: "bedrock".to_string(),
487        name: "BedRock".to_string(),
488        version: "1.0.0".to_string(),
489        description: "First principles decomposition - reduce to fundamental axioms".to_string(),
490        strategy: ReasoningStrategy::Analytical,
491        input: InputSpec {
492            required: vec!["statement".to_string()],
493            optional: vec!["domain".to_string()],
494        },
495        steps: vec![
496            ProtocolStep {
497                id: "decompose".to_string(),
498                action: StepAction::Analyze {
499                    criteria: vec!["fundamentality".to_string(), "independence".to_string()],
500                },
501                prompt_template: r#"Decompose this statement to first principles:
502
503Statement: {{statement}}
504{{#if domain}}Domain: {{domain}}{{/if}}
505
506Ask repeatedly: "What is this based on? Why is this true?"
507Continue until reaching fundamental axioms or assumptions.
508
509Format as a tree structure showing dependencies."#
510                    .to_string(),
511                output_format: StepOutputFormat::Structured,
512                min_confidence: 0.7,
513                depends_on: vec![],
514                branch: None,
515            },
516            ProtocolStep {
517                id: "identify_axioms".to_string(),
518                action: StepAction::Generate {
519                    min_count: 3,
520                    max_count: 7,
521                },
522                prompt_template: r#"From the decomposition, identify the foundational axioms:
523
524Decomposition:
525{{decompose}}
526
527For each axiom:
5281. State clearly
5292. Explain why it's fundamental (cannot be further reduced)
5303. Note if it's empirical, logical, or definitional
5314. Rate certainty (0.0-1.0)"#
532                    .to_string(),
533                output_format: StepOutputFormat::List,
534                min_confidence: 0.8,
535                depends_on: vec!["decompose".to_string()],
536                branch: None,
537            },
538            ProtocolStep {
539                id: "reconstruct".to_string(),
540                action: StepAction::Synthesize {
541                    aggregation: AggregationType::WeightedMerge,
542                },
543                prompt_template: r#"Reconstruct the original statement from axioms:
544
545Axioms:
546{{identify_axioms}}
547
548Original statement: {{statement}}
549
550Show the logical path from axioms to statement.
551Identify any gaps or leaps in reasoning.
552Calculate overall confidence based on axiom certainties."#
553                    .to_string(),
554                output_format: StepOutputFormat::Structured,
555                min_confidence: 0.75,
556                depends_on: vec!["identify_axioms".to_string()],
557                branch: None,
558            },
559        ],
560        output: OutputSpec {
561            format: "BedRockResult".to_string(),
562            fields: vec![
563                "axioms".to_string(),
564                "decomposition".to_string(),
565                "reconstruction".to_string(),
566                "gaps".to_string(),
567                "confidence".to_string(),
568            ],
569        },
570        validation: vec![],
571        metadata: ProtocolMetadata {
572            category: "analytical".to_string(),
573            composable_with: vec!["laserlogic".to_string(), "proofguard".to_string()],
574            typical_tokens: 2000,
575            estimated_latency_ms: 4500,
576            ..Default::default()
577        },
578    }
579}
580
581fn builtin_proofguard() -> Protocol {
582    Protocol {
583        id: "proofguard".to_string(),
584        name: "ProofGuard".to_string(),
585        version: "1.0.0".to_string(),
586        description: "Multi-source verification using triangulation protocol".to_string(),
587        strategy: ReasoningStrategy::Verification,
588        input: InputSpec {
589            required: vec!["claim".to_string()],
590            optional: vec!["sources".to_string()],
591        },
592        steps: vec![
593            ProtocolStep {
594                id: "identify_sources".to_string(),
595                action: StepAction::CrossReference { min_sources: 3 },
596                prompt_template: r#"Identify potential sources to verify this claim:
597
598Claim: {{claim}}
599{{#if sources}}Known sources: {{sources}}{{/if}}
600
601List 3+ independent sources that could verify or refute this claim.
602Prioritize: official docs, peer-reviewed, primary sources."#
603                    .to_string(),
604                output_format: StepOutputFormat::List,
605                min_confidence: 0.6,
606                depends_on: vec![],
607                branch: None,
608            },
609            ProtocolStep {
610                id: "verify_each".to_string(),
611                action: StepAction::Validate {
612                    rules: vec![
613                        "source_reliability".to_string(),
614                        "claim_support".to_string(),
615                    ],
616                },
617                prompt_template: r#"For each source, evaluate support for the claim:
618
619Claim: {{claim}}
620Sources to check:
621{{identify_sources}}
622
623For each source:
6241. What does it say about the claim?
6252. Support level: Confirms / Partially confirms / Neutral / Contradicts
6263. Source reliability (0.0-1.0)
6274. Key quote or evidence"#
628                    .to_string(),
629                output_format: StepOutputFormat::Structured,
630                min_confidence: 0.7,
631                depends_on: vec!["identify_sources".to_string()],
632                branch: None,
633            },
634            ProtocolStep {
635                id: "triangulate".to_string(),
636                action: StepAction::Synthesize {
637                    aggregation: AggregationType::Consensus,
638                },
639                prompt_template: r#"Apply triangulation to determine claim validity:
640
641Claim: {{claim}}
642Source evaluations:
643{{verify_each}}
644
645Triangulation rules:
646- 3+ independent confirming sources = HIGH confidence
647- 2 confirming, 1 neutral = MEDIUM confidence
648- Mixed results = LOW confidence, note discrepancies
649- Any contradiction = FLAG for review
650
651Provide final verdict and confidence score."#
652                    .to_string(),
653                output_format: StepOutputFormat::Structured,
654                min_confidence: 0.8,
655                depends_on: vec!["verify_each".to_string()],
656                branch: None,
657            },
658        ],
659        output: OutputSpec {
660            format: "ProofGuardResult".to_string(),
661            fields: vec![
662                "verdict".to_string(),
663                "sources".to_string(),
664                "evidence".to_string(),
665                "discrepancies".to_string(),
666                "confidence".to_string(),
667            ],
668        },
669        validation: vec![],
670        metadata: ProtocolMetadata {
671            category: "verification".to_string(),
672            composable_with: vec!["bedrock".to_string(), "brutalhonesty".to_string()],
673            typical_tokens: 2200,
674            estimated_latency_ms: 5000,
675            ..Default::default()
676        },
677    }
678}
679
680fn builtin_brutalhonesty() -> Protocol {
681    Protocol {
682        id: "brutalhonesty".to_string(),
683        name: "BrutalHonesty".to_string(),
684        version: "1.0.0".to_string(),
685        description: "Adversarial self-critique - find every flaw".to_string(),
686        strategy: ReasoningStrategy::Adversarial,
687        input: InputSpec {
688            required: vec!["work".to_string()],
689            optional: vec!["criteria".to_string()],
690        },
691        steps: vec![
692            ProtocolStep {
693                id: "steelman".to_string(),
694                action: StepAction::Analyze {
695                    criteria: vec!["strengths".to_string()],
696                },
697                prompt_template: r#"First, steelman the work - what are its genuine strengths?
698
699Work to critique:
700{{work}}
701
702Identify:
7031. What does this do well?
7042. What problems does it solve?
7053. What is genuinely valuable here?
706
707Be generous but honest."#
708                    .to_string(),
709                output_format: StepOutputFormat::List,
710                min_confidence: 0.7,
711                depends_on: vec![],
712                branch: None,
713            },
714            ProtocolStep {
715                id: "attack".to_string(),
716                action: StepAction::Critique {
717                    severity: CritiqueSeverity::Brutal,
718                },
719                prompt_template: r#"Now be brutally honest - what's wrong with this?
720
721Work:
722{{work}}
723
724Strengths identified:
725{{steelman}}
726
727Attack from all angles:
7281. Logical flaws
7292. Missing considerations
7303. Weak assumptions
7314. Implementation problems
7325. Unintended consequences
7336. What would a harsh critic say?
734
735Don't hold back. Be specific."#
736                    .to_string(),
737                output_format: StepOutputFormat::List,
738                min_confidence: 0.6,
739                depends_on: vec!["steelman".to_string()],
740                branch: None,
741            },
742            ProtocolStep {
743                id: "verdict".to_string(),
744                action: StepAction::Decide {
745                    method: super::protocol::DecisionMethod::ProsCons,
746                },
747                prompt_template: r#"Final verdict - is this work acceptable?
748
749Strengths:
750{{steelman}}
751
752Flaws:
753{{attack}}
754
755Provide:
7561. Overall assessment (Pass / Conditional Pass / Fail)
7572. Most critical issue to fix
7583. Confidence in verdict (0.0-1.0)
7594. What would make this excellent?"#
760                    .to_string(),
761                output_format: StepOutputFormat::Structured,
762                min_confidence: 0.75,
763                depends_on: vec!["steelman".to_string(), "attack".to_string()],
764                branch: None,
765            },
766        ],
767        output: OutputSpec {
768            format: "BrutalHonestyResult".to_string(),
769            fields: vec![
770                "strengths".to_string(),
771                "flaws".to_string(),
772                "verdict".to_string(),
773                "critical_fix".to_string(),
774                "confidence".to_string(),
775            ],
776        },
777        validation: vec![],
778        metadata: ProtocolMetadata {
779            category: "critique".to_string(),
780            composable_with: vec!["gigathink".to_string(), "proofguard".to_string()],
781            typical_tokens: 2000,
782            estimated_latency_ms: 4500,
783            ..Default::default()
784        },
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791
792    #[test]
793    fn test_registry_creation() {
794        let registry = ProtocolRegistry::new();
795        assert!(registry.is_empty());
796    }
797
798    #[test]
799    fn test_register_builtins() {
800        let mut registry = ProtocolRegistry::new();
801        registry.register_builtins().unwrap();
802
803        assert_eq!(registry.len(), 6);
804        assert!(registry.contains("gigathink"));
805        assert!(registry.contains("laserlogic"));
806        assert!(registry.contains("bedrock"));
807        assert!(registry.contains("proofguard"));
808        assert!(registry.contains("brutalhonesty"));
809        assert!(registry.contains("powercombo"));
810    }
811
812    #[test]
813    fn test_get_protocol() {
814        let mut registry = ProtocolRegistry::new();
815        registry.register_builtins().unwrap();
816
817        let gt = registry.get("gigathink").unwrap();
818        assert_eq!(gt.name, "GigaThink");
819        assert_eq!(gt.strategy, ReasoningStrategy::Expansive);
820    }
821
822    #[test]
823    fn test_list_ids() {
824        let mut registry = ProtocolRegistry::new();
825        registry.register_builtins().unwrap();
826
827        let ids = registry.list_ids();
828        assert_eq!(ids.len(), 6);
829        assert!(ids.contains(&"gigathink"));
830        assert!(ids.contains(&"powercombo"));
831    }
832}