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("llm_client", &self.llm_client.is_some())
25            .field("context_providers", &self.context_providers.len())
26            .field("confirmation_manager", &self.confirmation_manager.is_some())
27            .field("permission_checker", &self.permission_checker.is_some())
28            .field("permission_policy", &self.permission_policy.is_some())
29            .field("planning_mode", &self.planning_mode)
30            .field("goal_tracking", &self.goal_tracking)
31            .field(
32                "skill_registry",
33                &self
34                    .skill_registry
35                    .as_ref()
36                    .map(|r| format!("{} skills", r.len())),
37            )
38            .field(
39                "enforce_active_skill_tool_restrictions",
40                &self.enforce_active_skill_tool_restrictions,
41            )
42            .field("memory_store", &self.memory_store.is_some())
43            .field("file_memory_dir", &self.file_memory_dir)
44            .field("session_store", &self.session_store.is_some())
45            .field("file_session_store_dir", &self.file_session_store_dir)
46            .field("session_id", &self.session_id)
47            .field("rl_trajectory", &self.rl_trajectory)
48            .field("llm_logprobs", &self.llm_logprobs)
49            .field("llm_top_logprobs", &self.llm_top_logprobs)
50            .field("auto_save", &self.auto_save)
51            .field("artifact_store_limits", &self.artifact_store_limits)
52            .field("max_parse_retries", &self.max_parse_retries)
53            .field("tool_timeout_ms", &self.tool_timeout_ms)
54            .field("llm_api_timeout_ms", &self.llm_api_timeout_ms)
55            .field("circuit_breaker_threshold", &self.circuit_breaker_threshold)
56            .field(
57                "duplicate_tool_call_threshold",
58                &self.duplicate_tool_call_threshold,
59            )
60            .field("sandbox_handle", &self.sandbox_handle.is_some())
61            .field("workspace_services", &self.workspace_services.is_some())
62            .field("auto_compact", &self.auto_compact)
63            .field("auto_compact_threshold", &self.auto_compact_threshold)
64            .field("max_context_tokens", &self.max_context_tokens)
65            .field("continuation_enabled", &self.continuation_enabled)
66            .field("max_continuation_turns", &self.max_continuation_turns)
67            .field("mcp_manager", &self.mcp_manager.is_some())
68            .field("temperature", &self.temperature)
69            .field("thinking_budget", &self.thinking_budget)
70            .field("max_tool_rounds", &self.max_tool_rounds)
71            .field("max_parallel_tasks", &self.max_parallel_tasks)
72            .field("auto_delegation", &self.auto_delegation)
73            .field("manual_delegation_enabled", &self.manual_delegation_enabled)
74            .field("auto_parallel_delegation", &self.auto_parallel_delegation)
75            .field("prompt_slots", &self.prompt_slots.is_some())
76            .finish()
77    }
78}
79
80impl SessionOptions {
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    pub fn with_model(mut self, model: impl Into<String>) -> Self {
86        self.model = Some(model.into());
87        self
88    }
89
90    pub fn with_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
91        self.agent_dirs.push(dir.into());
92        self
93    }
94
95    /// Register a cattle-style worker with this session's task delegation registry.
96    pub fn with_worker_agent(mut self, spec: WorkerAgentSpec) -> Self {
97        self.worker_agents.push(spec);
98        self
99    }
100
101    /// Register multiple cattle-style workers with this session.
102    pub fn with_worker_agents<I>(mut self, specs: I) -> Self
103    where
104        I: IntoIterator<Item = WorkerAgentSpec>,
105    {
106        self.worker_agents.extend(specs);
107        self
108    }
109
110    pub fn with_queue_config(mut self, config: SessionQueueConfig) -> Self {
111        self.queue_config = Some(config);
112        self
113    }
114
115    /// Enable default security provider with taint tracking and output sanitization
116    pub fn with_default_security(mut self) -> Self {
117        self.security_provider = Some(Arc::new(crate::security::DefaultSecurityProvider::new()));
118        self
119    }
120
121    /// Set a custom security provider
122    pub fn with_security_provider(
123        mut self,
124        provider: Arc<dyn crate::security::SecurityProvider>,
125    ) -> Self {
126        self.security_provider = Some(provider);
127        self
128    }
129
130    /// Provide a custom LLM client for this session.
131    ///
132    /// When set, this client is used directly, overriding the `provider/model`
133    /// factory resolution. Use it to plug in a provider the built-in factory
134    /// does not cover, a deterministic record/replay client for tests, or an
135    /// HTTP-layer proxy/audit wrapper. Mirrors [`Self::with_workspace_backend`];
136    /// the `provider/model` config path remains the default when unset.
137    pub fn with_llm_client(mut self, client: Arc<dyn crate::llm::LlmClient>) -> Self {
138        self.llm_client = Some(client);
139        self
140    }
141
142    /// Add a file system context provider for simple RAG
143    pub fn with_fs_context(mut self, root_path: impl Into<PathBuf>) -> Self {
144        let config = crate::context::FileSystemContextConfig::new(root_path);
145        self.context_providers
146            .push(Arc::new(crate::context::FileSystemContextProvider::new(
147                config,
148            )));
149        self
150    }
151
152    /// Add a custom context provider
153    pub fn with_context_provider(
154        mut self,
155        provider: Arc<dyn crate::context::ContextProvider>,
156    ) -> Self {
157        self.context_providers.push(provider);
158        self
159    }
160
161    /// Set a confirmation manager for HITL
162    pub fn with_confirmation_manager(
163        mut self,
164        manager: Arc<dyn crate::hitl::ConfirmationProvider>,
165    ) -> Self {
166        self.confirmation_manager = Some(manager);
167        self
168    }
169
170    /// Set a confirmation policy for HITL
171    ///
172    /// The policy will be used to create a ConfirmationManager when the session is built.
173    /// This is the preferred way to configure HITL from the Node SDK.
174    pub fn with_confirmation_policy(mut self, policy: crate::hitl::ConfirmationPolicy) -> Self {
175        self.confirmation_policy = Some(policy);
176        self
177    }
178
179    /// Set a serializable permission policy for tool execution.
180    pub fn with_permission_policy(mut self, policy: crate::permissions::PermissionPolicy) -> Self {
181        self.permission_checker = Some(Arc::new(policy.clone()));
182        self.permission_policy = Some(policy);
183        self
184    }
185
186    /// Set a permission checker
187    pub fn with_permission_checker(
188        mut self,
189        checker: Arc<dyn crate::permissions::PermissionChecker>,
190    ) -> Self {
191        self.permission_checker = Some(checker);
192        self
193    }
194
195    /// Set planning mode
196    pub fn with_planning_mode(mut self, mode: PlanningMode) -> Self {
197        self.planning_mode = mode;
198        self
199    }
200
201    /// Enable planning (shortcut for `with_planning_mode(PlanningMode::Enabled)`)
202    pub fn with_planning(mut self, enabled: bool) -> Self {
203        self.planning_mode = if enabled {
204            PlanningMode::Enabled
205        } else {
206            PlanningMode::Disabled
207        };
208        self
209    }
210
211    /// Enable goal tracking
212    pub fn with_goal_tracking(mut self, enabled: bool) -> Self {
213        self.goal_tracking = enabled;
214        self
215    }
216
217    /// Add the compatibility built-in skill registry.
218    ///
219    /// A3S Code no longer ships embedded built-in skills, so this currently
220    /// installs an empty registry. Use skill directories, inline skills, or a
221    /// custom skill registry for reusable behavior.
222    pub fn with_builtin_skills(mut self) -> Self {
223        self.skill_registry = Some(Arc::new(crate::skills::SkillRegistry::with_builtins()));
224        self
225    }
226
227    /// Add a custom skill registry
228    pub fn with_skill_registry(mut self, registry: Arc<crate::skills::SkillRegistry>) -> Self {
229        self.skill_registry = Some(registry);
230        self
231    }
232
233    /// Enable or disable legacy global active-skill `allowed-tools` restrictions.
234    ///
235    /// The default is disabled: active skills do not block ordinary session
236    /// tools before the host permission/HITL approval chain runs.
237    pub fn with_active_skill_tool_restrictions(mut self, enabled: bool) -> Self {
238        self.enforce_active_skill_tool_restrictions = Some(enabled);
239        self
240    }
241
242    /// Add skill directories to scan for skill files (*.md).
243    /// Merged with any global `skill_dirs` from
244    /// [`CodeConfig`](crate::config::CodeConfig) at session build time.
245    pub fn with_skill_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
246        self.skill_dirs.extend(dirs.into_iter().map(Into::into));
247        self
248    }
249
250    /// Load skills from a directory (eager — scans immediately into a registry).
251    pub fn with_skills_from_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
252        let registry = self
253            .skill_registry
254            .unwrap_or_else(|| Arc::new(crate::skills::SkillRegistry::new()));
255        if let Err(e) = registry.load_from_dir(&dir) {
256            tracing::warn!(
257                dir = %dir.as_ref().display(),
258                error = %e,
259                "Failed to load skills from directory — continuing without them"
260            );
261        }
262        self.skill_registry = Some(registry);
263        self
264    }
265
266    /// Set a custom memory store override.
267    ///
268    /// Sessions resolve a default memory store when no override is provided.
269    pub fn with_memory(mut self, store: Arc<dyn MemoryStore>) -> Self {
270        self.memory_store = Some(store);
271        self.file_memory_dir = None;
272        self
273    }
274
275    /// Use a file-based memory store at the given directory instead of the default.
276    ///
277    /// The store is created lazily when the session is built (requires async).
278    /// This stores the directory path; `FileMemoryStore::new()` is called during
279    /// session construction.
280    pub fn with_file_memory(mut self, dir: impl Into<PathBuf>) -> Self {
281        self.memory_store = None;
282        self.file_memory_dir = Some(dir.into());
283        self
284    }
285
286    /// Set a session store for persistence
287    pub fn with_session_store(mut self, store: Arc<dyn crate::store::SessionStore>) -> Self {
288        self.session_store = Some(store);
289        self.file_session_store_dir = None;
290        self
291    }
292
293    /// Use a file-based session store at the given directory.
294    ///
295    /// The path is a typed construction specification. No I/O occurs until
296    /// [`SessionBuilder::build`](super::SessionBuilder::build) is awaited.
297    pub fn with_file_session_store(mut self, dir: impl Into<PathBuf>) -> Self {
298        self.session_store = None;
299        self.file_session_store_dir = Some(dir.into());
300        self
301    }
302
303    /// Set an explicit session ID (auto-generated UUID if not set)
304    pub fn with_session_id(mut self, id: impl Into<String>) -> Self {
305        self.session_id = Some(id.into());
306        self
307    }
308
309    /// Tag the session with a host-defined tenant id. Opaque to the
310    /// framework — propagated to `SessionData`, hooks, and traces.
311    pub fn with_tenant_id(mut self, tenant: impl Into<String>) -> Self {
312        self.tenant_id = Some(tenant.into());
313        self
314    }
315
316    /// Tag the session with the id of the principal (user / service
317    /// account / etc.) that triggered it.
318    pub fn with_principal(mut self, principal: impl Into<String>) -> Self {
319        self.principal = Some(principal.into());
320        self
321    }
322
323    /// Tag the session with the id of the agent template / definition it
324    /// was instantiated from.
325    pub fn with_agent_template_id(mut self, template_id: impl Into<String>) -> Self {
326        self.agent_template_id = Some(template_id.into());
327        self
328    }
329
330    /// Attach a distributed-trace correlation id so this session's events
331    /// can be joined with upstream/downstream work.
332    pub fn with_correlation_id(mut self, corr: impl Into<String>) -> Self {
333        self.correlation_id = Some(corr.into());
334        self
335    }
336
337    /// Install a host-supplied [`BudgetGuard`](crate::budget::BudgetGuard).
338    ///
339    /// The guard is consulted before every LLM call (and after, for
340    /// usage accounting). When unset, no budget enforcement happens.
341    pub fn with_budget_guard(mut self, guard: Arc<dyn crate::budget::BudgetGuard>) -> Self {
342        self.budget_guard = Some(guard);
343        self
344    }
345
346    /// Install a host-provided [`HostEnv`](crate::host_env::HostEnv) for
347    /// deterministic ID generation and time. Replaces the framework
348    /// default of `uuid::Uuid::new_v4()` + wall clock — used by
349    /// host replay infrastructure to recreate a run bit-identical on
350    /// another node.
351    pub fn with_host_env(mut self, env: Arc<crate::host_env::HostEnv>) -> Self {
352        self.host_env = Some(env);
353        self
354    }
355
356    /// Install FIFO retention caps for the session's in-memory stores.
357    ///
358    /// Without these caps the in-memory run store, trace sink, and
359    /// subagent task tracker grow unboundedly across long-running
360    /// sessions. Hosts running thousands of long-lived sessions per
361    /// node should set sensible caps (e.g. retain the last 100 runs,
362    /// 5000 events per run, 10000 trace events, 1000 terminal subagent
363    /// tasks). When unset, the framework keeps every record — the
364    /// pre-existing behaviour.
365    pub fn with_retention_limits(
366        mut self,
367        limits: crate::retention::SessionRetentionLimits,
368    ) -> Self {
369        self.retention_limits = Some(limits);
370        self
371    }
372
373    /// Enable structured JSONL trajectory capture for this session.
374    ///
375    /// This is the preferred programmatic path for RL training and deployed
376    /// service data collection. Environment-only deployments can instead set
377    /// `A3S_CODE_TRAJECTORY_PATH`.
378    pub fn with_rl_trajectory(mut self, config: crate::rl_trajectory::RlTrajectoryConfig) -> Self {
379        self.rl_trajectory = Some(config);
380        self
381    }
382
383    /// Request token-level log probabilities from compatible LLM providers.
384    pub fn with_llm_logprobs(mut self, enabled: bool) -> Self {
385        self.llm_logprobs = Some(enabled);
386        self
387    }
388
389    /// Request up to `top_logprobs` alternative logprobs per generated token.
390    pub fn with_llm_top_logprobs(mut self, top_logprobs: usize) -> Self {
391        self.llm_logprobs = Some(true);
392        self.llm_top_logprobs = Some(top_logprobs);
393        self
394    }
395
396    /// Enable auto-save after each `send()` call
397    pub fn with_auto_save(mut self, enabled: bool) -> Self {
398        self.auto_save = enabled;
399        self
400    }
401
402    /// Set artifact retention limits for this session.
403    pub fn with_artifact_store_limits(mut self, limits: crate::tools::ArtifactStoreLimits) -> Self {
404        self.artifact_store_limits = Some(limits);
405        self
406    }
407
408    /// Set the maximum number of consecutive malformed-tool-args errors before
409    /// the agent loop bails.
410    ///
411    /// Default: 2 (the LLM gets two chances to self-correct before the session
412    /// is aborted).
413    pub fn with_parse_retries(mut self, max: u32) -> Self {
414        self.max_parse_retries = Some(max);
415        self
416    }
417
418    /// Set a per-tool execution timeout.
419    ///
420    /// When set, each tool execution is wrapped in `tokio::time::timeout`.
421    /// A timeout produces an error message that is fed back to the LLM
422    /// (the session continues).
423    pub fn with_tool_timeout(mut self, timeout_ms: u64) -> Self {
424        self.tool_timeout_ms = Some(timeout_ms);
425        self
426    }
427
428    /// Set a per-model API HTTP timeout.
429    ///
430    /// This is separate from [`with_tool_timeout`](Self::with_tool_timeout):
431    /// tool calls may need long-running process limits while model API calls
432    /// should use provider/network-specific deadlines.
433    pub fn with_llm_api_timeout(mut self, timeout_ms: u64) -> Self {
434        self.llm_api_timeout_ms = Some(timeout_ms);
435        self
436    }
437
438    /// Set the circuit-breaker threshold.
439    ///
440    /// In non-streaming mode, the agent retries transient LLM API failures up
441    /// to this many times (with exponential backoff) before aborting.
442    /// Default: 3 attempts.
443    pub fn with_circuit_breaker(mut self, threshold: u32) -> Self {
444        self.circuit_breaker_threshold = Some(threshold);
445        self
446    }
447
448    /// Set the duplicate-tool-call threshold.
449    ///
450    /// When the same tool is called with identical arguments more than this
451    /// budget allows, the call is returned to the model as a failed tool result
452    /// instead of executing again. Default: 3.
453    pub fn with_duplicate_tool_call_threshold(mut self, threshold: u32) -> Self {
454        self.duplicate_tool_call_threshold = Some(threshold.max(1));
455        self
456    }
457
458    /// Enable all resilience defaults with sensible values:
459    ///
460    /// - `max_parse_retries = 2`
461    /// - `tool_timeout_ms = 120_000` (2 minutes)
462    /// - `circuit_breaker_threshold = 3`
463    pub fn with_resilience_defaults(self) -> Self {
464        self.with_parse_retries(2)
465            .with_tool_timeout(120_000)
466            .with_circuit_breaker(3)
467    }
468
469    /// Provide a concrete [`BashSandbox`] implementation for this session.
470    ///
471    /// When set, `bash` tool commands are routed through the given sandbox
472    /// instead of `std::process::Command`. The host application is responsible
473    /// for constructing and lifecycle-managing the sandbox.
474    ///
475    /// [`BashSandbox`]: crate::sandbox::BashSandbox
476    pub fn with_sandbox_handle(mut self, handle: Arc<dyn crate::sandbox::BashSandbox>) -> Self {
477        self.sandbox_handle = Some(handle);
478        self
479    }
480
481    /// Provide a workspace backend for this session.
482    ///
483    /// Built-in tools keep their stable names and schemas, while their backing
484    /// implementation can target a DFS, browser workspace, remote runner, or
485    /// any other host-provided backend.
486    pub fn with_workspace_backend(
487        mut self,
488        services: Arc<crate::workspace::WorkspaceServices>,
489    ) -> Self {
490        self.workspace_services = Some(services);
491        self
492    }
493
494    /// Enable auto-compaction when context usage exceeds threshold.
495    ///
496    /// When enabled, the agent loop automatically prunes large tool outputs
497    /// and summarizes old messages when context usage exceeds the threshold.
498    pub fn with_auto_compact(mut self, enabled: bool) -> Self {
499        self.auto_compact = enabled;
500        self
501    }
502
503    /// Set the auto-compact threshold (0.0 - 1.0). Default: 0.80 (80%).
504    pub fn with_auto_compact_threshold(mut self, threshold: f32) -> Self {
505        self.auto_compact_threshold = Some(threshold.clamp(0.0, 1.0));
506        self
507    }
508
509    /// Set the active model's context window for compaction accounting.
510    pub fn with_max_context_tokens(mut self, tokens: usize) -> Self {
511        self.max_context_tokens = Some(tokens);
512        self
513    }
514
515    /// Enable or disable continuation injection (default: enabled).
516    ///
517    /// When enabled, the loop injects a continuation message when the LLM stops
518    /// calling tools before the task appears complete, nudging it to keep working.
519    pub fn with_continuation(mut self, enabled: bool) -> Self {
520        self.continuation_enabled = Some(enabled);
521        self
522    }
523
524    /// Set the maximum number of continuation injections per execution (default: 3).
525    pub fn with_max_continuation_turns(mut self, turns: u32) -> Self {
526        self.max_continuation_turns = Some(turns);
527        self
528    }
529
530    /// Inherit tools from an existing MCP manager.
531    ///
532    /// The session reads the manager as a capability source but never mutates
533    /// or disconnects it. Live [`AgentSession::add_mcp_server`](super::AgentSession::add_mcp_server)
534    /// calls use a separate session-owned manager. Delegated child agents
535    /// inherit both sources, with session-owned tools taking precedence.
536    pub fn with_mcp(mut self, manager: Arc<crate::mcp::manager::McpManager>) -> Self {
537        self.mcp_manager = Some(manager);
538        self
539    }
540
541    pub fn with_temperature(mut self, temperature: f32) -> Self {
542        self.temperature = Some(temperature);
543        self
544    }
545
546    pub fn with_thinking_budget(mut self, budget: usize) -> Self {
547        self.thinking_budget = Some(budget);
548        self
549    }
550
551    /// Override the maximum number of tool execution rounds for this session.
552    ///
553    /// Useful when binding a markdown-defined subagent to a session —
554    /// pass the agent definition's `max_steps` value here to enforce its step budget.
555    pub fn with_max_tool_rounds(mut self, rounds: usize) -> Self {
556        self.max_tool_rounds = Some(rounds);
557        self
558    }
559
560    /// Override the maximum number of sibling parallel branches for this session.
561    pub fn with_max_parallel_tasks(mut self, tasks: usize) -> Self {
562        self.max_parallel_tasks = Some(tasks.max(1));
563        self
564    }
565
566    /// Override automatic subagent delegation for this session.
567    pub fn with_auto_delegation(mut self, config: crate::config::AutoDelegationConfig) -> Self {
568        self.auto_delegation = Some(config);
569        self
570    }
571
572    /// Enable or disable automatic subagent delegation for this session.
573    pub fn with_auto_delegation_enabled(mut self, enabled: bool) -> Self {
574        let mut config = self.auto_delegation.take().unwrap_or_default();
575        config.enabled = enabled;
576        self.auto_delegation = Some(config);
577        self
578    }
579
580    /// Enable or disable model-visible manual child-agent tools for this session.
581    ///
582    /// When false, `task` and `parallel_task` are not registered in the session
583    /// tool surface. Worker agents remain registered for introspection and hosts
584    /// that manage them directly. This is for cost control or debugging; it is
585    /// not a security sandbox for the parent agent.
586    pub fn with_manual_delegation_enabled(mut self, enabled: bool) -> Self {
587        if let Some(config) = &mut self.auto_delegation {
588            config.allow_manual_delegation = enabled;
589        }
590        self.manual_delegation_enabled = Some(enabled);
591        self
592    }
593
594    /// Globally enable or disable automatic parallel child-agent fan-out.
595    ///
596    /// Manual `parallel_task` calls remain available when this is false.
597    pub fn with_auto_parallel_delegation(mut self, enabled: bool) -> Self {
598        if let Some(config) = &mut self.auto_delegation {
599            config.auto_parallel = enabled;
600        }
601        self.auto_parallel_delegation = Some(enabled);
602        self
603    }
604
605    /// Set slot-based system prompt customization for this session.
606    ///
607    /// Allows customizing role, guidelines, response style, and extra instructions
608    /// without overriding the core agentic capabilities.
609    pub fn with_prompt_slots(mut self, slots: SystemPromptSlots) -> Self {
610        self.prompt_slots = Some(slots);
611        self
612    }
613
614    /// Replace the built-in hook engine with an external hook executor.
615    ///
616    /// All lifecycle events are forwarded to the executor instead of the
617    /// in-process `HookEngine`.
618    pub fn with_hook_executor(mut self, executor: Arc<dyn crate::hooks::HookExecutor>) -> Self {
619        self.hook_executor = Some(executor);
620        self
621    }
622}