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