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 [`AgentDefinition`].
139 ///
140 /// Maps the definition's `permissions`, `prompt`, `model`, and `max_steps`
141 /// directly into [`SessionOptions`], so markdown/YAML-defined subagents can
142 /// be used by delegation and advanced control-plane flows without manual wiring.
143 ///
144 /// The mapping follows the same logic as the built-in `task` tool:
145 /// - `permissions` → `permission_checker`
146 /// - `prompt` → `prompt_slots.extra`
147 /// - `max_steps` → `max_tool_rounds`
148 /// - `model` → `model` (as `"provider/model"` string)
149 ///
150 /// `extra` can supply additional overrides (e.g. `planning_enabled`) that
151 /// take precedence over the definition's values.
152 pub fn session_for_agent(
153 &self,
154 workspace: impl Into<String>,
155 def: &crate::subagent::AgentDefinition,
156 extra: Option<SessionOptions>,
157 ) -> Result<AgentSession> {
158 agent_sessions::create_session_for_agent(self, workspace, def, extra)
159 }
160
161 /// Async-first variant of [`session_for_agent`](Self::session_for_agent).
162 pub async fn session_for_agent_async(
163 &self,
164 workspace: impl Into<String>,
165 def: &crate::subagent::AgentDefinition,
166 extra: Option<SessionOptions>,
167 ) -> Result<AgentSession> {
168 agent_sessions::create_session_for_agent_async(self, workspace, def, extra).await
169 }
170
171 /// Create a session from a reproducible disposable worker recipe.
172 ///
173 /// This is the cattle-mode companion to [`Agent::session_for_agent`]: callers
174 /// provide a small [`WorkerAgentSpec`](crate::subagent::WorkerAgentSpec), and
175 /// A3S Code compiles it into the same runtime definition used by delegated agents.
176 pub fn session_for_worker(
177 &self,
178 workspace: impl Into<String>,
179 spec: crate::subagent::WorkerAgentSpec,
180 extra: Option<SessionOptions>,
181 ) -> Result<AgentSession> {
182 let def = spec.into_agent_definition();
183 self.session_for_agent(workspace, &def, extra)
184 }
185
186 /// Async-first variant of [`session_for_worker`](Self::session_for_worker).
187 pub async fn session_for_worker_async(
188 &self,
189 workspace: impl Into<String>,
190 spec: crate::subagent::WorkerAgentSpec,
191 extra: Option<SessionOptions>,
192 ) -> Result<AgentSession> {
193 let def = spec.into_agent_definition();
194 self.session_for_agent_async(workspace, &def, extra).await
195 }
196
197 /// Resume a previously saved session by ID.
198 ///
199 /// Loads the session data from the store, rebuilds the `AgentSession` with
200 /// the saved conversation history, and returns it ready for continued use.
201 ///
202 /// The `options` must include a `session_store` (or `with_file_session_store`)
203 /// that contains the saved session.
204 ///
205 /// The resumed session uses the **workspace stored in the snapshot**, not a
206 /// workspace from `options`. The store is therefore a trust boundary: its
207 /// contents drive the resumed workspace and the persisted runtime policies.
208 /// The requested ID must not already be live or under construction on this
209 /// `Agent`.
210 ///
211 /// This synchronous compatibility entry point returns
212 /// [`CodeError::AsyncSessionBuildRequired`](crate::error::CodeError::AsyncSessionBuildRequired);
213 /// use [`resume_session_async`](Self::resume_session_async).
214 pub fn resume_session(
215 &self,
216 session_id: &str,
217 options: SessionOptions,
218 ) -> Result<AgentSession> {
219 agent_sessions::resume_session(self, session_id, options)
220 }
221
222 /// Resume a persisted session without blocking the async runtime.
223 ///
224 /// The requested ID must not already be live or under construction on this
225 /// `Agent`.
226 pub async fn resume_session_async(
227 &self,
228 session_id: &str,
229 options: SessionOptions,
230 ) -> Result<AgentSession> {
231 agent_sessions::resume_session_async(self, session_id, options).await
232 }
233
234 /// Return the IDs of every live session created from this agent.
235 ///
236 /// "Live" means the caller still holds an [`AgentSession`] — sessions
237 /// that have been dropped are pruned lazily on each call. The list is
238 /// sorted to make output stable for tests/UIs.
239 pub async fn list_sessions(&self) -> Vec<String> {
240 agent_sessions::list_sessions(self).await
241 }
242
243 /// Close a specific live session by its session ID.
244 ///
245 /// Returns `true` when a live session with the given id was found and
246 /// transitioned from open to closed by this call; `false` when no live
247 /// session has that id, or when the session was already closed.
248 ///
249 /// This is the out-of-band counterpart to [`AgentSession::close`]: it
250 /// performs exactly the same cleanup but can be invoked without holding
251 /// a reference to the session itself — useful for control-plane code
252 /// that only knows the session ID.
253 pub async fn close_session(&self, session_id: &str) -> bool {
254 agent_sessions::close_session(self, session_id).await
255 }
256
257 /// Close every live session created from this agent and tear down
258 /// background resources owned by the agent (global MCP connections).
259 ///
260 /// After this call:
261 /// - Every live `AgentSession` is closed (same effect as calling
262 /// [`AgentSession::close`] on each).
263 /// - Subsequent [`Agent::session`] / [`Agent::resume_session`] calls
264 /// fail fast with [`CodeError::SessionClosed`](crate::error::CodeError::SessionClosed).
265 /// - Session builds admitted before close but not yet finalized are rejected
266 /// at finalization and cannot return an open session.
267 ///
268 /// Idempotent: subsequent calls are no-ops and are guaranteed not to
269 /// panic.
270 pub async fn close(&self) {
271 agent_sessions::close_agent(self).await
272 }
273
274 /// Return whether [`close`](Self::close) has been called on this agent.
275 pub fn is_closed(&self) -> bool {
276 self.closed.load(std::sync::atomic::Ordering::Acquire)
277 }
278
279 /// Disconnect every global MCP server whose last activity is older
280 /// than `idle_threshold_ms`. Returns the names of disconnected
281 /// servers (empty when there is no global MCP manager or when
282 /// nothing is idle).
283 ///
284 /// Hosts running thousands of long-lived sessions should call this
285 /// periodically (e.g. every 60s with a 5-min threshold) to release
286 /// file descriptors and background workers from quiet MCP servers
287 /// without losing the server's configuration. A subsequent tool
288 /// call on the same server will require an explicit reconnect.
289 pub async fn disconnect_idle_mcp(&self, idle_threshold_ms: u64) -> Vec<String> {
290 match &self.global_mcp {
291 Some(mcp) => mcp.disconnect_idle(idle_threshold_ms).await,
292 None => Vec::new(),
293 }
294 }
295
296 #[cfg(test)]
297 pub(super) fn build_session(
298 &self,
299 workspace: String,
300 llm_client: Arc<dyn LlmClient>,
301 opts: &SessionOptions,
302 ) -> Result<AgentSession> {
303 let mut opts = opts.clone().with_llm_client(llm_client);
304 if opts.memory_store.is_none() && opts.file_memory_dir.is_none() {
305 opts = opts.with_memory(Arc::new(a3s_memory::InMemoryStore::new()));
306 }
307 agent_sessions::create_session(self, workspace, Some(opts))
308 }
309}