1use 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#[derive(Debug, Default)]
15pub struct ProtocolRegistry {
16 protocols: HashMap<String, Protocol>,
18
19 search_paths: Vec<PathBuf>,
21}
22
23impl ProtocolRegistry {
24 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn with_defaults() -> Self {
31 let mut registry = Self::new();
32
33 if let Ok(cwd) = std::env::current_dir() {
35 registry.add_search_path(cwd.join("protocols"));
36 }
37
38 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 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 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 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 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 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 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 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 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 pub fn register(&mut self, protocol: Protocol) -> Result<()> {
178 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 pub fn get(&self, id: &str) -> Option<&Protocol> {
194 self.protocols.get(id)
195 }
196
197 pub fn contains(&self, id: &str) -> bool {
199 self.protocols.contains_key(id)
200 }
201
202 pub fn list_ids(&self) -> Vec<&str> {
204 self.protocols.keys().map(|s| s.as_str()).collect()
205 }
206
207 pub fn list(&self) -> Vec<&Protocol> {
209 self.protocols.values().collect()
210 }
211
212 pub fn len(&self) -> usize {
214 self.protocols.len()
215 }
216
217 pub fn is_empty(&self) -> bool {
219 self.protocols.is_empty()
220 }
221
222 pub fn remove(&mut self, id: &str) -> Option<Protocol> {
224 self.protocols.remove(id)
225 }
226
227 pub fn clear(&mut self) {
229 self.protocols.clear();
230 }
231
232 pub fn register_builtins(&mut self) -> Result<()> {
234 let mut loaded_from_yaml = false;
236 if let Ok(cwd) = std::env::current_dir() {
237 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 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
267fn dirs_config_path() -> Option<PathBuf> {
269 dirs::config_dir()
270}
271
272use 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}