Skip to main content

a3s_code_core/agent_api/
session_options.rs

1//! Session option builder interface.
2//!
3//! `SessionOptions` is the host-facing capability configuration for a session.
4//! Keeping the builder implementation here lets `agent_api.rs` keep the type
5//! shape visible while moving option construction behavior behind this module.
6
7use super::SessionOptions;
8use crate::prompts::{PlanningMode, SystemPromptSlots};
9use crate::queue::SessionQueueConfig;
10use crate::subagent::WorkerAgentSpec;
11use a3s_memory::MemoryStore;
12use std::path::PathBuf;
13use std::sync::Arc;
14
15impl std::fmt::Debug for SessionOptions {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        f.debug_struct("SessionOptions")
18            .field("model", &self.model)
19            .field("agent_dirs", &self.agent_dirs)
20            .field("worker_agents", &self.worker_agents.len())
21            .field("skill_dirs", &self.skill_dirs)
22            .field("queue_config", &self.queue_config)
23            .field("security_provider", &self.security_provider.is_some())
24            .field("context_providers", &self.context_providers.len())
25            .field("confirmation_manager", &self.confirmation_manager.is_some())
26            .field("permission_checker", &self.permission_checker.is_some())
27            .field("permission_policy", &self.permission_policy.is_some())
28            .field("planning_mode", &self.planning_mode)
29            .field("goal_tracking", &self.goal_tracking)
30            .field(
31                "skill_registry",
32                &self
33                    .skill_registry
34                    .as_ref()
35                    .map(|r| format!("{} skills", r.len())),
36            )
37            .field("memory_store", &self.memory_store.is_some())
38            .field("session_store", &self.session_store.is_some())
39            .field("session_id", &self.session_id)
40            .field("auto_save", &self.auto_save)
41            .field("artifact_store_limits", &self.artifact_store_limits)
42            .field("max_parse_retries", &self.max_parse_retries)
43            .field("tool_timeout_ms", &self.tool_timeout_ms)
44            .field("circuit_breaker_threshold", &self.circuit_breaker_threshold)
45            .field("sandbox_handle", &self.sandbox_handle.is_some())
46            .field("workspace_services", &self.workspace_services.is_some())
47            .field("auto_compact", &self.auto_compact)
48            .field("auto_compact_threshold", &self.auto_compact_threshold)
49            .field("continuation_enabled", &self.continuation_enabled)
50            .field("max_continuation_turns", &self.max_continuation_turns)
51            .field("mcp_manager", &self.mcp_manager.is_some())
52            .field("temperature", &self.temperature)
53            .field("thinking_budget", &self.thinking_budget)
54            .field("max_tool_rounds", &self.max_tool_rounds)
55            .field("prompt_slots", &self.prompt_slots.is_some())
56            .finish()
57    }
58}
59
60impl SessionOptions {
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    pub fn with_model(mut self, model: impl Into<String>) -> Self {
66        self.model = Some(model.into());
67        self
68    }
69
70    pub fn with_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
71        self.agent_dirs.push(dir.into());
72        self
73    }
74
75    /// Register a cattle-style worker with this session's task delegation registry.
76    pub fn with_worker_agent(mut self, spec: WorkerAgentSpec) -> Self {
77        self.worker_agents.push(spec);
78        self
79    }
80
81    /// Register multiple cattle-style workers with this session.
82    pub fn with_worker_agents<I>(mut self, specs: I) -> Self
83    where
84        I: IntoIterator<Item = WorkerAgentSpec>,
85    {
86        self.worker_agents.extend(specs);
87        self
88    }
89
90    pub fn with_queue_config(mut self, config: SessionQueueConfig) -> Self {
91        self.queue_config = Some(config);
92        self
93    }
94
95    /// Enable default security provider with taint tracking and output sanitization
96    pub fn with_default_security(mut self) -> Self {
97        self.security_provider = Some(Arc::new(crate::security::DefaultSecurityProvider::new()));
98        self
99    }
100
101    /// Set a custom security provider
102    pub fn with_security_provider(
103        mut self,
104        provider: Arc<dyn crate::security::SecurityProvider>,
105    ) -> Self {
106        self.security_provider = Some(provider);
107        self
108    }
109
110    /// Add a file system context provider for simple RAG
111    pub fn with_fs_context(mut self, root_path: impl Into<PathBuf>) -> Self {
112        let config = crate::context::FileSystemContextConfig::new(root_path);
113        self.context_providers
114            .push(Arc::new(crate::context::FileSystemContextProvider::new(
115                config,
116            )));
117        self
118    }
119
120    /// Add a custom context provider
121    pub fn with_context_provider(
122        mut self,
123        provider: Arc<dyn crate::context::ContextProvider>,
124    ) -> Self {
125        self.context_providers.push(provider);
126        self
127    }
128
129    /// Set a confirmation manager for HITL
130    pub fn with_confirmation_manager(
131        mut self,
132        manager: Arc<dyn crate::hitl::ConfirmationProvider>,
133    ) -> Self {
134        self.confirmation_manager = Some(manager);
135        self
136    }
137
138    /// Set a confirmation policy for HITL
139    ///
140    /// The policy will be used to create a ConfirmationManager when the session is built.
141    /// This is the preferred way to configure HITL from the Node SDK.
142    pub fn with_confirmation_policy(mut self, policy: crate::hitl::ConfirmationPolicy) -> Self {
143        self.confirmation_policy = Some(policy);
144        self
145    }
146
147    /// Set a serializable permission policy for tool execution.
148    pub fn with_permission_policy(mut self, policy: crate::permissions::PermissionPolicy) -> Self {
149        self.permission_checker = Some(Arc::new(policy.clone()));
150        self.permission_policy = Some(policy);
151        self
152    }
153
154    /// Set a permission checker
155    pub fn with_permission_checker(
156        mut self,
157        checker: Arc<dyn crate::permissions::PermissionChecker>,
158    ) -> Self {
159        self.permission_checker = Some(checker);
160        self
161    }
162
163    /// Set planning mode
164    pub fn with_planning_mode(mut self, mode: PlanningMode) -> Self {
165        self.planning_mode = mode;
166        self
167    }
168
169    /// Enable planning (shortcut for `with_planning_mode(PlanningMode::Enabled)`)
170    pub fn with_planning(mut self, enabled: bool) -> Self {
171        self.planning_mode = if enabled {
172            PlanningMode::Enabled
173        } else {
174            PlanningMode::Disabled
175        };
176        self
177    }
178
179    /// Enable goal tracking
180    pub fn with_goal_tracking(mut self, enabled: bool) -> Self {
181        self.goal_tracking = enabled;
182        self
183    }
184
185    /// Add a skill registry with built-in skills
186    pub fn with_builtin_skills(mut self) -> Self {
187        self.skill_registry = Some(Arc::new(crate::skills::SkillRegistry::with_builtins()));
188        self
189    }
190
191    /// Add a custom skill registry
192    pub fn with_skill_registry(mut self, registry: Arc<crate::skills::SkillRegistry>) -> Self {
193        self.skill_registry = Some(registry);
194        self
195    }
196
197    /// Add skill directories to scan for skill files (*.md).
198    /// Merged with any global `skill_dirs` from [`CodeConfig`] at session build time.
199    pub fn with_skill_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
200        self.skill_dirs.extend(dirs.into_iter().map(Into::into));
201        self
202    }
203
204    /// Load skills from a directory (eager — scans immediately into a registry).
205    pub fn with_skills_from_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
206        let registry = self
207            .skill_registry
208            .unwrap_or_else(|| Arc::new(crate::skills::SkillRegistry::new()));
209        if let Err(e) = registry.load_from_dir(&dir) {
210            tracing::warn!(
211                dir = %dir.as_ref().display(),
212                error = %e,
213                "Failed to load skills from directory — continuing without them"
214            );
215        }
216        self.skill_registry = Some(registry);
217        self
218    }
219
220    /// Set a custom memory store
221    pub fn with_memory(mut self, store: Arc<dyn MemoryStore>) -> Self {
222        self.memory_store = Some(store);
223        self
224    }
225
226    /// Use a file-based memory store at the given directory.
227    ///
228    /// The store is created lazily when the session is built (requires async).
229    /// This stores the directory path; `FileMemoryStore::new()` is called during
230    /// session construction.
231    pub fn with_file_memory(mut self, dir: impl Into<PathBuf>) -> Self {
232        self.file_memory_dir = Some(dir.into());
233        self
234    }
235
236    /// Set a session store for persistence
237    pub fn with_session_store(mut self, store: Arc<dyn crate::store::SessionStore>) -> Self {
238        self.session_store = Some(store);
239        self
240    }
241
242    /// Use a file-based session store at the given directory
243    pub fn with_file_session_store(mut self, dir: impl Into<PathBuf>) -> Self {
244        let dir = dir.into();
245        match tokio::runtime::Handle::try_current() {
246            Ok(handle) => {
247                match tokio::task::block_in_place(|| {
248                    handle.block_on(crate::store::FileSessionStore::new(dir))
249                }) {
250                    Ok(store) => {
251                        self.session_store =
252                            Some(Arc::new(store) as Arc<dyn crate::store::SessionStore>);
253                    }
254                    Err(e) => {
255                        tracing::warn!("Failed to create file session store: {}", e);
256                    }
257                }
258            }
259            Err(_) => {
260                tracing::warn!(
261                    "No async runtime available for file session store — persistence disabled"
262                );
263            }
264        }
265        self
266    }
267
268    /// Set an explicit session ID (auto-generated UUID if not set)
269    pub fn with_session_id(mut self, id: impl Into<String>) -> Self {
270        self.session_id = Some(id.into());
271        self
272    }
273
274    /// Enable auto-save after each `send()` call
275    pub fn with_auto_save(mut self, enabled: bool) -> Self {
276        self.auto_save = enabled;
277        self
278    }
279
280    /// Set artifact retention limits for this session.
281    pub fn with_artifact_store_limits(mut self, limits: crate::tools::ArtifactStoreLimits) -> Self {
282        self.artifact_store_limits = Some(limits);
283        self
284    }
285
286    /// Set the maximum number of consecutive malformed-tool-args errors before
287    /// the agent loop bails.
288    ///
289    /// Default: 2 (the LLM gets two chances to self-correct before the session
290    /// is aborted).
291    pub fn with_parse_retries(mut self, max: u32) -> Self {
292        self.max_parse_retries = Some(max);
293        self
294    }
295
296    /// Set a per-tool execution timeout.
297    ///
298    /// When set, each tool execution is wrapped in `tokio::time::timeout`.
299    /// A timeout produces an error message that is fed back to the LLM
300    /// (the session continues).
301    pub fn with_tool_timeout(mut self, timeout_ms: u64) -> Self {
302        self.tool_timeout_ms = Some(timeout_ms);
303        self
304    }
305
306    /// Set the circuit-breaker threshold.
307    ///
308    /// In non-streaming mode, the agent retries transient LLM API failures up
309    /// to this many times (with exponential backoff) before aborting.
310    /// Default: 3 attempts.
311    pub fn with_circuit_breaker(mut self, threshold: u32) -> Self {
312        self.circuit_breaker_threshold = Some(threshold);
313        self
314    }
315
316    /// Enable all resilience defaults with sensible values:
317    ///
318    /// - `max_parse_retries = 2`
319    /// - `tool_timeout_ms = 120_000` (2 minutes)
320    /// - `circuit_breaker_threshold = 3`
321    pub fn with_resilience_defaults(self) -> Self {
322        self.with_parse_retries(2)
323            .with_tool_timeout(120_000)
324            .with_circuit_breaker(3)
325    }
326
327    /// Provide a concrete [`BashSandbox`] implementation for this session.
328    ///
329    /// When set, `bash` tool commands are routed through the given sandbox
330    /// instead of `std::process::Command`. The host application is responsible
331    /// for constructing and lifecycle-managing the sandbox.
332    ///
333    /// [`BashSandbox`]: crate::sandbox::BashSandbox
334    pub fn with_sandbox_handle(mut self, handle: Arc<dyn crate::sandbox::BashSandbox>) -> Self {
335        self.sandbox_handle = Some(handle);
336        self
337    }
338
339    /// Provide a workspace backend for this session.
340    ///
341    /// Built-in tools keep their stable names and schemas, while their backing
342    /// implementation can target a DFS, browser workspace, remote runner, or
343    /// any other host-provided backend.
344    pub fn with_workspace_backend(
345        mut self,
346        services: Arc<crate::workspace::WorkspaceServices>,
347    ) -> Self {
348        self.workspace_services = Some(services);
349        self
350    }
351
352    /// Enable auto-compaction when context usage exceeds threshold.
353    ///
354    /// When enabled, the agent loop automatically prunes large tool outputs
355    /// and summarizes old messages when context usage exceeds the threshold.
356    pub fn with_auto_compact(mut self, enabled: bool) -> Self {
357        self.auto_compact = enabled;
358        self
359    }
360
361    /// Set the auto-compact threshold (0.0 - 1.0). Default: 0.80 (80%).
362    pub fn with_auto_compact_threshold(mut self, threshold: f32) -> Self {
363        self.auto_compact_threshold = Some(threshold.clamp(0.0, 1.0));
364        self
365    }
366
367    /// Enable or disable continuation injection (default: enabled).
368    ///
369    /// When enabled, the loop injects a continuation message when the LLM stops
370    /// calling tools before the task appears complete, nudging it to keep working.
371    pub fn with_continuation(mut self, enabled: bool) -> Self {
372        self.continuation_enabled = Some(enabled);
373        self
374    }
375
376    /// Set the maximum number of continuation injections per execution (default: 3).
377    pub fn with_max_continuation_turns(mut self, turns: u32) -> Self {
378        self.max_continuation_turns = Some(turns);
379        self
380    }
381
382    /// Set an MCP manager to connect to external MCP servers.
383    ///
384    /// All tools from connected servers will be available during execution
385    /// with names like `mcp__<server>__<tool>`.
386    pub fn with_mcp(mut self, manager: Arc<crate::mcp::manager::McpManager>) -> Self {
387        self.mcp_manager = Some(manager);
388        self
389    }
390
391    pub fn with_temperature(mut self, temperature: f32) -> Self {
392        self.temperature = Some(temperature);
393        self
394    }
395
396    pub fn with_thinking_budget(mut self, budget: usize) -> Self {
397        self.thinking_budget = Some(budget);
398        self
399    }
400
401    /// Override the maximum number of tool execution rounds for this session.
402    ///
403    /// Useful when binding a markdown-defined subagent to a session —
404    /// pass the agent definition's `max_steps` value here to enforce its step budget.
405    pub fn with_max_tool_rounds(mut self, rounds: usize) -> Self {
406        self.max_tool_rounds = Some(rounds);
407        self
408    }
409
410    /// Set slot-based system prompt customization for this session.
411    ///
412    /// Allows customizing role, guidelines, response style, and extra instructions
413    /// without overriding the core agentic capabilities.
414    pub fn with_prompt_slots(mut self, slots: SystemPromptSlots) -> Self {
415        self.prompt_slots = Some(slots);
416        self
417    }
418
419    /// Replace the built-in hook engine with an external hook executor.
420    ///
421    /// Use this to attach an AHP harness server (or any custom `HookExecutor`)
422    /// to the session. All lifecycle events will be forwarded to the executor
423    /// instead of the in-process `HookEngine`.
424    pub fn with_hook_executor(mut self, executor: Arc<dyn crate::hooks::HookExecutor>) -> Self {
425        self.hook_executor = Some(executor);
426        self
427    }
428}