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 /// Currently executing tools observed from runtime events.
433 active_tools: Arc<tokio::sync::RwLock<HashMap<String, ActiveToolState>>>,
434 /// Compact execution traces for this session.
435 trace_sink: crate::trace::InMemoryTraceSink,
436 /// Structured completion evidence collected from agent and explicit verification runs.
437 verification_reports: Arc<RwLock<Vec<crate::verification::VerificationReport>>>,
438}
439
440impl std::fmt::Debug for AgentSession {
441 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442 f.debug_struct("AgentSession")
443 .field("session_id", &self.session_id)
444 .field("workspace", &self.workspace.display().to_string())
445 .field("auto_save", &self.auto_save)
446 .finish()
447 }
448}
449
450impl AgentSession {
451 /// Get a snapshot of command entries (name, description, optional usage).
452 ///
453 /// Acquires the command registry lock briefly and returns owned data.
454 pub fn command_registry(&self) -> std::sync::MutexGuard<'_, CommandRegistry> {
455 session_commands::registry(self)
456 }
457
458 /// Register a custom slash command.
459 ///
460 /// Takes `&self` so it can be called on a shared `Arc<AgentSession>`.
461 pub fn register_command(&self, cmd: Arc<dyn crate::commands::SlashCommand>) {
462 session_commands::register(self, cmd);
463 }
464
465 /// Cancel any active operation and release session resources.
466 pub async fn close(&self) {
467 let _ = self.cancel().await;
468 }
469
470 /// Send a prompt and wait for the complete response.
471 ///
472 /// When `history` is `None`, uses (and auto-updates) the session's
473 /// internal conversation history. When `Some`, uses the provided
474 /// history instead (the internal history is **not** modified).
475 ///
476 /// If the prompt starts with `/`, it is dispatched as a slash command
477 /// and the result is returned without calling the LLM.
478 pub async fn send(&self, prompt: &str, history: Option<&[Message]>) -> Result<AgentResult> {
479 conversation_runtime::send(self, prompt, history).await
480 }
481
482 /// Send a prompt with image attachments and wait for the complete response.
483 ///
484 /// Images are included as multi-modal content blocks in the user message.
485 /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
486 pub async fn send_with_attachments(
487 &self,
488 prompt: &str,
489 attachments: &[crate::llm::Attachment],
490 history: Option<&[Message]>,
491 ) -> Result<AgentResult> {
492 conversation_runtime::send_with_attachments(self, prompt, attachments, history).await
493 }
494
495 /// Stream a prompt with image attachments.
496 ///
497 /// Images are included as multi-modal content blocks in the user message.
498 /// Requires a vision-capable model (e.g., Claude Sonnet, GPT-4o).
499 pub async fn stream_with_attachments(
500 &self,
501 prompt: &str,
502 attachments: &[crate::llm::Attachment],
503 history: Option<&[Message]>,
504 ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
505 conversation_runtime::stream_with_attachments(self, prompt, attachments, history).await
506 }
507
508 /// Send a prompt and stream events back.
509 ///
510 /// When `history` is `None`, uses the session's internal history
511 /// and updates it when the stream completes.
512 /// When `Some`, uses the provided history instead.
513 ///
514 /// If the prompt starts with `/`, it is dispatched as a slash command
515 /// and the result is emitted as a single `TextDelta` + `End` event.
516 pub async fn stream(
517 &self,
518 prompt: &str,
519 history: Option<&[Message]>,
520 ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
521 conversation_runtime::stream(self, prompt, history).await
522 }
523
524 /// Cancel the current ongoing operation (send/stream).
525 ///
526 /// If an operation is in progress, this will trigger cancellation of the LLM streaming
527 /// and tool execution. The operation will terminate as soon as possible.
528 ///
529 /// Returns `true` if an operation was cancelled, `false` if no operation was in progress.
530 pub async fn cancel(&self) -> bool {
531 RunControl::from_session(self).cancel_current().await
532 }
533
534 /// Cancel a specific run only if it is still the active run.
535 ///
536 /// This is useful for SDK callers that hold a previously observed run ID:
537 /// stale run IDs will not cancel a newer operation.
538 pub async fn cancel_run(&self, run_id: &str) -> bool {
539 RunControl::from_session(self).cancel_run(run_id).await
540 }
541
542 /// Return snapshots for runs recorded by this session.
543 pub async fn runs(&self) -> Vec<crate::run::RunSnapshot> {
544 RunControl::from_session(self).runs().await
545 }
546
547 /// Return a snapshot for a recorded run.
548 pub async fn run_snapshot(&self, run_id: &str) -> Option<crate::run::RunSnapshot> {
549 RunControl::from_session(self).run_snapshot(run_id).await
550 }
551
552 /// Return recorded runtime events for a run.
553 pub async fn run_events(&self, run_id: &str) -> Vec<crate::run::RunEventRecord> {
554 RunControl::from_session(self).run_events(run_id).await
555 }
556
557 /// Return a handle for the currently running operation, if any.
558 pub async fn current_run(&self) -> Option<crate::run::RunHandle> {
559 RunControl::from_session(self).current_run().await
560 }
561
562 /// Return active tool calls observed for the currently running operation.
563 pub async fn active_tools(&self) -> Vec<crate::run::ActiveToolSnapshot> {
564 SessionView::from_session(self).active_tools().await
565 }
566
567 /// Return a snapshot of the session's conversation history.
568 pub fn history(&self) -> Vec<Message> {
569 SessionView::from_session(self).history()
570 }
571
572 /// Return pending HITL tool confirmations for this session.
573 pub async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
574 HitlControl::from_session(self)
575 .pending_confirmations()
576 .await
577 }
578
579 /// Resolve a pending HITL tool confirmation.
580 ///
581 /// Returns `Ok(true)` when a pending confirmation was found and completed,
582 /// `Ok(false)` when the tool ID is not pending or HITL is not configured.
583 pub async fn confirm_tool_use(
584 &self,
585 tool_id: &str,
586 approved: bool,
587 reason: Option<String>,
588 ) -> Result<bool> {
589 HitlControl::from_session(self)
590 .confirm_tool_use(tool_id, approved, reason)
591 .await
592 }
593
594 /// Cancel all pending HITL confirmations for this session.
595 pub async fn cancel_confirmations(&self) -> usize {
596 HitlControl::from_session(self).cancel_confirmations().await
597 }
598
599 /// Return a reference to the session's memory, if configured.
600 pub fn memory(&self) -> Option<&Arc<crate::memory::AgentMemory>> {
601 SessionView::from_session(self).memory()
602 }
603
604 /// Return the session ID.
605 pub fn id(&self) -> &str {
606 SessionView::from_session(self).id()
607 }
608
609 /// Return the session workspace path.
610 pub fn workspace(&self) -> &std::path::Path {
611 SessionView::from_session(self).workspace()
612 }
613
614 /// Return any deferred init warning (e.g. memory store failed to initialize).
615 pub fn init_warning(&self) -> Option<&str> {
616 SessionView::from_session(self).init_warning()
617 }
618
619 /// Return the session ID.
620 pub fn session_id(&self) -> &str {
621 SessionView::from_session(self).id()
622 }
623
624 /// Return the definitions of all tools currently registered in this session.
625 ///
626 /// The list reflects the live state of the tool executor — tools added via
627 /// `add_mcp_server()` appear immediately; tools removed via
628 /// `remove_mcp_server()` disappear immediately.
629 pub fn tool_definitions(&self) -> Vec<crate::llm::ToolDefinition> {
630 DirectToolRuntime::from_session(self).definitions()
631 }
632
633 /// Return the names of all tools currently registered on this session.
634 ///
635 /// Equivalent to `tool_definitions().into_iter().map(|t| t.name).collect()`.
636 /// Tools added via [`add_mcp_server`] appear immediately; tools removed via
637 /// [`remove_mcp_server`] disappear immediately.
638 pub fn tool_names(&self) -> Vec<String> {
639 DirectToolRuntime::from_session(self).names()
640 }
641
642 /// Return a stored tool artifact by URI, if it exists in this session.
643 pub fn get_artifact(&self, artifact_uri: &str) -> Option<crate::tools::ToolArtifact> {
644 DirectToolRuntime::from_session(self).artifact(artifact_uri)
645 }
646
647 /// Return compact execution trace events recorded for this session.
648 pub fn trace_events(&self) -> Vec<crate::trace::TraceEvent> {
649 SessionView::from_session(self).trace_events()
650 }
651
652 /// Return structured verification reports recorded for this session.
653 pub fn verification_reports(&self) -> Vec<crate::verification::VerificationReport> {
654 VerificationRuntime::from_session(self).reports()
655 }
656
657 /// Return a structured summary of all verification reports recorded for this session.
658 pub fn verification_summary(&self) -> crate::verification::VerificationSummary {
659 VerificationRuntime::from_session(self).summary()
660 }
661
662 /// Return a concise human-readable verification summary for this session.
663 pub fn verification_summary_text(&self) -> String {
664 VerificationRuntime::from_session(self).summary_text()
665 }
666
667 /// Add externally produced verification reports to this session's completion evidence.
668 pub fn record_verification_reports(
669 &self,
670 reports: impl IntoIterator<Item = crate::verification::VerificationReport>,
671 ) {
672 VerificationRuntime::from_session(self).record(reports);
673 }
674
675 // ========================================================================
676 // Hook API
677 // ========================================================================
678
679 /// Register a hook for lifecycle event interception.
680 pub fn register_hook(&self, hook: crate::hooks::Hook) {
681 HookControl::from_session(self).register_hook(hook);
682 }
683
684 /// Unregister a hook by ID.
685 pub fn unregister_hook(&self, hook_id: &str) -> Option<crate::hooks::Hook> {
686 HookControl::from_session(self).unregister_hook(hook_id)
687 }
688
689 /// Register a handler for a specific hook.
690 pub fn register_hook_handler(
691 &self,
692 hook_id: &str,
693 handler: Arc<dyn crate::hooks::HookHandler>,
694 ) {
695 HookControl::from_session(self).register_hook_handler(hook_id, handler);
696 }
697
698 /// Unregister a hook handler by hook ID.
699 pub fn unregister_hook_handler(&self, hook_id: &str) {
700 HookControl::from_session(self).unregister_hook_handler(hook_id);
701 }
702
703 /// Get the number of registered hooks.
704 pub fn hook_count(&self) -> usize {
705 HookControl::from_session(self).hook_count()
706 }
707
708 /// Save the session to the configured store.
709 ///
710 /// Returns `Ok(())` if saved successfully, or if no store is configured (no-op).
711 pub async fn save(&self) -> Result<()> {
712 session_save::save(self).await
713 }
714
715 /// Read a file from the workspace.
716 pub async fn read_file(&self, path: &str) -> Result<String> {
717 DirectToolRuntime::from_session(self).read_file(path).await
718 }
719
720 /// Write a file in the workspace.
721 pub async fn write_file(&self, path: &str, content: &str) -> Result<ToolCallResult> {
722 DirectToolRuntime::from_session(self)
723 .write_file(path, content)
724 .await
725 }
726
727 /// List a directory in the workspace.
728 pub async fn ls(&self, path: Option<&str>) -> Result<ToolCallResult> {
729 DirectToolRuntime::from_session(self).ls(path).await
730 }
731
732 /// Edit a file by replacing text in the workspace.
733 pub async fn edit_file(
734 &self,
735 path: &str,
736 old_string: &str,
737 new_string: &str,
738 replace_all: bool,
739 ) -> Result<ToolCallResult> {
740 DirectToolRuntime::from_session(self)
741 .edit_file(path, old_string, new_string, replace_all)
742 .await
743 }
744
745 /// Apply a unified diff patch to a workspace file.
746 pub async fn patch_file(&self, path: &str, diff: &str) -> Result<ToolCallResult> {
747 DirectToolRuntime::from_session(self)
748 .patch_file(path, diff)
749 .await
750 }
751
752 /// Execute a bash command in the workspace.
753 ///
754 /// When a sandbox handle is configured via
755 /// [`SessionOptions::with_sandbox_handle()`], the command is routed through
756 /// that sandbox.
757 pub async fn bash(&self, command: &str) -> Result<String> {
758 DirectToolRuntime::from_session(self).bash(command).await
759 }
760
761 /// Run verification commands through the session's tool execution path.
762 pub async fn verify_commands(
763 &self,
764 subject: &str,
765 commands: &[crate::verification::VerificationCommand],
766 ) -> Result<crate::verification::VerificationReport> {
767 VerificationRuntime::from_session(self)
768 .verify_commands(subject, commands)
769 .await
770 }
771
772 /// Return project-aware verification command presets for this workspace.
773 pub fn verification_presets(&self) -> Vec<crate::verification::VerificationPreset> {
774 VerificationRuntime::from_session(self).presets()
775 }
776
777 /// Search for files matching a glob pattern.
778 pub async fn glob(&self, pattern: &str) -> Result<Vec<String>> {
779 DirectToolRuntime::from_session(self).glob(pattern).await
780 }
781
782 /// Search file contents with a regex pattern.
783 pub async fn grep(&self, pattern: &str) -> Result<String> {
784 DirectToolRuntime::from_session(self).grep(pattern).await
785 }
786
787 /// Execute a tool by name, bypassing the LLM.
788 pub async fn tool(&self, name: &str, args: serde_json::Value) -> Result<ToolCallResult> {
789 DirectToolRuntime::from_session(self).call(name, args).await
790 }
791
792 // ========================================================================
793 // Advanced optional Queue API
794 // ========================================================================
795
796 /// Returns whether this session has an advanced lane queue configured.
797 pub fn has_queue(&self) -> bool {
798 QueueControl::from_session(self).has_queue()
799 }
800
801 /// Configure a lane's handler mode for explicit external/hybrid dispatch.
802 ///
803 /// Only effective when a queue is configured via `SessionOptions::with_queue_config`.
804 pub async fn set_lane_handler(&self, lane: SessionLane, config: LaneHandlerConfig) {
805 QueueControl::from_session(self)
806 .set_lane_handler(lane, config)
807 .await;
808 }
809
810 /// Complete an external queue task by ID.
811 ///
812 /// Returns `true` if the task was found and completed, `false` if not found.
813 pub async fn complete_external_task(&self, task_id: &str, result: ExternalTaskResult) -> bool {
814 QueueControl::from_session(self)
815 .complete_external_task(task_id, result)
816 .await
817 }
818
819 /// Get pending external queue tasks awaiting completion by an external handler.
820 pub async fn pending_external_tasks(&self) -> Vec<ExternalTask> {
821 QueueControl::from_session(self)
822 .pending_external_tasks()
823 .await
824 }
825
826 /// Get optional queue statistics (pending, active, external counts per lane).
827 pub async fn queue_stats(&self) -> SessionQueueStats {
828 QueueControl::from_session(self).stats().await
829 }
830
831 /// Get a metrics snapshot from the optional queue (if metrics are enabled).
832 pub async fn queue_metrics(&self) -> Option<MetricsSnapshot> {
833 QueueControl::from_session(self).metrics().await
834 }
835
836 /// Get dead letters from the optional queue's DLQ (if DLQ is enabled).
837 pub async fn dead_letters(&self) -> Vec<DeadLetter> {
838 QueueControl::from_session(self).dead_letters().await
839 }
840
841 // ========================================================================
842 // MCP API
843 // ========================================================================
844
845 /// Register all agents found in a directory with the live session.
846 ///
847 /// Scans `dir` for `*.yaml`, `*.yml`, and `*.md` agent definition files,
848 /// parses them, and adds each one to the shared `AgentRegistry` used by the
849 /// `task` tool. New agents are immediately usable via `task(agent="…")` in
850 /// the same session — no restart required.
851 ///
852 /// Returns the number of agents successfully loaded from the directory.
853 pub fn register_agent_dir(&self, dir: &std::path::Path) -> usize {
854 SessionExtensionRuntime::from_session(self).register_agent_dir(dir)
855 }
856
857 /// Register a disposable worker agent with the live session.
858 ///
859 /// The returned definition is immediately available to the `task` tool by
860 /// worker name, so callers can create many reproducible workers without
861 /// writing temporary agent files or restarting the session.
862 pub fn register_worker_agent(
863 &self,
864 spec: crate::subagent::WorkerAgentSpec,
865 ) -> crate::subagent::AgentDefinition {
866 SessionExtensionRuntime::from_session(self).register_worker_agent(spec)
867 }
868
869 /// Register multiple disposable worker agents with the live session.
870 pub fn register_worker_agents<I>(&self, specs: I) -> Vec<crate::subagent::AgentDefinition>
871 where
872 I: IntoIterator<Item = crate::subagent::WorkerAgentSpec>,
873 {
874 SessionExtensionRuntime::from_session(self).register_worker_agents(specs)
875 }
876
877 /// Add an MCP server to this session.
878 ///
879 /// Registers, connects, and makes all tools immediately available for the
880 /// agent to call. Tool names follow the convention `mcp__<name>__<tool>`.
881 ///
882 /// Returns the number of tools registered from the server.
883 pub async fn add_mcp_server(
884 &self,
885 config: crate::mcp::McpServerConfig,
886 ) -> crate::error::Result<usize> {
887 SessionExtensionRuntime::from_session(self)
888 .add_mcp_server(config)
889 .await
890 }
891
892 /// Remove an MCP server from this session.
893 ///
894 /// Disconnects the server and unregisters all its tools from the executor.
895 /// No-op if the server was never added.
896 pub async fn remove_mcp_server(&self, server_name: &str) -> crate::error::Result<()> {
897 SessionExtensionRuntime::from_session(self)
898 .remove_mcp_server(server_name)
899 .await
900 }
901
902 /// Return the connection status of all MCP servers registered with this session.
903 pub async fn mcp_status(
904 &self,
905 ) -> std::collections::HashMap<String, crate::mcp::McpServerStatus> {
906 SessionExtensionRuntime::from_session(self)
907 .mcp_status()
908 .await
909 }
910}
911
912// ============================================================================
913// Tests
914// ============================================================================
915
916#[cfg(test)]
917mod tests;