Skip to main content

a3s_code_core/agent_api/
agent_facade.rs

1use super::*;
2
3// ============================================================================
4// Agent
5// ============================================================================
6
7/// High-level agent facade.
8///
9/// Holds the LLM client and agent config. Workspace-independent.
10/// Use [`Agent::session()`] to bind to a workspace.
11pub struct Agent {
12    pub(super) code_config: CodeConfig,
13    pub(super) config: AgentConfig,
14    /// Global MCP manager loaded from config.mcp_servers
15    pub(super) global_mcp: Option<Arc<crate::mcp::manager::McpManager>>,
16    /// Pre-fetched MCP tool definitions from global_mcp (cached at creation time).
17    /// Wrapped in Mutex so `refresh_mcp_tools()` can update the cache without `&mut self`.
18    pub(super) global_mcp_tools: std::sync::Mutex<Vec<(String, crate::mcp::McpTool)>>,
19    /// Tracks session IDs reserved by in-progress builds and every live session
20    /// created by this agent. Build reservations prevent duplicate IDs and make
21    /// construction finalization atomic with [`Agent::close`]. Live sessions
22    /// are held via `Weak` refs and pruned on registry access after drop.
23    ///
24    /// Uses a synchronous lock so the sync `Agent::session()` factory can
25    /// insert without nesting tokio runtimes. The lock is only held for
26    /// brief insert/scan operations — async close work happens after the
27    /// lock is released.
28    pub(super) sessions: Arc<std::sync::Mutex<agent_sessions::SessionRegistry>>,
29    /// Set once `Agent::close()` has been called. Subsequent `session()` /
30    /// `resume_session()` calls fail fast with `CodeError::SessionClosed`.
31    pub(super) closed: Arc<std::sync::atomic::AtomicBool>,
32}
33
34/// Async-first session construction API.
35///
36/// Builder methods only record typed configuration. Filesystem access, queue
37/// startup, and MCP discovery happen once in [`build`](Self::build).
38#[must_use = "a session builder does nothing until build() is awaited"]
39pub struct SessionBuilder<'a> {
40    agent: &'a Agent,
41    workspace: String,
42    options: SessionOptions,
43}
44
45impl<'a> SessionBuilder<'a> {
46    fn new(agent: &'a Agent, workspace: impl Into<String>) -> Self {
47        Self {
48            agent,
49            workspace: workspace.into(),
50            options: SessionOptions::default(),
51        }
52    }
53
54    /// Replace the per-session option patch.
55    pub fn options(mut self, options: SessionOptions) -> Self {
56        self.options = options;
57        self
58    }
59
60    /// Resolve configuration, initialize async resources, and build the session.
61    pub async fn build(self) -> Result<AgentSession> {
62        agent_sessions::create_session_async(self.agent, self.workspace, Some(self.options)).await
63    }
64}
65
66impl std::fmt::Debug for Agent {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("Agent").finish()
69    }
70}
71
72impl Agent {
73    /// Create from a config file path or inline ACL-compatible string.
74    ///
75    /// Auto-detects `.acl` file paths vs inline ACL-compatible config.
76    pub async fn new(config_source: impl Into<String>) -> Result<Self> {
77        let config = agent_bootstrap::load_code_config(config_source.into())?;
78        Self::from_config(config).await
79    }
80
81    /// Create from a config file path or inline ACL-compatible string.
82    ///
83    /// Alias for [`Agent::new()`] — provides a consistent API with
84    /// the Python and Node.js SDKs.
85    pub async fn create(config_source: impl Into<String>) -> Result<Self> {
86        Self::new(config_source).await
87    }
88
89    /// Create from a [`CodeConfig`] struct.
90    pub async fn from_config(config: CodeConfig) -> Result<Self> {
91        agent_bootstrap::build_agent_from_config(config).await
92    }
93
94    /// Re-fetch tool definitions from all connected global MCP servers and
95    /// update the internal cache.
96    ///
97    /// Call this when an MCP server has added or removed tools since the
98    /// agent was created. The refreshed tools will be visible to all
99    /// **new** sessions created after this call; existing sessions are
100    /// unaffected (their `ToolExecutor` snapshot is already built).
101    pub async fn refresh_mcp_tools(&self) -> Result<()> {
102        agent_sessions::refresh_mcp_tools(self).await
103    }
104
105    /// Start async-first construction of a workspace-bound session.
106    pub fn session_builder(&self, workspace: impl Into<String>) -> SessionBuilder<'_> {
107        SessionBuilder::new(self, workspace)
108    }
109
110    /// Build a workspace-bound session asynchronously.
111    ///
112    /// A session ID may have only one live or in-progress session per `Agent`.
113    /// Reusing an occupied ID returns a typed session-configuration error.
114    pub async fn session_async(
115        &self,
116        workspace: impl Into<String>,
117        options: Option<SessionOptions>,
118    ) -> Result<AgentSession> {
119        agent_sessions::create_session_async(self, workspace, options).await
120    }
121
122    /// Bind to a workspace directory, returning an [`AgentSession`].
123    ///
124    /// This compatibility entry point never starts or blocks an async runtime.
125    /// It requires an explicit, pre-initialized `memory_store`; an optional
126    /// pre-initialized session store and cached global MCP tools are accepted.
127    /// File-store specs/defaults, queues, RL trajectory files, and host-supplied
128    /// MCP sources require [`session_builder`](Self::session_builder).
129    /// A session ID may have only one live or in-progress session per `Agent`.
130    pub fn session(
131        &self,
132        workspace: impl Into<String>,
133        options: Option<SessionOptions>,
134    ) -> Result<AgentSession> {
135        agent_sessions::create_session(self, workspace, options)
136    }
137
138    /// Create a session pre-configured from an
139    /// [`AgentDefinition`](crate::subagent::AgentDefinition).
140    ///
141    /// Maps the definition's `permissions`, `prompt`, `model`, and `max_steps`
142    /// directly into [`SessionOptions`], so markdown/YAML-defined subagents can
143    /// be used by delegation and advanced control-plane flows without manual wiring.
144    ///
145    /// The mapping follows the same logic as the built-in `task` tool:
146    /// - `permissions` → `permission_checker`
147    /// - `prompt`      → `prompt_slots.extra`
148    /// - `max_steps`   → `max_tool_rounds`
149    /// - `model`       → `model` (as `"provider/model"` string)
150    ///
151    /// `extra` can supply additional overrides (e.g. `planning_enabled`) that
152    /// take precedence over the definition's values.
153    pub fn session_for_agent(
154        &self,
155        workspace: impl Into<String>,
156        def: &crate::subagent::AgentDefinition,
157        extra: Option<SessionOptions>,
158    ) -> Result<AgentSession> {
159        agent_sessions::create_session_for_agent(self, workspace, def, extra)
160    }
161
162    /// Async-first variant of [`session_for_agent`](Self::session_for_agent).
163    pub async fn session_for_agent_async(
164        &self,
165        workspace: impl Into<String>,
166        def: &crate::subagent::AgentDefinition,
167        extra: Option<SessionOptions>,
168    ) -> Result<AgentSession> {
169        agent_sessions::create_session_for_agent_async(self, workspace, def, extra).await
170    }
171
172    /// Create a session from a reproducible disposable worker recipe.
173    ///
174    /// This is the cattle-mode companion to [`Agent::session_for_agent`]: callers
175    /// provide a small [`WorkerAgentSpec`](crate::subagent::WorkerAgentSpec), and
176    /// A3S Code compiles it into the same runtime definition used by delegated agents.
177    pub fn session_for_worker(
178        &self,
179        workspace: impl Into<String>,
180        spec: crate::subagent::WorkerAgentSpec,
181        extra: Option<SessionOptions>,
182    ) -> Result<AgentSession> {
183        let def = spec.into_agent_definition();
184        self.session_for_agent(workspace, &def, extra)
185    }
186
187    /// Async-first variant of [`session_for_worker`](Self::session_for_worker).
188    pub async fn session_for_worker_async(
189        &self,
190        workspace: impl Into<String>,
191        spec: crate::subagent::WorkerAgentSpec,
192        extra: Option<SessionOptions>,
193    ) -> Result<AgentSession> {
194        let def = spec.into_agent_definition();
195        self.session_for_agent_async(workspace, &def, extra).await
196    }
197
198    /// Resume a previously saved session by ID.
199    ///
200    /// Loads the session data from the store, rebuilds the `AgentSession` with
201    /// the saved conversation history, and returns it ready for continued use.
202    ///
203    /// The `options` must include a `session_store` (or `with_file_session_store`)
204    /// that contains the saved session.
205    ///
206    /// The resumed session uses the **workspace stored in the snapshot**, not a
207    /// workspace from `options`. The store is therefore a trust boundary: its
208    /// contents drive the resumed workspace and the persisted runtime policies.
209    /// The requested ID must not already be live or under construction on this
210    /// `Agent`.
211    ///
212    /// This synchronous compatibility entry point returns
213    /// [`CodeError::AsyncSessionBuildRequired`](crate::error::CodeError::AsyncSessionBuildRequired);
214    /// use [`resume_session_async`](Self::resume_session_async).
215    pub fn resume_session(
216        &self,
217        session_id: &str,
218        options: SessionOptions,
219    ) -> Result<AgentSession> {
220        agent_sessions::resume_session(self, session_id, options)
221    }
222
223    /// Resume a persisted session without blocking the async runtime.
224    ///
225    /// The requested ID must not already be live or under construction on this
226    /// `Agent`.
227    pub async fn resume_session_async(
228        &self,
229        session_id: &str,
230        options: SessionOptions,
231    ) -> Result<AgentSession> {
232        agent_sessions::resume_session_async(self, session_id, options).await
233    }
234
235    /// Rebuild a live persisted session with new options without exposing a
236    /// closed-session gap to the caller.
237    ///
238    /// The current session is saved first and remains live while the
239    /// replacement is constructed and restored. If construction fails, the
240    /// current session stays registered and usable. On success, the registry
241    /// is switched to the replacement before the old session is closed.
242    ///
243    /// Callers must serialize this operation with conversation work on
244    /// `current` (for example, only reconfigure an idle interactive session).
245    /// The replacement keeps the same session ID and persisted history.
246    pub async fn replace_session_async(
247        &self,
248        current: &AgentSession,
249        options: SessionOptions,
250    ) -> Result<AgentSession> {
251        agent_sessions::replace_session_async(self, current, options).await
252    }
253
254    /// Return the IDs of every live session created from this agent.
255    ///
256    /// "Live" means the caller still holds an [`AgentSession`] — sessions
257    /// that have been dropped are pruned lazily on each call. The list is
258    /// sorted to make output stable for tests/UIs.
259    pub async fn list_sessions(&self) -> Vec<String> {
260        agent_sessions::list_sessions(self).await
261    }
262
263    /// Close a specific live session by its session ID.
264    ///
265    /// Returns `true` when a live session with the given id was found and
266    /// transitioned from open to closed by this call; `false` when no live
267    /// session has that id, or when the session was already closed.
268    ///
269    /// This is the out-of-band counterpart to [`AgentSession::close`]: it
270    /// performs exactly the same cleanup but can be invoked without holding
271    /// a reference to the session itself — useful for control-plane code
272    /// that only knows the session ID.
273    pub async fn close_session(&self, session_id: &str) -> bool {
274        agent_sessions::close_session(self, session_id).await
275    }
276
277    /// Close every live session created from this agent and tear down
278    /// background resources owned by the agent (global MCP connections).
279    ///
280    /// After this call:
281    /// - Every live `AgentSession` is closed (same effect as calling
282    ///   [`AgentSession::close`] on each).
283    /// - Subsequent [`Agent::session`] / [`Agent::resume_session`] calls
284    ///   fail fast with [`CodeError::SessionClosed`](crate::error::CodeError::SessionClosed).
285    /// - Session builds admitted before close but not yet finalized are rejected
286    ///   at finalization and cannot return an open session.
287    ///
288    /// Idempotent: subsequent calls are no-ops and are guaranteed not to
289    /// panic.
290    pub async fn close(&self) {
291        agent_sessions::close_agent(self).await
292    }
293
294    /// Return whether [`close`](Self::close) has been called on this agent.
295    pub fn is_closed(&self) -> bool {
296        self.closed.load(std::sync::atomic::Ordering::Acquire)
297    }
298
299    /// Disconnect every global MCP server whose last activity is older
300    /// than `idle_threshold_ms`. Returns the names of disconnected
301    /// servers (empty when there is no global MCP manager or when
302    /// nothing is idle).
303    ///
304    /// Hosts running thousands of long-lived sessions should call this
305    /// periodically (e.g. every 60s with a 5-min threshold) to release
306    /// file descriptors and background workers from quiet MCP servers
307    /// without losing the server's configuration. A subsequent tool
308    /// call on the same server will require an explicit reconnect.
309    pub async fn disconnect_idle_mcp(&self, idle_threshold_ms: u64) -> Vec<String> {
310        match &self.global_mcp {
311            Some(mcp) => mcp.disconnect_idle(idle_threshold_ms).await,
312            None => Vec::new(),
313        }
314    }
315
316    #[cfg(test)]
317    pub(super) fn build_session(
318        &self,
319        workspace: String,
320        llm_client: Arc<dyn LlmClient>,
321        opts: &SessionOptions,
322    ) -> Result<AgentSession> {
323        let mut opts = opts.clone().with_llm_client(llm_client);
324        if opts.memory_store.is_none() && opts.file_memory_dir.is_none() {
325            opts = opts.with_memory(Arc::new(a3s_memory::InMemoryStore::new()));
326        }
327        agent_sessions::create_session(self, workspace, Some(opts))
328    }
329}