Skip to main content

chimera_core/
backend.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{AgentError, McpConfigMode, Session, SessionConfig};
4
5/// Whether a backend implementation can handle a feature.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8#[non_exhaustive]
9pub enum CapabilitySupport {
10    /// The Chimera backend implementation does not support this feature.
11    Unsupported,
12    /// Support depends on the selected backend config or session mode.
13    ConfigDependent,
14    /// The feature is supported unconditionally by this backend implementation.
15    Supported,
16}
17
18impl CapabilitySupport {
19    pub fn is_supported(self) -> bool {
20        !matches!(self, Self::Unsupported)
21    }
22}
23
24/// How `resume()` behaves for a backend implementation.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27#[non_exhaustive]
28pub enum ResumeSemantics {
29    /// `resume()` is not supported.
30    Unsupported,
31    /// `resume()` accepts an ID but does not restore prior conversation state.
32    Stateless,
33    /// `resume()` restores prior conversation state.
34    Stateful,
35}
36
37impl ResumeSemantics {
38    pub fn accepts_resume_id(self) -> bool {
39        !matches!(self, Self::Unsupported)
40    }
41
42    pub fn preserves_history(self) -> bool {
43        matches!(self, Self::Stateful)
44    }
45}
46
47/// How a backend implementation handles MCP wiring.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50#[non_exhaustive]
51pub enum McpSupport {
52    /// MCP is not supported.
53    Unsupported,
54    /// Backend can use Chimera-provided MCP servers, but does not merge ambient
55    /// backend MCP config.
56    ExplicitServersOnly,
57    /// Backend supports ambient MCP merge only.
58    MergeOnly,
59    /// Backend supports both merged and explicit-only MCP modes.
60    MergeAndExplicitOnly,
61}
62
63impl McpSupport {
64    pub fn supports_mcp(self) -> bool {
65        !matches!(self, Self::Unsupported)
66    }
67
68    pub fn supports_mode(self, mode: McpConfigMode) -> bool {
69        match (self, mode) {
70            (Self::Unsupported, _) => false,
71            (Self::ExplicitServersOnly, McpConfigMode::Merge) => false,
72            (Self::ExplicitServersOnly, McpConfigMode::ExplicitOnly) => true,
73            (Self::MergeOnly, McpConfigMode::Merge) => true,
74            (Self::MergeOnly, McpConfigMode::ExplicitOnly) => false,
75            (Self::MergeAndExplicitOnly, _) => true,
76        }
77    }
78}
79
80/// Public feature matrix for a Chimera backend implementation.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub struct BackendCapabilities {
83    /// Whether structured image inputs are supported.
84    pub input_images: CapabilitySupport,
85    /// Whether structured output schemas are supported.
86    pub output_schema: CapabilitySupport,
87    /// Whether an in-flight turn can be interrupted.
88    pub interrupt: CapabilitySupport,
89    /// How `resume()` behaves.
90    pub resume: ResumeSemantics,
91    /// How MCP is supported.
92    pub mcp: McpSupport,
93    /// Whether tool/function call events are emitted.
94    pub tool_call_events: CapabilitySupport,
95}
96
97/// Factory for creating agent sessions.
98///
99/// Not object-safe (has associated types and RPITIT). Use `AgentBackend`
100/// from the facade crate for runtime dispatch.
101pub trait Backend: Send + Sync {
102    /// Backend-specific session configuration type.
103    type Config: Send + Sync + 'static;
104
105    /// Concrete session type returned by this backend.
106    type Session: Session + 'static;
107
108    /// Backend identifier (e.g. "codex", "claude-code").
109    fn name(&self) -> &'static str;
110
111    /// Backend feature support exposed by this implementation.
112    ///
113    /// This reports backend-level support. For config-dependent behavior,
114    /// callers should consult the backend config's own `capabilities()`
115    /// helper when available.
116    fn capabilities(&self) -> BackendCapabilities;
117
118    /// Start a new agent session.
119    fn session(
120        &self,
121        config: SessionConfig<Self::Config>,
122    ) -> impl std::future::Future<Output = Result<Self::Session, AgentError>> + Send;
123
124    /// Resume a previously saved session by ID.
125    fn resume(
126        &self,
127        session_id: &str,
128        config: SessionConfig<Self::Config>,
129    ) -> impl std::future::Future<Output = Result<Self::Session, AgentError>> + Send;
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn capability_support_reports_if_backend_can_handle_feature() {
138        assert!(!CapabilitySupport::Unsupported.is_supported());
139        assert!(CapabilitySupport::ConfigDependent.is_supported());
140        assert!(CapabilitySupport::Supported.is_supported());
141    }
142
143    #[test]
144    fn resume_semantics_distinguish_stateful_and_stateless() {
145        assert!(!ResumeSemantics::Unsupported.accepts_resume_id());
146        assert!(ResumeSemantics::Stateless.accepts_resume_id());
147        assert!(ResumeSemantics::Stateful.accepts_resume_id());
148        assert!(!ResumeSemantics::Stateless.preserves_history());
149        assert!(ResumeSemantics::Stateful.preserves_history());
150    }
151
152    #[test]
153    fn mcp_support_tracks_supported_modes() {
154        assert!(!McpSupport::Unsupported.supports_mcp());
155        assert!(McpSupport::MergeOnly.supports_mode(McpConfigMode::Merge));
156        assert!(!McpSupport::MergeOnly.supports_mode(McpConfigMode::ExplicitOnly));
157        assert!(McpSupport::ExplicitServersOnly.supports_mode(McpConfigMode::ExplicitOnly));
158        assert!(!McpSupport::ExplicitServersOnly.supports_mode(McpConfigMode::Merge));
159        assert!(McpSupport::MergeAndExplicitOnly.supports_mode(McpConfigMode::ExplicitOnly));
160    }
161}