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("max_parallel_tasks", &self.max_parallel_tasks)
56            .field("auto_delegation", &self.auto_delegation)
57            .field("auto_parallel_delegation", &self.auto_parallel_delegation)
58            .field("prompt_slots", &self.prompt_slots.is_some())
59            .finish()
60    }
61}
62
63impl SessionOptions {
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    pub fn with_model(mut self, model: impl Into<String>) -> Self {
69        self.model = Some(model.into());
70        self
71    }
72
73    pub fn with_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
74        self.agent_dirs.push(dir.into());
75        self
76    }
77
78    /// Register a cattle-style worker with this session's task delegation registry.
79    pub fn with_worker_agent(mut self, spec: WorkerAgentSpec) -> Self {
80        self.worker_agents.push(spec);
81        self
82    }
83
84    /// Register multiple cattle-style workers with this session.
85    pub fn with_worker_agents<I>(mut self, specs: I) -> Self
86    where
87        I: IntoIterator<Item = WorkerAgentSpec>,
88    {
89        self.worker_agents.extend(specs);
90        self
91    }
92
93    pub fn with_queue_config(mut self, config: SessionQueueConfig) -> Self {
94        self.queue_config = Some(config);
95        self
96    }
97
98    /// Enable default security provider with taint tracking and output sanitization
99    pub fn with_default_security(mut self) -> Self {
100        self.security_provider = Some(Arc::new(crate::security::DefaultSecurityProvider::new()));
101        self
102    }
103
104    /// Set a custom security provider
105    pub fn with_security_provider(
106        mut self,
107        provider: Arc<dyn crate::security::SecurityProvider>,
108    ) -> Self {
109        self.security_provider = Some(provider);
110        self
111    }
112
113    /// Add a file system context provider for simple RAG
114    pub fn with_fs_context(mut self, root_path: impl Into<PathBuf>) -> Self {
115        let config = crate::context::FileSystemContextConfig::new(root_path);
116        self.context_providers
117            .push(Arc::new(crate::context::FileSystemContextProvider::new(
118                config,
119            )));
120        self
121    }
122
123    /// Add a custom context provider
124    pub fn with_context_provider(
125        mut self,
126        provider: Arc<dyn crate::context::ContextProvider>,
127    ) -> Self {
128        self.context_providers.push(provider);
129        self
130    }
131
132    /// Set a confirmation manager for HITL
133    pub fn with_confirmation_manager(
134        mut self,
135        manager: Arc<dyn crate::hitl::ConfirmationProvider>,
136    ) -> Self {
137        self.confirmation_manager = Some(manager);
138        self
139    }
140
141    /// Set a confirmation policy for HITL
142    ///
143    /// The policy will be used to create a ConfirmationManager when the session is built.
144    /// This is the preferred way to configure HITL from the Node SDK.
145    pub fn with_confirmation_policy(mut self, policy: crate::hitl::ConfirmationPolicy) -> Self {
146        self.confirmation_policy = Some(policy);
147        self
148    }
149
150    /// Set a serializable permission policy for tool execution.
151    pub fn with_permission_policy(mut self, policy: crate::permissions::PermissionPolicy) -> Self {
152        self.permission_checker = Some(Arc::new(policy.clone()));
153        self.permission_policy = Some(policy);
154        self
155    }
156
157    /// Set a permission checker
158    pub fn with_permission_checker(
159        mut self,
160        checker: Arc<dyn crate::permissions::PermissionChecker>,
161    ) -> Self {
162        self.permission_checker = Some(checker);
163        self
164    }
165
166    /// Set planning mode
167    pub fn with_planning_mode(mut self, mode: PlanningMode) -> Self {
168        self.planning_mode = mode;
169        self
170    }
171
172    /// Enable planning (shortcut for `with_planning_mode(PlanningMode::Enabled)`)
173    pub fn with_planning(mut self, enabled: bool) -> Self {
174        self.planning_mode = if enabled {
175            PlanningMode::Enabled
176        } else {
177            PlanningMode::Disabled
178        };
179        self
180    }
181
182    /// Enable goal tracking
183    pub fn with_goal_tracking(mut self, enabled: bool) -> Self {
184        self.goal_tracking = enabled;
185        self
186    }
187
188    /// Add a skill registry with built-in skills
189    pub fn with_builtin_skills(mut self) -> Self {
190        self.skill_registry = Some(Arc::new(crate::skills::SkillRegistry::with_builtins()));
191        self
192    }
193
194    /// Add a custom skill registry
195    pub fn with_skill_registry(mut self, registry: Arc<crate::skills::SkillRegistry>) -> Self {
196        self.skill_registry = Some(registry);
197        self
198    }
199
200    /// Add skill directories to scan for skill files (*.md).
201    /// Merged with any global `skill_dirs` from [`CodeConfig`] at session build time.
202    pub fn with_skill_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
203        self.skill_dirs.extend(dirs.into_iter().map(Into::into));
204        self
205    }
206
207    /// Load skills from a directory (eager — scans immediately into a registry).
208    pub fn with_skills_from_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
209        let registry = self
210            .skill_registry
211            .unwrap_or_else(|| Arc::new(crate::skills::SkillRegistry::new()));
212        if let Err(e) = registry.load_from_dir(&dir) {
213            tracing::warn!(
214                dir = %dir.as_ref().display(),
215                error = %e,
216                "Failed to load skills from directory — continuing without them"
217            );
218        }
219        self.skill_registry = Some(registry);
220        self
221    }
222
223    /// Set a custom memory store
224    pub fn with_memory(mut self, store: Arc<dyn MemoryStore>) -> Self {
225        self.memory_store = Some(store);
226        self
227    }
228
229    /// Use a file-based memory store at the given directory.
230    ///
231    /// The store is created lazily when the session is built (requires async).
232    /// This stores the directory path; `FileMemoryStore::new()` is called during
233    /// session construction.
234    pub fn with_file_memory(mut self, dir: impl Into<PathBuf>) -> Self {
235        self.file_memory_dir = Some(dir.into());
236        self
237    }
238
239    /// Set a session store for persistence
240    pub fn with_session_store(mut self, store: Arc<dyn crate::store::SessionStore>) -> Self {
241        self.session_store = Some(store);
242        self
243    }
244
245    /// Use a file-based session store at the given directory
246    pub fn with_file_session_store(mut self, dir: impl Into<PathBuf>) -> Self {
247        let dir = dir.into();
248        match tokio::runtime::Handle::try_current() {
249            Ok(handle) => {
250                match tokio::task::block_in_place(|| {
251                    handle.block_on(crate::store::FileSessionStore::new(dir))
252                }) {
253                    Ok(store) => {
254                        self.session_store =
255                            Some(Arc::new(store) as Arc<dyn crate::store::SessionStore>);
256                    }
257                    Err(e) => {
258                        tracing::warn!("Failed to create file session store: {}", e);
259                    }
260                }
261            }
262            Err(_) => {
263                tracing::warn!(
264                    "No async runtime available for file session store — persistence disabled"
265                );
266            }
267        }
268        self
269    }
270
271    /// Set an explicit session ID (auto-generated UUID if not set)
272    pub fn with_session_id(mut self, id: impl Into<String>) -> Self {
273        self.session_id = Some(id.into());
274        self
275    }
276
277    /// Tag the session with a host-defined tenant id. Opaque to the
278    /// framework — propagated to `SessionData`, hooks, and traces.
279    pub fn with_tenant_id(mut self, tenant: impl Into<String>) -> Self {
280        self.tenant_id = Some(tenant.into());
281        self
282    }
283
284    /// Tag the session with the id of the principal (user / service
285    /// account / etc.) that triggered it.
286    pub fn with_principal(mut self, principal: impl Into<String>) -> Self {
287        self.principal = Some(principal.into());
288        self
289    }
290
291    /// Tag the session with the id of the agent template / definition it
292    /// was instantiated from.
293    pub fn with_agent_template_id(mut self, template_id: impl Into<String>) -> Self {
294        self.agent_template_id = Some(template_id.into());
295        self
296    }
297
298    /// Attach a distributed-trace correlation id so this session's events
299    /// can be joined with upstream/downstream work.
300    pub fn with_correlation_id(mut self, corr: impl Into<String>) -> Self {
301        self.correlation_id = Some(corr.into());
302        self
303    }
304
305    /// Install a host-supplied [`BudgetGuard`](crate::budget::BudgetGuard).
306    ///
307    /// The guard is consulted before every LLM call (and after, for
308    /// usage accounting). When unset, no budget enforcement happens.
309    pub fn with_budget_guard(mut self, guard: Arc<dyn crate::budget::BudgetGuard>) -> Self {
310        self.budget_guard = Some(guard);
311        self
312    }
313
314    /// Install a host-provided [`HostEnv`](crate::host_env::HostEnv) for
315    /// deterministic ID generation and time. Replaces the framework
316    /// default of `uuid::Uuid::new_v4()` + wall clock — used by
317    /// 书安OS replay infrastructure to recreate a run bit-identical on
318    /// another node.
319    pub fn with_host_env(mut self, env: Arc<crate::host_env::HostEnv>) -> Self {
320        self.host_env = Some(env);
321        self
322    }
323
324    /// Install FIFO retention caps for the session's in-memory stores.
325    ///
326    /// Without these caps the in-memory run store, trace sink, and
327    /// subagent task tracker grow unboundedly across long-running
328    /// sessions. Hosts running thousands of long-lived sessions per
329    /// node should set sensible caps (e.g. retain the last 100 runs,
330    /// 5000 events per run, 10000 trace events, 1000 terminal subagent
331    /// tasks). When unset, the framework keeps every record — the
332    /// pre-existing behaviour.
333    pub fn with_retention_limits(
334        mut self,
335        limits: crate::retention::SessionRetentionLimits,
336    ) -> Self {
337        self.retention_limits = Some(limits);
338        self
339    }
340
341    /// Enable auto-save after each `send()` call
342    pub fn with_auto_save(mut self, enabled: bool) -> Self {
343        self.auto_save = enabled;
344        self
345    }
346
347    /// Set artifact retention limits for this session.
348    pub fn with_artifact_store_limits(mut self, limits: crate::tools::ArtifactStoreLimits) -> Self {
349        self.artifact_store_limits = Some(limits);
350        self
351    }
352
353    /// Set the maximum number of consecutive malformed-tool-args errors before
354    /// the agent loop bails.
355    ///
356    /// Default: 2 (the LLM gets two chances to self-correct before the session
357    /// is aborted).
358    pub fn with_parse_retries(mut self, max: u32) -> Self {
359        self.max_parse_retries = Some(max);
360        self
361    }
362
363    /// Set a per-tool execution timeout.
364    ///
365    /// When set, each tool execution is wrapped in `tokio::time::timeout`.
366    /// A timeout produces an error message that is fed back to the LLM
367    /// (the session continues).
368    pub fn with_tool_timeout(mut self, timeout_ms: u64) -> Self {
369        self.tool_timeout_ms = Some(timeout_ms);
370        self
371    }
372
373    /// Set the circuit-breaker threshold.
374    ///
375    /// In non-streaming mode, the agent retries transient LLM API failures up
376    /// to this many times (with exponential backoff) before aborting.
377    /// Default: 3 attempts.
378    pub fn with_circuit_breaker(mut self, threshold: u32) -> Self {
379        self.circuit_breaker_threshold = Some(threshold);
380        self
381    }
382
383    /// Enable all resilience defaults with sensible values:
384    ///
385    /// - `max_parse_retries = 2`
386    /// - `tool_timeout_ms = 120_000` (2 minutes)
387    /// - `circuit_breaker_threshold = 3`
388    pub fn with_resilience_defaults(self) -> Self {
389        self.with_parse_retries(2)
390            .with_tool_timeout(120_000)
391            .with_circuit_breaker(3)
392    }
393
394    /// Provide a concrete [`BashSandbox`] implementation for this session.
395    ///
396    /// When set, `bash` tool commands are routed through the given sandbox
397    /// instead of `std::process::Command`. The host application is responsible
398    /// for constructing and lifecycle-managing the sandbox.
399    ///
400    /// [`BashSandbox`]: crate::sandbox::BashSandbox
401    pub fn with_sandbox_handle(mut self, handle: Arc<dyn crate::sandbox::BashSandbox>) -> Self {
402        self.sandbox_handle = Some(handle);
403        self
404    }
405
406    /// Provide a workspace backend for this session.
407    ///
408    /// Built-in tools keep their stable names and schemas, while their backing
409    /// implementation can target a DFS, browser workspace, remote runner, or
410    /// any other host-provided backend.
411    pub fn with_workspace_backend(
412        mut self,
413        services: Arc<crate::workspace::WorkspaceServices>,
414    ) -> Self {
415        self.workspace_services = Some(services);
416        self
417    }
418
419    /// Enable auto-compaction when context usage exceeds threshold.
420    ///
421    /// When enabled, the agent loop automatically prunes large tool outputs
422    /// and summarizes old messages when context usage exceeds the threshold.
423    pub fn with_auto_compact(mut self, enabled: bool) -> Self {
424        self.auto_compact = enabled;
425        self
426    }
427
428    /// Set the auto-compact threshold (0.0 - 1.0). Default: 0.80 (80%).
429    pub fn with_auto_compact_threshold(mut self, threshold: f32) -> Self {
430        self.auto_compact_threshold = Some(threshold.clamp(0.0, 1.0));
431        self
432    }
433
434    /// Enable or disable continuation injection (default: enabled).
435    ///
436    /// When enabled, the loop injects a continuation message when the LLM stops
437    /// calling tools before the task appears complete, nudging it to keep working.
438    pub fn with_continuation(mut self, enabled: bool) -> Self {
439        self.continuation_enabled = Some(enabled);
440        self
441    }
442
443    /// Set the maximum number of continuation injections per execution (default: 3).
444    pub fn with_max_continuation_turns(mut self, turns: u32) -> Self {
445        self.max_continuation_turns = Some(turns);
446        self
447    }
448
449    /// Set an MCP manager to connect to external MCP servers.
450    ///
451    /// All tools from connected servers will be available during execution
452    /// with names like `mcp__<server>__<tool>`.
453    pub fn with_mcp(mut self, manager: Arc<crate::mcp::manager::McpManager>) -> Self {
454        self.mcp_manager = Some(manager);
455        self
456    }
457
458    pub fn with_temperature(mut self, temperature: f32) -> Self {
459        self.temperature = Some(temperature);
460        self
461    }
462
463    pub fn with_thinking_budget(mut self, budget: usize) -> Self {
464        self.thinking_budget = Some(budget);
465        self
466    }
467
468    /// Override the maximum number of tool execution rounds for this session.
469    ///
470    /// Useful when binding a markdown-defined subagent to a session —
471    /// pass the agent definition's `max_steps` value here to enforce its step budget.
472    pub fn with_max_tool_rounds(mut self, rounds: usize) -> Self {
473        self.max_tool_rounds = Some(rounds);
474        self
475    }
476
477    /// Override the maximum number of sibling parallel branches for this session.
478    pub fn with_max_parallel_tasks(mut self, tasks: usize) -> Self {
479        self.max_parallel_tasks = Some(tasks.max(1));
480        self
481    }
482
483    /// Override automatic subagent delegation for this session.
484    pub fn with_auto_delegation(mut self, config: crate::config::AutoDelegationConfig) -> Self {
485        self.auto_delegation = Some(config);
486        self
487    }
488
489    /// Enable or disable automatic subagent delegation for this session.
490    pub fn with_auto_delegation_enabled(mut self, enabled: bool) -> Self {
491        let mut config = self.auto_delegation.take().unwrap_or_default();
492        config.enabled = enabled;
493        self.auto_delegation = Some(config);
494        self
495    }
496
497    /// Globally enable or disable automatic parallel child-agent fan-out.
498    ///
499    /// Manual `parallel_task` calls remain available when this is false.
500    pub fn with_auto_parallel_delegation(mut self, enabled: bool) -> Self {
501        if let Some(config) = &mut self.auto_delegation {
502            config.auto_parallel = enabled;
503        }
504        self.auto_parallel_delegation = Some(enabled);
505        self
506    }
507
508    /// Set slot-based system prompt customization for this session.
509    ///
510    /// Allows customizing role, guidelines, response style, and extra instructions
511    /// without overriding the core agentic capabilities.
512    pub fn with_prompt_slots(mut self, slots: SystemPromptSlots) -> Self {
513        self.prompt_slots = Some(slots);
514        self
515    }
516
517    /// Replace the built-in hook engine with an external hook executor.
518    ///
519    /// Use this to attach an AHP harness server (or any custom `HookExecutor`)
520    /// to the session. All lifecycle events will be forwarded to the executor
521    /// instead of the in-process `HookEngine`.
522    pub fn with_hook_executor(mut self, executor: Arc<dyn crate::hooks::HookExecutor>) -> Self {
523        self.hook_executor = Some(executor);
524        self
525    }
526}