Skip to main content

a3s_code_core/agent_api/
capability_facade.rs

1use super::*;
2
3impl AgentSession {
4    // Advanced optional Queue API
5    // ========================================================================
6
7    /// Returns whether this session has an advanced lane queue configured.
8    pub fn has_queue(&self) -> bool {
9        QueueControl::from_session(self).has_queue()
10    }
11
12    /// Configure a lane's handler mode for explicit external/hybrid dispatch.
13    ///
14    /// Only effective when a queue is configured via `SessionOptions::with_queue_config`.
15    pub async fn set_lane_handler(
16        &self,
17        lane: SessionLane,
18        config: LaneHandlerConfig,
19    ) -> crate::error::Result<()> {
20        let _mutation = self.close_handle.extension_mutation.lock().await;
21        if self.is_closed() {
22            return Err(crate::error::CodeError::SessionClosed {
23                session_id: self.session_id.clone(),
24            });
25        }
26        QueueControl::from_session(self)
27            .set_lane_handler(lane, config)
28            .await;
29        if self.is_closed() {
30            return Err(crate::error::CodeError::SessionClosed {
31                session_id: self.session_id.clone(),
32            });
33        }
34        Ok(())
35    }
36
37    /// Complete an external queue task by ID.
38    ///
39    /// Returns `true` if the task was found and completed, `false` if not found.
40    pub async fn complete_external_task(&self, task_id: &str, result: ExternalTaskResult) -> bool {
41        QueueControl::from_session(self)
42            .complete_external_task(task_id, result)
43            .await
44    }
45
46    /// Get pending external queue tasks awaiting completion by an external handler.
47    pub async fn pending_external_tasks(&self) -> Vec<ExternalTask> {
48        QueueControl::from_session(self)
49            .pending_external_tasks()
50            .await
51    }
52
53    /// Get optional queue statistics (pending, active, external counts per lane).
54    pub async fn queue_stats(&self) -> SessionQueueStats {
55        QueueControl::from_session(self).stats().await
56    }
57
58    /// Get a metrics snapshot from the optional queue (if metrics are enabled).
59    pub async fn queue_metrics(&self) -> Option<MetricsSnapshot> {
60        QueueControl::from_session(self).metrics().await
61    }
62
63    /// Get dead letters from the optional queue's DLQ (if DLQ is enabled).
64    pub async fn dead_letters(&self) -> Vec<DeadLetter> {
65        QueueControl::from_session(self).dead_letters().await
66    }
67
68    // ========================================================================
69    // MCP API
70    // ========================================================================
71
72    /// Register all agents found in a directory with the live session.
73    ///
74    /// Scans `dir` for `*.yaml`, `*.yml`, and `*.md` agent definition files,
75    /// parses them, and adds each one to the shared `AgentRegistry` used by the
76    /// `task` tool.  New agents are immediately usable via `task(agent="…")` in
77    /// the same session — no restart required.
78    ///
79    /// Returns the number of agents successfully loaded from the directory.
80    pub fn register_agent_dir(&self, dir: &std::path::Path) -> crate::error::Result<usize> {
81        let agents = crate::subagent::load_agents_from_dir(dir);
82        self.close_handle.mutate_immediate(|| {
83            let count = agents.len();
84            for agent in agents {
85                tracing::info!(
86                    session_id = %self.session_id,
87                    agent = agent.name,
88                    dir = %dir.display(),
89                    "Dynamically registered agent"
90                );
91                self.agent_registry.register(agent);
92            }
93            count
94        })
95    }
96
97    /// Register a disposable worker agent with the live session.
98    ///
99    /// The returned definition is immediately available to the `task` tool by
100    /// worker name. Its canonical name and purpose also enter the model-facing
101    /// `task` and `parallel_task` definitions on the next run, so callers can
102    /// create discoverable reproducible workers without writing temporary agent
103    /// files or restarting the session.
104    pub fn register_worker_agent(
105        &self,
106        spec: crate::subagent::WorkerAgentSpec,
107    ) -> crate::error::Result<crate::subagent::AgentDefinition> {
108        self.close_handle.mutate_immediate(|| {
109            SessionExtensionRuntime::from_session(self).register_worker_agent(spec)
110        })
111    }
112
113    /// Register multiple disposable worker agents with the live session.
114    pub fn register_worker_agents<I>(
115        &self,
116        specs: I,
117    ) -> crate::error::Result<Vec<crate::subagent::AgentDefinition>>
118    where
119        I: IntoIterator<Item = crate::subagent::WorkerAgentSpec>,
120    {
121        self.close_handle.mutate_immediate(|| {
122            SessionExtensionRuntime::from_session(self).register_worker_agents(specs)
123        })
124    }
125
126    /// Add or replace a skill in this live session.
127    ///
128    /// The Skill and `search_skills` tools observe the new definition
129    /// immediately, and the model-visible skills catalog observes it on the
130    /// next turn. Removing the live definition restores any session skill it
131    /// shadowed at installation time.
132    pub fn add_skill(&self, skill: Arc<crate::skills::Skill>) -> crate::error::Result<()> {
133        self.close_handle
134            .mutate_immediate(|| SessionExtensionRuntime::from_session(self).add_skill(skill))?
135    }
136
137    /// Remove a skill previously installed through [`Self::add_skill`].
138    ///
139    /// This is a no-op when the name is not owned by the live session API; base
140    /// session skills and later host registrations are never removed.
141    pub fn remove_skill(&self, name: &str) -> crate::error::Result<()> {
142        self.close_handle
143            .mutate_immediate(|| SessionExtensionRuntime::from_session(self).remove_skill(name))
144    }
145
146    /// Return the names in the session's current live skill registry.
147    pub fn skill_names(&self) -> Vec<String> {
148        self.close_handle.skill_registry.list()
149    }
150
151    /// Add an MCP server to this session.
152    ///
153    /// Registers, connects, and makes all tools immediately available for the
154    /// agent to call. Tool names follow the convention `mcp__<name>__<tool>`.
155    ///
156    /// Returns the number of tools registered from the server.
157    pub async fn add_mcp_server(
158        &self,
159        config: crate::mcp::McpServerConfig,
160    ) -> crate::error::Result<usize> {
161        SessionExtensionRuntime::from_session(self)
162            .add_mcp_server(config)
163            .await
164    }
165
166    /// The session's tool executor, for installing agent-dir `tools/` entries
167    /// (e.g. a `kind = "script"` tool) into the live registry. Internal seam used
168    /// by [`serve::install_agent_dir_tools`](crate::serve::install_agent_dir_tools)
169    /// (the only caller, hence the `serve` gate).
170    #[cfg(feature = "serve")]
171    pub(crate) fn tool_executor(&self) -> &Arc<crate::tools::ToolExecutor> {
172        &self.tool_executor
173    }
174
175    /// Register a host-provided dynamic tool into the live session. Enables an
176    /// embedding app (e.g. the a3s-code CLI's login-gated `runtime` A3S Runtime
177    /// offload tool) to add a native tool at runtime; it enters the LLM's toolset
178    /// on the next run (`build_agent_loop` re-snapshots `definitions()` per run),
179    /// the same way MCP tools surface after `add_mcp_server`. Idempotent by name.
180    pub fn register_dynamic_tool(
181        &self,
182        tool: Arc<dyn crate::tools::Tool>,
183    ) -> crate::error::Result<()> {
184        self.close_handle
185            .mutate_immediate(|| self.tool_executor.register_dynamic_tool(tool))
186    }
187
188    /// Register the A3S Flow-backed dynamic workflow tool for this live session.
189    ///
190    /// The tool is named `dynamic_workflow`. It accepts a sandboxed JavaScript
191    /// PTC workflow script and executes it through
192    /// [`crate::DynamicWorkflowRuntime`], so A3S Flow owns workflow replay while
193    /// the script can still call A3S Code tools.
194    pub fn register_dynamic_workflow_runtime(&self) -> crate::error::Result<()> {
195        self.close_handle.mutate_immediate(|| {
196            crate::tools::register_dynamic_workflow(self.tool_executor.registry())
197        })
198    }
199
200    /// Remove a previously host-registered dynamic tool by name (e.g. on logout).
201    /// No-op if no tool of that name is registered.
202    pub fn unregister_dynamic_tool(&self, name: &str) -> crate::error::Result<()> {
203        self.close_handle
204            .mutate_immediate(|| self.tool_executor.unregister_dynamic_tool(name))
205    }
206
207    /// Remove an MCP server from this session.
208    ///
209    /// Disconnects the server and unregisters all its tools from the executor.
210    /// No-op if the server was never added.
211    pub async fn remove_mcp_server(&self, server_name: &str) -> crate::error::Result<()> {
212        SessionExtensionRuntime::from_session(self)
213            .remove_mcp_server(server_name)
214            .await
215    }
216
217    /// Return the connection status of all MCP servers registered with this session.
218    pub async fn mcp_status(
219        &self,
220    ) -> std::collections::HashMap<String, crate::mcp::McpServerStatus> {
221        SessionExtensionRuntime::from_session(self)
222            .mcp_status()
223            .await
224    }
225}