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}