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 usable by the next admitted Run in the same
77    /// Session; an already admitted Run retains its frozen registry.
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            for agent in &agents {
84                self.ensure_compatibility_name_available(
85                    crate::capability::CapabilityKind::Agent,
86                    &agent.name,
87                )?;
88            }
89            let count = agents.len();
90            for agent in agents {
91                tracing::info!(
92                    session_id = %self.session_id,
93                    agent = agent.name,
94                    dir = %dir.display(),
95                    "Dynamically registered agent"
96                );
97                self.agent_registry.register(agent);
98            }
99            Ok(count)
100        })?
101    }
102
103    /// Register a disposable worker agent with the live session.
104    ///
105    /// The returned definition enters the `task` lookup and the model-facing
106    /// `task` and `parallel_task` definitions on the next admitted Run. Callers
107    /// can create discoverable reproducible workers without writing temporary
108    /// agent files or restarting the Session, while an active Run remains
109    /// generation-stable.
110    pub fn register_worker_agent(
111        &self,
112        spec: crate::subagent::WorkerAgentSpec,
113    ) -> crate::error::Result<crate::subagent::AgentDefinition> {
114        self.close_handle.mutate_immediate(|| {
115            self.ensure_compatibility_name_available(
116                crate::capability::CapabilityKind::Agent,
117                &spec.name,
118            )?;
119            Ok(SessionExtensionRuntime::from_session(self).register_worker_agent(spec))
120        })?
121    }
122
123    /// Register multiple disposable worker agents with the live session.
124    pub fn register_worker_agents<I>(
125        &self,
126        specs: I,
127    ) -> crate::error::Result<Vec<crate::subagent::AgentDefinition>>
128    where
129        I: IntoIterator<Item = crate::subagent::WorkerAgentSpec>,
130    {
131        let specs = specs.into_iter().collect::<Vec<_>>();
132        self.close_handle.mutate_immediate(|| {
133            for spec in &specs {
134                self.ensure_compatibility_name_available(
135                    crate::capability::CapabilityKind::Agent,
136                    &spec.name,
137                )?;
138            }
139            Ok(SessionExtensionRuntime::from_session(self).register_worker_agents(specs))
140        })?
141    }
142
143    /// Add or replace a skill in this live session.
144    ///
145    /// The Skill and `search_skills` tools observe the new definition
146    /// immediately, and the model-visible skills catalog observes it on the
147    /// next turn. Removing the live definition restores any session skill it
148    /// shadowed at installation time.
149    pub fn add_skill(&self, skill: Arc<crate::skills::Skill>) -> crate::error::Result<()> {
150        self.close_handle.mutate_immediate(|| {
151            self.ensure_compatibility_name_available(
152                crate::capability::CapabilityKind::Skill,
153                &skill.name,
154            )?;
155            SessionExtensionRuntime::from_session(self).add_skill(skill)
156        })?
157    }
158
159    /// Remove a skill previously installed through [`Self::add_skill`].
160    ///
161    /// This is a no-op when the name is not owned by the live session API; base
162    /// session skills and later host registrations are never removed.
163    pub fn remove_skill(&self, name: &str) -> crate::error::Result<()> {
164        self.close_handle
165            .mutate_immediate(|| SessionExtensionRuntime::from_session(self).remove_skill(name))
166    }
167
168    /// Return the names in the session's current live skill registry.
169    pub fn skill_names(&self) -> Vec<String> {
170        self.close_handle.skill_registry.list()
171    }
172
173    /// Add an MCP server to this session.
174    ///
175    /// Registers, connects, and makes all tools immediately available for the
176    /// agent to call. Tool names follow the convention `mcp__<name>__<tool>`.
177    ///
178    /// Returns the number of tools registered from the server.
179    pub async fn add_mcp_server(
180        &self,
181        config: crate::mcp::McpServerConfig,
182    ) -> crate::error::Result<usize> {
183        SessionExtensionRuntime::from_session(self)
184            .add_mcp_server(config)
185            .await
186    }
187
188    /// The session's tool executor, for installing agent-dir `tools/` entries
189    /// (e.g. a `kind = "script"` tool) into the live registry. Internal seam used
190    /// by [`serve::install_agent_dir_tools`](crate::serve::install_agent_dir_tools)
191    /// (the only caller, hence the `serve` gate).
192    #[cfg(feature = "serve")]
193    pub(crate) fn tool_executor(&self) -> &Arc<crate::tools::ToolExecutor> {
194        &self.tool_executor
195    }
196
197    /// Register a host-provided dynamic tool into the live session. Enables an
198    /// embedding app (e.g. the a3s-code CLI's login-gated `runtime` A3S Runtime
199    /// offload tool) to add a native tool at runtime; it enters the LLM's toolset
200    /// on the next run (`build_agent_loop` re-snapshots `definitions()` per run),
201    /// the same way MCP tools surface after `add_mcp_server`. Idempotent by name.
202    pub fn register_dynamic_tool(
203        &self,
204        tool: Arc<dyn crate::tools::Tool>,
205    ) -> crate::error::Result<()> {
206        self.close_handle.mutate_immediate(|| {
207            self.ensure_compatibility_name_available(
208                crate::capability::CapabilityKind::Tool,
209                tool.name(),
210            )?;
211            self.tool_executor.register_dynamic_tool(tool);
212            Ok(())
213        })?
214    }
215
216    /// Register the A3S Flow-backed dynamic workflow tool for this live session.
217    ///
218    /// The tool is named `dynamic_workflow`. It accepts a sandboxed JavaScript
219    /// PTC workflow script and executes it through
220    /// [`crate::DynamicWorkflowRuntime`], so A3S Flow owns workflow replay while
221    /// the script can still call A3S Code tools.
222    pub fn register_dynamic_workflow_runtime(&self) -> crate::error::Result<()> {
223        self.close_handle.mutate_immediate(|| {
224            self.ensure_compatibility_name_available(
225                crate::capability::CapabilityKind::Tool,
226                "dynamic_workflow",
227            )?;
228            crate::tools::register_dynamic_workflow(self.tool_executor.registry());
229            Ok(())
230        })?
231    }
232
233    /// Remove a previously host-registered dynamic tool by name (e.g. on logout).
234    /// No-op if no tool of that name is registered.
235    pub fn unregister_dynamic_tool(&self, name: &str) -> crate::error::Result<()> {
236        self.close_handle
237            .mutate_immediate(|| self.tool_executor.unregister_dynamic_tool(name))
238    }
239
240    /// Remove an MCP server from this session.
241    ///
242    /// Disconnects the server and unregisters all its tools from the executor.
243    /// No-op if the server was never added.
244    pub async fn remove_mcp_server(&self, server_name: &str) -> crate::error::Result<()> {
245        SessionExtensionRuntime::from_session(self)
246            .remove_mcp_server(server_name)
247            .await
248    }
249
250    /// Return current projected and compatibility MCP server status.
251    pub async fn mcp_status(
252        &self,
253    ) -> std::collections::HashMap<String, crate::mcp::McpServerStatus> {
254        SessionExtensionRuntime::from_session(self)
255            .mcp_status()
256            .await
257    }
258
259    /// Return the exact generation and digest currently visible to new Runs.
260    pub fn capability_catalog_stamp(&self) -> crate::capability::CapabilityCatalogStamp {
261        self.capability_catalog.current_stamp()
262    }
263
264    /// Verify that a new Run on this Session would receive the exact persisted
265    /// scoped capability catalog and authority ceiling.
266    pub fn ensure_recovery_capability_binding(
267        &self,
268        expected: &crate::capability::RunCapabilityBindingV1,
269    ) -> std::result::Result<(), crate::capability::RunCapabilityBindingError> {
270        super::agent_loop_runtime::validate_run_capability_binding(self, expected)
271    }
272
273    /// Prepare and atomically publish one complete host capability generation.
274    ///
275    /// Preparation may perform asynchronous work, but no value becomes visible
276    /// until every adapter has succeeded and the complete projection wins its
277    /// generation-and-digest compare-and-swap. A Use-backed batch publishes its
278    /// generation-specific lease provider in that same commit.
279    pub async fn apply_capability_batch(
280        &self,
281        batch: crate::capability::SessionCapabilityBatch,
282        cancellation: tokio_util::sync::CancellationToken,
283    ) -> std::result::Result<
284        crate::capability::CapabilityCommitReceipt,
285        crate::capability::CapabilityRuntimeError,
286    > {
287        let _mutation = self.close_handle.extension_mutation.lock().await;
288        if self.is_closed() {
289            return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
290        }
291
292        let preparation_cancellation = tokio_util::sync::CancellationToken::new();
293        let prepared = tokio::select! {
294            biased;
295            _ = self.session_cancel.cancelled() => {
296                preparation_cancellation.cancel();
297                return Err(if self.is_closed() {
298                    crate::capability::CapabilityRuntimeError::SessionClosed
299                } else {
300                    crate::capability::CapabilityRuntimeError::Cancelled
301                });
302            }
303            _ = cancellation.cancelled() => {
304                preparation_cancellation.cancel();
305                return Err(crate::capability::CapabilityRuntimeError::Cancelled);
306            }
307            result = batch.prepare(
308                &self.capability_catalog,
309                preparation_cancellation.clone(),
310            ) => result?,
311        };
312        self.ensure_projected_mcp_server_names_available(prepared.projection()?)
313            .await?;
314
315        // The close boundary and Run pinning use this same short mutex. The
316        // prepared transaction holds no registry write lock while waiting.
317        let _publication = self
318            .close_handle
319            .immediate_extension_mutation
320            .lock()
321            .unwrap_or_else(std::sync::PoisonError::into_inner);
322        if self.is_closed() {
323            return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
324        }
325        if cancellation.is_cancelled() || self.session_cancel.is_cancelled() {
326            return Err(crate::capability::CapabilityRuntimeError::Cancelled);
327        }
328        // The legacy public registry guard does not participate in the
329        // Session mutation gate. Keep its lock through catalog publication so
330        // direct mutation linearizes wholly before validation or after CAS.
331        let command_registry = self
332            .command_registry
333            .lock()
334            .unwrap_or_else(std::sync::PoisonError::into_inner);
335        let current_projection = self.capability_catalog.pin();
336        super::agent_loop_runtime::validate_capability_projection_runtime(
337            self,
338            prepared.projection()?,
339            &command_registry,
340        )?;
341        super::agent_loop_runtime::validate_capability_projection_transition(
342            self,
343            current_projection.projection(),
344            prepared.projection()?,
345        )?;
346        prepared.commit()
347    }
348
349    /// Reconstruct one exact historical capability generation on an untouched
350    /// recovery Session.
351    ///
352    /// Unlike ordinary [`Self::apply_capability_batch`], this one-time path may
353    /// jump directly from the empty generation-zero catalog to the generation
354    /// named by `expected`. It never resolves packages or `latest`: the host
355    /// supplies every runtime adapter and any exact A3S Use lease provider in
356    /// `batch`, and Code verifies the resulting catalog plus authority ceiling
357    /// before publication.
358    pub async fn bootstrap_recovery_capability_batch(
359        &self,
360        expected: &crate::capability::RunCapabilityBindingV1,
361        batch: crate::capability::SessionCapabilityBatch,
362        cancellation: tokio_util::sync::CancellationToken,
363    ) -> std::result::Result<
364        crate::capability::CapabilityCommitReceipt,
365        crate::capability::CapabilityRuntimeError,
366    > {
367        let _mutation = self.close_handle.extension_mutation.lock().await;
368        if self.is_closed() {
369            return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
370        }
371
372        let target_ceiling = self.capability_run_ceiling(batch.target())?;
373        expected
374            .ensure_matches(batch.target(), &target_ceiling)
375            .map_err(
376                |error| crate::capability::CapabilityRuntimeError::RecoveryBinding {
377                    message: error.to_string(),
378                },
379            )?;
380
381        let preparation_cancellation = tokio_util::sync::CancellationToken::new();
382        let prepared = tokio::select! {
383            biased;
384            _ = self.session_cancel.cancelled() => {
385                preparation_cancellation.cancel();
386                return Err(if self.is_closed() {
387                    crate::capability::CapabilityRuntimeError::SessionClosed
388                } else {
389                    crate::capability::CapabilityRuntimeError::Cancelled
390                });
391            }
392            _ = cancellation.cancelled() => {
393                preparation_cancellation.cancel();
394                return Err(crate::capability::CapabilityRuntimeError::Cancelled);
395            }
396            result = batch.prepare_recovery_bootstrap(
397                &self.capability_catalog,
398                preparation_cancellation.clone(),
399            ) => result?,
400        };
401        self.ensure_projected_mcp_server_names_available(prepared.projection()?)
402            .await?;
403
404        let _publication = self
405            .close_handle
406            .immediate_extension_mutation
407            .lock()
408            .unwrap_or_else(std::sync::PoisonError::into_inner);
409        if self.is_closed() {
410            return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
411        }
412        if cancellation.is_cancelled() || self.session_cancel.is_cancelled() {
413            return Err(crate::capability::CapabilityRuntimeError::Cancelled);
414        }
415        let command_registry = self
416            .command_registry
417            .lock()
418            .unwrap_or_else(std::sync::PoisonError::into_inner);
419        let current_projection = self.capability_catalog.pin();
420        super::agent_loop_runtime::validate_capability_projection_runtime(
421            self,
422            prepared.projection()?,
423            &command_registry,
424        )?;
425        super::agent_loop_runtime::validate_capability_projection_transition(
426            self,
427            current_projection.projection(),
428            prepared.projection()?,
429        )?;
430        prepared.commit()
431    }
432
433    /// Drain prepared effects from failed or retired host generations.
434    pub async fn drain_capability_cleanup(&self) -> crate::capability::CapabilityCleanupReport {
435        self.capability_catalog.drain_cleanup().await
436    }
437
438    #[cfg(test)]
439    pub(crate) async fn admit_capability_run(
440        &self,
441    ) -> std::result::Result<
442        crate::capability::SessionCapabilityRun,
443        crate::capability::CapabilityRuntimeError,
444    > {
445        let projection = {
446            let _admission = self
447                .close_handle
448                .immediate_extension_mutation
449                .lock()
450                .unwrap_or_else(std::sync::PoisonError::into_inner);
451            if self.is_closed() {
452                return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
453            }
454            self.capability_catalog.pin()
455        };
456        let ceiling = self.capability_run_ceiling(projection.projection().set())?;
457        crate::capability::SessionCapabilityRun::admit(
458            projection,
459            "active",
460            "active",
461            ceiling,
462            self.session_cancel.child_token(),
463        )
464        .await
465    }
466
467    pub(super) fn capability_run_ceiling(
468        &self,
469        set: &crate::capability::CapabilitySet,
470    ) -> std::result::Result<
471        crate::capability::CapabilityCeiling,
472        crate::capability::CapabilityRuntimeError,
473    > {
474        let mut governance = crate::capability::GovernanceCapabilityCeiling::none_required();
475        if self.config.permission_checker.is_some() || self.config.permission_policy.is_some() {
476            governance = governance.require_permission_guard();
477        }
478        if self.config.confirmation_manager.is_some() || self.config.confirmation_policy.is_some() {
479            governance = governance.require_confirmation_guard();
480        }
481        if self.config.security_provider.is_some() {
482            governance = governance.require_security_guard();
483        }
484        if self.config.budget_guard.is_some() || self.budget_guard().is_some() {
485            governance = governance.require_budget_guard();
486        }
487        if self.config.enforce_active_skill_tool_restrictions {
488            governance = governance.require_active_skill_restrictions();
489        }
490        let execution = crate::capability::CapabilityExecutionCeiling::new(
491            self.config.max_tool_rounds,
492            self.config.max_parallel_tasks,
493            self.config.tool_timeout_ms,
494            self.config.llm_api_timeout_ms,
495            self.config.max_execution_time_ms,
496        )?;
497        crate::capability::CapabilityCeiling::all(
498            set,
499            crate::capability::WorkspaceCapabilityCeiling::all(),
500            governance,
501            execution,
502        )
503        .map_err(Into::into)
504    }
505
506    pub(super) fn ensure_compatibility_name_available(
507        &self,
508        kind: crate::capability::CapabilityKind,
509        public_name: &str,
510    ) -> crate::error::Result<()> {
511        let projection = self.capability_catalog.pin();
512        if projection
513            .projection()
514            .iter()
515            .any(|(_, value)| match value {
516                crate::capability::CapabilityValue::Mcp(binding)
517                    if kind == crate::capability::CapabilityKind::Tool =>
518                {
519                    binding.contains_public_tool_name(public_name)
520                }
521                crate::capability::CapabilityValue::Agent(agent) => {
522                    kind == crate::capability::CapabilityKind::Agent
523                        && crate::subagent::agent_names_conflict(&agent.name, public_name)
524                }
525                _ => value.kind() == kind && value.public_name() == Some(public_name),
526            })
527        {
528            return Err(
529                crate::capability::CapabilityRuntimeError::RuntimeNameConflict {
530                    kind,
531                    public_name: public_name.to_owned(),
532                }
533                .into(),
534            );
535        }
536        Ok(())
537    }
538
539    async fn ensure_projected_mcp_server_names_available(
540        &self,
541        projection: &crate::capability::CapabilityProjection,
542    ) -> std::result::Result<(), crate::capability::CapabilityRuntimeError> {
543        let server_names = projection
544            .iter()
545            .filter_map(|(_, value)| match value {
546                crate::capability::CapabilityValue::Mcp(binding) => {
547                    Some(binding.server_name().to_owned())
548                }
549                _ => None,
550            })
551            .collect::<Vec<_>>();
552        for server_name in server_names {
553            for manager in &self.mcp_managers {
554                if manager.contains_server(&server_name).await {
555                    return Err(
556                        crate::capability::CapabilityRuntimeError::RuntimeNameConflict {
557                            kind: crate::capability::CapabilityKind::Mcp,
558                            public_name: server_name,
559                        },
560                    );
561                }
562            }
563        }
564        Ok(())
565    }
566}