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_commands;
53mod session_config;
54mod session_extensions;
55mod session_hitl;
56mod session_options;
57mod session_persistence;
58mod session_queue;
59mod session_runs;
60mod session_runtime;
61mod session_save;
62mod session_verification;
63mod session_view;
64use direct_tools::DirectToolRuntime;
65use hook_control::HookControl;
66use runtime_events::ActiveToolState;
67use session_extensions::SessionExtensionRuntime;
68use session_hitl::HitlControl;
69use session_queue::QueueControl;
70use session_runs::RunControl;
71use session_verification::VerificationRuntime;
72use session_view::SessionView;
73
74/// Canonicalize a path, stripping the Windows `\\?\` UNC prefix to avoid
75/// polluting workspace strings throughout the system (prompts, session data, etc.).
76fn safe_canonicalize(path: &Path) -> PathBuf {
77    match std::fs::canonicalize(path) {
78        Ok(p) => strip_unc_prefix(p),
79        Err(_) => path.to_path_buf(),
80    }
81}
82
83/// Strip the Windows extended-length path prefix (`\\?\`) that `canonicalize()` adds.
84/// On non-Windows this is a no-op.
85fn strip_unc_prefix(path: PathBuf) -> PathBuf {
86    #[cfg(windows)]
87    {
88        let s = path.to_string_lossy();
89        if let Some(stripped) = s.strip_prefix(r"\\?\") {
90            return PathBuf::from(stripped);
91        }
92    }
93    path
94}
95
96// ============================================================================
97// ToolCallResult
98// ============================================================================
99
100/// Result of a direct tool execution (no LLM).
101#[derive(Debug, Clone)]
102pub struct ToolCallResult {
103    pub name: String,
104    pub output: String,
105    pub exit_code: i32,
106    pub metadata: Option<serde_json::Value>,
107    /// Structured discriminant for tool failures. `None` when the tool
108    /// either succeeded or failed without a typed reason (the message in
109    /// `output` is then the only diagnostic). Populated for known
110    /// kinds such as `VersionConflict` so SDK callers can branch on the
111    /// `type` field instead of regex-matching `output`.
112    pub error_kind: Option<crate::tools::ToolErrorKind>,
113}
114
115// ============================================================================
116// SessionOptions
117// ============================================================================
118
119/// Optional per-session overrides.
120#[derive(Clone, Default)]
121pub struct SessionOptions {
122    /// Override the default model. Format: `"provider/model"` (e.g., `"openai/gpt-4o"`).
123    pub model: Option<String>,
124    /// Extra directories to scan for agent files.
125    /// Merged with any global `agent_dirs` from [`CodeConfig`].
126    pub agent_dirs: Vec<PathBuf>,
127    /// Reproducible disposable workers registered for task delegation.
128    /// Explicit session workers override agents loaded from directories by name.
129    pub worker_agents: Vec<crate::subagent::WorkerAgentSpec>,
130    /// Optional queue configuration for lane-based tool execution.
131    ///
132    /// When set, enables priority-based tool scheduling with parallel execution
133    /// of read-only (Query-lane) tools, DLQ, metrics, and external task handling.
134    pub queue_config: Option<SessionQueueConfig>,
135    /// Optional security provider for taint tracking and output sanitization
136    pub security_provider: Option<Arc<dyn crate::security::SecurityProvider>>,
137    /// Optional context providers for RAG
138    pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
139    /// Optional confirmation manager for HITL
140    pub confirmation_manager: Option<Arc<dyn crate::hitl::ConfirmationProvider>>,
141    /// Optional confirmation policy (will be used to create ConfirmationManager if confirmation_manager is not set)
142    pub confirmation_policy: Option<crate::hitl::ConfirmationPolicy>,
143    /// Optional permission checker
144    pub permission_checker: Option<Arc<dyn crate::permissions::PermissionChecker>>,
145    /// Serializable permission policy used to build the checker, when available.
146    pub permission_policy: Option<crate::permissions::PermissionPolicy>,
147    /// Enable planning
148    pub planning_mode: PlanningMode,
149    /// Enable goal tracking
150    pub goal_tracking: bool,
151    /// Extra directories to scan for skill files (*.md).
152    /// Merged with any global `skill_dirs` from [`CodeConfig`].
153    pub skill_dirs: Vec<PathBuf>,
154    /// Optional skill registry for instruction injection
155    pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
156    /// Optional memory store for long-term memory persistence
157    pub memory_store: Option<Arc<dyn MemoryStore>>,
158    /// Deferred file memory directory — constructed async in `build_session()`
159    pub(crate) file_memory_dir: Option<PathBuf>,
160    /// Optional session store for persistence
161    pub session_store: Option<Arc<dyn crate::store::SessionStore>>,
162    /// Explicit session ID (auto-generated if not set)
163    pub session_id: Option<String>,
164    /// Auto-save after each completed `send()` or default-history `stream()` call.
165    pub auto_save: bool,
166    /// Optional artifact retention limits for large tool/program outputs.
167    pub artifact_store_limits: Option<crate::tools::ArtifactStoreLimits>,
168    /// Max consecutive parse errors before aborting (overrides default of 2).
169    /// `None` uses the `AgentConfig` default.
170    pub max_parse_retries: Option<u32>,
171    /// Per-tool execution timeout in milliseconds.
172    /// `None` = no timeout (default).
173    pub tool_timeout_ms: Option<u64>,
174    /// Circuit-breaker threshold: max consecutive LLM API failures before
175    /// aborting in non-streaming mode (overrides default of 3).
176    /// `None` uses the `AgentConfig` default.
177    pub circuit_breaker_threshold: Option<u32>,
178    /// Optional concrete sandbox implementation.
179    ///
180    /// When set, `bash` tool commands are routed through this sandbox instead
181    /// of `std::process::Command`. The host application constructs and owns
182    /// the implementation (e.g., an A3S Box–backed handle).
183    pub sandbox_handle: Option<Arc<dyn crate::sandbox::BashSandbox>>,
184    /// Optional host-provided workspace backend.
185    ///
186    /// When set, built-in tools such as `read`, `write`, `ls`, and `bash`
187    /// execute against these workspace capabilities instead of assuming the
188    /// server-local filesystem. This is the primary extension point for DFS,
189    /// browser, container, and remote workspace deployments.
190    pub workspace_services: Option<Arc<crate::workspace::WorkspaceServices>>,
191    /// Enable auto-compaction when context usage exceeds threshold.
192    pub auto_compact: bool,
193    /// Context usage percentage threshold for auto-compaction (0.0 - 1.0).
194    /// Default: 0.80 (80%).
195    pub auto_compact_threshold: Option<f32>,
196    /// Inject a continuation message when the LLM stops without completing the task.
197    /// `None` uses the `AgentConfig` default (true).
198    pub continuation_enabled: Option<bool>,
199    /// Maximum continuation injections per execution.
200    /// `None` uses the `AgentConfig` default (3).
201    pub max_continuation_turns: Option<u32>,
202    /// Maximum execution time in milliseconds.
203    /// `None` = no timeout (default).
204    /// When set, the execution loop will abort if it exceeds this duration.
205    pub max_execution_time_ms: Option<u64>,
206    /// Optional MCP manager for connecting to external MCP servers.
207    ///
208    /// When set, all tools from connected MCP servers are registered and
209    /// available during agent execution with names like `mcp__server__tool`.
210    pub mcp_manager: Option<Arc<crate::mcp::manager::McpManager>>,
211    /// Sampling temperature (0.0–1.0). Overrides the provider default.
212    pub temperature: Option<f32>,
213    /// Extended thinking budget in tokens (Anthropic only).
214    pub thinking_budget: Option<usize>,
215    /// Per-session tool round limit override.
216    ///
217    /// When set, overrides the agent-level `max_tool_rounds` for this session only.
218    /// Maps directly from [`AgentDefinition::max_steps`] when creating sessions
219    /// via [`Agent::session_for_agent`].
220    pub max_tool_rounds: Option<usize>,
221    /// Per-session parallel fan-out limit override.
222    ///
223    /// Applies to delegated `parallel_task`, plan wave execution, and safe
224    /// parallel write batches.
225    pub max_parallel_tasks: Option<usize>,
226    /// Per-session automatic subagent delegation override.
227    pub auto_delegation: Option<crate::config::AutoDelegationConfig>,
228    /// Per-session kill switch for automatic parallel child-agent fan-out.
229    ///
230    /// This overlays the effective automatic delegation config instead of
231    /// replacing it, so callers can disable auto fan-out without disabling
232    /// automatic delegation itself.
233    pub auto_parallel_delegation: Option<bool>,
234    /// Slot-based system prompt customization.
235    ///
236    /// When set, overrides the agent-level prompt slots for this session.
237    /// Users can customize role, guidelines, response style, and extra instructions
238    /// without losing the core agentic capabilities.
239    pub prompt_slots: Option<SystemPromptSlots>,
240    /// Optional external hook executor (e.g. an AHP harness server).
241    ///
242    /// When set, **replaces** the built-in `HookEngine` for this session.
243    /// All 11 lifecycle events are forwarded to the executor instead of being
244    /// dispatched locally. The executor is also propagated to sub-agents via
245    /// the sentinel hook mechanism.
246    pub hook_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
247}
248
249// ============================================================================
250// Agent
251// ============================================================================
252
253/// High-level agent facade.
254///
255/// Holds the LLM client and agent config. Workspace-independent.
256/// Use [`Agent::session()`] to bind to a workspace.
257pub struct Agent {
258    code_config: CodeConfig,
259    config: AgentConfig,
260    /// Global MCP manager loaded from config.mcp_servers
261    global_mcp: Option<Arc<crate::mcp::manager::McpManager>>,
262    /// Pre-fetched MCP tool definitions from global_mcp (cached at creation time).
263    /// Wrapped in Mutex so `refresh_mcp_tools()` can update the cache without `&mut self`.
264    global_mcp_tools: std::sync::Mutex<Vec<(String, crate::mcp::McpTool)>>,
265}
266
267impl std::fmt::Debug for Agent {
268    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        f.debug_struct("Agent").finish()
270    }
271}
272
273impl Agent {
274    /// Create from a config file path or inline ACL-compatible string.
275    ///
276    /// Auto-detects `.acl` file paths vs inline ACL-compatible config.
277    pub async fn new(config_source: impl Into<String>) -> Result<Self> {
278        let config = agent_bootstrap::load_code_config(config_source.into())?;
279        Self::from_config(config).await
280    }
281
282    /// Create from a config file path or inline ACL-compatible string.
283    ///
284    /// Alias for [`Agent::new()`] — provides a consistent API with
285    /// the Python and Node.js SDKs.
286    pub async fn create(config_source: impl Into<String>) -> Result<Self> {
287        Self::new(config_source).await
288    }
289
290    /// Create from a [`CodeConfig`] struct.
291    pub async fn from_config(config: CodeConfig) -> Result<Self> {
292        agent_bootstrap::build_agent_from_config(config).await
293    }
294
295    /// Re-fetch tool definitions from all connected global MCP servers and
296    /// update the internal cache.
297    ///
298    /// Call this when an MCP server has added or removed tools since the
299    /// agent was created. The refreshed tools will be visible to all
300    /// **new** sessions created after this call; existing sessions are
301    /// unaffected (their `ToolExecutor` snapshot is already built).
302    pub async fn refresh_mcp_tools(&self) -> Result<()> {
303        agent_sessions::refresh_mcp_tools(self).await
304    }
305
306    /// Bind to a workspace directory, returning an [`AgentSession`].
307    ///
308    /// Pass `None` for defaults, or `Some(SessionOptions)` to override
309    /// the model, agent directories for this session.
310    pub fn session(
311        &self,
312        workspace: impl Into<String>,
313        options: Option<SessionOptions>,
314    ) -> Result<AgentSession> {
315        agent_sessions::create_session(self, workspace, options)
316    }
317
318    /// Create a session pre-configured from an [`AgentDefinition`].
319    ///
320    /// Maps the definition's `permissions`, `prompt`, `model`, and `max_steps`
321    /// directly into [`SessionOptions`], so markdown/YAML-defined subagents can
322    /// be used by delegation and advanced control-plane flows without manual wiring.
323    ///
324    /// The mapping follows the same logic as the built-in `task` tool:
325    /// - `permissions` → `permission_checker`
326    /// - `prompt`      → `prompt_slots.extra`
327    /// - `max_steps`   → `max_tool_rounds`
328    /// - `model`       → `model` (as `"provider/model"` string)
329    ///
330    /// `extra` can supply additional overrides (e.g. `planning_enabled`) that
331    /// take precedence over the definition's values.
332    pub fn session_for_agent(
333        &self,
334        workspace: impl Into<String>,
335        def: &crate::subagent::AgentDefinition,
336        extra: Option<SessionOptions>,
337    ) -> Result<AgentSession> {
338        agent_sessions::create_session_for_agent(self, workspace, def, extra)
339    }
340
341    /// Create a session from a reproducible disposable worker recipe.
342    ///
343    /// This is the cattle-mode companion to [`Agent::session_for_agent`]: callers
344    /// provide a small [`WorkerAgentSpec`](crate::subagent::WorkerAgentSpec), and
345    /// A3S Code compiles it into the same runtime definition used by delegated agents.
346    pub fn session_for_worker(
347        &self,
348        workspace: impl Into<String>,
349        spec: crate::subagent::WorkerAgentSpec,
350        extra: Option<SessionOptions>,
351    ) -> Result<AgentSession> {
352        let def = spec.into_agent_definition();
353        self.session_for_agent(workspace, &def, extra)
354    }
355
356    /// Resume a previously saved session by ID.
357    ///
358    /// Loads the session data from the store, rebuilds the `AgentSession` with
359    /// the saved conversation history, and returns it ready for continued use.
360    ///
361    /// The `options` must include a `session_store` (or `with_file_session_store`)
362    /// that contains the saved session.
363    pub fn resume_session(
364        &self,
365        session_id: &str,
366        options: SessionOptions,
367    ) -> Result<AgentSession> {
368        agent_sessions::resume_session(self, session_id, options)
369    }
370
371    #[cfg(test)]
372    fn build_session(
373        &self,
374        workspace: String,
375        llm_client: Arc<dyn LlmClient>,
376        opts: &SessionOptions,
377    ) -> Result<AgentSession> {
378        session_builder::build_agent_session(self, workspace, llm_client, opts)
379    }
380}
381
382// ============================================================================
383// AgentSession
384// ============================================================================
385
386/// Workspace-bound session. All LLM and tool operations happen here.
387///
388/// History is automatically accumulated after each `send()` call and after
389/// `stream()` completes when no custom history is supplied.
390/// Use `history()` to retrieve the current conversation log.
391pub struct AgentSession {
392    llm_client: Arc<dyn LlmClient>,
393    tool_executor: Arc<ToolExecutor>,
394    tool_context: ToolContext,
395    config: AgentConfig,
396    workspace: PathBuf,
397    /// Unique session identifier.
398    session_id: String,
399    /// Internal conversation history, auto-updated after each `send()` and default-history `stream()`.
400    history: Arc<RwLock<Vec<Message>>>,
401    /// Optional lane queue for priority-based tool execution.
402    command_queue: Option<Arc<crate::session_lane_queue::SessionLaneQueue>>,
403    /// Optional long-term memory.
404    memory: Option<Arc<crate::memory::AgentMemory>>,
405    /// Optional session store for persistence.
406    session_store: Option<Arc<dyn crate::store::SessionStore>>,
407    /// Auto-save after each completed `send()` or default-history `stream()`.
408    auto_save: bool,
409    /// Hook engine for lifecycle event interception.
410    hook_engine: Arc<crate::hooks::HookEngine>,
411    /// Optional external hook executor (e.g. AHP harness). When set, replaces
412    /// `hook_engine` as the executor passed to each `AgentLoop`.
413    ahp_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
414    /// Deferred init warning: emitted as PersistenceFailed on first send() if set.
415    init_warning: Option<String>,
416    /// Slash command registry for `/command` dispatch.
417    /// Uses interior mutability so commands can be registered on a shared `Arc<AgentSession>`.
418    command_registry: std::sync::Mutex<CommandRegistry>,
419    /// Model identifier for display (e.g., "anthropic/claude-sonnet-4-20250514").
420    model_name: String,
421    /// Shared MCP manager — all add_mcp_server / remove_mcp_server calls go here.
422    mcp_manager: Arc<crate::mcp::manager::McpManager>,
423    /// Shared agent registry — populated at session creation; extended via register_agent_dir().
424    agent_registry: Arc<crate::subagent::AgentRegistry>,
425    /// Cancellation token for the current operation (send/stream).
426    /// Stored so that cancel() can abort ongoing LLM calls.
427    cancel_token: Arc<tokio::sync::Mutex<Option<tokio_util::sync::CancellationToken>>>,
428    /// ID of the run currently attached to the active cancellation token.
429    current_run_id: Arc<tokio::sync::Mutex<Option<String>>>,
430    /// In-memory run snapshots and event replay buffer for this session.
431    run_store: Arc<crate::run::InMemoryRunStore>,
432    /// Materialized view of delegated subagent task lifecycle, populated from runtime events.
433    subagent_tasks: Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>,
434    /// Currently executing tools observed from runtime events.
435    active_tools: Arc<tokio::sync::RwLock<HashMap<String, ActiveToolState>>>,
436    /// Compact execution traces for this session.
437    trace_sink: crate::trace::InMemoryTraceSink,
438    /// Structured completion evidence collected from agent and explicit verification runs.
439    verification_reports: Arc<RwLock<Vec<crate::verification::VerificationReport>>>,
440}
441
442impl std::fmt::Debug for AgentSession {
443    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444        f.debug_struct("AgentSession")
445            .field("session_id", &self.session_id)
446            .field("workspace", &self.workspace.display().to_string())
447            .field("auto_save", &self.auto_save)
448            .finish()
449    }
450}
451
452impl AgentSession {
453    /// Get a snapshot of command entries (name, description, optional usage).
454    ///
455    /// Acquires the command registry lock briefly and returns owned data.
456    pub fn command_registry(&self) -> std::sync::MutexGuard<'_, CommandRegistry> {
457        session_commands::registry(self)
458    }
459
460    /// Register a custom slash command.
461    ///
462    /// Takes `&self` so it can be called on a shared `Arc<AgentSession>`.
463    pub fn register_command(&self, cmd: Arc<dyn crate::commands::SlashCommand>) {
464        session_commands::register(self, cmd);
465    }
466
467    /// Cancel any active operation and release session resources.
468    pub async fn close(&self) {
469        let _ = self.cancel().await;
470    }
471
472    /// Send a prompt and wait for the complete response.
473    ///
474    /// When `history` is `None`, uses (and auto-updates) the session's
475    /// internal conversation history. When `Some`, uses the provided
476    /// history instead (the internal history is **not** modified).
477    ///
478    /// If the prompt starts with `/`, it is dispatched as a slash command
479    /// and the result is returned without calling the LLM.
480    pub async fn send(&self, prompt: &str, history: Option<&[Message]>) -> Result<AgentResult> {
481        conversation_runtime::send(self, prompt, history).await
482    }
483
484    /// Send a prompt with image attachments and wait for the complete response.
485    ///
486    /// Images are included as multi-modal content blocks in the user message.
487    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
488    pub async fn send_with_attachments(
489        &self,
490        prompt: &str,
491        attachments: &[crate::llm::Attachment],
492        history: Option<&[Message]>,
493    ) -> Result<AgentResult> {
494        conversation_runtime::send_with_attachments(self, prompt, attachments, history).await
495    }
496
497    /// Stream a prompt with image attachments.
498    ///
499    /// Images are included as multi-modal content blocks in the user message.
500    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
501    pub async fn stream_with_attachments(
502        &self,
503        prompt: &str,
504        attachments: &[crate::llm::Attachment],
505        history: Option<&[Message]>,
506    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
507        conversation_runtime::stream_with_attachments(self, prompt, attachments, history).await
508    }
509
510    /// Send a prompt and stream events back.
511    ///
512    /// When `history` is `None`, uses the session's internal history
513    /// and updates it when the stream completes.
514    /// When `Some`, uses the provided history instead.
515    ///
516    /// If the prompt starts with `/`, it is dispatched as a slash command
517    /// and the result is emitted as a single `TextDelta` + `End` event.
518    pub async fn stream(
519        &self,
520        prompt: &str,
521        history: Option<&[Message]>,
522    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
523        conversation_runtime::stream(self, prompt, history).await
524    }
525
526    /// Cancel the current ongoing operation (send/stream).
527    ///
528    /// If an operation is in progress, this will trigger cancellation of the LLM streaming
529    /// and tool execution. The operation will terminate as soon as possible.
530    ///
531    /// Returns `true` if an operation was cancelled, `false` if no operation was in progress.
532    pub async fn cancel(&self) -> bool {
533        RunControl::from_session(self).cancel_current().await
534    }
535
536    /// Cancel a specific run only if it is still the active run.
537    ///
538    /// This is useful for SDK callers that hold a previously observed run ID:
539    /// stale run IDs will not cancel a newer operation.
540    pub async fn cancel_run(&self, run_id: &str) -> bool {
541        RunControl::from_session(self).cancel_run(run_id).await
542    }
543
544    /// Return snapshots for runs recorded by this session.
545    pub async fn runs(&self) -> Vec<crate::run::RunSnapshot> {
546        RunControl::from_session(self).runs().await
547    }
548
549    /// Return a snapshot for a recorded run.
550    pub async fn run_snapshot(&self, run_id: &str) -> Option<crate::run::RunSnapshot> {
551        RunControl::from_session(self).run_snapshot(run_id).await
552    }
553
554    /// Return recorded runtime events for a run.
555    pub async fn run_events(&self, run_id: &str) -> Vec<crate::run::RunEventRecord> {
556        RunControl::from_session(self).run_events(run_id).await
557    }
558
559    /// Return a handle for the currently running operation, if any.
560    pub async fn current_run(&self) -> Option<crate::run::RunHandle> {
561        RunControl::from_session(self).current_run().await
562    }
563
564    /// Return active tool calls observed for the currently running operation.
565    pub async fn active_tools(&self) -> Vec<crate::run::ActiveToolSnapshot> {
566        SessionView::from_session(self).active_tools().await
567    }
568
569    /// Look up a delegated subagent task by id. Returns `None` if no such task
570    /// has been observed in this session.
571    pub async fn subagent_task(
572        &self,
573        task_id: &str,
574    ) -> Option<crate::subagent_task_tracker::SubagentTaskSnapshot> {
575        self.subagent_tasks.get(task_id).await
576    }
577
578    /// Return snapshots of every delegated subagent task observed in this
579    /// session (including completed and failed ones), oldest first.
580    pub async fn subagent_tasks(&self) -> Vec<crate::subagent_task_tracker::SubagentTaskSnapshot> {
581        self.subagent_tasks.list_for_parent(&self.session_id).await
582    }
583
584    /// Return snapshots of subagent tasks still in `Running` state.
585    pub async fn pending_subagent_tasks(
586        &self,
587    ) -> Vec<crate::subagent_task_tracker::SubagentTaskSnapshot> {
588        use crate::subagent_task_tracker::SubagentStatus;
589        self.subagent_tasks
590            .list_for_parent(&self.session_id)
591            .await
592            .into_iter()
593            .filter(|task| task.status == SubagentStatus::Running)
594            .collect()
595    }
596
597    /// Cancel an in-flight delegated subagent task by id. Returns `true`
598    /// when a cancellation token was found and fired, `false` when the
599    /// task id is unknown or the task has already finished. The eventual
600    /// `SubagentEnd` from the cancelled child loop won't downgrade the
601    /// terminal status — it stays `Cancelled`.
602    pub async fn cancel_subagent_task(&self, task_id: &str) -> bool {
603        self.subagent_tasks.cancel(task_id).await
604    }
605
606    /// Return a snapshot of the session's conversation history.
607    pub fn history(&self) -> Vec<Message> {
608        SessionView::from_session(self).history()
609    }
610
611    /// Return pending HITL tool confirmations for this session.
612    pub async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
613        HitlControl::from_session(self)
614            .pending_confirmations()
615            .await
616    }
617
618    /// Resolve a pending HITL tool confirmation.
619    ///
620    /// Returns `Ok(true)` when a pending confirmation was found and completed,
621    /// `Ok(false)` when the tool ID is not pending or HITL is not configured.
622    pub async fn confirm_tool_use(
623        &self,
624        tool_id: &str,
625        approved: bool,
626        reason: Option<String>,
627    ) -> Result<bool> {
628        HitlControl::from_session(self)
629            .confirm_tool_use(tool_id, approved, reason)
630            .await
631    }
632
633    /// Cancel all pending HITL confirmations for this session.
634    pub async fn cancel_confirmations(&self) -> usize {
635        HitlControl::from_session(self).cancel_confirmations().await
636    }
637
638    /// Return a reference to the session's memory, if configured.
639    pub fn memory(&self) -> Option<&Arc<crate::memory::AgentMemory>> {
640        SessionView::from_session(self).memory()
641    }
642
643    /// Return the session ID.
644    pub fn id(&self) -> &str {
645        SessionView::from_session(self).id()
646    }
647
648    /// Return the session workspace path.
649    pub fn workspace(&self) -> &std::path::Path {
650        SessionView::from_session(self).workspace()
651    }
652
653    /// Return any deferred init warning (e.g. memory store failed to initialize).
654    pub fn init_warning(&self) -> Option<&str> {
655        SessionView::from_session(self).init_warning()
656    }
657
658    /// Return the session ID.
659    pub fn session_id(&self) -> &str {
660        SessionView::from_session(self).id()
661    }
662
663    /// Return the definitions of all tools currently registered in this session.
664    ///
665    /// The list reflects the live state of the tool executor — tools added via
666    /// `add_mcp_server()` appear immediately; tools removed via
667    /// `remove_mcp_server()` disappear immediately.
668    pub fn tool_definitions(&self) -> Vec<crate::llm::ToolDefinition> {
669        DirectToolRuntime::from_session(self).definitions()
670    }
671
672    /// Return the names of all tools currently registered on this session.
673    ///
674    /// Equivalent to `tool_definitions().into_iter().map(|t| t.name).collect()`.
675    /// Tools added via [`add_mcp_server`] appear immediately; tools removed via
676    /// [`remove_mcp_server`] disappear immediately.
677    pub fn tool_names(&self) -> Vec<String> {
678        DirectToolRuntime::from_session(self).names()
679    }
680
681    /// Return a stored tool artifact by URI, if it exists in this session.
682    pub fn get_artifact(&self, artifact_uri: &str) -> Option<crate::tools::ToolArtifact> {
683        DirectToolRuntime::from_session(self).artifact(artifact_uri)
684    }
685
686    /// Return compact execution trace events recorded for this session.
687    pub fn trace_events(&self) -> Vec<crate::trace::TraceEvent> {
688        SessionView::from_session(self).trace_events()
689    }
690
691    /// Return structured verification reports recorded for this session.
692    pub fn verification_reports(&self) -> Vec<crate::verification::VerificationReport> {
693        VerificationRuntime::from_session(self).reports()
694    }
695
696    /// Return a structured summary of all verification reports recorded for this session.
697    pub fn verification_summary(&self) -> crate::verification::VerificationSummary {
698        VerificationRuntime::from_session(self).summary()
699    }
700
701    /// Return a concise human-readable verification summary for this session.
702    pub fn verification_summary_text(&self) -> String {
703        VerificationRuntime::from_session(self).summary_text()
704    }
705
706    /// Add externally produced verification reports to this session's completion evidence.
707    pub fn record_verification_reports(
708        &self,
709        reports: impl IntoIterator<Item = crate::verification::VerificationReport>,
710    ) {
711        VerificationRuntime::from_session(self).record(reports);
712    }
713
714    // ========================================================================
715    // Hook API
716    // ========================================================================
717
718    /// Register a hook for lifecycle event interception.
719    pub fn register_hook(&self, hook: crate::hooks::Hook) {
720        HookControl::from_session(self).register_hook(hook);
721    }
722
723    /// Unregister a hook by ID.
724    pub fn unregister_hook(&self, hook_id: &str) -> Option<crate::hooks::Hook> {
725        HookControl::from_session(self).unregister_hook(hook_id)
726    }
727
728    /// Register a handler for a specific hook.
729    pub fn register_hook_handler(
730        &self,
731        hook_id: &str,
732        handler: Arc<dyn crate::hooks::HookHandler>,
733    ) {
734        HookControl::from_session(self).register_hook_handler(hook_id, handler);
735    }
736
737    /// Unregister a hook handler by hook ID.
738    pub fn unregister_hook_handler(&self, hook_id: &str) {
739        HookControl::from_session(self).unregister_hook_handler(hook_id);
740    }
741
742    /// Get the number of registered hooks.
743    pub fn hook_count(&self) -> usize {
744        HookControl::from_session(self).hook_count()
745    }
746
747    /// Save the session to the configured store.
748    ///
749    /// Returns `Ok(())` if saved successfully, or if no store is configured (no-op).
750    pub async fn save(&self) -> Result<()> {
751        session_save::save(self).await
752    }
753
754    /// Read a file from the workspace.
755    pub async fn read_file(&self, path: &str) -> Result<String> {
756        DirectToolRuntime::from_session(self).read_file(path).await
757    }
758
759    /// Write a file in the workspace.
760    pub async fn write_file(&self, path: &str, content: &str) -> Result<ToolCallResult> {
761        DirectToolRuntime::from_session(self)
762            .write_file(path, content)
763            .await
764    }
765
766    /// List a directory in the workspace.
767    pub async fn ls(&self, path: Option<&str>) -> Result<ToolCallResult> {
768        DirectToolRuntime::from_session(self).ls(path).await
769    }
770
771    /// Edit a file by replacing text in the workspace.
772    pub async fn edit_file(
773        &self,
774        path: &str,
775        old_string: &str,
776        new_string: &str,
777        replace_all: bool,
778    ) -> Result<ToolCallResult> {
779        DirectToolRuntime::from_session(self)
780            .edit_file(path, old_string, new_string, replace_all)
781            .await
782    }
783
784    /// Apply a unified diff patch to a workspace file.
785    pub async fn patch_file(&self, path: &str, diff: &str) -> Result<ToolCallResult> {
786        DirectToolRuntime::from_session(self)
787            .patch_file(path, diff)
788            .await
789    }
790
791    /// Execute a bash command in the workspace.
792    ///
793    /// When a sandbox handle is configured via
794    /// [`SessionOptions::with_sandbox_handle()`], the command is routed through
795    /// that sandbox.
796    pub async fn bash(&self, command: &str) -> Result<String> {
797        DirectToolRuntime::from_session(self).bash(command).await
798    }
799
800    /// Run verification commands through the session's tool execution path.
801    pub async fn verify_commands(
802        &self,
803        subject: &str,
804        commands: &[crate::verification::VerificationCommand],
805    ) -> Result<crate::verification::VerificationReport> {
806        VerificationRuntime::from_session(self)
807            .verify_commands(subject, commands)
808            .await
809    }
810
811    /// Return project-aware verification command presets for this workspace.
812    pub fn verification_presets(&self) -> Vec<crate::verification::VerificationPreset> {
813        VerificationRuntime::from_session(self).presets()
814    }
815
816    /// Search for files matching a glob pattern.
817    pub async fn glob(&self, pattern: &str) -> Result<Vec<String>> {
818        DirectToolRuntime::from_session(self).glob(pattern).await
819    }
820
821    /// Search file contents with a regex pattern.
822    pub async fn grep(&self, pattern: &str) -> Result<String> {
823        DirectToolRuntime::from_session(self).grep(pattern).await
824    }
825
826    /// Execute a tool by name, bypassing the LLM.
827    pub async fn tool(&self, name: &str, args: serde_json::Value) -> Result<ToolCallResult> {
828        DirectToolRuntime::from_session(self).call(name, args).await
829    }
830
831    // ========================================================================
832    // Advanced optional Queue API
833    // ========================================================================
834
835    /// Returns whether this session has an advanced lane queue configured.
836    pub fn has_queue(&self) -> bool {
837        QueueControl::from_session(self).has_queue()
838    }
839
840    /// Configure a lane's handler mode for explicit external/hybrid dispatch.
841    ///
842    /// Only effective when a queue is configured via `SessionOptions::with_queue_config`.
843    pub async fn set_lane_handler(&self, lane: SessionLane, config: LaneHandlerConfig) {
844        QueueControl::from_session(self)
845            .set_lane_handler(lane, config)
846            .await;
847    }
848
849    /// Complete an external queue task by ID.
850    ///
851    /// Returns `true` if the task was found and completed, `false` if not found.
852    pub async fn complete_external_task(&self, task_id: &str, result: ExternalTaskResult) -> bool {
853        QueueControl::from_session(self)
854            .complete_external_task(task_id, result)
855            .await
856    }
857
858    /// Get pending external queue tasks awaiting completion by an external handler.
859    pub async fn pending_external_tasks(&self) -> Vec<ExternalTask> {
860        QueueControl::from_session(self)
861            .pending_external_tasks()
862            .await
863    }
864
865    /// Get optional queue statistics (pending, active, external counts per lane).
866    pub async fn queue_stats(&self) -> SessionQueueStats {
867        QueueControl::from_session(self).stats().await
868    }
869
870    /// Get a metrics snapshot from the optional queue (if metrics are enabled).
871    pub async fn queue_metrics(&self) -> Option<MetricsSnapshot> {
872        QueueControl::from_session(self).metrics().await
873    }
874
875    /// Get dead letters from the optional queue's DLQ (if DLQ is enabled).
876    pub async fn dead_letters(&self) -> Vec<DeadLetter> {
877        QueueControl::from_session(self).dead_letters().await
878    }
879
880    // ========================================================================
881    // MCP API
882    // ========================================================================
883
884    /// Register all agents found in a directory with the live session.
885    ///
886    /// Scans `dir` for `*.yaml`, `*.yml`, and `*.md` agent definition files,
887    /// parses them, and adds each one to the shared `AgentRegistry` used by the
888    /// `task` tool.  New agents are immediately usable via `task(agent="…")` in
889    /// the same session — no restart required.
890    ///
891    /// Returns the number of agents successfully loaded from the directory.
892    pub fn register_agent_dir(&self, dir: &std::path::Path) -> usize {
893        SessionExtensionRuntime::from_session(self).register_agent_dir(dir)
894    }
895
896    /// Register a disposable worker agent with the live session.
897    ///
898    /// The returned definition is immediately available to the `task` tool by
899    /// worker name, so callers can create many reproducible workers without
900    /// writing temporary agent files or restarting the session.
901    pub fn register_worker_agent(
902        &self,
903        spec: crate::subagent::WorkerAgentSpec,
904    ) -> crate::subagent::AgentDefinition {
905        SessionExtensionRuntime::from_session(self).register_worker_agent(spec)
906    }
907
908    /// Register multiple disposable worker agents with the live session.
909    pub fn register_worker_agents<I>(&self, specs: I) -> Vec<crate::subagent::AgentDefinition>
910    where
911        I: IntoIterator<Item = crate::subagent::WorkerAgentSpec>,
912    {
913        SessionExtensionRuntime::from_session(self).register_worker_agents(specs)
914    }
915
916    /// Add an MCP server to this session.
917    ///
918    /// Registers, connects, and makes all tools immediately available for the
919    /// agent to call. Tool names follow the convention `mcp__<name>__<tool>`.
920    ///
921    /// Returns the number of tools registered from the server.
922    pub async fn add_mcp_server(
923        &self,
924        config: crate::mcp::McpServerConfig,
925    ) -> crate::error::Result<usize> {
926        SessionExtensionRuntime::from_session(self)
927            .add_mcp_server(config)
928            .await
929    }
930
931    /// Remove an MCP server from this session.
932    ///
933    /// Disconnects the server and unregisters all its tools from the executor.
934    /// No-op if the server was never added.
935    pub async fn remove_mcp_server(&self, server_name: &str) -> crate::error::Result<()> {
936        SessionExtensionRuntime::from_session(self)
937            .remove_mcp_server(server_name)
938            .await
939    }
940
941    /// Return the connection status of all MCP servers registered with this session.
942    pub async fn mcp_status(
943        &self,
944    ) -> std::collections::HashMap<String, crate::mcp::McpServerStatus> {
945        SessionExtensionRuntime::from_session(self)
946            .mcp_status()
947            .await
948    }
949}
950
951// ============================================================================
952// Tests
953// ============================================================================
954
955#[cfg(test)]
956mod tests;