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// ReadFileOptions
119// ============================================================================
120
121/// Optional line-range controls for direct `read` tool calls.
122#[derive(Debug, Clone, Copy, Default)]
123pub struct ReadFileOptions {
124    /// 0-indexed line offset to start reading from.
125    pub offset: Option<usize>,
126    /// Maximum number of lines to read.
127    pub limit: Option<usize>,
128}
129
130// ============================================================================
131// SessionOptions
132// ============================================================================
133
134/// Optional per-session overrides.
135#[derive(Clone, Default)]
136pub struct SessionOptions {
137    /// Override the default model. Format: `"provider/model"` (e.g., `"openai/gpt-4o"`).
138    pub model: Option<String>,
139    /// Extra directories to scan for agent files.
140    /// Merged with any global `agent_dirs` from [`CodeConfig`].
141    pub agent_dirs: Vec<PathBuf>,
142    /// Reproducible disposable workers registered for task delegation.
143    /// Explicit session workers override agents loaded from directories by name.
144    pub worker_agents: Vec<crate::subagent::WorkerAgentSpec>,
145    /// Optional queue configuration for lane-based tool execution.
146    ///
147    /// When set, enables priority-based tool scheduling with parallel execution
148    /// of read-only (Query-lane) tools, DLQ, metrics, and external task handling.
149    pub queue_config: Option<SessionQueueConfig>,
150    /// Optional security provider for taint tracking and output sanitization
151    pub security_provider: Option<Arc<dyn crate::security::SecurityProvider>>,
152    /// Optional host-supplied LLM client.
153    ///
154    /// When set, it is used directly, overriding the `provider/model`
155    /// factory resolution — the one Action-layer backend that was previously
156    /// only injectable in test code. Lets a host plug in a provider the
157    /// built-in factory does not cover, a deterministic record/replay client,
158    /// or an HTTP-layer proxy/audit wrapper. Mirrors `workspace_services`.
159    pub llm_client: Option<Arc<dyn crate::llm::LlmClient>>,
160    /// Optional context providers for RAG
161    pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
162    /// Optional confirmation manager for HITL
163    pub confirmation_manager: Option<Arc<dyn crate::hitl::ConfirmationProvider>>,
164    /// Optional confirmation policy (will be used to create ConfirmationManager if confirmation_manager is not set)
165    pub confirmation_policy: Option<crate::hitl::ConfirmationPolicy>,
166    /// Optional permission checker
167    pub permission_checker: Option<Arc<dyn crate::permissions::PermissionChecker>>,
168    /// Serializable permission policy used to build the checker, when available.
169    pub permission_policy: Option<crate::permissions::PermissionPolicy>,
170    /// Enable planning
171    pub planning_mode: PlanningMode,
172    /// Enable goal tracking
173    pub goal_tracking: bool,
174    /// Extra directories to scan for skill files (*.md).
175    /// Merged with any global `skill_dirs` from [`CodeConfig`].
176    pub skill_dirs: Vec<PathBuf>,
177    /// Optional skill registry for instruction injection
178    pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
179    /// Whether active skill `allowed-tools` restrict ordinary session tool calls.
180    ///
181    /// Defaults to false so ordinary tools continue through permission policy,
182    /// hooks, HITL, and AHP. Set true to restore the legacy global active-skill
183    /// restriction behavior.
184    pub enforce_active_skill_tool_restrictions: Option<bool>,
185    /// Custom memory store override for long-term memory persistence.
186    ///
187    /// Sessions resolve a default store when this is not set.
188    pub memory_store: Option<Arc<dyn MemoryStore>>,
189    /// Deferred file memory directory — constructed async in `build_session()`
190    pub(crate) file_memory_dir: Option<PathBuf>,
191    /// Optional session store for persistence
192    pub session_store: Option<Arc<dyn crate::store::SessionStore>>,
193    /// Explicit session ID (auto-generated if not set)
194    pub session_id: Option<String>,
195    /// Multi-tenant identifier. Framework only transports this string;
196    /// the host decides what "tenant" means and how to
197    /// aggregate/bill on it. Emitted to hooks/traces, persisted in
198    /// `SessionData`, never interpreted by core.
199    pub tenant_id: Option<String>,
200    /// Identity of the principal that triggered this session (user id,
201    /// service account, etc). Treated as opaque.
202    pub principal: Option<String>,
203    /// Logical identifier of the agent template / definition the session
204    /// was instantiated from. Lets the host aggregate sessions by
205    /// "which agent recipe" independent of the concrete session id.
206    pub agent_template_id: Option<String>,
207    /// Distributed-trace correlation id. Propagated through hooks/traces
208    /// so a session's events join with upstream/downstream work in the
209    /// host's observability pipeline.
210    pub correlation_id: Option<String>,
211    /// Optional host-supplied budget / quota guard. The framework calls
212    /// into it before each LLM call (and reports actuals after) so the
213    /// host can refuse or rate-limit at the cluster level. Default is
214    /// `None` (no enforcement — equivalent to
215    /// [`NoopBudgetGuard`](crate::budget::NoopBudgetGuard)).
216    pub budget_guard: Option<Arc<dyn crate::budget::BudgetGuard>>,
217    /// Optional host-provided ID/Clock pair. Replaces the default
218    /// random-UUID + wall-clock pair, enabling deterministic replay
219    /// on another node. `None` keeps pre-P2 behaviour.
220    pub host_env: Option<Arc<crate::host_env::HostEnv>>,
221    /// Optional FIFO retention caps on the session's in-memory stores
222    /// (run records, run events, trace events, terminal subagent
223    /// tasks). `None` (default) keeps everything — fine for short
224    /// sessions, a memory leak for hours-long cluster workloads.
225    pub retention_limits: Option<crate::retention::SessionRetentionLimits>,
226    /// Optional structured JSONL trajectory config.
227    ///
228    /// When set, a3s-code records user prompts, LLM turns, tool calls,
229    /// tool observations, token usage, and execution end status for RL
230    /// training or service data collection. If unset, the same config can
231    /// be enabled by `A3S_CODE_TRAJECTORY_PATH`.
232    pub rl_trajectory: Option<crate::rl_trajectory::RlTrajectoryConfig>,
233    /// Request token-level log probabilities from compatible LLM providers.
234    ///
235    /// This is off by default because many public providers reject logprob
236    /// requests with tool calls. Training/evaluation harnesses using compatible
237    /// OpenAI-style backends can enable it explicitly.
238    pub llm_logprobs: Option<bool>,
239    /// Number of alternative token logprobs to request per generated token.
240    pub llm_top_logprobs: Option<usize>,
241    /// Auto-save after each completed `send()` or default-history `stream()` call.
242    pub auto_save: bool,
243    /// Optional artifact retention limits for large tool/program outputs.
244    pub artifact_store_limits: Option<crate::tools::ArtifactStoreLimits>,
245    /// Max consecutive parse errors before aborting (overrides default of 2).
246    /// `None` uses the `AgentConfig` default.
247    pub max_parse_retries: Option<u32>,
248    /// Per-tool execution timeout in milliseconds.
249    /// `None` = no timeout (default).
250    pub tool_timeout_ms: Option<u64>,
251    /// Per-model API HTTP timeout in milliseconds.
252    /// `None` = no timeout (default).
253    pub llm_api_timeout_ms: Option<u64>,
254    /// Circuit-breaker threshold: max consecutive LLM API failures before
255    /// aborting in non-streaming mode (overrides default of 3).
256    /// `None` uses the `AgentConfig` default.
257    pub circuit_breaker_threshold: Option<u32>,
258    /// Max consecutive identical tool signatures before the guard turns the
259    /// duplicate call into a failed tool observation.
260    ///
261    /// `None` uses the `AgentConfig` default. Hosts can raise this for long
262    /// research sessions where repeating a read/search with the same arguments
263    /// is wasteful but should not make the whole run brittle.
264    pub duplicate_tool_call_threshold: Option<u32>,
265    /// Optional concrete sandbox implementation.
266    ///
267    /// When set, `bash` tool commands are routed through this sandbox instead
268    /// of `std::process::Command`. The host application constructs and owns
269    /// the implementation (e.g., an A3S Box–backed handle).
270    pub sandbox_handle: Option<Arc<dyn crate::sandbox::BashSandbox>>,
271    /// Optional host-provided workspace backend.
272    ///
273    /// When set, built-in tools such as `read`, `write`, `ls`, and `bash`
274    /// execute against these workspace capabilities instead of assuming the
275    /// server-local filesystem. This is the primary extension point for DFS,
276    /// browser, container, and remote workspace deployments.
277    pub workspace_services: Option<Arc<crate::workspace::WorkspaceServices>>,
278    /// Enable auto-compaction when context usage exceeds threshold.
279    pub auto_compact: bool,
280    /// Context usage percentage threshold for auto-compaction (0.0 - 1.0).
281    /// Default: 0.80 (80%).
282    pub auto_compact_threshold: Option<f32>,
283    /// Inject a continuation message when the LLM stops without completing the task.
284    /// `None` uses the `AgentConfig` default (true).
285    pub continuation_enabled: Option<bool>,
286    /// Maximum continuation injections per execution.
287    /// `None` uses the `AgentConfig` default (3).
288    pub max_continuation_turns: Option<u32>,
289    /// Maximum execution time in milliseconds.
290    /// `None` = no timeout (default).
291    /// When set, the execution loop will abort if it exceeds this duration.
292    pub max_execution_time_ms: Option<u64>,
293    /// Optional MCP manager for connecting to external MCP servers.
294    ///
295    /// When set, all tools from connected MCP servers are registered and
296    /// available during agent execution with names like `mcp__server__tool`.
297    pub mcp_manager: Option<Arc<crate::mcp::manager::McpManager>>,
298    /// Sampling temperature (0.0–1.0). Overrides the provider default.
299    pub temperature: Option<f32>,
300    /// Extended thinking budget in tokens (Anthropic only).
301    pub thinking_budget: Option<usize>,
302    /// Per-session tool round limit override.
303    ///
304    /// When set, overrides the agent-level `max_tool_rounds` for this session only.
305    /// Maps directly from [`AgentDefinition::max_steps`] when creating sessions
306    /// via [`Agent::session_for_agent`].
307    pub max_tool_rounds: Option<usize>,
308    /// Per-session parallel fan-out limit override.
309    ///
310    /// Applies to delegated `parallel_task`, plan wave execution, and safe
311    /// parallel write batches.
312    pub max_parallel_tasks: Option<usize>,
313    /// Per-session automatic subagent delegation override.
314    pub auto_delegation: Option<crate::config::AutoDelegationConfig>,
315    /// Per-session switch for model-visible manual child-agent tools.
316    ///
317    /// This overlays the effective automatic delegation config instead of
318    /// replacing it, so callers can hide `task` / `parallel_task` while
319    /// preserving other delegation settings.
320    pub manual_delegation_enabled: Option<bool>,
321    /// Per-session kill switch for automatic parallel child-agent fan-out.
322    ///
323    /// This overlays the effective automatic delegation config instead of
324    /// replacing it, so callers can disable auto fan-out without disabling
325    /// automatic delegation itself.
326    pub auto_parallel_delegation: Option<bool>,
327    /// Slot-based system prompt customization.
328    ///
329    /// When set, overrides the agent-level prompt slots for this session.
330    /// Users can customize role, guidelines, response style, and extra instructions
331    /// without losing the core agentic capabilities.
332    pub prompt_slots: Option<SystemPromptSlots>,
333    /// Optional external hook executor (e.g. an AHP harness server).
334    ///
335    /// When set, **replaces** the built-in `HookEngine` for this session.
336    /// All 11 lifecycle events are forwarded to the executor instead of being
337    /// dispatched locally. The executor is also propagated to sub-agents via
338    /// the sentinel hook mechanism.
339    pub hook_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
340}
341
342// ============================================================================
343// Agent
344// ============================================================================
345
346/// High-level agent facade.
347///
348/// Holds the LLM client and agent config. Workspace-independent.
349/// Use [`Agent::session()`] to bind to a workspace.
350pub struct Agent {
351    code_config: CodeConfig,
352    config: AgentConfig,
353    /// Global MCP manager loaded from config.mcp_servers
354    global_mcp: Option<Arc<crate::mcp::manager::McpManager>>,
355    /// Pre-fetched MCP tool definitions from global_mcp (cached at creation time).
356    /// Wrapped in Mutex so `refresh_mcp_tools()` can update the cache without `&mut self`.
357    global_mcp_tools: std::sync::Mutex<Vec<(String, crate::mcp::McpTool)>>,
358    /// Tracks every live session created by this agent via `Weak` refs so
359    /// the agent can enumerate and forcibly close them. Sessions register
360    /// themselves at construction and become dangling `Weak`s on drop —
361    /// `list_sessions()` / `close_session()` prune dead entries on access.
362    ///
363    /// Uses a synchronous lock so the sync `Agent::session()` factory can
364    /// insert without nesting tokio runtimes. The lock is only held for
365    /// brief insert/scan operations — async close work happens after the
366    /// lock is released.
367    sessions: Arc<std::sync::Mutex<HashMap<String, std::sync::Weak<SessionCloseHandle>>>>,
368    /// Set once `Agent::close()` has been called. Subsequent `session()` /
369    /// `resume_session()` calls fail fast with `CodeError::SessionClosed`.
370    closed: Arc<std::sync::atomic::AtomicBool>,
371}
372
373impl std::fmt::Debug for Agent {
374    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375        f.debug_struct("Agent").finish()
376    }
377}
378
379impl Agent {
380    /// Create from a config file path or inline ACL-compatible string.
381    ///
382    /// Auto-detects `.acl` file paths vs inline ACL-compatible config.
383    pub async fn new(config_source: impl Into<String>) -> Result<Self> {
384        let config = agent_bootstrap::load_code_config(config_source.into())?;
385        Self::from_config(config).await
386    }
387
388    /// Create from a config file path or inline ACL-compatible string.
389    ///
390    /// Alias for [`Agent::new()`] — provides a consistent API with
391    /// the Python and Node.js SDKs.
392    pub async fn create(config_source: impl Into<String>) -> Result<Self> {
393        Self::new(config_source).await
394    }
395
396    /// Create from a [`CodeConfig`] struct.
397    pub async fn from_config(config: CodeConfig) -> Result<Self> {
398        agent_bootstrap::build_agent_from_config(config).await
399    }
400
401    /// Re-fetch tool definitions from all connected global MCP servers and
402    /// update the internal cache.
403    ///
404    /// Call this when an MCP server has added or removed tools since the
405    /// agent was created. The refreshed tools will be visible to all
406    /// **new** sessions created after this call; existing sessions are
407    /// unaffected (their `ToolExecutor` snapshot is already built).
408    pub async fn refresh_mcp_tools(&self) -> Result<()> {
409        agent_sessions::refresh_mcp_tools(self).await
410    }
411
412    /// Bind to a workspace directory, returning an [`AgentSession`].
413    ///
414    /// Pass `None` for defaults, or `Some(SessionOptions)` to override
415    /// the model, agent directories for this session.
416    pub fn session(
417        &self,
418        workspace: impl Into<String>,
419        options: Option<SessionOptions>,
420    ) -> Result<AgentSession> {
421        agent_sessions::create_session(self, workspace, options)
422    }
423
424    /// Create a session pre-configured from an [`AgentDefinition`].
425    ///
426    /// Maps the definition's `permissions`, `prompt`, `model`, and `max_steps`
427    /// directly into [`SessionOptions`], so markdown/YAML-defined subagents can
428    /// be used by delegation and advanced control-plane flows without manual wiring.
429    ///
430    /// The mapping follows the same logic as the built-in `task` tool:
431    /// - `permissions` → `permission_checker`
432    /// - `prompt`      → `prompt_slots.extra`
433    /// - `max_steps`   → `max_tool_rounds`
434    /// - `model`       → `model` (as `"provider/model"` string)
435    ///
436    /// `extra` can supply additional overrides (e.g. `planning_enabled`) that
437    /// take precedence over the definition's values.
438    pub fn session_for_agent(
439        &self,
440        workspace: impl Into<String>,
441        def: &crate::subagent::AgentDefinition,
442        extra: Option<SessionOptions>,
443    ) -> Result<AgentSession> {
444        agent_sessions::create_session_for_agent(self, workspace, def, extra)
445    }
446
447    /// Create a session from a reproducible disposable worker recipe.
448    ///
449    /// This is the cattle-mode companion to [`Agent::session_for_agent`]: callers
450    /// provide a small [`WorkerAgentSpec`](crate::subagent::WorkerAgentSpec), and
451    /// A3S Code compiles it into the same runtime definition used by delegated agents.
452    pub fn session_for_worker(
453        &self,
454        workspace: impl Into<String>,
455        spec: crate::subagent::WorkerAgentSpec,
456        extra: Option<SessionOptions>,
457    ) -> Result<AgentSession> {
458        let def = spec.into_agent_definition();
459        self.session_for_agent(workspace, &def, extra)
460    }
461
462    /// Resume a previously saved session by ID.
463    ///
464    /// Loads the session data from the store, rebuilds the `AgentSession` with
465    /// the saved conversation history, and returns it ready for continued use.
466    ///
467    /// The `options` must include a `session_store` (or `with_file_session_store`)
468    /// that contains the saved session.
469    ///
470    /// The resumed session uses the **workspace stored in the snapshot**, not a
471    /// workspace from `options`. The store is therefore a trust boundary: its
472    /// contents drive the resumed workspace and the persisted runtime policies.
473    ///
474    /// Runtime: this loads the snapshot via `block_in_place`, so it must be called
475    /// on a multi-threaded Tokio runtime (it panics on a current-thread runtime).
476    pub fn resume_session(
477        &self,
478        session_id: &str,
479        options: SessionOptions,
480    ) -> Result<AgentSession> {
481        agent_sessions::resume_session(self, session_id, options)
482    }
483
484    /// Return the IDs of every live session created from this agent.
485    ///
486    /// "Live" means the caller still holds an [`AgentSession`] — sessions
487    /// that have been dropped are pruned lazily on each call. The list is
488    /// sorted to make output stable for tests/UIs.
489    pub async fn list_sessions(&self) -> Vec<String> {
490        agent_sessions::list_sessions(self).await
491    }
492
493    /// Close a specific live session by its session ID.
494    ///
495    /// Returns `true` when a live session with the given id was found and
496    /// transitioned from open to closed by this call; `false` when no live
497    /// session has that id, or when the session was already closed.
498    ///
499    /// This is the out-of-band counterpart to [`AgentSession::close`]: it
500    /// performs exactly the same cleanup but can be invoked without holding
501    /// a reference to the session itself — useful for control-plane code
502    /// that only knows the session ID.
503    pub async fn close_session(&self, session_id: &str) -> bool {
504        agent_sessions::close_session(self, session_id).await
505    }
506
507    /// Close every live session created from this agent and tear down
508    /// background resources owned by the agent (global MCP connections).
509    ///
510    /// After this call:
511    /// - Every live `AgentSession` is closed (same effect as calling
512    ///   [`AgentSession::close`] on each).
513    /// - Subsequent [`Agent::session`] / [`Agent::resume_session`] calls
514    ///   fail fast with [`CodeError::SessionClosed`](crate::error::CodeError::SessionClosed).
515    ///
516    /// Idempotent: subsequent calls are no-ops and are guaranteed not to
517    /// panic.
518    pub async fn close(&self) {
519        agent_sessions::close_agent(self).await
520    }
521
522    /// Return whether [`close`](Self::close) has been called on this agent.
523    pub fn is_closed(&self) -> bool {
524        self.closed.load(std::sync::atomic::Ordering::Acquire)
525    }
526
527    /// Disconnect every global MCP server whose last activity is older
528    /// than `idle_threshold_ms`. Returns the names of disconnected
529    /// servers (empty when there is no global MCP manager or when
530    /// nothing is idle).
531    ///
532    /// Hosts running thousands of long-lived sessions should call this
533    /// periodically (e.g. every 60s with a 5-min threshold) to release
534    /// file descriptors and background workers from quiet MCP servers
535    /// without losing the server's configuration. A subsequent tool
536    /// call on the same server will require an explicit reconnect.
537    pub async fn disconnect_idle_mcp(&self, idle_threshold_ms: u64) -> Vec<String> {
538        match &self.global_mcp {
539            Some(mcp) => mcp.disconnect_idle(idle_threshold_ms).await,
540            None => Vec::new(),
541        }
542    }
543
544    #[cfg(test)]
545    fn build_session(
546        &self,
547        workspace: String,
548        llm_client: Arc<dyn LlmClient>,
549        opts: &SessionOptions,
550    ) -> Result<AgentSession> {
551        session_builder::build_agent_session(self, workspace, llm_client, opts)
552    }
553}
554
555// ============================================================================
556// AgentSession
557// ============================================================================
558
559/// Workspace-bound session. All LLM and tool operations happen here.
560///
561/// History is automatically accumulated after each `send()` call and after
562/// `stream()` completes when no custom history is supplied.
563/// Use `history()` to retrieve the current conversation log.
564pub struct AgentSession {
565    llm_client: Arc<dyn LlmClient>,
566    tool_executor: Arc<ToolExecutor>,
567    tool_context: ToolContext,
568    config: AgentConfig,
569    workspace: PathBuf,
570    /// Unique session identifier.
571    session_id: String,
572    /// Internal conversation history, auto-updated after each `send()` and default-history `stream()`.
573    history: Arc<RwLock<Vec<Message>>>,
574    /// Optional lane queue for priority-based tool execution.
575    command_queue: Option<Arc<crate::session_lane_queue::SessionLaneQueue>>,
576    /// Long-term memory handle.
577    ///
578    /// Built sessions resolve a default memory store. This remains optional for
579    /// compatibility with lower-level/manual construction paths.
580    memory: Option<Arc<crate::memory::AgentMemory>>,
581    /// Optional session store for persistence.
582    session_store: Option<Arc<dyn crate::store::SessionStore>>,
583    /// Auto-save after each completed `send()` or default-history `stream()`.
584    auto_save: bool,
585    /// Hook engine for lifecycle event interception.
586    hook_engine: Arc<crate::hooks::HookEngine>,
587    /// Optional external hook executor (e.g. AHP harness). When set, replaces
588    /// `hook_engine` as the executor passed to each `AgentLoop`.
589    ahp_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
590    /// Deferred init warning: emitted as PersistenceFailed on first send() if set.
591    init_warning: Option<String>,
592    /// Slash command registry for `/command` dispatch.
593    /// Uses interior mutability so commands can be registered on a shared `Arc<AgentSession>`.
594    command_registry: std::sync::Mutex<CommandRegistry>,
595    /// Model identifier for display (e.g., "anthropic/claude-sonnet-4-20250514").
596    model_name: String,
597    /// Shared MCP manager — all add_mcp_server / remove_mcp_server calls go here.
598    mcp_manager: Arc<crate::mcp::manager::McpManager>,
599    /// Shared agent registry — populated at session creation; extended via register_agent_dir().
600    agent_registry: Arc<crate::subagent::AgentRegistry>,
601    /// Cancellation token for the current operation (send/stream).
602    /// Stored so that cancel() can abort ongoing LLM calls.
603    cancel_token: Arc<tokio::sync::Mutex<Option<tokio_util::sync::CancellationToken>>>,
604    /// ID of the run currently attached to the active cancellation token.
605    current_run_id: Arc<tokio::sync::Mutex<Option<String>>>,
606    /// In-memory run snapshots and event replay buffer for this session.
607    run_store: Arc<crate::run::InMemoryRunStore>,
608    /// Materialized view of delegated subagent task lifecycle, populated from runtime events.
609    subagent_tasks: Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>,
610    /// Currently executing tools observed from runtime events.
611    active_tools: Arc<tokio::sync::RwLock<HashMap<String, ActiveToolState>>>,
612    /// Compact execution traces for this session.
613    trace_sink: crate::trace::InMemoryTraceSink,
614    /// Structured completion evidence collected from agent and explicit verification runs.
615    verification_reports: Arc<RwLock<Vec<crate::verification::VerificationReport>>>,
616    /// Set once `close()` has been called. Subsequent send/stream calls
617    /// fast-fail with [`crate::error::CodeError::SessionClosed`].
618    closed: Arc<std::sync::atomic::AtomicBool>,
619    /// Session-level parent cancellation token.
620    ///
621    /// Every in-flight run (blocking send, stream, delegated subagent task)
622    /// derives its per-operation token from this one via `child_token()`,
623    /// so `session_cancel.cancel()` cascades to all of them. `close()` fires
624    /// this token first, after which any new `child_token()` returns an
625    /// already-cancelled token (defending against close/spawn races).
626    pub(crate) session_cancel: tokio_util::sync::CancellationToken,
627    /// Shared `Arc`-handle used by both [`AgentSession::close`] and the
628    /// parent [`Agent`]'s registry. The handle bundles every field needed
629    /// to perform the close sequence so the two entry points cannot drift.
630    close_handle: Arc<SessionCloseHandle>,
631    /// Runtime-mutable override for the budget guard. When set, takes
632    /// precedence over `config.budget_guard` on the next agent-loop
633    /// build. Lets SDK callers (Node especially) install a host-side
634    /// guard after `session()` has returned without ever putting a
635    /// JS callable into `SessionOptions`.
636    runtime_budget_guard: std::sync::Mutex<Option<Arc<dyn crate::budget::BudgetGuard>>>,
637    /// Multi-tenant label. Framework only carries the string; semantics
638    /// belong to the host.
639    pub(crate) tenant_id: Option<String>,
640    /// Principal that triggered the session (user / service / etc.).
641    pub(crate) principal: Option<String>,
642    /// Logical identifier of the agent template the session was
643    /// instantiated from.
644    pub(crate) agent_template_id: Option<String>,
645    /// Distributed-trace correlation id propagated to hooks / traces.
646    pub(crate) correlation_id: Option<String>,
647}
648
649impl std::fmt::Debug for AgentSession {
650    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651        f.debug_struct("AgentSession")
652            .field("session_id", &self.session_id)
653            .field("workspace", &self.workspace.display().to_string())
654            .field("auto_save", &self.auto_save)
655            .finish()
656    }
657}
658
659impl AgentSession {
660    /// Get a snapshot of command entries (name, description, optional usage).
661    ///
662    /// Acquires the command registry lock briefly and returns owned data.
663    pub fn command_registry(&self) -> std::sync::MutexGuard<'_, CommandRegistry> {
664        session_commands::registry(self)
665    }
666
667    /// Register a custom slash command.
668    ///
669    /// Takes `&self` so it can be called on a shared `Arc<AgentSession>`.
670    pub fn register_command(&self, cmd: Arc<dyn crate::commands::SlashCommand>) {
671        session_commands::register(self, cmd);
672    }
673
674    /// Return whether [`close`](Self::close) has been called on this session.
675    ///
676    /// Once closed, `send`/`stream` and their attachment variants fast-fail
677    /// with [`crate::error::CodeError::SessionClosed`] instead of starting a
678    /// new run.
679    pub fn is_closed(&self) -> bool {
680        self.closed.load(std::sync::atomic::Ordering::Acquire)
681    }
682
683    /// Clone the session-level [`CancellationToken`](tokio_util::sync::CancellationToken).
684    ///
685    /// All in-flight runs derive their per-operation token from this one via
686    /// `child_token()`, so embedders can:
687    ///
688    /// - Observe the token (e.g. wire it into a host-side `select!`) to
689    ///   react to session shutdown without polling [`is_closed`](Self::is_closed);
690    /// - Call `.cancel()` on it to abort every operation in the session
691    ///   without going through `close()` (no run-store / hook side effects).
692    ///
693    /// For graceful shutdown prefer [`close`](Self::close), which also marks
694    /// runs as cancelled in the store and fires AHP hooks.
695    pub fn session_cancel_token(&self) -> tokio_util::sync::CancellationToken {
696        self.session_cancel.clone()
697    }
698
699    /// Return the host-defined tenant id, if any.
700    ///
701    /// The framework only transports this string — it never interprets
702    /// or enforces tenant boundaries itself. Use this from custom
703    /// `HookExecutor` / `PermissionChecker` / `BudgetGuard` impls to
704    /// route logic by tenant.
705    pub fn tenant_id(&self) -> Option<&str> {
706        self.tenant_id.as_deref()
707    }
708
709    /// Return the principal that triggered the session, if any.
710    pub fn principal(&self) -> Option<&str> {
711        self.principal.as_deref()
712    }
713
714    /// Return the id of the agent template/definition the session was
715    /// instantiated from, if any.
716    pub fn agent_template_id(&self) -> Option<&str> {
717        self.agent_template_id.as_deref()
718    }
719
720    /// Return the distributed-trace correlation id propagated through
721    /// this session's events, if any.
722    pub fn correlation_id(&self) -> Option<&str> {
723        self.correlation_id.as_deref()
724    }
725
726    /// Install or replace a runtime budget guard. Takes effect on the
727    /// next `send` / `stream` call (the guard is consulted at agent-
728    /// loop build time, not on the live execution). Setting `None`
729    /// clears the override so `config.budget_guard` takes over again.
730    ///
731    /// This is the entry point SDKs use to wire a host-supplied guard
732    /// after the session has already been constructed — useful when
733    /// the guard's transport (e.g. a JS callable) cannot live inside
734    /// the value-typed `SessionOptions`.
735    pub fn set_budget_guard(&self, guard: Option<Arc<dyn crate::budget::BudgetGuard>>) {
736        let mut slot = self
737            .runtime_budget_guard
738            .lock()
739            .unwrap_or_else(|p| p.into_inner());
740        *slot = guard;
741    }
742
743    /// Return the currently-installed runtime budget guard, if any.
744    /// `None` means the loop falls back to `config.budget_guard`.
745    pub fn budget_guard(&self) -> Option<Arc<dyn crate::budget::BudgetGuard>> {
746        self.runtime_budget_guard
747            .lock()
748            .unwrap_or_else(|p| p.into_inner())
749            .clone()
750    }
751
752    /// Proactively close the session and release its in-flight work.
753    ///
754    /// On the first call this:
755    /// 1. flips the session into the **closed** state so further `send`/`stream`
756    ///    calls fast-fail with [`crate::error::CodeError::SessionClosed`];
757    /// 2. fires the session-level cancellation token so every derived
758    ///    run/subagent token cascades to cancelled;
759    /// 3. marks the active run `Cancelled` in the run store and fires AHP
760    ///    hook side effects;
761    /// 4. cancels every still-running delegated subagent task spawned from
762    ///    this session;
763    /// 5. cancels all pending human-in-the-loop tool confirmations.
764    ///
765    /// Subsequent calls are no-ops and are guaranteed not to panic.
766    pub async fn close(&self) {
767        // Delegate to the shared handle so this entry point and
768        // `Agent::close_session(id)` cannot drift in behaviour.
769        self.close_handle.close().await;
770    }
771
772    /// Send a prompt and wait for the complete response.
773    ///
774    /// When `history` is `None`, uses (and auto-updates) the session's
775    /// internal conversation history. When `Some`, uses the provided
776    /// history instead (the internal history is **not** modified).
777    ///
778    /// If the prompt starts with `/`, it is dispatched as a slash command
779    /// and the result is returned without calling the LLM.
780    pub async fn send(&self, prompt: &str, history: Option<&[Message]>) -> Result<AgentResult> {
781        conversation_runtime::send(self, prompt, history).await
782    }
783
784    /// Resume a previously-checkpointed run on this session.
785    ///
786    /// Loads the latest [`LoopCheckpoint`](crate::loop_checkpoint::LoopCheckpoint)
787    /// stored under `checkpoint_run_id` and replays the agent loop from
788    /// that boundary state. A **new** run id is allocated for the
789    /// resumed work; the relationship between the old and new run is
790    /// host-tracked — the framework does not interpret
791    /// it.
792    ///
793    /// Returns an error when no `SessionStore` is configured on this
794    /// session, or when no checkpoint exists for `checkpoint_run_id`.
795    pub async fn resume_run(&self, checkpoint_run_id: &str) -> Result<AgentResult> {
796        conversation_runtime::resume_run(self, checkpoint_run_id).await
797    }
798
799    /// Send a prompt with image attachments and wait for the complete response.
800    ///
801    /// Images are included as multi-modal content blocks in the user message.
802    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
803    pub async fn send_with_attachments(
804        &self,
805        prompt: &str,
806        attachments: &[crate::llm::Attachment],
807        history: Option<&[Message]>,
808    ) -> Result<AgentResult> {
809        conversation_runtime::send_with_attachments(self, prompt, attachments, history).await
810    }
811
812    /// Stream a prompt with image attachments.
813    ///
814    /// Images are included as multi-modal content blocks in the user message.
815    /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
816    pub async fn stream_with_attachments(
817        &self,
818        prompt: &str,
819        attachments: &[crate::llm::Attachment],
820        history: Option<&[Message]>,
821    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
822        conversation_runtime::stream_with_attachments(self, prompt, attachments, history).await
823    }
824
825    /// Send a prompt and stream events back.
826    ///
827    /// When `history` is `None`, uses the session's internal history
828    /// and updates it when the stream completes.
829    /// When `Some`, uses the provided history instead.
830    ///
831    /// If the prompt starts with `/`, it is dispatched as a slash command
832    /// and the result is emitted as a single `TextDelta` + `End` event.
833    pub async fn stream(
834        &self,
835        prompt: &str,
836        history: Option<&[Message]>,
837    ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
838        conversation_runtime::stream(self, prompt, history).await
839    }
840
841    /// Cancel the current ongoing operation (send/stream).
842    ///
843    /// If an operation is in progress, this will trigger cancellation of the LLM streaming
844    /// and tool execution. The operation will terminate as soon as possible.
845    ///
846    /// Returns `true` if an operation was cancelled, `false` if no operation was in progress.
847    pub async fn cancel(&self) -> bool {
848        RunControl::from_session(self).cancel_current().await
849    }
850
851    /// Cancel a specific run only if it is still the active run.
852    ///
853    /// This is useful for SDK callers that hold a previously observed run ID:
854    /// stale run IDs will not cancel a newer operation.
855    pub async fn cancel_run(&self, run_id: &str) -> bool {
856        RunControl::from_session(self).cancel_run(run_id).await
857    }
858
859    /// Return snapshots for runs recorded by this session.
860    pub async fn runs(&self) -> Vec<crate::run::RunSnapshot> {
861        RunControl::from_session(self).runs().await
862    }
863
864    /// Return a snapshot for a recorded run.
865    pub async fn run_snapshot(&self, run_id: &str) -> Option<crate::run::RunSnapshot> {
866        RunControl::from_session(self).run_snapshot(run_id).await
867    }
868
869    /// Return recorded runtime events for a run.
870    pub async fn run_events(&self, run_id: &str) -> Vec<crate::run::RunEventRecord> {
871        RunControl::from_session(self).run_events(run_id).await
872    }
873
874    /// Return a handle for the currently running operation, if any.
875    pub async fn current_run(&self) -> Option<crate::run::RunHandle> {
876        RunControl::from_session(self).current_run().await
877    }
878
879    /// Return active tool calls observed for the currently running operation.
880    pub async fn active_tools(&self) -> Vec<crate::run::ActiveToolSnapshot> {
881        SessionView::from_session(self).active_tools().await
882    }
883
884    /// Look up a delegated subagent task by id. Returns `None` if no such task
885    /// has been observed in this session.
886    pub async fn subagent_task(
887        &self,
888        task_id: &str,
889    ) -> Option<crate::subagent_task_tracker::SubagentTaskSnapshot> {
890        self.subagent_tasks.get(task_id).await
891    }
892
893    /// Return snapshots of every delegated subagent task observed in this
894    /// session (including completed and failed ones), oldest first.
895    pub async fn subagent_tasks(&self) -> Vec<crate::subagent_task_tracker::SubagentTaskSnapshot> {
896        self.subagent_tasks.list_for_parent(&self.session_id).await
897    }
898
899    /// Return snapshots of subagent tasks still in `Running` state.
900    pub async fn pending_subagent_tasks(
901        &self,
902    ) -> Vec<crate::subagent_task_tracker::SubagentTaskSnapshot> {
903        use crate::subagent_task_tracker::SubagentStatus;
904        self.subagent_tasks
905            .list_for_parent(&self.session_id)
906            .await
907            .into_iter()
908            .filter(|task| task.status == SubagentStatus::Running)
909            .collect()
910    }
911
912    /// Cancel an in-flight delegated subagent task by id. Returns `true`
913    /// when a cancellation token was found and fired, `false` when the
914    /// task id is unknown or the task has already finished. The eventual
915    /// `SubagentEnd` from the cancelled child loop won't downgrade the
916    /// terminal status — it stays `Cancelled`.
917    pub async fn cancel_subagent_task(&self, task_id: &str) -> bool {
918        self.subagent_tasks.cancel(task_id).await
919    }
920
921    /// Return a shared handle to the session's subagent task tracker.
922    ///
923    /// Advanced: embedders implementing a custom subagent execution path
924    /// (i.e. spawning child loops outside the built-in `task` tool) can use
925    /// this to register cancellation tokens and feed `AgentEvent`s into the
926    /// tracker so the standard
927    /// [`subagent_task`](Self::subagent_task) / [`pending_subagent_tasks`](Self::pending_subagent_tasks) /
928    /// [`cancel_subagent_task`](Self::cancel_subagent_task) APIs and
929    /// [`close`](Self::close) keep working uniformly across execution paths.
930    pub fn subagent_tracker(
931        &self,
932    ) -> Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker> {
933        Arc::clone(&self.subagent_tasks)
934    }
935
936    /// Return a snapshot of the session's conversation history.
937    pub fn history(&self) -> Vec<Message> {
938        SessionView::from_session(self).history()
939    }
940
941    /// Return pending HITL tool confirmations for this session.
942    pub async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
943        HitlControl::from_session(self)
944            .pending_confirmations()
945            .await
946    }
947
948    /// Resolve a pending HITL tool confirmation.
949    ///
950    /// Returns `Ok(true)` when a pending confirmation was found and completed,
951    /// `Ok(false)` when the tool ID is not pending or HITL is not configured.
952    pub async fn confirm_tool_use(
953        &self,
954        tool_id: &str,
955        approved: bool,
956        reason: Option<String>,
957    ) -> Result<bool> {
958        HitlControl::from_session(self)
959            .confirm_tool_use(tool_id, approved, reason)
960            .await
961    }
962
963    /// Cancel all pending HITL confirmations for this session.
964    pub async fn cancel_confirmations(&self) -> usize {
965        HitlControl::from_session(self).cancel_confirmations().await
966    }
967
968    /// Return a reference to the session's memory.
969    ///
970    /// Normal sessions always have memory; `None` is reserved for
971    /// lower-level/manual construction compatibility.
972    pub fn memory(&self) -> Option<&Arc<crate::memory::AgentMemory>> {
973        SessionView::from_session(self).memory()
974    }
975
976    /// Return the session ID.
977    pub fn id(&self) -> &str {
978        SessionView::from_session(self).id()
979    }
980
981    /// Return the session workspace path.
982    pub fn workspace(&self) -> &std::path::Path {
983        SessionView::from_session(self).workspace()
984    }
985
986    /// Return any deferred init warning (e.g. memory store failed to initialize).
987    pub fn init_warning(&self) -> Option<&str> {
988        SessionView::from_session(self).init_warning()
989    }
990
991    /// Return the session ID.
992    pub fn session_id(&self) -> &str {
993        SessionView::from_session(self).id()
994    }
995
996    /// An [`AgentExecutor`](crate::orchestration::AgentExecutor) backed by this
997    /// session — runs each orchestrated step as a child agent on this node,
998    /// inheriting the session's agent registry, LLM client, workspace, MCP
999    /// tools, and subagent tracker.
1000    ///
1001    /// This is what the orchestration combinators
1002    /// ([`execute_steps_parallel`](crate::orchestration::execute_steps_parallel),
1003    /// [`execute_pipeline`](crate::orchestration::execute_pipeline),
1004    /// [`execute_steps_parallel_resumable`](crate::orchestration::execute_steps_parallel_resumable))
1005    /// run against; a host can instead supply its own executor to place steps
1006    /// across a cluster.
1007    pub fn agent_executor(&self) -> Arc<dyn crate::orchestration::AgentExecutor> {
1008        Arc::new(self.build_task_executor(self.parent_run_context()))
1009    }
1010
1011    /// Re-register `task`/`parallel_task` with the finalized session runtime.
1012    ///
1013    /// Session capability assembly happens before the per-session HITL manager is
1014    /// constructed from `confirmation_policy`. Refreshing after `AgentConfig` is
1015    /// final keeps model-driven delegation, workflow delegation, and
1016    /// `agent_executor()` on the same permission/HITL/workspace context.
1017    pub(crate) fn refresh_task_delegation_tools(&self) {
1018        if !self.config.auto_delegation.allow_manual_delegation {
1019            return;
1020        }
1021        crate::tools::register_task_with_mcp(
1022            self.tool_executor.registry(),
1023            Arc::clone(&self.llm_client),
1024            Arc::clone(&self.agent_registry),
1025            self.workspace.display().to_string(),
1026            Some(Arc::clone(&self.mcp_manager)),
1027            Some(self.parent_run_context()),
1028            Some(Arc::clone(&self.subagent_tasks)),
1029        );
1030    }
1031
1032    /// Build the in-box [`TaskExecutor`](crate::tools::TaskExecutor) for this
1033    /// session, applying `parent` as the child-run capability context. Shared by
1034    /// [`agent_executor`](Self::agent_executor) and [`workflow`](Self::workflow)
1035    /// so both wire children identically.
1036    fn build_task_executor(
1037        &self,
1038        parent: crate::child_run::ChildRunContext,
1039    ) -> crate::tools::TaskExecutor {
1040        crate::tools::TaskExecutor::with_mcp(
1041            Arc::clone(&self.agent_registry),
1042            Arc::clone(&self.llm_client),
1043            self.workspace.display().to_string(),
1044            Arc::clone(&self.mcp_manager),
1045        )
1046        .with_parent_context(parent)
1047        .with_subagent_tracker(Arc::clone(&self.subagent_tasks))
1048        .with_max_parallel_tasks(self.config.max_parallel_tasks)
1049    }
1050
1051    /// A programmable [`Workflow`](crate::orchestration::Workflow) bound to this
1052    /// session.
1053    ///
1054    /// Pre-wired with this session's executor (inheriting the same governance as
1055    /// model-driven delegation), persistence store (so each
1056    /// [`phase`](crate::orchestration::Workflow::phase) is a resume boundary),
1057    /// per-step event stream, and a session-derived stable root id. Control flow
1058    /// is ordinary Rust: `await` a verb, inspect the outcomes, decide what runs
1059    /// next.
1060    pub fn workflow(&self) -> crate::orchestration::Workflow {
1061        self.workflow_with_token_budget(None)
1062    }
1063
1064    /// Like [`workflow`](Self::workflow) but with a hard token ceiling shared
1065    /// across every step. The cap is a best-effort *soft* cost ceiling — under a
1066    /// wide fan-out a few in-flight turns can race past it before the shared
1067    /// ledger catches up (see [`WorkflowBudget`](crate::orchestration::WorkflowBudget)).
1068    pub fn workflow_with_token_budget(
1069        &self,
1070        limit_tokens: Option<u64>,
1071    ) -> crate::orchestration::Workflow {
1072        use crate::budget::BudgetGuard;
1073
1074        // One shared ledger for the whole workflow, wrapping the session's own
1075        // budget guard (if any) so a host's per-tenant accounting keeps working.
1076        let mut budget = crate::orchestration::WorkflowBudget::new(limit_tokens);
1077        if let Some(inner) = self.config.budget_guard.clone() {
1078            budget = budget.with_inner(inner);
1079        }
1080        let budget = Arc::new(budget);
1081
1082        // Install the shared ledger as the child runs' budget guard so every
1083        // step's per-turn LLM accounting feeds it.
1084        let mut parent = self.parent_run_context();
1085        parent.budget_guard = Some(Arc::clone(&budget) as Arc<dyn BudgetGuard>);
1086        let executor: Arc<dyn crate::orchestration::AgentExecutor> =
1087            Arc::new(self.build_task_executor(parent));
1088
1089        let mut builder = crate::orchestration::Workflow::builder(executor)
1090            .with_root_id(format!("wf-{}", self.session_id))
1091            .with_budget(Arc::clone(&budget));
1092        if let Some(store) = self.session_store.clone() {
1093            builder = builder.with_store(store);
1094        }
1095        if let Some(step_events) = self.tool_context.agent_event_tx.clone() {
1096            builder = builder.with_step_events(step_events);
1097        }
1098        builder.build()
1099    }
1100
1101    /// Build the [`ChildRunContext`](crate::child_run::ChildRunContext) that
1102    /// orchestrated / delegated child runs inherit from this session.
1103    ///
1104    /// Mirrors the context the model-driven `task` / `parallel_task` path
1105    /// installs (see `register_task_capability` in `agent_api/capabilities.rs`)
1106    /// so a step run through [`agent_executor`](Self::agent_executor) carries the
1107    /// SAME governance — security provider, skill restrictions, confirmation,
1108    /// the shared workspace, and the safety limits — instead of weaker, ambient
1109    /// authority. Sourced from the session's resolved config; `hook_engine`
1110    /// stays `None` to match the model-driven path.
1111    pub(crate) fn parent_run_context(&self) -> crate::child_run::ChildRunContext {
1112        crate::child_run::ChildRunContext {
1113            security_provider: self.config.security_provider.clone(),
1114            hook_engine: None,
1115            skill_registry: self.config.skill_registry.clone(),
1116            permission_checker: self.config.permission_checker.clone(),
1117            permission_policy: self.config.permission_policy.clone(),
1118            tool_timeout_ms: self.config.tool_timeout_ms,
1119            llm_api_timeout_ms: self.config.llm_api_timeout_ms,
1120            max_parallel_tasks: Some(self.config.max_parallel_tasks),
1121            max_execution_time_ms: self.config.max_execution_time_ms,
1122            circuit_breaker_threshold: Some(self.config.circuit_breaker_threshold),
1123            duplicate_tool_call_threshold: Some(self.config.duplicate_tool_call_threshold),
1124            confirmation_manager: self.config.confirmation_manager.clone(),
1125            enforce_active_skill_tool_restrictions: Some(
1126                self.config.enforce_active_skill_tool_restrictions,
1127            ),
1128            workspace_services: Some(Arc::clone(&self.tool_context.workspace_services)),
1129            budget_guard: self.config.budget_guard.clone(),
1130        }
1131    }
1132
1133    /// The session's persistence store, if one is configured — needed by the
1134    /// resumable orchestration combinator to journal workflow progress.
1135    pub fn session_store(&self) -> Option<Arc<dyn crate::store::SessionStore>> {
1136        self.session_store.clone()
1137    }
1138
1139    /// Return the definitions of all tools currently registered in this session.
1140    ///
1141    /// The list reflects the live state of the tool executor — tools added via
1142    /// `add_mcp_server()` appear immediately; tools removed via
1143    /// `remove_mcp_server()` disappear immediately.
1144    pub fn tool_definitions(&self) -> Vec<crate::llm::ToolDefinition> {
1145        DirectToolRuntime::from_session(self).definitions()
1146    }
1147
1148    /// Return the names of all tools currently registered on this session.
1149    ///
1150    /// Equivalent to `tool_definitions().into_iter().map(|t| t.name).collect()`.
1151    /// Tools added via [`add_mcp_server`] appear immediately; tools removed via
1152    /// [`remove_mcp_server`] disappear immediately.
1153    pub fn tool_names(&self) -> Vec<String> {
1154        DirectToolRuntime::from_session(self).names()
1155    }
1156
1157    /// Return a stored tool artifact by URI, if it exists in this session.
1158    pub fn get_artifact(&self, artifact_uri: &str) -> Option<crate::tools::ToolArtifact> {
1159        DirectToolRuntime::from_session(self).artifact(artifact_uri)
1160    }
1161
1162    /// Return compact execution trace events recorded for this session.
1163    pub fn trace_events(&self) -> Vec<crate::trace::TraceEvent> {
1164        SessionView::from_session(self).trace_events()
1165    }
1166
1167    /// Return structured verification reports recorded for this session.
1168    pub fn verification_reports(&self) -> Vec<crate::verification::VerificationReport> {
1169        VerificationRuntime::from_session(self).reports()
1170    }
1171
1172    /// Return a structured summary of all verification reports recorded for this session.
1173    pub fn verification_summary(&self) -> crate::verification::VerificationSummary {
1174        VerificationRuntime::from_session(self).summary()
1175    }
1176
1177    /// Return a concise human-readable verification summary for this session.
1178    pub fn verification_summary_text(&self) -> String {
1179        VerificationRuntime::from_session(self).summary_text()
1180    }
1181
1182    /// Add externally produced verification reports to this session's completion evidence.
1183    pub fn record_verification_reports(
1184        &self,
1185        reports: impl IntoIterator<Item = crate::verification::VerificationReport>,
1186    ) {
1187        VerificationRuntime::from_session(self).record(reports);
1188    }
1189
1190    // ========================================================================
1191    // Hook API
1192    // ========================================================================
1193
1194    /// Register a hook for lifecycle event interception.
1195    pub fn register_hook(&self, hook: crate::hooks::Hook) {
1196        HookControl::from_session(self).register_hook(hook);
1197    }
1198
1199    /// Unregister a hook by ID.
1200    pub fn unregister_hook(&self, hook_id: &str) -> Option<crate::hooks::Hook> {
1201        HookControl::from_session(self).unregister_hook(hook_id)
1202    }
1203
1204    /// Register a handler for a specific hook.
1205    pub fn register_hook_handler(
1206        &self,
1207        hook_id: &str,
1208        handler: Arc<dyn crate::hooks::HookHandler>,
1209    ) {
1210        HookControl::from_session(self).register_hook_handler(hook_id, handler);
1211    }
1212
1213    /// Unregister a hook handler by hook ID.
1214    pub fn unregister_hook_handler(&self, hook_id: &str) {
1215        HookControl::from_session(self).unregister_hook_handler(hook_id);
1216    }
1217
1218    /// Get the number of registered hooks.
1219    pub fn hook_count(&self) -> usize {
1220        HookControl::from_session(self).hook_count()
1221    }
1222
1223    /// Save the session to the configured store.
1224    ///
1225    /// Returns `Ok(())` if saved successfully, or if no store is configured (no-op).
1226    pub async fn save(&self) -> Result<()> {
1227        session_save::save(self).await
1228    }
1229
1230    /// Read a file from the workspace.
1231    pub async fn read_file(&self, path: &str) -> Result<String> {
1232        self.read_file_with_options(path, ReadFileOptions::default())
1233            .await
1234    }
1235
1236    /// Read a file from the workspace with optional line-range controls.
1237    pub async fn read_file_with_options(
1238        &self,
1239        path: &str,
1240        options: ReadFileOptions,
1241    ) -> Result<String> {
1242        DirectToolRuntime::from_session(self)
1243            .read_file(path, options)
1244            .await
1245    }
1246
1247    /// Write a file in the workspace.
1248    pub async fn write_file(&self, path: &str, content: &str) -> Result<ToolCallResult> {
1249        DirectToolRuntime::from_session(self)
1250            .write_file(path, content)
1251            .await
1252    }
1253
1254    /// List a directory in the workspace.
1255    pub async fn ls(&self, path: Option<&str>) -> Result<ToolCallResult> {
1256        DirectToolRuntime::from_session(self).ls(path).await
1257    }
1258
1259    /// Edit a file by replacing text in the workspace.
1260    pub async fn edit_file(
1261        &self,
1262        path: &str,
1263        old_string: &str,
1264        new_string: &str,
1265        replace_all: bool,
1266    ) -> Result<ToolCallResult> {
1267        DirectToolRuntime::from_session(self)
1268            .edit_file(path, old_string, new_string, replace_all)
1269            .await
1270    }
1271
1272    /// Apply a unified diff patch to a workspace file.
1273    pub async fn patch_file(&self, path: &str, diff: &str) -> Result<ToolCallResult> {
1274        DirectToolRuntime::from_session(self)
1275            .patch_file(path, diff)
1276            .await
1277    }
1278
1279    /// Execute a bash command in the workspace.
1280    ///
1281    /// When a sandbox handle is configured via
1282    /// [`SessionOptions::with_sandbox_handle()`], the command is routed through
1283    /// that sandbox.
1284    pub async fn bash(&self, command: &str) -> Result<String> {
1285        DirectToolRuntime::from_session(self).bash(command).await
1286    }
1287
1288    /// Run verification commands through the session's tool execution path.
1289    pub async fn verify_commands(
1290        &self,
1291        subject: &str,
1292        commands: &[crate::verification::VerificationCommand],
1293    ) -> Result<crate::verification::VerificationReport> {
1294        VerificationRuntime::from_session(self)
1295            .verify_commands(subject, commands)
1296            .await
1297    }
1298
1299    /// Return project-aware verification command presets for this workspace.
1300    pub fn verification_presets(&self) -> Vec<crate::verification::VerificationPreset> {
1301        VerificationRuntime::from_session(self).presets()
1302    }
1303
1304    /// Search for files matching a glob pattern.
1305    pub async fn glob(&self, pattern: &str) -> Result<Vec<String>> {
1306        DirectToolRuntime::from_session(self).glob(pattern).await
1307    }
1308
1309    /// Search file contents with a regex pattern.
1310    pub async fn grep(&self, pattern: &str) -> Result<String> {
1311        DirectToolRuntime::from_session(self).grep(pattern).await
1312    }
1313
1314    /// Execute a tool by name, bypassing the LLM.
1315    pub async fn tool(&self, name: &str, args: serde_json::Value) -> Result<ToolCallResult> {
1316        DirectToolRuntime::from_session(self).call(name, args).await
1317    }
1318
1319    /// Execute a tool by name and expose high-level agent events emitted by that
1320    /// tool while it runs.
1321    ///
1322    /// This keeps the normal direct `tool()` API unchanged while giving embedded
1323    /// hosts a live progress stream for tools such as `dynamic_workflow` and
1324    /// `parallel_task`.
1325    pub fn tool_with_events(
1326        &self,
1327        name: &str,
1328        args: serde_json::Value,
1329    ) -> (
1330        mpsc::Receiver<AgentEvent>,
1331        JoinHandle<Result<ToolCallResult>>,
1332    ) {
1333        DirectToolRuntime::from_session(self).spawn_call_with_agent_events(name.to_string(), args)
1334    }
1335
1336    // ========================================================================
1337    // Advanced optional Queue API
1338    // ========================================================================
1339
1340    /// Returns whether this session has an advanced lane queue configured.
1341    pub fn has_queue(&self) -> bool {
1342        QueueControl::from_session(self).has_queue()
1343    }
1344
1345    /// Configure a lane's handler mode for explicit external/hybrid dispatch.
1346    ///
1347    /// Only effective when a queue is configured via `SessionOptions::with_queue_config`.
1348    pub async fn set_lane_handler(&self, lane: SessionLane, config: LaneHandlerConfig) {
1349        QueueControl::from_session(self)
1350            .set_lane_handler(lane, config)
1351            .await;
1352    }
1353
1354    /// Complete an external queue task by ID.
1355    ///
1356    /// Returns `true` if the task was found and completed, `false` if not found.
1357    pub async fn complete_external_task(&self, task_id: &str, result: ExternalTaskResult) -> bool {
1358        QueueControl::from_session(self)
1359            .complete_external_task(task_id, result)
1360            .await
1361    }
1362
1363    /// Get pending external queue tasks awaiting completion by an external handler.
1364    pub async fn pending_external_tasks(&self) -> Vec<ExternalTask> {
1365        QueueControl::from_session(self)
1366            .pending_external_tasks()
1367            .await
1368    }
1369
1370    /// Get optional queue statistics (pending, active, external counts per lane).
1371    pub async fn queue_stats(&self) -> SessionQueueStats {
1372        QueueControl::from_session(self).stats().await
1373    }
1374
1375    /// Get a metrics snapshot from the optional queue (if metrics are enabled).
1376    pub async fn queue_metrics(&self) -> Option<MetricsSnapshot> {
1377        QueueControl::from_session(self).metrics().await
1378    }
1379
1380    /// Get dead letters from the optional queue's DLQ (if DLQ is enabled).
1381    pub async fn dead_letters(&self) -> Vec<DeadLetter> {
1382        QueueControl::from_session(self).dead_letters().await
1383    }
1384
1385    // ========================================================================
1386    // MCP API
1387    // ========================================================================
1388
1389    /// Register all agents found in a directory with the live session.
1390    ///
1391    /// Scans `dir` for `*.yaml`, `*.yml`, and `*.md` agent definition files,
1392    /// parses them, and adds each one to the shared `AgentRegistry` used by the
1393    /// `task` tool.  New agents are immediately usable via `task(agent="…")` in
1394    /// the same session — no restart required.
1395    ///
1396    /// Returns the number of agents successfully loaded from the directory.
1397    pub fn register_agent_dir(&self, dir: &std::path::Path) -> usize {
1398        SessionExtensionRuntime::from_session(self).register_agent_dir(dir)
1399    }
1400
1401    /// Register a disposable worker agent with the live session.
1402    ///
1403    /// The returned definition is immediately available to the `task` tool by
1404    /// worker name, so callers can create many reproducible workers without
1405    /// writing temporary agent files or restarting the session.
1406    pub fn register_worker_agent(
1407        &self,
1408        spec: crate::subagent::WorkerAgentSpec,
1409    ) -> crate::subagent::AgentDefinition {
1410        SessionExtensionRuntime::from_session(self).register_worker_agent(spec)
1411    }
1412
1413    /// Register multiple disposable worker agents with the live session.
1414    pub fn register_worker_agents<I>(&self, specs: I) -> Vec<crate::subagent::AgentDefinition>
1415    where
1416        I: IntoIterator<Item = crate::subagent::WorkerAgentSpec>,
1417    {
1418        SessionExtensionRuntime::from_session(self).register_worker_agents(specs)
1419    }
1420
1421    /// Add an MCP server to this session.
1422    ///
1423    /// Registers, connects, and makes all tools immediately available for the
1424    /// agent to call. Tool names follow the convention `mcp__<name>__<tool>`.
1425    ///
1426    /// Returns the number of tools registered from the server.
1427    pub async fn add_mcp_server(
1428        &self,
1429        config: crate::mcp::McpServerConfig,
1430    ) -> crate::error::Result<usize> {
1431        SessionExtensionRuntime::from_session(self)
1432            .add_mcp_server(config)
1433            .await
1434    }
1435
1436    /// The session's tool executor, for installing agent-dir `tools/` entries
1437    /// (e.g. a `kind = "script"` tool) into the live registry. Internal seam used
1438    /// by [`serve::install_agent_dir_tools`](crate::serve::install_agent_dir_tools)
1439    /// (the only caller, hence the `serve` gate).
1440    #[cfg(feature = "serve")]
1441    pub(crate) fn tool_executor(&self) -> &Arc<crate::tools::ToolExecutor> {
1442        &self.tool_executor
1443    }
1444
1445    /// Register a host-provided dynamic tool into the live session. Enables an
1446    /// embedding app (e.g. the a3s-code CLI's login-gated `runtime` A3S Runtime
1447    /// offload tool) to add a native tool at runtime; it enters the LLM's toolset
1448    /// on the next run (`build_agent_loop` re-snapshots `definitions()` per run),
1449    /// the same way MCP tools surface after `add_mcp_server`. Idempotent by name.
1450    pub fn register_dynamic_tool(&self, tool: Arc<dyn crate::tools::Tool>) {
1451        self.tool_executor.register_dynamic_tool(tool);
1452    }
1453
1454    /// Register the A3S Flow-backed dynamic workflow tool for this live session.
1455    ///
1456    /// The tool is named `dynamic_workflow`. It accepts a sandboxed JavaScript
1457    /// PTC workflow script and executes it through
1458    /// [`crate::DynamicWorkflowRuntime`], so A3S Flow owns workflow replay while
1459    /// the script can still call A3S Code tools.
1460    pub fn register_dynamic_workflow_runtime(&self) {
1461        crate::tools::register_dynamic_workflow(self.tool_executor.registry());
1462    }
1463
1464    /// Remove a previously host-registered dynamic tool by name (e.g. on logout).
1465    /// No-op if no tool of that name is registered.
1466    pub fn unregister_dynamic_tool(&self, name: &str) {
1467        self.tool_executor.unregister_dynamic_tool(name);
1468    }
1469
1470    /// Remove an MCP server from this session.
1471    ///
1472    /// Disconnects the server and unregisters all its tools from the executor.
1473    /// No-op if the server was never added.
1474    pub async fn remove_mcp_server(&self, server_name: &str) -> crate::error::Result<()> {
1475        SessionExtensionRuntime::from_session(self)
1476            .remove_mcp_server(server_name)
1477            .await
1478    }
1479
1480    /// Return the connection status of all MCP servers registered with this session.
1481    pub async fn mcp_status(
1482        &self,
1483    ) -> std::collections::HashMap<String, crate::mcp::McpServerStatus> {
1484        SessionExtensionRuntime::from_session(self)
1485            .mcp_status()
1486            .await
1487    }
1488}
1489
1490// ============================================================================
1491// Tests
1492// ============================================================================
1493
1494#[cfg(test)]
1495mod tests;