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_async("/my-project", None).await?;
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_facade;
41mod agent_loop_runtime;
42mod agent_sessions;
43mod capabilities;
44mod capability_facade;
45mod command_runtime;
46mod conversation_runtime;
47mod direct_tool_facade;
48mod direct_tools;
49mod governance_facade;
50mod hook_control;
51mod run_admission;
52mod run_facade;
53mod run_lifecycle;
54mod runtime;
55mod runtime_events;
56mod session_builder;
57mod session_clock;
58mod session_close;
59mod session_commands;
60mod session_config;
61mod session_extensions;
62mod session_facade;
63mod session_hitl;
64mod session_options;
65mod session_persistence;
66mod session_queue;
67mod session_runs;
68mod session_runtime;
69mod session_save;
70mod session_verification;
71mod session_view;
72mod workflow_facade;
73pub use agent_facade::{Agent, SessionBuilder};
74use direct_tools::DirectToolRuntime;
75use hook_control::HookControl;
76use runtime_events::ActiveToolState;
77use session_close::SessionCloseHandle;
78use session_extensions::SessionExtensionRuntime;
79use session_hitl::HitlControl;
80use session_queue::QueueControl;
81use session_runs::RunControl;
82use session_verification::VerificationRuntime;
83use session_view::SessionView;
84
85/// Canonicalize a path, stripping the Windows `\\?\` UNC prefix to avoid
86/// polluting workspace strings throughout the system (prompts, session data, etc.).
87fn safe_canonicalize(path: &Path) -> PathBuf {
88    match std::fs::canonicalize(path) {
89        Ok(p) => strip_unc_prefix(p),
90        Err(_) => path.to_path_buf(),
91    }
92}
93
94/// Strip the Windows extended-length path prefix (`\\?\`) that `canonicalize()` adds.
95/// On non-Windows this is a no-op.
96fn strip_unc_prefix(path: PathBuf) -> PathBuf {
97    #[cfg(windows)]
98    {
99        let s = path.to_string_lossy();
100        if let Some(stripped) = s.strip_prefix(r"\\?\") {
101            return PathBuf::from(stripped);
102        }
103    }
104    path
105}
106
107// ============================================================================
108// ToolCallResult
109// ============================================================================
110
111/// Result of a direct tool execution (no LLM).
112#[derive(Debug, Clone)]
113pub struct ToolCallResult {
114    pub name: String,
115    pub output: String,
116    pub exit_code: i32,
117    pub metadata: Option<serde_json::Value>,
118    /// Structured discriminant for tool failures. `None` when the tool
119    /// either succeeded or failed without a typed reason (the message in
120    /// `output` is then the only diagnostic). Populated for known
121    /// kinds such as `VersionConflict` so SDK callers can branch on the
122    /// `type` field instead of regex-matching `output`.
123    pub error_kind: Option<crate::tools::ToolErrorKind>,
124}
125
126// ============================================================================
127// ReadFileOptions
128// ============================================================================
129
130/// Optional line-range controls for direct `read` tool calls.
131#[derive(Debug, Clone, Copy, Default)]
132pub struct ReadFileOptions {
133    /// 0-indexed line offset to start reading from.
134    pub offset: Option<usize>,
135    /// Maximum number of lines to read.
136    pub limit: Option<usize>,
137}
138
139// ============================================================================
140// SessionOptions
141// ============================================================================
142
143/// Optional per-session overrides.
144#[derive(Clone, Default)]
145pub struct SessionOptions {
146    /// Override the default model. Format: `"provider/model"` (e.g., `"openai/gpt-4o"`).
147    pub model: Option<String>,
148    /// Extra directories to scan for agent files.
149    /// Merged with any global `agent_dirs` from [`CodeConfig`].
150    pub agent_dirs: Vec<PathBuf>,
151    /// Reproducible disposable workers registered for task delegation.
152    /// Explicit session workers override agents loaded from directories by name.
153    pub worker_agents: Vec<crate::subagent::WorkerAgentSpec>,
154    /// Optional queue configuration for lane-based tool execution.
155    ///
156    /// When set, enables priority-based tool scheduling with parallel execution
157    /// of read-only (Query-lane) tools, DLQ, metrics, and external task handling.
158    pub queue_config: Option<SessionQueueConfig>,
159    /// Optional security provider for taint tracking and output sanitization
160    pub security_provider: Option<Arc<dyn crate::security::SecurityProvider>>,
161    /// Optional host-supplied LLM client.
162    ///
163    /// When set, it is used directly, overriding the `provider/model`
164    /// factory resolution — the one Action-layer backend that was previously
165    /// only injectable in test code. Lets a host plug in a provider the
166    /// built-in factory does not cover, a deterministic record/replay client,
167    /// or an HTTP-layer proxy/audit wrapper. Mirrors `workspace_services`.
168    pub llm_client: Option<Arc<dyn crate::llm::LlmClient>>,
169    /// Optional context providers for RAG
170    pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
171    /// Optional confirmation manager for HITL
172    pub confirmation_manager: Option<Arc<dyn crate::hitl::ConfirmationProvider>>,
173    /// Optional confirmation policy (will be used to create ConfirmationManager if confirmation_manager is not set)
174    pub confirmation_policy: Option<crate::hitl::ConfirmationPolicy>,
175    /// Optional permission checker
176    pub permission_checker: Option<Arc<dyn crate::permissions::PermissionChecker>>,
177    /// Serializable permission policy used to build the checker, when available.
178    pub permission_policy: Option<crate::permissions::PermissionPolicy>,
179    /// Enable planning
180    pub planning_mode: PlanningMode,
181    /// Enable goal tracking
182    pub goal_tracking: bool,
183    /// Extra directories to scan for skill files (*.md).
184    /// Merged with any global `skill_dirs` from [`CodeConfig`].
185    pub skill_dirs: Vec<PathBuf>,
186    /// Optional skill registry for instruction injection
187    pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
188    /// Whether active skill `allowed-tools` restrict ordinary session tool calls.
189    ///
190    /// Defaults to false so ordinary tools continue through permission policy,
191    /// hooks, and HITL. Set true to restore the legacy global active-skill
192    /// restriction behavior.
193    pub enforce_active_skill_tool_restrictions: Option<bool>,
194    /// Custom memory store override for long-term memory persistence.
195    ///
196    /// Sessions resolve a default store when this is not set.
197    pub memory_store: Option<Arc<dyn MemoryStore>>,
198    /// Host observers notified after successful durable memory writes.
199    pub memory_observers: Vec<Arc<dyn crate::memory::MemoryObserver>>,
200    /// Deferred file memory directory — constructed async in `build_session()`
201    pub(crate) file_memory_dir: Option<PathBuf>,
202    /// Optional session store for persistence
203    pub session_store: Option<Arc<dyn crate::store::SessionStore>>,
204    /// Deferred file session-store directory, initialized by the async builder.
205    pub(crate) file_session_store_dir: Option<PathBuf>,
206    /// Explicit session ID (auto-generated if not set)
207    pub session_id: Option<String>,
208    /// Multi-tenant identifier. Framework only transports this string;
209    /// the host decides what "tenant" means and how to
210    /// aggregate/bill on it. Emitted to hooks/traces, persisted in
211    /// `SessionData`, never interpreted by core.
212    pub tenant_id: Option<String>,
213    /// Identity of the principal that triggered this session (user id,
214    /// service account, etc). Treated as opaque.
215    pub principal: Option<String>,
216    /// Logical identifier of the agent template / definition the session
217    /// was instantiated from. Lets the host aggregate sessions by
218    /// "which agent recipe" independent of the concrete session id.
219    pub agent_template_id: Option<String>,
220    /// Distributed-trace correlation id. Propagated through hooks/traces
221    /// so a session's events join with upstream/downstream work in the
222    /// host's observability pipeline.
223    pub correlation_id: Option<String>,
224    /// Optional host-supplied budget / quota guard. The framework calls
225    /// into it before each LLM call (and reports actuals after) so the
226    /// host can refuse or rate-limit at the cluster level. Default is
227    /// `None` (no enforcement — equivalent to
228    /// [`NoopBudgetGuard`](crate::budget::NoopBudgetGuard)).
229    pub budget_guard: Option<Arc<dyn crate::budget::BudgetGuard>>,
230    /// Optional host-provided ID/Clock pair. Replaces the default
231    /// random-UUID + wall-clock pair, enabling deterministic replay
232    /// on another node. `None` keeps pre-P2 behaviour.
233    pub host_env: Option<Arc<crate::host_env::HostEnv>>,
234    /// Optional FIFO retention caps on the session's in-memory stores
235    /// (run records, run events, trace events, terminal subagent
236    /// tasks). `None` selects [`SessionRetentionLimits::default`](crate::retention::SessionRetentionLimits::default),
237    /// which is finite. Pass [`SessionRetentionLimits::unbounded`](crate::retention::SessionRetentionLimits::unbounded)
238    /// explicitly to retain everything.
239    pub retention_limits: Option<crate::retention::SessionRetentionLimits>,
240    /// Optional structured JSONL trajectory config.
241    ///
242    /// When set, a3s-code records user prompts, LLM turns, tool calls,
243    /// tool observations, token usage, and execution end status for RL
244    /// training or service data collection. If unset, the same config can
245    /// be enabled by `A3S_CODE_TRAJECTORY_PATH`.
246    pub rl_trajectory: Option<crate::rl_trajectory::RlTrajectoryConfig>,
247    /// Request token-level log probabilities from compatible LLM providers.
248    ///
249    /// This is off by default because many public providers reject logprob
250    /// requests with tool calls. Training/evaluation harnesses using compatible
251    /// OpenAI-style backends can enable it explicitly.
252    pub llm_logprobs: Option<bool>,
253    /// Number of alternative token logprobs to request per generated token.
254    pub llm_top_logprobs: Option<usize>,
255    /// Auto-save after each completed `send()` or default-history `stream()` call.
256    pub auto_save: bool,
257    /// Optional artifact retention limits for large tool/program outputs.
258    pub artifact_store_limits: Option<crate::tools::ArtifactStoreLimits>,
259    /// Max consecutive parse errors before aborting (overrides default of 2).
260    /// `None` uses the `AgentConfig` default.
261    pub max_parse_retries: Option<u32>,
262    /// Per-tool execution timeout in milliseconds.
263    /// `None` = no timeout (default).
264    pub tool_timeout_ms: Option<u64>,
265    /// Per-model API HTTP timeout in milliseconds.
266    /// `None` = no timeout (default).
267    pub llm_api_timeout_ms: Option<u64>,
268    /// Circuit-breaker threshold: max consecutive LLM API failures before
269    /// aborting in non-streaming mode (overrides default of 3).
270    /// `None` uses the `AgentConfig` default.
271    pub circuit_breaker_threshold: Option<u32>,
272    /// Max consecutive identical tool signatures before the guard turns the
273    /// duplicate call into a failed tool observation.
274    ///
275    /// `None` uses the `AgentConfig` default. Hosts can raise this for long
276    /// research sessions where repeating a read/search with the same arguments
277    /// is wasteful but should not make the whole run brittle.
278    pub duplicate_tool_call_threshold: Option<u32>,
279    /// Optional concrete sandbox implementation.
280    ///
281    /// When set, `bash` tool commands are routed through this sandbox instead
282    /// of `std::process::Command`. The host application constructs and owns
283    /// the implementation (e.g., an A3S Box–backed handle).
284    pub sandbox_handle: Option<Arc<dyn crate::sandbox::BashSandbox>>,
285    /// Optional host-provided workspace backend.
286    ///
287    /// When set, built-in tools such as `read`, `write`, `ls`, and `bash`
288    /// execute against these workspace capabilities instead of assuming the
289    /// server-local filesystem. This is the primary extension point for DFS,
290    /// browser, container, and remote workspace deployments.
291    pub workspace_services: Option<Arc<crate::workspace::WorkspaceServices>>,
292    /// Enable auto-compaction when context usage exceeds threshold.
293    pub auto_compact: bool,
294    /// Context usage percentage threshold for auto-compaction (0.0 - 1.0).
295    /// Default: 0.80 (80%).
296    pub auto_compact_threshold: Option<f32>,
297    /// Model context window used for automatic compaction accounting.
298    ///
299    /// When omitted, the configured model's declared context limit is used,
300    /// falling back to the agent default when the model has no declaration.
301    pub max_context_tokens: Option<usize>,
302    /// Inject a continuation message when the LLM stops without completing the task.
303    /// `None` uses the `AgentConfig` default (true).
304    pub continuation_enabled: Option<bool>,
305    /// Maximum continuation injections per execution.
306    /// `None` uses the `AgentConfig` default (3).
307    pub max_continuation_turns: Option<u32>,
308    /// Maximum execution time in milliseconds.
309    /// `None` = no timeout (default).
310    /// When set, the execution loop will abort if it exceeds this duration.
311    pub max_execution_time_ms: Option<u64>,
312    /// Optional MCP manager for connecting to external MCP servers.
313    ///
314    /// When set, all tools from connected MCP servers are registered and
315    /// available during agent execution with names like `mcp__server__tool`.
316    pub mcp_manager: Option<Arc<crate::mcp::manager::McpManager>>,
317    /// Sampling temperature (0.0–1.0). Overrides the provider default.
318    pub temperature: Option<f32>,
319    /// Extended thinking budget in tokens (Anthropic only).
320    pub thinking_budget: Option<usize>,
321    /// Per-session tool round limit override.
322    ///
323    /// When set, overrides the agent-level `max_tool_rounds` for this session only.
324    /// Maps directly from [`AgentDefinition::max_steps`](crate::subagent::AgentDefinition::max_steps)
325    /// when creating sessions
326    /// via [`Agent::session_for_agent`].
327    pub max_tool_rounds: Option<usize>,
328    /// Per-session parallel fan-out limit override.
329    ///
330    /// Applies to delegated `parallel_task`, plan wave execution, and safe
331    /// parallel write batches.
332    pub max_parallel_tasks: Option<usize>,
333    /// Per-session automatic subagent delegation override.
334    pub auto_delegation: Option<crate::config::AutoDelegationConfig>,
335    /// Per-session switch for model-visible manual child-agent tools.
336    ///
337    /// This overlays the effective automatic delegation config instead of
338    /// replacing it, so callers can hide `task` / `parallel_task` while
339    /// preserving other delegation settings.
340    pub manual_delegation_enabled: Option<bool>,
341    /// Per-session kill switch for automatic parallel child-agent fan-out.
342    ///
343    /// This overlays the effective automatic delegation config instead of
344    /// replacing it, so callers can disable auto fan-out without disabling
345    /// automatic delegation itself.
346    pub auto_parallel_delegation: Option<bool>,
347    /// Slot-based system prompt customization.
348    ///
349    /// When set, overrides the agent-level prompt slots for this session.
350    /// Users can customize role, guidelines, response style, and extra instructions
351    /// without losing the core agentic capabilities.
352    pub prompt_slots: Option<SystemPromptSlots>,
353    /// Optional external hook executor.
354    ///
355    /// When set, it replaces the built-in `HookEngine` for this session.
356    pub hook_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
357}
358
359// ============================================================================
360// AgentSession
361// ============================================================================
362
363/// Workspace-bound session. All LLM and tool operations happen here.
364///
365/// History is automatically accumulated after each `send()` call and after
366/// `stream()` completes when no custom history is supplied.
367/// Use `history()` to retrieve the current conversation log.
368///
369/// Conversation operations are single-flight, including slash commands and
370/// checkpoint resume. An overlapping call returns
371/// [`CodeError::SessionBusy`](crate::error::CodeError::SessionBusy) immediately.
372/// A streaming operation remains active until its returned handle completes.
373pub struct AgentSession {
374    llm_client: Arc<dyn LlmClient>,
375    /// Provider-reported generation capacity shared by every loop and
376    /// host-direct tool call created for this session.
377    model_generation_admission: crate::llm::ModelGenerationAdmission,
378    tool_executor: Arc<ToolExecutor>,
379    tool_context: ToolContext,
380    config: AgentConfig,
381    workspace: PathBuf,
382    /// Unique session identifier.
383    session_id: String,
384    /// Internal conversation history, auto-updated after each `send()` and default-history `stream()`.
385    history: Arc<RwLock<Vec<Message>>>,
386    /// Fail-fast single-flight admission for transcript-affecting operations.
387    run_admission: Arc<run_admission::RunAdmission>,
388    /// Optional lane queue for priority-based tool execution.
389    command_queue: Option<Arc<crate::session_lane_queue::SessionLaneQueue>>,
390    /// Long-term memory handle.
391    ///
392    /// Built sessions resolve a default memory store. This remains optional for
393    /// compatibility with lower-level/manual construction paths.
394    memory: Option<Arc<crate::memory::AgentMemory>>,
395    /// Optional session store for persistence.
396    session_store: Option<Arc<dyn crate::store::SessionStore>>,
397    /// Runtime-owned fields used to build lossless persistence generations.
398    persistence_state: Arc<RwLock<session_persistence::SessionPersistenceState>>,
399    /// Auto-save after each completed `send()` or default-history `stream()`.
400    auto_save: bool,
401    /// Hook engine for lifecycle event interception.
402    hook_engine: Arc<crate::hooks::HookEngine>,
403    /// Optional external hook executor. When set, replaces `hook_engine` as the
404    /// executor passed to each `AgentLoop`.
405    hook_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
406    /// Deferred init warning: emitted as PersistenceFailed on first send() if set.
407    init_warning: Option<String>,
408    /// Slash command registry for `/command` dispatch.
409    /// Uses interior mutability so commands can be registered on a shared `Arc<AgentSession>`.
410    command_registry: std::sync::Mutex<CommandRegistry>,
411    /// Model identifier for display (e.g., "anthropic/claude-sonnet-4-20250514").
412    model_name: String,
413    /// Session-private MCP manager used by live add/remove operations.
414    mcp_manager: Arc<crate::mcp::manager::McpManager>,
415    /// Read-only MCP sources inherited from the parent agent and session options.
416    inherited_mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
417    /// Ordered MCP capability sources inherited by delegated child runs.
418    mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
419    /// Shared agent registry — populated at session creation; extended via register_agent_dir().
420    agent_registry: Arc<crate::subagent::AgentRegistry>,
421    /// Cancellation token for the current operation (send/stream).
422    /// Stored so that cancel() can abort ongoing LLM calls.
423    cancel_token: Arc<tokio::sync::Mutex<Option<tokio_util::sync::CancellationToken>>>,
424    /// ID of the run currently attached to the active cancellation token.
425    current_run_id: Arc<tokio::sync::Mutex<Option<String>>>,
426    /// In-memory run snapshots and event replay buffer for this session.
427    run_store: Arc<crate::run::InMemoryRunStore>,
428    /// Materialized view of delegated subagent task lifecycle, populated from runtime events.
429    subagent_tasks: Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>,
430    /// Currently executing tools observed from runtime events.
431    active_tools: Arc<tokio::sync::RwLock<HashMap<String, ActiveToolState>>>,
432    /// Compact execution traces for this session.
433    trace_sink: crate::trace::InMemoryTraceSink,
434    /// Structured completion evidence collected from agent and explicit verification runs.
435    verification_reports: Arc<RwLock<Vec<crate::verification::VerificationReport>>>,
436    /// Set once `close()` has been called. Subsequent send/stream calls
437    /// fast-fail with [`crate::error::CodeError::SessionClosed`].
438    closed: Arc<std::sync::atomic::AtomicBool>,
439    /// Session-level parent cancellation token.
440    ///
441    /// Every in-flight run (blocking send, stream, delegated subagent task)
442    /// derives its per-operation token from this one via `child_token()`,
443    /// so `session_cancel.cancel()` cascades to all of them. `close()` fires
444    /// this token first, after which any new `child_token()` returns an
445    /// already-cancelled token (defending against close/spawn races).
446    pub(crate) session_cancel: tokio_util::sync::CancellationToken,
447    /// Shared `Arc`-handle used by both [`AgentSession::close`] and the
448    /// parent [`Agent`]'s registry. The handle bundles every field needed
449    /// to perform the close sequence so the two entry points cannot drift.
450    close_handle: Arc<SessionCloseHandle>,
451    /// Runtime-mutable override for the budget guard. When set, takes
452    /// precedence over `config.budget_guard` on the next agent-loop
453    /// build. Lets SDK callers (Node especially) install a host-side
454    /// guard after `session()` has returned without ever putting a
455    /// JS callable into `SessionOptions`.
456    runtime_budget_guard: std::sync::Mutex<Option<Arc<dyn crate::budget::BudgetGuard>>>,
457    /// Multi-tenant label. Framework only carries the string; semantics
458    /// belong to the host.
459    pub(crate) tenant_id: Option<String>,
460    /// Principal that triggered the session (user / service / etc.).
461    pub(crate) principal: Option<String>,
462    /// Logical identifier of the agent template the session was
463    /// instantiated from.
464    pub(crate) agent_template_id: Option<String>,
465    /// Distributed-trace correlation id propagated to hooks / traces.
466    pub(crate) correlation_id: Option<String>,
467}
468
469// ============================================================================
470// Tests
471// ============================================================================
472
473#[cfg(test)]
474mod replacement_tests;
475#[cfg(test)]
476mod tests;