Skip to main content

symposium_acp_proxy/
lib.rs

1//! Symposium ACP Proxy
2//!
3//! This crate provides the Symposium proxy functionality. It sits between an
4//! editor and an agent, using sacp-conductor to orchestrate a dynamic chain
5//! of component proxies that enrich the agent's capabilities.
6//!
7//! Two modes are supported:
8//! - `Symposium`: Proxy mode - sits between editor and an existing agent
9//! - `SymposiumAgent`: Agent mode - wraps a downstream agent
10//!
11//! Architecture:
12//! 1. Receive Initialize request from editor
13//! 2. Examine capabilities to determine what components are needed
14//! 3. Build proxy chain dynamically using conductor's lazy initialization
15//! 4. Forward Initialize through the chain
16//! 5. Bidirectionally forward all subsequent messages
17
18use anyhow::Result;
19use sacp::link::{AgentToClient, ConductorToProxy, ProxyToConductor};
20use sacp::{Component, DynComponent};
21use sacp_conductor::{Conductor, McpBridgeMode};
22use std::path::PathBuf;
23
24/// Shared configuration for Symposium proxy chains.
25struct SymposiumConfig {
26    ferris: Option<symposium_ferris::Ferris>,
27    cargo: bool,
28    sparkle: bool,
29    trace_dir: Option<PathBuf>,
30}
31
32impl SymposiumConfig {
33    fn new() -> Self {
34        SymposiumConfig {
35            sparkle: true,
36            ferris: Some(symposium_ferris::Ferris::default()),
37            cargo: true,
38            trace_dir: None,
39        }
40    }
41}
42
43/// Symposium in proxy mode - sits between an editor and an existing agent.
44///
45/// Use this when you want to add Symposium's capabilities to an existing
46/// agent setup without Symposium managing the agent lifecycle.
47pub struct Symposium {
48    config: SymposiumConfig,
49}
50
51impl Symposium {
52    pub fn new() -> Self {
53        Symposium {
54            config: SymposiumConfig::new(),
55        }
56    }
57
58    pub fn sparkle(mut self, enable: bool) -> Self {
59        self.config.sparkle = enable;
60        self
61    }
62
63    /// Configure Ferris tools. Pass `None` to disable Ferris entirely.
64    pub fn ferris(mut self, config: Option<symposium_ferris::Ferris>) -> Self {
65        self.config.ferris = config;
66        self
67    }
68
69    /// Enable or disable Cargo tools.
70    pub fn cargo(mut self, enable: bool) -> Self {
71        self.config.cargo = enable;
72        self
73    }
74
75    /// Enable trace logging to a directory.
76    /// Traces will be written as `<timestamp>.jsons` files.
77    pub fn trace_dir(mut self, dir: impl Into<PathBuf>) -> Self {
78        self.config.trace_dir = Some(dir.into());
79        self
80    }
81
82    /// Pair the symposium proxy with an agent, producing a new composite agent
83    pub fn with_agent(self, agent: impl Component<AgentToClient>) -> SymposiumAgent {
84        let Symposium { config } = self;
85        SymposiumAgent::new(config, agent)
86    }
87}
88
89impl Component<ProxyToConductor> for Symposium {
90    async fn serve(self, client: impl Component<ConductorToProxy>) -> Result<(), sacp::Error> {
91        tracing::debug!("Symposium::serve starting (proxy mode)");
92        let Self { config } = self;
93
94        let ferris = config.ferris;
95        let cargo = config.cargo;
96        let sparkle = config.sparkle;
97        let trace_dir = config.trace_dir;
98
99        tracing::debug!("Creating conductor (proxy mode)");
100        let mut conductor = Conductor::new_proxy(
101            "symposium",
102            move |init_req| async move {
103                tracing::info!("Building proxy chain based on capabilities");
104
105                let mut proxies: Vec<DynComponent<ProxyToConductor>> = vec![];
106
107                if let Some(ferris_config) = ferris {
108                    proxies.push(DynComponent::new(symposium_ferris::FerrisComponent::new(
109                        ferris_config,
110                    )));
111                }
112
113                if cargo {
114                    proxies.push(DynComponent::new(symposium_cargo::CargoProxy));
115                }
116
117                if sparkle {
118                    proxies.push(DynComponent::new(sparkle::SparkleComponent::new()));
119                }
120
121                Ok((init_req, proxies))
122            },
123            McpBridgeMode::default(),
124        );
125
126        // Enable tracing if a directory was specified
127        if let Some(dir) = trace_dir {
128            std::fs::create_dir_all(&dir).map_err(sacp::Error::into_internal_error)?;
129            let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
130            let trace_path = dir.join(format!("{}.jsons", timestamp));
131            conductor = conductor
132                .trace_to_path(&trace_path)
133                .map_err(sacp::Error::into_internal_error)?;
134            tracing::info!("Tracing to {}", trace_path.display());
135        }
136
137        tracing::debug!("Starting conductor.run()");
138        conductor.run(client).await
139    }
140}
141
142/// Symposium in agent mode - wraps a downstream agent.
143///
144/// Use this when Symposium should manage the agent lifecycle, e.g., when
145/// building a standalone enriched agent binary.
146pub struct SymposiumAgent {
147    config: SymposiumConfig,
148    agent: DynComponent<AgentToClient>,
149}
150
151impl SymposiumAgent {
152    fn new<C: Component<AgentToClient>>(config: SymposiumConfig, agent: C) -> Self {
153        SymposiumAgent {
154            config,
155            agent: DynComponent::new(agent),
156        }
157    }
158}
159
160impl Component<AgentToClient> for SymposiumAgent {
161    async fn serve(
162        self,
163        client: impl Component<sacp::link::ClientToAgent>,
164    ) -> Result<(), sacp::Error> {
165        tracing::debug!("SymposiumAgent::serve starting (agent mode)");
166        let Self { config, agent } = self;
167
168        let ferris = config.ferris;
169        let cargo = config.cargo;
170        let sparkle = config.sparkle;
171        let trace_dir = config.trace_dir;
172
173        tracing::debug!("Creating conductor (agent mode)");
174        let mut conductor = Conductor::new_agent(
175            "symposium",
176            move |init_req| async move {
177                tracing::info!("Building proxy chain based on capabilities");
178
179                let mut proxies: Vec<DynComponent<ProxyToConductor>> = vec![];
180
181                if let Some(ferris_config) = ferris {
182                    proxies.push(DynComponent::new(symposium_ferris::FerrisComponent::new(
183                        ferris_config,
184                    )));
185                }
186
187                if cargo {
188                    proxies.push(DynComponent::new(symposium_cargo::CargoProxy));
189                }
190
191                if sparkle {
192                    proxies.push(DynComponent::new(sparkle::SparkleComponent::new()));
193                }
194
195                Ok((init_req, proxies, agent))
196            },
197            McpBridgeMode::default(),
198        );
199
200        // Enable tracing if a directory was specified
201        if let Some(dir) = trace_dir {
202            std::fs::create_dir_all(&dir).map_err(sacp::Error::into_internal_error)?;
203            let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
204            let trace_path = dir.join(format!("{}.jsons", timestamp));
205            conductor = conductor
206                .trace_to_path(&trace_path)
207                .map_err(sacp::Error::into_internal_error)?;
208            tracing::info!("Tracing to {}", trace_path.display());
209        }
210
211        tracing::debug!("Starting conductor.run()");
212        conductor.run(client).await
213    }
214}