Skip to main content

a3s_code_core/
agent_api.rs

1//! Agent Facade API
2//!
3//! High-level, ergonomic API for using A3S Code as an embedded library.
4//!
5//! ## Example
6//!
7//! ```rust,no_run
8//! use a3s_code_core::Agent;
9//!
10//! # async fn run() -> anyhow::Result<()> {
11//! let agent = Agent::new("agent.acl").await?;
12//! let session = agent.session("/my-project", None)?;
13//! let result = session.send("Explain the auth module", None).await?;
14//! println!("{}", result.text);
15//! # Ok(())
16//! # }
17//! ```
18
19use crate::agent::{AgentConfig, AgentEvent, AgentResult};
20use crate::commands::CommandRegistry;
21use crate::config::CodeConfig;
22use crate::error::Result;
23use crate::hitl::PendingConfirmationInfo;
24use crate::llm::{LlmClient, Message};
25use crate::prompts::{PlanningMode, SystemPromptSlots};
26use crate::queue::{
27    ExternalTask, ExternalTaskResult, LaneHandlerConfig, SessionLane, SessionQueueConfig,
28    SessionQueueStats,
29};
30use crate::tools::{ToolContext, ToolExecutor};
31use a3s_lane::{DeadLetter, MetricsSnapshot};
32use a3s_memory::MemoryStore;
33use std::collections::HashMap;
34use std::path::{Path, PathBuf};
35use std::sync::{Arc, RwLock};
36use tokio::sync::mpsc;
37use tokio::task::JoinHandle;
38mod agent_binding;
39mod agent_bootstrap;
40mod agent_loop_runtime;
41mod agent_sessions;
42mod capabilities;
43mod command_runtime;
44mod conversation_runtime;
45mod direct_tools;
46mod hook_control;
47mod run_lifecycle;
48mod runtime;
49mod runtime_events;
50mod session_builder;
51mod session_clock;
52mod session_close;
53mod session_commands;
54mod session_config;
55mod session_extensions;
56mod session_hitl;
57mod session_options;
58mod session_persistence;
59mod session_queue;
60mod session_runs;
61mod session_runtime;
62mod session_save;
63mod session_verification;
64mod session_view;
65use direct_tools::DirectToolRuntime;
66use hook_control::HookControl;
67use runtime_events::ActiveToolState;
68use session_close::SessionCloseHandle;
69use session_extensions::SessionExtensionRuntime;
70use session_hitl::HitlControl;
71use session_queue::QueueControl;
72use session_runs::RunControl;
73use session_verification::VerificationRuntime;
74use session_view::SessionView;
75
76/// Canonicalize a path, stripping the Windows `\\?\` UNC prefix to avoid
77/// polluting workspace strings throughout the system (prompts, session data, etc.).
78fn safe_canonicalize(path: &Path) -> PathBuf {
79    match std::fs::canonicalize(path) {
80        Ok(p) => strip_unc_prefix(p),
81        Err(_) => path.to_path_buf(),
82    }
83}
84
85/// Strip the Windows extended-length path prefix (`\\?\`) that `canonicalize()` adds.
86/// On non-Windows this is a no-op.
87fn strip_unc_prefix(path: PathBuf) -> PathBuf {
88    #[cfg(windows)]
89    {
90        let s = path.to_string_lossy();
91        if let Some(stripped) = s.strip_prefix(r"\\?\") {
92            return PathBuf::from(stripped);
93        }
94    }
95    path
96}
97
98// ============================================================================
99// ToolCallResult
100// ============================================================================
101
102/// Result of a direct tool execution (no LLM).
103#[derive(Debug, Clone)]
104pub struct ToolCallResult {
105    pub name: String,
106    pub output: String,
107    pub exit_code: i32,
108    pub metadata: Option<serde_json::Value>,
109    /// Structured discriminant for tool failures. `None` when the tool
110    /// either succeeded or failed without a typed reason (the message in
111    /// `output` is then the only diagnostic). Populated for known
112    /// kinds such as `VersionConflict` so SDK callers can branch on the
113    /// `type` field instead of regex-matching `output`.
114    pub error_kind: Option<crate::tools::ToolErrorKind>,
115}
116
117// ============================================================================
118// SessionOptions
119// ============================================================================
120
121/// Optional per-session overrides.
122#[derive(Clone, Default)]
123pub struct SessionOptions {
124    /// Override the default model. Format: `"provider/model"` (e.g., `"openai/gpt-4o"`).
125    pub model: Option<String>,
126    /// Extra directories to scan for agent files.
127    /// Merged with any global `agent_dirs` from [`CodeConfig`].
128    pub agent_dirs: Vec<PathBuf>,
129    /// Reproducible disposable workers registered for task delegation.
130    /// Explicit session workers override agents loaded from directories by name.
131    pub worker_agents: Vec<crate::subagent::WorkerAgentSpec>,
132    /// Optional queue configuration for lane-based tool execution.
133    ///
134    /// When set, enables priority-based tool scheduling with parallel execution
135    /// of read-only (Query-lane) tools, DLQ, metrics, and external task handling.
136    pub queue_config: Option<SessionQueueConfig>,
137    /// Optional security provider for taint tracking and output sanitization
138    pub security_provider: Option<Arc<dyn crate::security::SecurityProvider>>,
139    /// Optional context providers for RAG
140    pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
141    /// Optional confirmation manager for HITL
142    pub confirmation_manager: Option<Arc<dyn crate::hitl::ConfirmationProvider>>,
143    /// Optional confirmation policy (will be used to create ConfirmationManager if confirmation_manager is not set)
144    pub confirmation_policy: Option<crate::hitl::ConfirmationPolicy>,
145    /// Optional permission checker
146    pub permission_checker: Option<Arc<dyn crate::permissions::PermissionChecker>>,
147    /// Serializable permission policy used to build the checker, when available.
148    pub permission_policy: Option<crate::permissions::PermissionPolicy>,
149    /// Enable planning
150    pub planning_mode: PlanningMode,
151    /// Enable goal tracking
152    pub goal_tracking: bool,
153    /// Extra directories to scan for skill files (*.md).
154    /// Merged with any global `skill_dirs` from [`CodeConfig`].
155    pub skill_dirs: Vec<PathBuf>,
156    /// Optional skill registry for instruction injection
157    pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
158    /// Optional memory store for long-term memory persistence
159    pub memory_store: Option<Arc<dyn MemoryStore>>,
160    /// Deferred file memory directory — constructed async in `build_session()`
161    pub(crate) file_memory_dir: Option<PathBuf>,
162    /// Optional session store for persistence
163    pub session_store: Option<Arc<dyn crate::store::SessionStore>>,
164    /// Explicit session ID (auto-generated if not set)
165    pub session_id: Option<String>,
166    /// Multi-tenant identifier. Framework only transports this string;
167    /// the host (e.g. 书安OS) decides what "tenant" means and how to
168    /// aggregate/bill on it. Emitted to hooks/traces, persisted in
169    /// `SessionData`, never interpreted by core.
170    pub tenant_id: Option<String>,
171    /// Identity of the principal that triggered this session (user id,
172    /// service account, etc). Treated as opaque.
173    pub principal: Option<String>,
174    /// Logical identifier of the agent template / definition the session
175    /// was instantiated from. Lets the host aggregate sessions by
176    /// "which agent recipe" independent of the concrete session id.
177    pub agent_template_id: Option<String>,
178    /// Distributed-trace correlation id. Propagated through hooks/traces
179    /// so a session's events join with upstream/downstream work in the
180    /// host's observability pipeline.
181    pub correlation_id: Option<String>,
182    /// Optional host-supplied budget / quota guard. The framework calls
183    /// into it before each LLM call (and reports actuals after) so the
184    /// host can refuse or rate-limit at the cluster level. Default is
185    /// `None` (no enforcement — equivalent to
186    /// [`NoopBudgetGuard`](crate::budget::NoopBudgetGuard)).
187    pub budget_guard: Option<Arc<dyn crate::budget::BudgetGuard>>,
188    /// Optional host-provided ID/Clock pair. Replaces the default
189    /// random-UUID + wall-clock pair, enabling deterministic replay
190    /// on another node. `None` keeps pre-P2 behaviour.
191    pub host_env: Option<Arc<crate::host_env::HostEnv>>,
192    /// Optional FIFO retention caps on the session's in-memory stores
193    /// (run records, run events, trace events, terminal subagent
194    /// tasks). `None` (default) keeps everything — fine for short
195    /// sessions, a memory leak for hours-long cluster workloads.
196    pub retention_limits: Option<crate::retention::SessionRetentionLimits>,
197    /// Auto-save after each completed `send()` or default-history `stream()` call.
198    pub auto_save: bool,
199    /// Optional artifact retention limits for large tool/program outputs.
200    pub artifact_store_limits: Option<crate::tools::ArtifactStoreLimits>,
201    /// Max consecutive parse errors before aborting (overrides default of 2).
202    /// `None` uses the `AgentConfig` default.
203    pub max_parse_retries: Option<u32>,
204    /// Per-tool execution timeout in milliseconds.
205    /// `None` = no timeout (default).
206    pub tool_timeout_ms: Option<u64>,
207    /// Circuit-breaker threshold: max consecutive LLM API failures before
208    /// aborting in non-streaming mode (overrides default of 3).
209    /// `None` uses the `AgentConfig` default.
210    pub circuit_breaker_threshold: Option<u32>,
211    /// Optional concrete sandbox implementation.
212    ///
213    /// When set, `bash` tool commands are routed through this sandbox instead
214    /// of `std::process::Command`. The host application constructs and owns
215    /// the implementation (e.g., an A3S Box–backed handle).
216    pub sandbox_handle: Option<Arc<dyn crate::sandbox::BashSandbox>>,
217    /// Optional host-provided workspace backend.
218    ///
219    /// When set, built-in tools such as `read`, `write`, `ls`, and `bash`
220    /// execute against these workspace capabilities instead of assuming the
221    /// server-local filesystem. This is the primary extension point for DFS,
222    /// browser, container, and remote workspace deployments.
223    pub workspace_services: Option<Arc<crate::workspace::WorkspaceServices>>,
224    /// Enable auto-compaction when context usage exceeds threshold.
225    pub auto_compact: bool,
226    /// Context usage percentage threshold for auto-compaction (0.0 - 1.0).
227    /// Default: 0.80 (80%).
228    pub auto_compact_threshold: Option<f32>,
229    /// Inject a continuation message when the LLM stops without completing the task.
230    /// `None` uses the `AgentConfig` default (true).
231    pub continuation_enabled: Option<bool>,
232    /// Maximum continuation injections per execution.
233    /// `None` uses the `AgentConfig` default (3).
234    pub max_continuation_turns: Option<u32>,
235    /// Maximum execution time in milliseconds.
236    /// `None` = no timeout (default).
237    /// When set, the execution loop will abort if it exceeds this duration.
238    pub max_execution_time_ms: Option<u64>,
239    /// Optional MCP manager for connecting to external MCP servers.
240    ///
241    /// When set, all tools from connected MCP servers are registered and
242    /// available during agent execution with names like `mcp__server__tool`.
243    pub mcp_manager: Option<Arc<crate::mcp::manager::McpManager>>,
244    /// Sampling temperature (0.0–1.0). Overrides the provider default.
245    pub temperature: Option<f32>,
246    /// Extended thinking budget in tokens (Anthropic only).
247    pub thinking_budget: Option<usize>,
248    /// Per-session tool round limit override.
249    ///
250    /// When set, overrides the agent-level `max_tool_rounds` for this session only.
251    /// Maps directly from [`AgentDefinition::max_steps`] when creating sessions
252    /// via [`Agent::session_for_agent`].
253    pub max_tool_rounds: Option<usize>,
254    /// Per-session parallel fan-out limit override.
255    ///
256    /// Applies to delegated `parallel_task`, plan wave execution, and safe
257    /// parallel write batches.
258    pub max_parallel_tasks: Option<usize>,
259    /// Per-session automatic subagent delegation override.
260    pub auto_delegation: Option<crate::config::AutoDelegationConfig>,
261    /// Per-session kill switch for automatic parallel child-agent fan-out.
262    ///
263    /// This overlays the effective automatic delegation config instead of
264    /// replacing it, so callers can disable auto fan-out without disabling
265    /// automatic delegation itself.
266    pub auto_parallel_delegation: Option<bool>,
267    /// Slot-based system prompt customization.
268    ///
269    /// When set, overrides the agent-level prompt slots for this session.
270    /// Users can customize role, guidelines, response style, and extra instructions
271    /// without losing the core agentic capabilities.
272    pub prompt_slots: Option<SystemPromptSlots>,
273    /// Optional external hook executor (e.g. an AHP harness server).
274    ///
275    /// When set, **replaces** the built-in `HookEngine` for this session.
276    /// All 11 lifecycle events are forwarded to the executor instead of being
277    /// dispatched locally. The executor is also propagated to sub-agents via
278    /// the sentinel hook mechanism.
279    pub hook_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
280}
281
282// ============================================================================
283// Agent
284// ============================================================================
285
286/// High-level agent facade.
287///
288/// Holds the LLM client and agent config. Workspace-independent.
289/// Use [`Agent::session()`] to bind to a workspace.
290pub struct Agent {
291    code_config: CodeConfig,
292    config: AgentConfig,
293    /// Global MCP manager loaded from config.mcp_servers
294    global_mcp: Option<Arc<crate::mcp::manager::McpManager>>,
295    /// Pre-fetched MCP tool definitions from global_mcp (cached at creation time).
296    /// Wrapped in Mutex so `refresh_mcp_tools()` can update the cache without `&mut self`.
297    global_mcp_tools: std::sync::Mutex<Vec<(String, crate::mcp::McpTool)>>,
298    /// Tracks every live session created by this agent via `Weak` refs so
299    /// the agent can enumerate and forcibly close them. Sessions register
300    /// themselves at construction and become dangling `Weak`s on drop —
301    /// `list_sessions()` / `close_session()` prune dead entries on access.
302    ///
303    /// Uses a synchronous lock so the sync `Agent::session()` factory can
304    /// insert without nesting tokio runtimes. The lock is only held for
305    /// brief insert/scan operations — async close work happens after the
306    /// lock is released.
307    sessions: Arc<std::sync::Mutex<HashMap<String, std::sync::Weak<SessionCloseHandle>>>>,
308    /// Set once `Agent::close()` has been called. Subsequent `session()` /
309    /// `resume_session()` calls fail fast with `CodeError::SessionClosed`.
310    closed: Arc<std::sync::atomic::AtomicBool>,
311}
312
313impl std::fmt::Debug for Agent {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        f.debug_struct("Agent").finish()
316    }
317}
318
319impl Agent {
320    /// Create from a config file path or inline ACL-compatible string.
321    ///
322    /// Auto-detects `.acl` file paths vs inline ACL-compatible config.
323    pub async fn new(config_source: impl Into<String>) -> Result<Self> {
324        let config = agent_bootstrap::load_code_config(config_source.into())?;
325        Self::from_config(config).await
326    }
327
328    /// Create from a config file path or inline ACL-compatible string.
329    ///
330    /// Alias for [`Agent::new()`] — provides a consistent API with
331    /// the Python and Node.js SDKs.
332    pub async fn create(config_source: impl Into<String>) -> Result<Self> {
333        Self::new(config_source).await
334    }
335
336    /// Create from a [`CodeConfig`] struct.
337    pub async fn from_config(config: CodeConfig) -> Result<Self> {
338        agent_bootstrap::build_agent_from_config(config).await
339    }
340
341    /// Re-fetch tool definitions from all connected global MCP servers and
342    /// update the internal cache.
343    ///
344    /// Call this when an MCP server has added or removed tools since the
345    /// agent was created. The refreshed tools will be visible to all
346    /// **new** sessions created after this call; existing sessions are
347    /// unaffected (their `ToolExecutor` snapshot is already built).
348    pub async fn refresh_mcp_tools(&self) -> Result<()> {
349        agent_sessions::refresh_mcp_tools(self).await
350    }
351
352    /// Bind to a workspace directory, returning an [`AgentSession`].
353    ///
354    /// Pass `None` for defaults, or `Some(SessionOptions)` to override
355    /// the model, agent directories for this session.
356    pub fn session(
357        &self,
358        workspace: impl Into<String>,
359        options: Option<SessionOptions>,
360    ) -> Result<AgentSession> {
361        agent_sessions::create_session(self, workspace, options)
362    }
363
364    /// Create a session pre-configured from an [`AgentDefinition`].
365    ///
366    /// Maps the definition's `permissions`, `prompt`, `model`, and `max_steps`
367    /// directly into [`SessionOptions`], so markdown/YAML-defined subagents can
368    /// be used by delegation and advanced control-plane flows without manual wiring.
369    ///
370    /// The mapping follows the same logic as the built-in `task` tool:
371    /// - `permissions` → `permission_checker`
372    /// - `prompt`      → `prompt_slots.extra`
373    /// - `max_steps`   → `max_tool_rounds`
374    /// - `model`       → `model` (as `"provider/model"` string)
375    ///
376    /// `extra` can supply additional overrides (e.g. `planning_enabled`) that
377    /// take precedence over the definition's values.
378    pub fn session_for_agent(
379        &self,
380        workspace: impl Into<String>,
381        def: &crate::subagent::AgentDefinition,
382        extra: Option<SessionOptions>,
383    ) -> Result<AgentSession> {
384        agent_sessions::create_session_for_agent(self, workspace, def, extra)
385    }
386
387    /// Create a session from a reproducible disposable worker recipe.
388    ///
389    /// This is the cattle-mode companion to [`Agent::session_for_agent`]: callers
390    /// provide a small [`WorkerAgentSpec`](crate::subagent::WorkerAgentSpec), and
391    /// A3S Code compiles it into the same runtime definition used by delegated agents.
392    pub fn session_for_worker(
393        &self,
394        workspace: impl Into<String>,
395        spec: crate::subagent::WorkerAgentSpec,
396        extra: Option<SessionOptions>,
397    ) -> Result<AgentSession> {
398        let def = spec.into_agent_definition();
399        self.session_for_agent(workspace, &def, extra)
400    }
401
402    /// Resume a previously saved session by ID.
403    ///
404    /// Loads the session data from the store, rebuilds the `AgentSession` with
405    /// the saved conversation history, and returns it ready for continued use.
406    ///
407    /// The `options` must include a `session_store` (or `with_file_session_store`)
408    /// that contains the saved session.
409    pub fn resume_session(
410        &self,
411        session_id: &str,
412        options: SessionOptions,
413    ) -> Result<AgentSession> {
414        agent_sessions::resume_session(self, session_id, options)
415    }
416
417    /// Return the IDs of every live session created from this agent.
418    ///
419    /// "Live" means the caller still holds an [`AgentSession`] — sessions
420    /// that have been dropped are pruned lazily on each call. The list is
421    /// sorted to make output stable for tests/UIs.
422    pub async fn list_sessions(&self) -> Vec<String> {
423        agent_sessions::list_sessions(self).await
424    }
425
426    /// Close a specific live session by its session ID.
427    ///
428    /// Returns `true` when a live session with the given id was found and
429    /// transitioned from open to closed by this call; `false` when no live
430    /// session has that id, or when the session was already closed.
431    ///
432    /// This is the out-of-band counterpart to [`AgentSession::close`]: it
433    /// performs exactly the same cleanup but can be invoked without holding
434    /// a reference to the session itself — useful for control-plane code
435    /// that only knows the session ID.
436    pub async fn close_session(&self, session_id: &str) -> bool {
437        agent_sessions::close_session(self, session_id).await
438    }
439
440    /// Close every live session created from this agent and tear down
441    /// background resources owned by the agent (global MCP connections).
442    ///
443    /// After this call:
444    /// - Every live `AgentSession` is closed (same effect as calling
445    ///   [`AgentSession::close`] on each).
446    /// - Subsequent [`Agent::session`] / [`Agent::resume_session`] calls
447    ///   fail fast with [`CodeError::SessionClosed`](crate::error::CodeError::SessionClosed).
448    ///
449    /// Idempotent: subsequent calls are no-ops and are guaranteed not to
450    /// panic.
451    pub async fn close(&self) {
452        agent_sessions::close_agent(self).await
453    }
454
455    /// Return whether [`close`](Self::close) has been called on this agent.
456    pub fn is_closed(&self) -> bool {
457        self.closed.load(std::sync::atomic::Ordering::Acquire)
458    }
459
460    /// Disconnect every global MCP server whose last activity is older
461    /// than `idle_threshold_ms`. Returns the names of disconnected
462    /// servers (empty when there is no global MCP manager or when
463    /// nothing is idle).
464    ///
465    /// Hosts running thousands of long-lived sessions should call this
466    /// periodically (e.g. every 60s with a 5-min threshold) to release
467    /// file descriptors and background workers from quiet MCP servers
468    /// without losing the server's configuration. A subsequent tool
469    /// call on the same server will require an explicit reconnect.
470    pub async fn disconnect_idle_mcp(&self, idle_threshold_ms: u64) -> Vec<String> {
471        match &self.global_mcp {
472            Some(mcp) => mcp.disconnect_idle(idle_threshold_ms).await,
473            None => Vec::new(),
474        }
475    }
476
477    #[cfg(test)]
478    fn build_session(
479        &self,
480        workspace: String,
481        llm_client: Arc<dyn LlmClient>,
482        opts: &SessionOptions,
483    ) -> Result<AgentSession> {
484        session_builder::build_agent_session(self, workspace, llm_client, opts)
485    }
486}
487
488// ============================================================================
489// AgentSession
490// ============================================================================
491
492/// Workspace-bound session. All LLM and tool operations happen here.
493///
494/// History is automatically accumulated after each `send()` call and after
495/// `stream()` completes when no custom history is supplied.
496/// Use `history()` to retrieve the current conversation log.
497pub struct AgentSession {
498    llm_client: Arc<dyn LlmClient>,
499    tool_executor: Arc<ToolExecutor>,
500    tool_context: ToolContext,
501    config: AgentConfig,
502    workspace: PathBuf,
503    /// Unique session identifier.
504    session_id: String,
505    /// Internal conversation history, auto-updated after each `send()` and default-history `stream()`.
506    history: Arc<RwLock<Vec<Message>>>,
507    /// Optional lane queue for priority-based tool execution.
508    command_queue: Option<Arc<crate::session_lane_queue::SessionLaneQueue>>,
509    /// Optional long-term memory.
510    memory: Option<Arc<crate::memory::AgentMemory>>,
511    /// Optional session store for persistence.
512    session_store: Option<Arc<dyn crate::store::SessionStore>>,
513    /// Auto-save after each completed `send()` or default-history `stream()`.
514    auto_save: bool,
515    /// Hook engine for lifecycle event interception.
516    hook_engine: Arc<crate::hooks::HookEngine>,
517    /// Optional external hook executor (e.g. AHP harness). When set, replaces
518    /// `hook_engine` as the executor passed to each `AgentLoop`.
519    ahp_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
520    /// Deferred init warning: emitted as PersistenceFailed on first send() if set.
521    init_warning: Option<String>,
522    /// Slash command registry for `/command` dispatch.
523    /// Uses interior mutability so commands can be registered on a shared `Arc<AgentSession>`.
524    command_registry: std::sync::Mutex<CommandRegistry>,
525    /// Model identifier for display (e.g., "anthropic/claude-sonnet-4-20250514").
526    model_name: String,
527    /// Shared MCP manager — all add_mcp_server / remove_mcp_server calls go here.
528    mcp_manager: Arc<crate::mcp::manager::McpManager>,
529    /// Shared agent registry — populated at session creation; extended via register_agent_dir().
530    agent_registry: Arc<crate::subagent::AgentRegistry>,
531    /// Cancellation token for the current operation (send/stream).
532    /// Stored so that cancel() can abort ongoing LLM calls.
533    cancel_token: Arc<tokio::sync::Mutex<Option<tokio_util::sync::CancellationToken>>>,
534    /// ID of the run currently attached to the active cancellation token.
535    current_run_id: Arc<tokio::sync::Mutex<Option<String>>>,
536    /// In-memory run snapshots and event replay buffer for this session.
537    run_store: Arc<crate::run::InMemoryRunStore>,
538    /// Materialized view of delegated subagent task lifecycle, populated from runtime events.
539    subagent_tasks: Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>,
540    /// Currently executing tools observed from runtime events.
541    active_tools: Arc<tokio::sync::RwLock<HashMap<String, ActiveToolState>>>,
542    /// Compact execution traces for this session.
543    trace_sink: crate::trace::InMemoryTraceSink,
544    /// Structured completion evidence collected from agent and explicit verification runs.
545    verification_reports: Arc<RwLock<Vec<crate::verification::VerificationReport>>>,
546    /// Set once `close()` has been called. Subsequent send/stream calls
547    /// fast-fail with [`crate::error::CodeError::SessionClosed`].
548    closed: Arc<std::sync::atomic::AtomicBool>,
549    /// Session-level parent cancellation token.
550    ///
551    /// Every in-flight run (blocking send, stream, delegated subagent task)
552    /// derives its per-operation token from this one via `child_token()`,
553    /// so `session_cancel.cancel()` cascades to all of them. `close()` fires
554    /// this token first, after which any new `child_token()` returns an
555    /// already-cancelled token (defending against close/spawn races).
556    pub(crate) session_cancel: tokio_util::sync::CancellationToken,
557    /// Shared `Arc`-handle used by both [`AgentSession::close`] and the
558    /// parent [`Agent`]'s registry. The handle bundles every field needed
559    /// to perform the close sequence so the two entry points cannot drift.
560    close_handle: Arc<SessionCloseHandle>,
561    /// Runtime-mutable override for the budget guard. When set, takes
562    /// precedence over `config.budget_guard` on the next agent-loop
563    /// build. Lets SDK callers (Node especially) install a host-side
564    /// guard after `session()` has returned without ever putting a
565    /// JS callable into `SessionOptions`.
566    runtime_budget_guard: std::sync::Mutex<Option<Arc<dyn crate::budget::BudgetGuard>>>,
567    /// Multi-tenant label. Framework only carries the string; semantics
568    /// belong to the host.
569    pub(crate) tenant_id: Option<String>,
570    /// Principal that triggered the session (user / service / etc.).
571    pub(crate) principal: Option<String>,
572    /// Logical identifier of the agent template the session was
573    /// instantiated from.
574    pub(crate) agent_template_id: Option<String>,
575    /// Distributed-trace correlation id propagated to hooks / traces.
576    pub(crate) correlation_id: Option<String>,
577}
578
579impl std::fmt::Debug for AgentSession {
580    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581        f.debug_struct("AgentSession")
582            .field("session_id", &self.session_id)
583            .field("workspace", &self.workspace.display().to_string())
584            .field("auto_save", &self.auto_save)
585            .finish()
586    }
587}
588
589impl AgentSession {
590    /// Get a snapshot of command entries (name, description, optional usage).
591    ///
592    /// Acquires the command registry lock briefly and returns owned data.
593    pub fn command_registry(&self) -> std::sync::MutexGuard<'_, CommandRegistry> {
594        session_commands::registry(self)
595    }
596
597    /// Register a custom slash command.
598    ///
599    /// Takes `&self` so it can be called on a shared `Arc<AgentSession>`.
600    pub fn register_command(&self, cmd: Arc<dyn crate::commands::SlashCommand>) {
601        session_commands::register(self, cmd);
602    }
603
604    /// Return whether [`close`](Self::close) has been called on this session.
605    ///
606    /// Once closed, `send`/`stream` and their attachment variants fast-fail
607    /// with [`crate::error::CodeError::SessionClosed`] instead of starting a
608    /// new run.
609    pub fn is_closed(&self) -> bool {
610        self.closed.load(std::sync::atomic::Ordering::Acquire)
611    }
612
613    /// Clone the session-level [`CancellationToken`](tokio_util::sync::CancellationToken).
614    ///
615    /// All in-flight runs derive their per-operation token from this one via
616    /// `child_token()`, so embedders can:
617    ///
618    /// - Observe the token (e.g. wire it into a host-side `select!`) to
619    ///   react to session shutdown without polling [`is_closed`](Self::is_closed);
620    /// - Call `.cancel()` on it to abort every operation in the session
621    ///   without going through `close()` (no run-store / hook side effects).
622    ///
623    /// For graceful shutdown prefer [`close`](Self::close), which also marks
624    /// runs as cancelled in the store and fires AHP hooks.
625    pub fn session_cancel_token(&self) -> tokio_util::sync::CancellationToken {
626        self.session_cancel.clone()
627    }
628
629    /// Return the host-defined tenant id, if any.
630    ///
631    /// The framework only transports this string — it never interprets
632    /// or enforces tenant boundaries itself. Use this from custom
633    /// `HookExecutor` / `PermissionChecker` / `BudgetGuard` impls to
634    /// route logic by tenant.
635    pub fn tenant_id(&self) -> Option<&str> {
636        self.tenant_id.as_deref()
637    }
638
639    /// Return the principal that triggered the session, if any.
640    pub fn principal(&self) -> Option<&str> {
641        self.principal.as_deref()
642    }
643
644    /// Return the id of the agent template/definition the session was
645    /// instantiated from, if any.
646    pub fn agent_template_id(&self) -> Option<&str> {
647        self.agent_template_id.as_deref()
648    }
649
650    /// Return the distributed-trace correlation id propagated through
651    /// this session's events, if any.
652    pub fn correlation_id(&self) -> Option<&str> {
653        self.correlation_id.as_deref()
654    }
655
656    /// Install or replace a runtime budget guard. Takes effect on the
657    /// next `send` / `stream` call (the guard is consulted at agent-
658    /// loop build time, not on the live execution). Setting `None`
659    /// clears the override so `config.budget_guard` takes over again.
660    ///
661    /// This is the entry point SDKs use to wire a host-supplied guard
662    /// after the session has already been constructed — useful when
663    /// the guard's transport (e.g. a JS callable) cannot live inside
664    /// the value-typed `SessionOptions`.
665    pub fn set_budget_guard(&self, guard: Option<Arc<dyn crate::budget::BudgetGuard>>) {
666        let mut slot = self
667            .runtime_budget_guard
668            .lock()
669            .unwrap_or_else(|p| p.into_inner());
670        *slot = guard;
671    }
672
673    /// Return the currently-installed runtime budget guard, if any.
674    /// `None` means the loop falls back to `config.budget_guard`.
675    pub fn budget_guard(&self) -> Option<Arc<dyn crate::budget::BudgetGuard>> {
676        self.runtime_budget_guard
677            .lock()
678            .unwrap_or_else(|p| p.into_inner())
679            .clone()
680    }
681
682    /// Proactively close the session and release its in-flight work.
683    ///
684    /// On the first call this:
685    /// 1. flips the session into the **closed** state so further `send`/`stream`
686    ///    calls fast-fail with [`crate::error::CodeError::SessionClosed`];
687    /// 2. fires the session-level cancellation token so every derived
688    ///    run/subagent token cascades to cancelled;
689    /// 3. marks the active run `Cancelled` in the run store and fires AHP
690    ///    hook side effects;
691    /// 4. cancels every still-running delegated subagent task spawned from
692    ///    this session;
693    /// 5. cancels all pending human-in-the-loop tool confirmations.
694    ///
695    /// Subsequent calls are no-ops and are guaranteed not to panic.
696    pub async fn close(&self) {
697        // Delegate to the shared handle so this entry point and
698        // `Agent::close_session(id)` cannot drift in behaviour.
699        self.close_handle.close().await;
700    }
701
702    /// Send a prompt and wait for the complete response.
703    ///
704    /// When `history` is `None`, uses (and auto-updates) the session's
705    /// internal conversation history. When `Some`, uses the provided
706    /// history instead (the internal history is **not** modified).
707    ///
708    /// If the prompt starts with `/`, it is dispatched as a slash command
709    /// and the result is returned without calling the LLM.
710    pub async fn send(&self, prompt: &str, history: Option<&[Message]>) -> Result<AgentResult> {
711        conversation_runtime::send(self, prompt, history).await
712    }
713
714    /// Resume a previously-checkpointed run on this session.
715    ///
716    /// Loads the latest [`LoopCheckpoint`](crate::loop_checkpoint::LoopCheckpoint)
717    /// stored under `checkpoint_run_id` and replays the agent loop from
718    /// that boundary state. A **new** run id is allocated for the
719    /// resumed work; the relationship between the old and new run is
720    /// host-tracked (e.g. by 书安OS) — the framework does not interpret
721    /// it.
722    ///
723    /// Returns an error when no `SessionStore` is configured on this
724    /// session, or when no checkpoint exists for `checkpoint_run_id`.
725    pub async fn resume_run(&self, checkpoint_run_id: &str) -> Result<AgentResult> {
726        conversation_runtime::resume_run(self, checkpoint_run_id).await
727    }
728
729    /// Send a prompt with image attachments and wait for the complete response.
730    ///
731    /// Images are included as multi-modal content blocks in the user message.
732    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
733    pub async fn send_with_attachments(
734        &self,
735        prompt: &str,
736        attachments: &[crate::llm::Attachment],
737        history: Option<&[Message]>,
738    ) -> Result<AgentResult> {
739        conversation_runtime::send_with_attachments(self, prompt, attachments, history).await
740    }
741
742    /// Stream a prompt with image attachments.
743    ///
744    /// Images are included as multi-modal content blocks in the user message.
745    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
746    pub async fn stream_with_attachments(
747        &self,
748        prompt: &str,
749        attachments: &[crate::llm::Attachment],
750        history: Option<&[Message]>,
751    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
752        conversation_runtime::stream_with_attachments(self, prompt, attachments, history).await
753    }
754
755    /// Send a prompt and stream events back.
756    ///
757    /// When `history` is `None`, uses the session's internal history
758    /// and updates it when the stream completes.
759    /// When `Some`, uses the provided history instead.
760    ///
761    /// If the prompt starts with `/`, it is dispatched as a slash command
762    /// and the result is emitted as a single `TextDelta` + `End` event.
763    pub async fn stream(
764        &self,
765        prompt: &str,
766        history: Option<&[Message]>,
767    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
768        conversation_runtime::stream(self, prompt, history).await
769    }
770
771    /// Cancel the current ongoing operation (send/stream).
772    ///
773    /// If an operation is in progress, this will trigger cancellation of the LLM streaming
774    /// and tool execution. The operation will terminate as soon as possible.
775    ///
776    /// Returns `true` if an operation was cancelled, `false` if no operation was in progress.
777    pub async fn cancel(&self) -> bool {
778        RunControl::from_session(self).cancel_current().await
779    }
780
781    /// Cancel a specific run only if it is still the active run.
782    ///
783    /// This is useful for SDK callers that hold a previously observed run ID:
784    /// stale run IDs will not cancel a newer operation.
785    pub async fn cancel_run(&self, run_id: &str) -> bool {
786        RunControl::from_session(self).cancel_run(run_id).await
787    }
788
789    /// Return snapshots for runs recorded by this session.
790    pub async fn runs(&self) -> Vec<crate::run::RunSnapshot> {
791        RunControl::from_session(self).runs().await
792    }
793
794    /// Return a snapshot for a recorded run.
795    pub async fn run_snapshot(&self, run_id: &str) -> Option<crate::run::RunSnapshot> {
796        RunControl::from_session(self).run_snapshot(run_id).await
797    }
798
799    /// Return recorded runtime events for a run.
800    pub async fn run_events(&self, run_id: &str) -> Vec<crate::run::RunEventRecord> {
801        RunControl::from_session(self).run_events(run_id).await
802    }
803
804    /// Return a handle for the currently running operation, if any.
805    pub async fn current_run(&self) -> Option<crate::run::RunHandle> {
806        RunControl::from_session(self).current_run().await
807    }
808
809    /// Return active tool calls observed for the currently running operation.
810    pub async fn active_tools(&self) -> Vec<crate::run::ActiveToolSnapshot> {
811        SessionView::from_session(self).active_tools().await
812    }
813
814    /// Look up a delegated subagent task by id. Returns `None` if no such task
815    /// has been observed in this session.
816    pub async fn subagent_task(
817        &self,
818        task_id: &str,
819    ) -> Option<crate::subagent_task_tracker::SubagentTaskSnapshot> {
820        self.subagent_tasks.get(task_id).await
821    }
822
823    /// Return snapshots of every delegated subagent task observed in this
824    /// session (including completed and failed ones), oldest first.
825    pub async fn subagent_tasks(&self) -> Vec<crate::subagent_task_tracker::SubagentTaskSnapshot> {
826        self.subagent_tasks.list_for_parent(&self.session_id).await
827    }
828
829    /// Return snapshots of subagent tasks still in `Running` state.
830    pub async fn pending_subagent_tasks(
831        &self,
832    ) -> Vec<crate::subagent_task_tracker::SubagentTaskSnapshot> {
833        use crate::subagent_task_tracker::SubagentStatus;
834        self.subagent_tasks
835            .list_for_parent(&self.session_id)
836            .await
837            .into_iter()
838            .filter(|task| task.status == SubagentStatus::Running)
839            .collect()
840    }
841
842    /// Cancel an in-flight delegated subagent task by id. Returns `true`
843    /// when a cancellation token was found and fired, `false` when the
844    /// task id is unknown or the task has already finished. The eventual
845    /// `SubagentEnd` from the cancelled child loop won't downgrade the
846    /// terminal status — it stays `Cancelled`.
847    pub async fn cancel_subagent_task(&self, task_id: &str) -> bool {
848        self.subagent_tasks.cancel(task_id).await
849    }
850
851    /// Return a shared handle to the session's subagent task tracker.
852    ///
853    /// Advanced: embedders implementing a custom subagent execution path
854    /// (i.e. spawning child loops outside the built-in `task` tool) can use
855    /// this to register cancellation tokens and feed `AgentEvent`s into the
856    /// tracker so the standard
857    /// [`subagent_task`](Self::subagent_task) / [`pending_subagent_tasks`](Self::pending_subagent_tasks) /
858    /// [`cancel_subagent_task`](Self::cancel_subagent_task) APIs and
859    /// [`close`](Self::close) keep working uniformly across execution paths.
860    pub fn subagent_tracker(
861        &self,
862    ) -> Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker> {
863        Arc::clone(&self.subagent_tasks)
864    }
865
866    /// Return a snapshot of the session's conversation history.
867    pub fn history(&self) -> Vec<Message> {
868        SessionView::from_session(self).history()
869    }
870
871    /// Return pending HITL tool confirmations for this session.
872    pub async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
873        HitlControl::from_session(self)
874            .pending_confirmations()
875            .await
876    }
877
878    /// Resolve a pending HITL tool confirmation.
879    ///
880    /// Returns `Ok(true)` when a pending confirmation was found and completed,
881    /// `Ok(false)` when the tool ID is not pending or HITL is not configured.
882    pub async fn confirm_tool_use(
883        &self,
884        tool_id: &str,
885        approved: bool,
886        reason: Option<String>,
887    ) -> Result<bool> {
888        HitlControl::from_session(self)
889            .confirm_tool_use(tool_id, approved, reason)
890            .await
891    }
892
893    /// Cancel all pending HITL confirmations for this session.
894    pub async fn cancel_confirmations(&self) -> usize {
895        HitlControl::from_session(self).cancel_confirmations().await
896    }
897
898    /// Return a reference to the session's memory, if configured.
899    pub fn memory(&self) -> Option<&Arc<crate::memory::AgentMemory>> {
900        SessionView::from_session(self).memory()
901    }
902
903    /// Return the session ID.
904    pub fn id(&self) -> &str {
905        SessionView::from_session(self).id()
906    }
907
908    /// Return the session workspace path.
909    pub fn workspace(&self) -> &std::path::Path {
910        SessionView::from_session(self).workspace()
911    }
912
913    /// Return any deferred init warning (e.g. memory store failed to initialize).
914    pub fn init_warning(&self) -> Option<&str> {
915        SessionView::from_session(self).init_warning()
916    }
917
918    /// Return the session ID.
919    pub fn session_id(&self) -> &str {
920        SessionView::from_session(self).id()
921    }
922
923    /// Return the definitions of all tools currently registered in this session.
924    ///
925    /// The list reflects the live state of the tool executor — tools added via
926    /// `add_mcp_server()` appear immediately; tools removed via
927    /// `remove_mcp_server()` disappear immediately.
928    pub fn tool_definitions(&self) -> Vec<crate::llm::ToolDefinition> {
929        DirectToolRuntime::from_session(self).definitions()
930    }
931
932    /// Return the names of all tools currently registered on this session.
933    ///
934    /// Equivalent to `tool_definitions().into_iter().map(|t| t.name).collect()`.
935    /// Tools added via [`add_mcp_server`] appear immediately; tools removed via
936    /// [`remove_mcp_server`] disappear immediately.
937    pub fn tool_names(&self) -> Vec<String> {
938        DirectToolRuntime::from_session(self).names()
939    }
940
941    /// Return a stored tool artifact by URI, if it exists in this session.
942    pub fn get_artifact(&self, artifact_uri: &str) -> Option<crate::tools::ToolArtifact> {
943        DirectToolRuntime::from_session(self).artifact(artifact_uri)
944    }
945
946    /// Return compact execution trace events recorded for this session.
947    pub fn trace_events(&self) -> Vec<crate::trace::TraceEvent> {
948        SessionView::from_session(self).trace_events()
949    }
950
951    /// Return structured verification reports recorded for this session.
952    pub fn verification_reports(&self) -> Vec<crate::verification::VerificationReport> {
953        VerificationRuntime::from_session(self).reports()
954    }
955
956    /// Return a structured summary of all verification reports recorded for this session.
957    pub fn verification_summary(&self) -> crate::verification::VerificationSummary {
958        VerificationRuntime::from_session(self).summary()
959    }
960
961    /// Return a concise human-readable verification summary for this session.
962    pub fn verification_summary_text(&self) -> String {
963        VerificationRuntime::from_session(self).summary_text()
964    }
965
966    /// Add externally produced verification reports to this session's completion evidence.
967    pub fn record_verification_reports(
968        &self,
969        reports: impl IntoIterator<Item = crate::verification::VerificationReport>,
970    ) {
971        VerificationRuntime::from_session(self).record(reports);
972    }
973
974    // ========================================================================
975    // Hook API
976    // ========================================================================
977
978    /// Register a hook for lifecycle event interception.
979    pub fn register_hook(&self, hook: crate::hooks::Hook) {
980        HookControl::from_session(self).register_hook(hook);
981    }
982
983    /// Unregister a hook by ID.
984    pub fn unregister_hook(&self, hook_id: &str) -> Option<crate::hooks::Hook> {
985        HookControl::from_session(self).unregister_hook(hook_id)
986    }
987
988    /// Register a handler for a specific hook.
989    pub fn register_hook_handler(
990        &self,
991        hook_id: &str,
992        handler: Arc<dyn crate::hooks::HookHandler>,
993    ) {
994        HookControl::from_session(self).register_hook_handler(hook_id, handler);
995    }
996
997    /// Unregister a hook handler by hook ID.
998    pub fn unregister_hook_handler(&self, hook_id: &str) {
999        HookControl::from_session(self).unregister_hook_handler(hook_id);
1000    }
1001
1002    /// Get the number of registered hooks.
1003    pub fn hook_count(&self) -> usize {
1004        HookControl::from_session(self).hook_count()
1005    }
1006
1007    /// Save the session to the configured store.
1008    ///
1009    /// Returns `Ok(())` if saved successfully, or if no store is configured (no-op).
1010    pub async fn save(&self) -> Result<()> {
1011        session_save::save(self).await
1012    }
1013
1014    /// Read a file from the workspace.
1015    pub async fn read_file(&self, path: &str) -> Result<String> {
1016        DirectToolRuntime::from_session(self).read_file(path).await
1017    }
1018
1019    /// Write a file in the workspace.
1020    pub async fn write_file(&self, path: &str, content: &str) -> Result<ToolCallResult> {
1021        DirectToolRuntime::from_session(self)
1022            .write_file(path, content)
1023            .await
1024    }
1025
1026    /// List a directory in the workspace.
1027    pub async fn ls(&self, path: Option<&str>) -> Result<ToolCallResult> {
1028        DirectToolRuntime::from_session(self).ls(path).await
1029    }
1030
1031    /// Edit a file by replacing text in the workspace.
1032    pub async fn edit_file(
1033        &self,
1034        path: &str,
1035        old_string: &str,
1036        new_string: &str,
1037        replace_all: bool,
1038    ) -> Result<ToolCallResult> {
1039        DirectToolRuntime::from_session(self)
1040            .edit_file(path, old_string, new_string, replace_all)
1041            .await
1042    }
1043
1044    /// Apply a unified diff patch to a workspace file.
1045    pub async fn patch_file(&self, path: &str, diff: &str) -> Result<ToolCallResult> {
1046        DirectToolRuntime::from_session(self)
1047            .patch_file(path, diff)
1048            .await
1049    }
1050
1051    /// Execute a bash command in the workspace.
1052    ///
1053    /// When a sandbox handle is configured via
1054    /// [`SessionOptions::with_sandbox_handle()`], the command is routed through
1055    /// that sandbox.
1056    pub async fn bash(&self, command: &str) -> Result<String> {
1057        DirectToolRuntime::from_session(self).bash(command).await
1058    }
1059
1060    /// Run verification commands through the session's tool execution path.
1061    pub async fn verify_commands(
1062        &self,
1063        subject: &str,
1064        commands: &[crate::verification::VerificationCommand],
1065    ) -> Result<crate::verification::VerificationReport> {
1066        VerificationRuntime::from_session(self)
1067            .verify_commands(subject, commands)
1068            .await
1069    }
1070
1071    /// Return project-aware verification command presets for this workspace.
1072    pub fn verification_presets(&self) -> Vec<crate::verification::VerificationPreset> {
1073        VerificationRuntime::from_session(self).presets()
1074    }
1075
1076    /// Search for files matching a glob pattern.
1077    pub async fn glob(&self, pattern: &str) -> Result<Vec<String>> {
1078        DirectToolRuntime::from_session(self).glob(pattern).await
1079    }
1080
1081    /// Search file contents with a regex pattern.
1082    pub async fn grep(&self, pattern: &str) -> Result<String> {
1083        DirectToolRuntime::from_session(self).grep(pattern).await
1084    }
1085
1086    /// Execute a tool by name, bypassing the LLM.
1087    pub async fn tool(&self, name: &str, args: serde_json::Value) -> Result<ToolCallResult> {
1088        DirectToolRuntime::from_session(self).call(name, args).await
1089    }
1090
1091    // ========================================================================
1092    // Advanced optional Queue API
1093    // ========================================================================
1094
1095    /// Returns whether this session has an advanced lane queue configured.
1096    pub fn has_queue(&self) -> bool {
1097        QueueControl::from_session(self).has_queue()
1098    }
1099
1100    /// Configure a lane's handler mode for explicit external/hybrid dispatch.
1101    ///
1102    /// Only effective when a queue is configured via `SessionOptions::with_queue_config`.
1103    pub async fn set_lane_handler(&self, lane: SessionLane, config: LaneHandlerConfig) {
1104        QueueControl::from_session(self)
1105            .set_lane_handler(lane, config)
1106            .await;
1107    }
1108
1109    /// Complete an external queue task by ID.
1110    ///
1111    /// Returns `true` if the task was found and completed, `false` if not found.
1112    pub async fn complete_external_task(&self, task_id: &str, result: ExternalTaskResult) -> bool {
1113        QueueControl::from_session(self)
1114            .complete_external_task(task_id, result)
1115            .await
1116    }
1117
1118    /// Get pending external queue tasks awaiting completion by an external handler.
1119    pub async fn pending_external_tasks(&self) -> Vec<ExternalTask> {
1120        QueueControl::from_session(self)
1121            .pending_external_tasks()
1122            .await
1123    }
1124
1125    /// Get optional queue statistics (pending, active, external counts per lane).
1126    pub async fn queue_stats(&self) -> SessionQueueStats {
1127        QueueControl::from_session(self).stats().await
1128    }
1129
1130    /// Get a metrics snapshot from the optional queue (if metrics are enabled).
1131    pub async fn queue_metrics(&self) -> Option<MetricsSnapshot> {
1132        QueueControl::from_session(self).metrics().await
1133    }
1134
1135    /// Get dead letters from the optional queue's DLQ (if DLQ is enabled).
1136    pub async fn dead_letters(&self) -> Vec<DeadLetter> {
1137        QueueControl::from_session(self).dead_letters().await
1138    }
1139
1140    // ========================================================================
1141    // MCP API
1142    // ========================================================================
1143
1144    /// Register all agents found in a directory with the live session.
1145    ///
1146    /// Scans `dir` for `*.yaml`, `*.yml`, and `*.md` agent definition files,
1147    /// parses them, and adds each one to the shared `AgentRegistry` used by the
1148    /// `task` tool.  New agents are immediately usable via `task(agent="…")` in
1149    /// the same session — no restart required.
1150    ///
1151    /// Returns the number of agents successfully loaded from the directory.
1152    pub fn register_agent_dir(&self, dir: &std::path::Path) -> usize {
1153        SessionExtensionRuntime::from_session(self).register_agent_dir(dir)
1154    }
1155
1156    /// Register a disposable worker agent with the live session.
1157    ///
1158    /// The returned definition is immediately available to the `task` tool by
1159    /// worker name, so callers can create many reproducible workers without
1160    /// writing temporary agent files or restarting the session.
1161    pub fn register_worker_agent(
1162        &self,
1163        spec: crate::subagent::WorkerAgentSpec,
1164    ) -> crate::subagent::AgentDefinition {
1165        SessionExtensionRuntime::from_session(self).register_worker_agent(spec)
1166    }
1167
1168    /// Register multiple disposable worker agents with the live session.
1169    pub fn register_worker_agents<I>(&self, specs: I) -> Vec<crate::subagent::AgentDefinition>
1170    where
1171        I: IntoIterator<Item = crate::subagent::WorkerAgentSpec>,
1172    {
1173        SessionExtensionRuntime::from_session(self).register_worker_agents(specs)
1174    }
1175
1176    /// Add an MCP server to this session.
1177    ///
1178    /// Registers, connects, and makes all tools immediately available for the
1179    /// agent to call. Tool names follow the convention `mcp__<name>__<tool>`.
1180    ///
1181    /// Returns the number of tools registered from the server.
1182    pub async fn add_mcp_server(
1183        &self,
1184        config: crate::mcp::McpServerConfig,
1185    ) -> crate::error::Result<usize> {
1186        SessionExtensionRuntime::from_session(self)
1187            .add_mcp_server(config)
1188            .await
1189    }
1190
1191    /// Remove an MCP server from this session.
1192    ///
1193    /// Disconnects the server and unregisters all its tools from the executor.
1194    /// No-op if the server was never added.
1195    pub async fn remove_mcp_server(&self, server_name: &str) -> crate::error::Result<()> {
1196        SessionExtensionRuntime::from_session(self)
1197            .remove_mcp_server(server_name)
1198            .await
1199    }
1200
1201    /// Return the connection status of all MCP servers registered with this session.
1202    pub async fn mcp_status(
1203        &self,
1204    ) -> std::collections::HashMap<String, crate::mcp::McpServerStatus> {
1205        SessionExtensionRuntime::from_session(self)
1206            .mcp_status()
1207            .await
1208    }
1209}
1210
1211// ============================================================================
1212// Tests
1213// ============================================================================
1214
1215#[cfg(test)]
1216mod tests;