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