Skip to main content

perspt_agent/tools/handlers/
mod.rs

1//! The open execution plane: an exact-name handler registry the governed
2//! candidate dispatches through.
3//!
4//! A new tool family is a *registration* at the composition root — catalog
5//! entries via `StaticCatalog::with_base`, handlers via
6//! [`CandidateHandlerRegistry::register`], and a capability grant — never an
7//! edit to the loop, the candidate, or the node assembly. The governance
8//! wrapper (catalog → validate → budget → certify → execute → re-certify)
9//! is uniform and lives outside the handlers.
10
11mod deps;
12mod exec;
13pub mod external;
14mod fs;
15mod lsp;
16mod verify;
17
18use std::collections::BTreeMap;
19use std::sync::Arc;
20
21use anyhow::Result;
22use perspt_sdk::ToolEntry;
23
24use crate::candidate::CandidateWorkspace;
25use crate::toolloop::EffectOutcome;
26
27pub(crate) use lsp::LspSessions;
28
29/// One governed tool executor. Handlers never decide admission, budgets, or
30/// certification — the kernel already did; they only realize the effect
31/// against the reversible candidate.
32#[async_trait::async_trait]
33pub trait CandidateToolHandler: Send + Sync {
34    async fn apply(
35        &self,
36        workspace: &CandidateWorkspace,
37        call: &perspt_sdk::ProviderToolCall,
38        entry: &ToolEntry,
39    ) -> Result<EffectOutcome>;
40}
41
42/// Exact-name registry. Duplicate registration fails closed so a later
43/// family can never silently shadow a builtin.
44pub struct CandidateHandlerRegistry {
45    handlers: BTreeMap<String, Arc<dyn CandidateToolHandler>>,
46    /// Consulted only when no exact name matches — the external (MCP)
47    /// dispatcher, whose namespaced tool names are discovered at runtime.
48    fallback: Option<Arc<dyn CandidateToolHandler>>,
49}
50
51impl std::fmt::Debug for CandidateHandlerRegistry {
52    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        formatter
54            .debug_struct("CandidateHandlerRegistry")
55            .field("handlers", &self.handlers.keys().collect::<Vec<_>>())
56            .finish()
57    }
58}
59
60impl CandidateHandlerRegistry {
61    /// An empty registry, for tests that assemble their own surface.
62    pub fn empty() -> Self {
63        Self {
64            handlers: BTreeMap::new(),
65            fallback: None,
66        }
67    }
68
69    /// The builtin coding-domain execution surface, matching the base
70    /// catalog's executor-backed entries.
71    pub fn with_builtins() -> Self {
72        let mut registry = Self::empty();
73        fs::register_workspace_ops(&mut registry);
74        verify::register(&mut registry);
75        exec::register(&mut registry);
76        lsp::register(&mut registry);
77        deps::register(&mut registry);
78        registry
79    }
80
81    pub fn register(
82        &mut self,
83        name: impl Into<String>,
84        handler: Arc<dyn CandidateToolHandler>,
85    ) -> Result<()> {
86        let name = name.into();
87        anyhow::ensure!(
88            !self.handlers.contains_key(&name),
89            "duplicate tool handler registration: {name}"
90        );
91        self.handlers.insert(name, handler);
92        Ok(())
93    }
94
95    pub fn get(&self, name: &str) -> Option<&Arc<dyn CandidateToolHandler>> {
96        self.handlers.get(name)
97    }
98
99    /// Install the fallback handler for names with no exact registration.
100    pub fn set_fallback(&mut self, handler: Arc<dyn CandidateToolHandler>) {
101        self.fallback = Some(handler);
102    }
103
104    pub fn has_fallback(&self) -> bool {
105        self.fallback.is_some()
106    }
107
108    /// Exact name first, then the fallback.
109    pub fn resolve(&self, name: &str) -> Option<&Arc<dyn CandidateToolHandler>> {
110        self.handlers.get(name).or(self.fallback.as_ref())
111    }
112
113    pub fn names(&self) -> impl Iterator<Item = &str> {
114        self.handlers.keys().map(String::as_str)
115    }
116}
117
118impl Default for CandidateHandlerRegistry {
119    fn default() -> Self {
120        Self::with_builtins()
121    }
122}