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` 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    /// Register a host-provided dynamic tool into the live session. Enables an
189    /// embedding app (e.g. the a3s-code CLI's login-gated `runtime` A3S Runtime
190    /// offload tool) to add a native tool at runtime; it enters the LLM's toolset
191    /// on the next run (`build_agent_loop` re-snapshots `definitions()` per run),
192    /// the same way MCP tools surface after `add_mcp_server`. Idempotent by name.
193    pub fn register_dynamic_tool(
194        &self,
195        tool: Arc<dyn crate::tools::Tool>,
196    ) -> crate::error::Result<()> {
197        self.close_handle.mutate_immediate(|| {
198            self.ensure_compatibility_name_available(
199                crate::capability::CapabilityKind::Tool,
200                tool.name(),
201            )?;
202            self.tool_executor.register_dynamic_tool(tool);
203            Ok(())
204        })?
205    }
206
207    /// Register the A3S Flow-backed dynamic workflow tool for this live session.
208    ///
209    /// The tool is named `dynamic_workflow`. It accepts a sandboxed JavaScript
210    /// PTC workflow script and executes it through
211    /// [`crate::DynamicWorkflowRuntime`], so A3S Flow owns workflow replay while
212    /// the script can still call A3S Code tools.
213    #[cfg(feature = "dynamic-workflow")]
214    pub fn register_dynamic_workflow_runtime(&self) -> crate::error::Result<()> {
215        self.close_handle.mutate_immediate(|| {
216            self.ensure_compatibility_name_available(
217                crate::capability::CapabilityKind::Tool,
218                "dynamic_workflow",
219            )?;
220            crate::tools::register_dynamic_workflow(self.tool_executor.registry());
221            Ok(())
222        })?
223    }
224
225    /// Coding-only builds omit the dynamic workflow module; keep the method so
226    /// hosts fail closed instead of losing the API surface at compile time.
227    #[cfg(not(feature = "dynamic-workflow"))]
228    pub fn register_dynamic_workflow_runtime(&self) -> crate::error::Result<()> {
229        self.close_handle.mutate_immediate(|| {
230            Err(crate::error::CodeError::Config(
231                "dynamic_workflow requires the advanced-harness / dynamic-workflow feature".into(),
232            ))
233        })?
234    }
235
236    /// Remove a previously host-registered dynamic tool by name (e.g. on logout).
237    /// No-op if no tool of that name is registered.
238    pub fn unregister_dynamic_tool(&self, name: &str) -> crate::error::Result<()> {
239        self.close_handle
240            .mutate_immediate(|| self.tool_executor.unregister_dynamic_tool(name))
241    }
242
243    /// Remove an MCP server from this session.
244    ///
245    /// Disconnects the server and unregisters all its tools from the executor.
246    /// No-op if the server was never added.
247    pub async fn remove_mcp_server(&self, server_name: &str) -> crate::error::Result<()> {
248        SessionExtensionRuntime::from_session(self)
249            .remove_mcp_server(server_name)
250            .await
251    }
252
253    /// Rebuild executor tool registrations from inherited MCP managers.
254    ///
255    /// Call after [`Agent::sync_global_mcp_servers`] so live sessions pick up
256    /// added/removed/updated global connectors without a process restart.
257    /// Session-local servers installed via [`Self::add_mcp_server`] are left
258    /// untouched.
259    pub async fn republish_inherited_mcp_tools(&self) -> crate::error::Result<()> {
260        SessionExtensionRuntime::from_session(self)
261            .republish_inherited_mcp_tools()
262            .await
263    }
264
265    /// Return whether this session inherits at least one shared MCP manager
266    /// (typically the agent's global manager).
267    pub fn inherits_mcp_managers(&self) -> bool {
268        !self.inherited_mcp_managers.is_empty()
269    }
270
271    /// Return current projected and compatibility MCP server status.
272    pub async fn mcp_status(
273        &self,
274    ) -> std::collections::HashMap<String, crate::mcp::McpServerStatus> {
275        SessionExtensionRuntime::from_session(self)
276            .mcp_status()
277            .await
278    }
279
280    /// Return the exact generation and digest currently visible to new Runs.
281    pub fn capability_catalog_stamp(&self) -> crate::capability::CapabilityCatalogStamp {
282        self.capability_catalog.current_stamp()
283    }
284
285    /// Verify that a new Run on this Session would receive the exact persisted
286    /// scoped capability catalog and authority ceiling.
287    pub fn ensure_recovery_capability_binding(
288        &self,
289        expected: &crate::capability::RunCapabilityBindingV1,
290    ) -> std::result::Result<(), crate::capability::RunCapabilityBindingError> {
291        super::agent_loop_runtime::validate_run_capability_binding(self, expected)
292    }
293
294    /// Prepare and atomically publish one complete host capability generation.
295    ///
296    /// Preparation may perform asynchronous work, but no value becomes visible
297    /// until every adapter has succeeded and the complete projection wins its
298    /// generation-and-digest compare-and-swap. A Use-backed batch publishes its
299    /// generation-specific lease provider in that same commit.
300    pub async fn apply_capability_batch(
301        &self,
302        batch: crate::capability::SessionCapabilityBatch,
303        cancellation: tokio_util::sync::CancellationToken,
304    ) -> std::result::Result<
305        crate::capability::CapabilityCommitReceipt,
306        crate::capability::CapabilityRuntimeError,
307    > {
308        let _mutation = self.close_handle.extension_mutation.lock().await;
309        if self.is_closed() {
310            return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
311        }
312
313        let preparation_cancellation = tokio_util::sync::CancellationToken::new();
314        let prepared = tokio::select! {
315            biased;
316            _ = self.session_cancel.cancelled() => {
317                preparation_cancellation.cancel();
318                return Err(if self.is_closed() {
319                    crate::capability::CapabilityRuntimeError::SessionClosed
320                } else {
321                    crate::capability::CapabilityRuntimeError::Cancelled
322                });
323            }
324            _ = cancellation.cancelled() => {
325                preparation_cancellation.cancel();
326                return Err(crate::capability::CapabilityRuntimeError::Cancelled);
327            }
328            result = batch.prepare(
329                &self.capability_catalog,
330                preparation_cancellation.clone(),
331            ) => result?,
332        };
333        self.ensure_projected_mcp_server_names_available(prepared.projection()?)
334            .await?;
335
336        // The close boundary and Run pinning use this same short mutex. The
337        // prepared transaction holds no registry write lock while waiting.
338        let _publication = self
339            .close_handle
340            .immediate_extension_mutation
341            .lock()
342            .unwrap_or_else(std::sync::PoisonError::into_inner);
343        if self.is_closed() {
344            return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
345        }
346        if cancellation.is_cancelled() || self.session_cancel.is_cancelled() {
347            return Err(crate::capability::CapabilityRuntimeError::Cancelled);
348        }
349        // The legacy public registry guard does not participate in the
350        // Session mutation gate. Keep its lock through catalog publication so
351        // direct mutation linearizes wholly before validation or after CAS.
352        let command_registry = self
353            .command_registry
354            .lock()
355            .unwrap_or_else(std::sync::PoisonError::into_inner);
356        let current_projection = self.capability_catalog.pin();
357        super::agent_loop_runtime::validate_capability_projection_runtime(
358            self,
359            prepared.projection()?,
360            &command_registry,
361        )?;
362        super::agent_loop_runtime::validate_capability_projection_transition(
363            self,
364            current_projection.projection(),
365            prepared.projection()?,
366        )?;
367        prepared.commit()
368    }
369
370    /// Apply one SDK-transported capability batch (SDK-CAP1 Skill slice).
371    ///
372    /// Cross-language hosts stage serializable Skill values. The session cancel
373    /// token bounds preparation; hosts that need cooperative abort should close
374    /// the session or cancel the owning Run.
375    pub async fn apply_sdk_capability_batch(
376        &self,
377        batch: crate::capability::SdkCapabilityBatchV1,
378    ) -> std::result::Result<
379        crate::capability::SdkCapabilityCommitReceiptV1,
380        crate::capability::SdkCapabilityBatchError,
381    > {
382        let session_batch = batch.into_session_batch()?;
383        let receipt = self
384            .apply_capability_batch(session_batch, self.session_cancel.child_token())
385            .await?;
386        Ok(crate::capability::SdkCapabilityCommitReceiptV1::from_receipt(&receipt))
387    }
388
389    /// Reconstruct one exact historical capability generation on an untouched
390    /// recovery Session.
391    ///
392    /// Unlike ordinary [`Self::apply_capability_batch`], this one-time path may
393    /// jump directly from the empty generation-zero catalog to the generation
394    /// named by `expected`. It never resolves packages or `latest`: the host
395    /// supplies every runtime adapter and any exact A3S Use lease provider in
396    /// `batch`, and Code verifies the resulting catalog plus authority ceiling
397    /// before publication.
398    pub async fn bootstrap_recovery_capability_batch(
399        &self,
400        expected: &crate::capability::RunCapabilityBindingV1,
401        batch: crate::capability::SessionCapabilityBatch,
402        cancellation: tokio_util::sync::CancellationToken,
403    ) -> std::result::Result<
404        crate::capability::CapabilityCommitReceipt,
405        crate::capability::CapabilityRuntimeError,
406    > {
407        let _mutation = self.close_handle.extension_mutation.lock().await;
408        if self.is_closed() {
409            return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
410        }
411
412        let target_ceiling = self.capability_run_ceiling(batch.target())?;
413        expected
414            .ensure_matches(batch.target(), &target_ceiling)
415            .map_err(
416                |error| crate::capability::CapabilityRuntimeError::RecoveryBinding {
417                    message: error.to_string(),
418                },
419            )?;
420
421        let preparation_cancellation = tokio_util::sync::CancellationToken::new();
422        let prepared = tokio::select! {
423            biased;
424            _ = self.session_cancel.cancelled() => {
425                preparation_cancellation.cancel();
426                return Err(if self.is_closed() {
427                    crate::capability::CapabilityRuntimeError::SessionClosed
428                } else {
429                    crate::capability::CapabilityRuntimeError::Cancelled
430                });
431            }
432            _ = cancellation.cancelled() => {
433                preparation_cancellation.cancel();
434                return Err(crate::capability::CapabilityRuntimeError::Cancelled);
435            }
436            result = batch.prepare_recovery_bootstrap(
437                &self.capability_catalog,
438                preparation_cancellation.clone(),
439            ) => result?,
440        };
441        self.ensure_projected_mcp_server_names_available(prepared.projection()?)
442            .await?;
443
444        let _publication = self
445            .close_handle
446            .immediate_extension_mutation
447            .lock()
448            .unwrap_or_else(std::sync::PoisonError::into_inner);
449        if self.is_closed() {
450            return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
451        }
452        if cancellation.is_cancelled() || self.session_cancel.is_cancelled() {
453            return Err(crate::capability::CapabilityRuntimeError::Cancelled);
454        }
455        let command_registry = self
456            .command_registry
457            .lock()
458            .unwrap_or_else(std::sync::PoisonError::into_inner);
459        let current_projection = self.capability_catalog.pin();
460        super::agent_loop_runtime::validate_capability_projection_runtime(
461            self,
462            prepared.projection()?,
463            &command_registry,
464        )?;
465        super::agent_loop_runtime::validate_capability_projection_transition(
466            self,
467            current_projection.projection(),
468            prepared.projection()?,
469        )?;
470        prepared.commit()
471    }
472
473    /// Drain prepared effects from failed or retired host generations.
474    pub async fn drain_capability_cleanup(&self) -> crate::capability::CapabilityCleanupReport {
475        self.capability_catalog.drain_cleanup().await
476    }
477
478    #[cfg(test)]
479    pub(crate) async fn admit_capability_run(
480        &self,
481    ) -> std::result::Result<
482        crate::capability::SessionCapabilityRun,
483        crate::capability::CapabilityRuntimeError,
484    > {
485        let projection = {
486            let _admission = self
487                .close_handle
488                .immediate_extension_mutation
489                .lock()
490                .unwrap_or_else(std::sync::PoisonError::into_inner);
491            if self.is_closed() {
492                return Err(crate::capability::CapabilityRuntimeError::SessionClosed);
493            }
494            self.capability_catalog.pin()
495        };
496        let ceiling = self.capability_run_ceiling(projection.projection().set())?;
497        crate::capability::SessionCapabilityRun::admit(
498            projection,
499            "active",
500            "active",
501            ceiling,
502            self.session_cancel.child_token(),
503        )
504        .await
505    }
506
507    pub(super) fn capability_run_ceiling(
508        &self,
509        set: &crate::capability::CapabilitySet,
510    ) -> std::result::Result<
511        crate::capability::CapabilityCeiling,
512        crate::capability::CapabilityRuntimeError,
513    > {
514        let mut governance = crate::capability::GovernanceCapabilityCeiling::none_required();
515        if self.config.permission_checker.is_some() || self.config.permission_policy.is_some() {
516            governance = governance.require_permission_guard();
517        }
518        if self.config.confirmation_manager.is_some() || self.config.confirmation_policy.is_some() {
519            governance = governance.require_confirmation_guard();
520        }
521        if self.config.security_provider.is_some() {
522            governance = governance.require_security_guard();
523        }
524        if self.config.budget_guard.is_some() || self.budget_guard().is_some() {
525            governance = governance.require_budget_guard();
526        }
527        if self.config.enforce_active_skill_tool_restrictions {
528            governance = governance.require_active_skill_restrictions();
529        }
530        let execution = crate::capability::CapabilityExecutionCeiling::new(
531            self.config.max_tool_rounds,
532            self.config.max_parallel_tasks,
533            self.config.tool_timeout_ms,
534            self.config.llm_api_timeout_ms,
535            self.config.max_execution_time_ms,
536        )?;
537        crate::capability::CapabilityCeiling::all(
538            set,
539            crate::capability::WorkspaceCapabilityCeiling::all(),
540            governance,
541            execution,
542        )
543        .map_err(Into::into)
544    }
545
546    pub(super) fn ensure_compatibility_name_available(
547        &self,
548        kind: crate::capability::CapabilityKind,
549        public_name: &str,
550    ) -> crate::error::Result<()> {
551        let projection = self.capability_catalog.pin();
552        if projection
553            .projection()
554            .iter()
555            .any(|(_, value)| match value {
556                crate::capability::CapabilityValue::Mcp(binding)
557                    if kind == crate::capability::CapabilityKind::Tool =>
558                {
559                    binding.contains_public_tool_name(public_name)
560                }
561                crate::capability::CapabilityValue::Agent(agent) => {
562                    kind == crate::capability::CapabilityKind::Agent
563                        && crate::subagent::agent_names_conflict(&agent.name, public_name)
564                }
565                _ => value.kind() == kind && value.public_name() == Some(public_name),
566            })
567        {
568            return Err(
569                crate::capability::CapabilityRuntimeError::RuntimeNameConflict {
570                    kind,
571                    public_name: public_name.to_owned(),
572                }
573                .into(),
574            );
575        }
576        Ok(())
577    }
578
579    async fn ensure_projected_mcp_server_names_available(
580        &self,
581        projection: &crate::capability::CapabilityProjection,
582    ) -> std::result::Result<(), crate::capability::CapabilityRuntimeError> {
583        let server_names = projection
584            .iter()
585            .filter_map(|(_, value)| match value {
586                crate::capability::CapabilityValue::Mcp(binding) => {
587                    Some(binding.server_name().to_owned())
588                }
589                _ => None,
590            })
591            .collect::<Vec<_>>();
592        for server_name in server_names {
593            for manager in &self.mcp_managers {
594                if manager.contains_server(&server_name).await {
595                    return Err(
596                        crate::capability::CapabilityRuntimeError::RuntimeNameConflict {
597                            kind: crate::capability::CapabilityKind::Mcp,
598                            public_name: server_name,
599                        },
600                    );
601                }
602            }
603        }
604        Ok(())
605    }
606}