Skip to main content

converge_provider_api/
backend.rs

1// Copyright 2024-2026 Reflective Labs
2
3// SPDX-License-Identifier: MIT
4
5//! Backend identity and kind.
6//!
7//! Every external capability in Converge implements [`Backend`]. This trait
8//! captures identity and capability declarations — not invocation. Each
9//! backend kind has its own invocation traits in its own crate.
10
11use serde::{Deserialize, Serialize};
12
13use crate::capability::Capability;
14
15/// The kind of backend — which agent instantiation strategy it supports.
16///
17/// This is not a capability (capabilities are declared separately via
18/// [`Capability`]). This is the *category* of the backend, used for
19/// coarse routing before capability matching.
20///
21/// # Extensibility
22///
23/// The `Other(String)` variant allows future backend kinds without
24/// breaking the enum. Use it for experimental or domain-specific backends.
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub enum BackendKind {
27    /// Large language model — cloud or local inference.
28    ///
29    /// Examples: Anthropic Claude, `OpenAI` GPT, local `llama-burn`.
30    Llm,
31
32    /// Policy engine — rule evaluation and access control.
33    ///
34    /// Examples: Cedar, OPA, Polar (policy mode).
35    Policy,
36
37    /// Constraint optimization — resource allocation, scheduling.
38    ///
39    /// Examples: CP-SAT (OR-Tools), Polar (constraint mode), custom solvers.
40    Optimization,
41
42    /// Analytics and ML — embeddings, classification, regression.
43    ///
44    /// Examples: Burn, `LanceDB`, Polars.
45    Analytics,
46
47    /// Search — vector similarity, full-text, semantic.
48    ///
49    /// Examples: `LanceDB` (vector), Qdrant, Meilisearch.
50    Search,
51
52    /// Storage — persistence, event sourcing, document store.
53    ///
54    /// Examples: `SurrealDB`, `PostgreSQL`, `SQLite`.
55    Storage,
56
57    /// Extension point for future or domain-specific backends.
58    Other(String),
59}
60
61impl std::fmt::Display for BackendKind {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Self::Llm => write!(f, "llm"),
65            Self::Policy => write!(f, "policy"),
66            Self::Optimization => write!(f, "optimization"),
67            Self::Analytics => write!(f, "analytics"),
68            Self::Search => write!(f, "search"),
69            Self::Storage => write!(f, "storage"),
70            Self::Other(name) => write!(f, "other:{name}"),
71        }
72    }
73}
74
75/// Identity and capability declaration for any backend in the Converge platform.
76///
77/// This is the foundational trait that all backend implementations must satisfy.
78/// It captures *who* a backend is and *what it can do*, not *how to call it*.
79///
80/// Specific invocation traits extend this in their own crates:
81/// - `ChatBackend` / `EmbedBackend` in converge-core (canonical LLM interfaces)
82/// - `PolicyEngine` in converge-policy (Cedar/OPA)
83/// - `Solver` in converge-optimization (CP-SAT/Polar)
84/// - `Pipeline` in converge-analytics (Burn/LanceDB)
85///
86/// # Thread Safety
87///
88/// Backends must be `Send + Sync` for use in concurrent agent execution.
89///
90/// # Example
91///
92/// ```
93/// use converge_provider_api::{Backend, BackendKind, Capability};
94///
95/// struct MockLlm;
96///
97/// impl Backend for MockLlm {
98///     fn name(&self) -> &str { "mock-llm" }
99///     fn kind(&self) -> BackendKind { BackendKind::Llm }
100///     fn capabilities(&self) -> Vec<Capability> {
101///         vec![Capability::TextGeneration, Capability::Reasoning]
102///     }
103/// }
104///
105/// let backend = MockLlm;
106/// assert_eq!(backend.kind(), BackendKind::Llm);
107/// assert!(backend.has_capability(Capability::TextGeneration));
108/// ```
109pub trait Backend: Send + Sync {
110    /// Human-readable name for identification and routing.
111    ///
112    /// Examples: "anthropic-claude", "cedar-policy", "cpsat-optimizer",
113    /// "burn-analytics", "lancedb-vector".
114    fn name(&self) -> &str;
115
116    /// The kind of backend this is.
117    fn kind(&self) -> BackendKind;
118
119    /// Declared capabilities of this backend.
120    ///
121    /// Used by [`BackendSelector`](crate::selection::BackendSelector) to match
122    /// requirements to backends.
123    fn capabilities(&self) -> Vec<Capability>;
124
125    /// Provenance string for audit trail.
126    ///
127    /// Default: `"name:request_id"`. Override for richer provenance.
128    fn provenance(&self, request_id: &str) -> String {
129        format!("{}:{}", self.name(), request_id)
130    }
131
132    /// Check if this backend has a specific capability.
133    fn has_capability(&self, capability: Capability) -> bool {
134        self.capabilities().contains(&capability)
135    }
136
137    /// Whether this backend supports deterministic replay.
138    ///
139    /// - Local backends with fixed seeds: `true`
140    /// - Remote API backends: `false` (model versions can change)
141    /// - Policy/optimization engines: typically `true`
142    fn supports_replay(&self) -> bool {
143        false
144    }
145
146    /// Whether this backend requires network access.
147    ///
148    /// Used for offline-capable deployment decisions.
149    fn requires_network(&self) -> bool {
150        true
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    struct MockLlm;
159
160    impl Backend for MockLlm {
161        fn name(&self) -> &str {
162            "mock-llm"
163        }
164        fn kind(&self) -> BackendKind {
165            BackendKind::Llm
166        }
167        fn capabilities(&self) -> Vec<Capability> {
168            vec![Capability::TextGeneration, Capability::Reasoning]
169        }
170    }
171
172    struct OfflinePolicy;
173
174    impl Backend for OfflinePolicy {
175        fn name(&self) -> &str {
176            "local-policy"
177        }
178        fn kind(&self) -> BackendKind {
179            BackendKind::Policy
180        }
181        fn capabilities(&self) -> Vec<Capability> {
182            vec![Capability::AccessControl]
183        }
184        fn supports_replay(&self) -> bool {
185            true
186        }
187        fn requires_network(&self) -> bool {
188            false
189        }
190    }
191
192    #[test]
193    fn provenance_default() {
194        let b = MockLlm;
195        assert_eq!(b.provenance("req-123"), "mock-llm:req-123");
196    }
197
198    #[test]
199    fn has_capability_found() {
200        let b = MockLlm;
201        assert!(b.has_capability(Capability::TextGeneration));
202        assert!(b.has_capability(Capability::Reasoning));
203    }
204
205    #[test]
206    fn has_capability_not_found() {
207        let b = MockLlm;
208        assert!(!b.has_capability(Capability::Embedding));
209        assert!(!b.has_capability(Capability::AccessControl));
210    }
211
212    #[test]
213    fn supports_replay_default_false() {
214        let b = MockLlm;
215        assert!(!b.supports_replay());
216    }
217
218    #[test]
219    fn supports_replay_override() {
220        let b = OfflinePolicy;
221        assert!(b.supports_replay());
222    }
223
224    #[test]
225    fn requires_network_default_true() {
226        let b = MockLlm;
227        assert!(b.requires_network());
228    }
229
230    #[test]
231    fn requires_network_override() {
232        let b = OfflinePolicy;
233        assert!(!b.requires_network());
234    }
235
236    #[test]
237    fn backend_kind_display() {
238        assert_eq!(BackendKind::Llm.to_string(), "llm");
239        assert_eq!(BackendKind::Policy.to_string(), "policy");
240        assert_eq!(BackendKind::Optimization.to_string(), "optimization");
241        assert_eq!(BackendKind::Analytics.to_string(), "analytics");
242        assert_eq!(BackendKind::Search.to_string(), "search");
243        assert_eq!(BackendKind::Storage.to_string(), "storage");
244        assert_eq!(
245            BackendKind::Other("custom".into()).to_string(),
246            "other:custom"
247        );
248    }
249}