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::collections::HashMap;
13use std::path::PathBuf;
14use std::sync::Arc;
15
16impl std::fmt::Debug for SessionOptions {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.debug_struct("SessionOptions")
19            .field("model", &self.model)
20            .field("task_priority", &self.task_priority)
21            .field("agent_dirs", &self.agent_dirs)
22            .field("worker_agents", &self.worker_agents.len())
23            .field("skill_dirs", &self.skill_dirs)
24            .field(
25                "command_env",
26                &self.command_env.as_ref().map(|env| env.len()),
27            )
28            .field("queue_config", &self.queue_config)
29            .field("search_config", &self.search_config)
30            .field("security_provider", &self.security_provider.is_some())
31            .field("llm_client", &self.llm_client.is_some())
32            .field("context_providers", &self.context_providers.len())
33            .field("cognitive_context", &self.cognitive_context)
34            .field("confirmation_manager", &self.confirmation_manager.is_some())
35            .field("permission_checker", &self.permission_checker.is_some())
36            .field("permission_policy", &self.permission_policy.is_some())
37            .field("planning_mode", &self.planning_mode)
38            .field("goal_tracking", &self.goal_tracking)
39            .field(
40                "skill_registry",
41                &self
42                    .skill_registry
43                    .as_ref()
44                    .map(|r| format!("{} skills", r.len())),
45            )
46            .field("host_skills", &self.host_skills.len())
47            .field(
48                "enforce_active_skill_tool_restrictions",
49                &self.enforce_active_skill_tool_restrictions,
50            )
51            .field("memory_store", &self.memory_store.is_some())
52            .field("durable_memory", &self.durable_memory)
53            .field("memory_observers", &self.memory_observers.len())
54            .field("memory_maintenance", &self.memory_maintenance)
55            .field("file_memory_dir", &self.file_memory_dir)
56            .field("session_store", &self.session_store.is_some())
57            .field(
58                "session_checkpoint_export_sink",
59                &self.session_checkpoint_export_sink.is_some(),
60            )
61            .field("file_session_store_dir", &self.file_session_store_dir)
62            .field("session_id", &self.session_id)
63            .field("rl_trajectory", &self.rl_trajectory)
64            .field("llm_logprobs", &self.llm_logprobs)
65            .field("llm_top_logprobs", &self.llm_top_logprobs)
66            .field("auto_save", &self.auto_save)
67            .field("artifact_store_limits", &self.artifact_store_limits)
68            .field("immutable_content_adapter", &self.immutable_content_adapter)
69            .field(
70                "tool_result_transform_policy",
71                &self.tool_result_transform_policy,
72            )
73            .field("tool_presentation_profile", &self.tool_presentation_profile)
74            .field("max_parse_retries", &self.max_parse_retries)
75            .field("tool_timeout_ms", &self.tool_timeout_ms)
76            .field("llm_api_timeout_ms", &self.llm_api_timeout_ms)
77            .field("circuit_breaker_threshold", &self.circuit_breaker_threshold)
78            .field(
79                "duplicate_tool_call_threshold",
80                &self.duplicate_tool_call_threshold,
81            )
82            .field("sandbox_handle", &self.sandbox_handle.is_some())
83            .field(
84                "allow_process_host_sandbox",
85                &self.allow_process_host_sandbox,
86            )
87            .field("workspace_services", &self.workspace_services.is_some())
88            .field("workspace_retrieval", &self.workspace_retrieval)
89            .field("auto_compact", &self.auto_compact)
90            .field("auto_compact_threshold", &self.auto_compact_threshold)
91            .field("max_context_tokens", &self.max_context_tokens)
92            .field("continuation_enabled", &self.continuation_enabled)
93            .field("max_continuation_turns", &self.max_continuation_turns)
94            .field("mcp_manager", &self.mcp_manager.is_some())
95            .field("temperature", &self.temperature)
96            .field("thinking_budget", &self.thinking_budget)
97            .field("max_tool_rounds", &self.max_tool_rounds)
98            .field("max_parallel_tasks", &self.max_parallel_tasks)
99            .field("auto_delegation", &self.auto_delegation)
100            .field("manual_delegation_enabled", &self.manual_delegation_enabled)
101            .field("auto_parallel_delegation", &self.auto_parallel_delegation)
102            .field("prompt_slots", &self.prompt_slots.is_some())
103            .finish()
104    }
105}
106
107impl SessionOptions {
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    pub fn with_model(mut self, model: impl Into<String>) -> Self {
113        self.model = Some(model.into());
114        self
115    }
116
117    /// Set the priority of top-level work submitted by this session.
118    pub fn with_task_priority(mut self, priority: crate::task_scheduler::TaskPriority) -> Self {
119        self.task_priority = priority;
120        self
121    }
122
123    pub fn with_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
124        self.agent_dirs.push(dir.into());
125        self
126    }
127
128    /// Register a cattle-style worker with this session's task delegation registry.
129    pub fn with_worker_agent(mut self, spec: WorkerAgentSpec) -> Self {
130        self.worker_agents.push(spec);
131        self
132    }
133
134    /// Register multiple cattle-style workers with this session.
135    pub fn with_worker_agents<I>(mut self, specs: I) -> Self
136    where
137        I: IntoIterator<Item = WorkerAgentSpec>,
138    {
139        self.worker_agents.extend(specs);
140        self
141    }
142
143    pub fn with_queue_config(mut self, config: SessionQueueConfig) -> Self {
144        self.queue_config = Some(config);
145        self
146    }
147
148    /// Override the agent-level web-search configuration for this session.
149    ///
150    /// The configuration is value-typed and therefore safe to pass through
151    /// the Node, Python, and Go SDK boundaries. `None` keeps the agent/global
152    /// configuration unchanged.
153    pub fn with_search_config(mut self, config: crate::config::SearchConfig) -> Self {
154        self.search_config = Some(config);
155        self
156    }
157
158    /// Enable default security provider with taint tracking and output sanitization
159    pub fn with_default_security(mut self) -> Self {
160        self.security_provider = Some(Arc::new(crate::security::DefaultSecurityProvider::new()));
161        self
162    }
163
164    /// Set a custom security provider
165    pub fn with_security_provider(
166        mut self,
167        provider: Arc<dyn crate::security::SecurityProvider>,
168    ) -> Self {
169        self.security_provider = Some(provider);
170        self
171    }
172
173    /// Provide a custom LLM client for this session.
174    ///
175    /// When set, this client is used directly, overriding the `provider/model`
176    /// factory resolution. Use it to plug in a provider the built-in factory
177    /// does not cover, a deterministic record/replay client for tests, or an
178    /// HTTP-layer proxy/audit wrapper. Mirrors [`Self::with_workspace_backend`];
179    /// the `provider/model` config path remains the default when unset.
180    pub fn with_llm_client(mut self, client: Arc<dyn crate::llm::LlmClient>) -> Self {
181        self.llm_client = Some(client);
182        self
183    }
184
185    /// Add a file system context provider for simple RAG
186    pub fn with_fs_context(mut self, root_path: impl Into<PathBuf>) -> Self {
187        let config = crate::context::FileSystemContextConfig::new(root_path);
188        self.context_providers
189            .push(Arc::new(crate::context::FileSystemContextProvider::new(
190                config,
191            )));
192        self
193    }
194
195    /// Add a custom context provider
196    pub fn with_context_provider(
197        mut self,
198        provider: Arc<dyn crate::context::ContextProvider>,
199    ) -> Self {
200        self.context_providers.push(provider);
201        self
202    }
203
204    /// Bind this session to one exact A3S Use cognitive-package generation.
205    ///
206    /// The supplied runtime value contains a serializable immutable binding
207    /// and a host-owned provider. On restart the host must inject the same
208    /// binding again; Code never resolves `latest`, opens a package path, or
209    /// substitutes graph/personal-memory context.
210    pub fn with_cognitive_context(
211        mut self,
212        context: crate::cognitive_context::CognitiveContextSession,
213    ) -> Self {
214        self.cognitive_context = Some(context);
215        self
216    }
217
218    /// Set a confirmation manager for HITL
219    pub fn with_confirmation_manager(
220        mut self,
221        manager: Arc<dyn crate::hitl::ConfirmationProvider>,
222    ) -> Self {
223        self.confirmation_manager = Some(manager);
224        self
225    }
226
227    /// Set a confirmation policy for HITL
228    ///
229    /// The policy will be used to create a ConfirmationManager when the session is built.
230    /// This is the preferred way to configure HITL from the Node SDK.
231    pub fn with_confirmation_policy(mut self, policy: crate::hitl::ConfirmationPolicy) -> Self {
232        self.confirmation_policy = Some(policy);
233        self
234    }
235
236    /// Set a serializable permission policy for tool execution.
237    pub fn with_permission_policy(mut self, policy: crate::permissions::PermissionPolicy) -> Self {
238        self.permission_checker = Some(Arc::new(policy.clone()));
239        self.permission_policy = Some(policy);
240        self
241    }
242
243    /// Set a permission checker
244    pub fn with_permission_checker(
245        mut self,
246        checker: Arc<dyn crate::permissions::PermissionChecker>,
247    ) -> Self {
248        self.permission_checker = Some(checker);
249        self
250    }
251
252    /// Set planning mode
253    pub fn with_planning_mode(mut self, mode: PlanningMode) -> Self {
254        self.planning_mode = mode;
255        self
256    }
257
258    /// Enable planning (shortcut for `with_planning_mode(PlanningMode::Enabled)`)
259    pub fn with_planning(mut self, enabled: bool) -> Self {
260        self.planning_mode = if enabled {
261            PlanningMode::Enabled
262        } else {
263            PlanningMode::Disabled
264        };
265        self
266    }
267
268    /// Enable goal tracking
269    pub fn with_goal_tracking(mut self, enabled: bool) -> Self {
270        self.goal_tracking = enabled;
271        self
272    }
273
274    /// Add the compatibility built-in skill registry.
275    ///
276    /// A3S Code no longer ships embedded built-in skills, so this currently
277    /// installs an empty registry. Use skill directories, inline skills, or a
278    /// custom skill registry for reusable behavior.
279    pub fn with_builtin_skills(mut self) -> Self {
280        self.skill_registry = Some(Arc::new(crate::skills::SkillRegistry::with_builtins()));
281        self
282    }
283
284    /// Add a custom skill registry
285    pub fn with_skill_registry(mut self, registry: Arc<crate::skills::SkillRegistry>) -> Self {
286        self.skill_registry = Some(registry);
287        self
288    }
289
290    /// Register one host-owned skill after skill directories.
291    ///
292    /// Same-name directory skills do not replace it.
293    pub fn with_host_skill(mut self, skill: Arc<crate::skills::Skill>) -> Self {
294        self.host_skills.push(skill);
295        self
296    }
297
298    /// Enable or disable legacy global active-skill `allowed-tools` restrictions.
299    ///
300    /// The default is disabled: active skills do not block ordinary session
301    /// tools before the host permission/HITL approval chain runs.
302    pub fn with_active_skill_tool_restrictions(mut self, enabled: bool) -> Self {
303        self.enforce_active_skill_tool_restrictions = Some(enabled);
304        self
305    }
306
307    /// Add skill directories to scan for skill files (*.md).
308    /// Merged with any global `skill_dirs` from
309    /// [`CodeConfig`](crate::config::CodeConfig) at session build time.
310    pub fn with_skill_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
311        self.skill_dirs.extend(dirs.into_iter().map(Into::into));
312        self
313    }
314
315    /// Merge environment variables into Bash / sandbox command execution.
316    ///
317    /// Replaces any previously configured `command_env` map.
318    pub fn with_command_env(mut self, env: HashMap<String, String>) -> Self {
319        self.command_env = Some(env);
320        self
321    }
322
323    /// Load skills from a directory (eager — scans immediately into a registry).
324    pub fn with_skills_from_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
325        let registry = self
326            .skill_registry
327            .unwrap_or_else(|| Arc::new(crate::skills::SkillRegistry::new()));
328        if let Err(e) = registry.load_from_dir(&dir) {
329            tracing::warn!(
330                dir = %dir.as_ref().display(),
331                error = %e,
332                "Failed to load skills from directory — continuing without them"
333            );
334        }
335        self.skill_registry = Some(registry);
336        self
337    }
338
339    /// Set a custom memory store override.
340    ///
341    /// Sessions resolve a default memory store when no override is provided.
342    pub fn with_memory(mut self, store: Arc<dyn MemoryStore>) -> Self {
343        self.memory_store = Some(store);
344        self.file_memory_dir = None;
345        self
346    }
347
348    /// Install an exact, typed durable-memory repository binding.
349    ///
350    /// Serving mode is **active-only recall** (`HARNESS-CONV4` / `CAP-GA1`).
351    /// Candidate rows may still be written inactive until a host activates
352    /// them; there is no model-visible shadow-candidates serving mode.
353    /// The live repository is runtime-only, while its secret-free typed
354    /// identity is persisted; hosts restoring a session must inject the exact
355    /// same visible binding again.
356    pub fn with_durable_memory(
357        mut self,
358        binding: crate::durable_memory::DurableMemorySession,
359    ) -> Self {
360        self.durable_memory = Some(binding);
361        self
362    }
363
364    /// Use a file-based memory store at the given directory instead of the default.
365    ///
366    /// The store is created lazily when the session is built (requires async).
367    /// This stores the directory path; `FileMemoryStore::new()` is called during
368    /// session construction.
369    pub fn with_file_memory(mut self, dir: impl Into<PathBuf>) -> Self {
370        self.memory_store = None;
371        self.file_memory_dir = Some(dir.into());
372        self
373    }
374
375    /// Observe successful durable memory writes without replacing the memory
376    /// backend. Observers are best-effort derived projections: their failures
377    /// never undo a persisted memory.
378    pub fn with_memory_observer(
379        mut self,
380        observer: Arc<dyn crate::memory::MemoryObserver>,
381    ) -> Self {
382        self.memory_observers.push(observer);
383        self
384    }
385
386    /// Install typed scheduled memory jobs and their bounded close policy.
387    /// Built-in V1 pruning is included automatically when configured in
388    /// [`MemoryConfig`](crate::memory::MemoryConfig); verified semantic refresh
389    /// remains an explicit schedule in these options.
390    pub fn with_memory_maintenance(
391        mut self,
392        maintenance: crate::memory::MemoryMaintenanceOptions,
393    ) -> Self {
394        self.memory_maintenance = maintenance;
395        self
396    }
397
398    /// Set a session store for persistence
399    pub fn with_session_store(mut self, store: Arc<dyn crate::store::SessionStore>) -> Self {
400        self.session_store = Some(store);
401        self.file_session_store_dir = None;
402        self
403    }
404
405    /// Export exact portable checkpoints at completed tool-round boundaries.
406    ///
407    /// The sink receives an owned, canonical export only after all events from
408    /// that tool round have entered the matching Session snapshot. Sink errors
409    /// are warn-logged and never turn a healthy live Run into a failure.
410    pub fn with_session_checkpoint_export_sink(
411        mut self,
412        sink: Arc<dyn crate::session_checkpoint::SessionCheckpointExportSink>,
413    ) -> Self {
414        self.session_checkpoint_export_sink = Some(sink);
415        self
416    }
417
418    /// Use a file-based session store at the given directory.
419    ///
420    /// The path is a typed construction specification. No I/O occurs until
421    /// [`SessionBuilder::build`](super::SessionBuilder::build) is awaited.
422    pub fn with_file_session_store(mut self, dir: impl Into<PathBuf>) -> Self {
423        self.session_store = None;
424        self.file_session_store_dir = Some(dir.into());
425        self
426    }
427
428    /// Set an explicit session ID (auto-generated UUID if not set)
429    pub fn with_session_id(mut self, id: impl Into<String>) -> Self {
430        self.session_id = Some(id.into());
431        self
432    }
433
434    /// Tag the session with a host-defined tenant id. Opaque to the
435    /// framework — propagated to `SessionData`, hooks, and traces.
436    pub fn with_tenant_id(mut self, tenant: impl Into<String>) -> Self {
437        self.tenant_id = Some(tenant.into());
438        self
439    }
440
441    /// Tag the session with the id of the principal (user / service
442    /// account / etc.) that triggered it.
443    pub fn with_principal(mut self, principal: impl Into<String>) -> Self {
444        self.principal = Some(principal.into());
445        self
446    }
447
448    /// Tag the session with the id of the agent template / definition it
449    /// was instantiated from.
450    pub fn with_agent_template_id(mut self, template_id: impl Into<String>) -> Self {
451        self.agent_template_id = Some(template_id.into());
452        self
453    }
454
455    /// Attach a distributed-trace correlation id so this session's events
456    /// can be joined with upstream/downstream work.
457    pub fn with_correlation_id(mut self, corr: impl Into<String>) -> Self {
458        self.correlation_id = Some(corr.into());
459        self
460    }
461
462    /// Install a host-supplied [`BudgetGuard`](crate::budget::BudgetGuard).
463    ///
464    /// The guard is consulted before every LLM call (and after, for
465    /// usage accounting). When unset, no budget enforcement happens.
466    pub fn with_budget_guard(mut self, guard: Arc<dyn crate::budget::BudgetGuard>) -> Self {
467        self.budget_guard = Some(guard);
468        self
469    }
470
471    /// Install a host-provided [`HostEnv`](crate::host_env::HostEnv) for
472    /// deterministic ID generation and time. Replaces the framework
473    /// default of `uuid::Uuid::new_v4()` + wall clock — used by
474    /// host replay infrastructure to recreate a run bit-identical on
475    /// another node.
476    pub fn with_host_env(mut self, env: Arc<crate::host_env::HostEnv>) -> Self {
477        self.host_env = Some(env);
478        self
479    }
480
481    /// Install FIFO retention caps for the session's in-memory stores.
482    ///
483    /// `None` selects [`SessionRetentionLimits::default`](crate::retention::SessionRetentionLimits::default),
484    /// which is finite. Pass [`SessionRetentionLimits::unbounded`](crate::retention::SessionRetentionLimits::unbounded)
485    /// to retain every record. Hosts running thousands of long-lived
486    /// sessions per node should set explicit caps.
487    pub fn with_retention_limits(
488        mut self,
489        limits: crate::retention::SessionRetentionLimits,
490    ) -> Self {
491        self.retention_limits = Some(limits);
492        self
493    }
494
495    /// Enable structured JSONL trajectory capture for this session.
496    ///
497    /// This is the preferred programmatic path for RL training and deployed
498    /// service data collection. Environment-only deployments can instead set
499    /// `A3S_CODE_TRAJECTORY_PATH`.
500    pub fn with_rl_trajectory(mut self, config: crate::rl_trajectory::RlTrajectoryConfig) -> Self {
501        self.rl_trajectory = Some(config);
502        self
503    }
504
505    /// Request token-level log probabilities from compatible LLM providers.
506    pub fn with_llm_logprobs(mut self, enabled: bool) -> Self {
507        self.llm_logprobs = Some(enabled);
508        self
509    }
510
511    /// Request up to `top_logprobs` alternative logprobs per generated token.
512    pub fn with_llm_top_logprobs(mut self, top_logprobs: usize) -> Self {
513        self.llm_logprobs = Some(true);
514        self.llm_top_logprobs = Some(top_logprobs);
515        self
516    }
517
518    /// Enable auto-save after each `send()` call
519    pub fn with_auto_save(mut self, enabled: bool) -> Self {
520        self.auto_save = enabled;
521        self
522    }
523
524    /// Set artifact retention limits for this session.
525    pub fn with_artifact_store_limits(mut self, limits: crate::tools::ArtifactStoreLimits) -> Self {
526        self.artifact_store_limits = Some(limits);
527        self
528    }
529
530    /// Install a session-scoped host adapter for authorized immutable Tool
531    /// content. Every raw output returned by a Tool writes through this port
532    /// before release; lossy projections expose its validated reference
533    /// instead of retaining a second local copy.
534    pub fn with_immutable_content_adapter(
535        mut self,
536        adapter: crate::tools::ImmutableContentAdapterSession,
537    ) -> Self {
538        self.immutable_content_adapter = Some(adapter);
539        self
540    }
541
542    /// Pin the deterministic projection policy for Tool results.
543    pub fn with_tool_result_transform_policy(
544        mut self,
545        policy: crate::tools::ToolResultTransformPolicyV1,
546    ) -> Self {
547        self.tool_result_transform_policy = Some(policy);
548        self
549    }
550
551    /// Select the typed model-facing Tool presentation profile.
552    pub fn with_tool_presentation_profile(
553        mut self,
554        profile: crate::tools::ToolPresentationProfileV1,
555    ) -> Self {
556        self.tool_presentation_profile = Some(profile);
557        self
558    }
559
560    /// Set the maximum number of consecutive malformed-tool-args errors before
561    /// the agent loop bails.
562    ///
563    /// Default: 2 (the LLM gets two chances to self-correct before the session
564    /// is aborted).
565    pub fn with_parse_retries(mut self, max: u32) -> Self {
566        self.max_parse_retries = Some(max);
567        self
568    }
569
570    /// Set a per-tool execution timeout.
571    ///
572    /// When set, each tool execution is wrapped in `tokio::time::timeout`.
573    /// A timeout produces an error message that is fed back to the LLM
574    /// (the session continues).
575    pub fn with_tool_timeout(mut self, timeout_ms: u64) -> Self {
576        self.tool_timeout_ms = Some(timeout_ms);
577        self
578    }
579
580    /// Set a per-model API HTTP timeout.
581    ///
582    /// This is separate from [`with_tool_timeout`](Self::with_tool_timeout):
583    /// tool calls may need long-running process limits while model API calls
584    /// should use provider/network-specific deadlines.
585    pub fn with_llm_api_timeout(mut self, timeout_ms: u64) -> Self {
586        self.llm_api_timeout_ms = Some(timeout_ms);
587        self
588    }
589
590    /// Set the circuit-breaker threshold.
591    ///
592    /// In non-streaming mode, the agent retries transient LLM API failures up
593    /// to this many times (with exponential backoff) before aborting.
594    /// Default: 3 attempts.
595    pub fn with_circuit_breaker(mut self, threshold: u32) -> Self {
596        self.circuit_breaker_threshold = Some(threshold);
597        self
598    }
599
600    /// Set the duplicate-tool-call threshold.
601    ///
602    /// When the same tool is called with identical arguments more than this
603    /// budget allows, the call is returned to the model as a failed tool result
604    /// instead of executing again. Default: 3.
605    pub fn with_duplicate_tool_call_threshold(mut self, threshold: u32) -> Self {
606        self.duplicate_tool_call_threshold = Some(threshold.max(1));
607        self
608    }
609
610    /// Enable all resilience defaults with sensible values:
611    ///
612    /// - `max_parse_retries = 2`
613    /// - `tool_timeout_ms = 120_000` (2 minutes)
614    /// - `llm_api_timeout_ms = 120_000` (2 minutes)
615    /// - `circuit_breaker_threshold = 3`
616    pub fn with_resilience_defaults(self) -> Self {
617        self.with_parse_retries(2)
618            .with_tool_timeout(120_000)
619            .with_llm_api_timeout(120_000)
620            .with_circuit_breaker(3)
621    }
622
623    /// Override the default native [`BashSandbox`] for this session.
624    ///
625    /// Local sessions automatically bind the A3S native sandbox. Use this
626    /// option only when the host owns another equivalent isolation boundary.
627    /// The host remains responsible for constructing and lifecycle-managing a
628    /// custom sandbox.
629    ///
630    /// [`BashSandbox`]: crate::sandbox::BashSandbox
631    pub fn with_sandbox_handle(mut self, handle: Arc<dyn crate::sandbox::BashSandbox>) -> Self {
632        self.sandbox_handle = Some(handle);
633        self
634    }
635
636    /// Allow a process-host Bash runner when the native sandbox cannot start.
637    ///
638    /// This does **not** weaken the native fail-closed default on developer
639    /// machines. It only applies when native initialization fails and the host
640    /// already provides an outer isolation boundary (Harbor task containers).
641    /// Equivalent environment opt-in:
642    /// `A3S_CODE_ALLOW_PROCESS_HOST_SANDBOX=1`.
643    pub fn with_allow_process_host_sandbox(mut self, allow: bool) -> Self {
644        self.allow_process_host_sandbox = allow;
645        self
646    }
647
648    /// Provide a workspace backend for this session.
649    ///
650    /// Built-in tools keep their stable names and schemas, while their backing
651    /// implementation can target a DFS, browser workspace, remote runner, or
652    /// any other host-provided backend.
653    pub fn with_workspace_backend(
654        mut self,
655        services: Arc<crate::workspace::WorkspaceServices>,
656    ) -> Self {
657        self.workspace_services = Some(services);
658        self
659    }
660
661    /// Enable session-bound semantic workspace indexing.
662    ///
663    /// The session builder returns without waiting for corpus embeddings. The
664    /// caller can observe partial readiness through
665    /// [`AgentSession::workspace_retrieval_status`](super::AgentSession::workspace_retrieval_status).
666    pub fn with_workspace_retrieval(
667        mut self,
668        options: crate::workspace::WorkspaceRetrievalOptions,
669    ) -> Self {
670        self.workspace_retrieval = Some(options);
671        self
672    }
673
674    /// Explicitly disable session-bound semantic workspace indexing.
675    ///
676    /// This clears an earlier [`Self::with_workspace_retrieval`] choice without
677    /// constructing a replacement backend or calling the embedding provider.
678    /// It is useful when a host applies layered configuration and a later,
679    /// trusted layer deliberately opts the session out.
680    pub fn without_workspace_retrieval(mut self) -> Self {
681        self.workspace_retrieval = None;
682        self
683    }
684
685    /// Enable auto-compaction when context usage exceeds threshold.
686    ///
687    /// When enabled, the agent loop automatically prunes large tool outputs
688    /// and summarizes old messages when context usage exceeds the threshold.
689    pub fn with_auto_compact(mut self, enabled: bool) -> Self {
690        self.auto_compact = enabled;
691        self
692    }
693
694    /// Set the auto-compact threshold (0.0 - 1.0). Default: 0.80 (80%).
695    pub fn with_auto_compact_threshold(mut self, threshold: f32) -> Self {
696        self.auto_compact_threshold = Some(threshold.clamp(0.0, 1.0));
697        self
698    }
699
700    /// Set the active model's context window for compaction accounting.
701    pub fn with_max_context_tokens(mut self, tokens: usize) -> Self {
702        self.max_context_tokens = Some(tokens);
703        self
704    }
705
706    /// Enable or disable continuation injection (default: enabled).
707    ///
708    /// When enabled, the loop injects a continuation message when the LLM stops
709    /// calling tools before the task appears complete, nudging it to keep working.
710    pub fn with_continuation(mut self, enabled: bool) -> Self {
711        self.continuation_enabled = Some(enabled);
712        self
713    }
714
715    /// Set the maximum number of continuation injections per execution (default: 3).
716    pub fn with_max_continuation_turns(mut self, turns: u32) -> Self {
717        self.max_continuation_turns = Some(turns);
718        self
719    }
720
721    /// Inherit tools from an existing MCP manager.
722    ///
723    /// The session reads the manager as a capability source but never mutates
724    /// or disconnects it. Live [`AgentSession::add_mcp_server`](super::AgentSession::add_mcp_server)
725    /// calls use a separate session-owned manager. Delegated child agents
726    /// inherit both sources, with session-owned tools taking precedence.
727    pub fn with_mcp(mut self, manager: Arc<crate::mcp::manager::McpManager>) -> Self {
728        self.mcp_manager = Some(manager);
729        self
730    }
731
732    pub fn with_temperature(mut self, temperature: f32) -> Self {
733        self.temperature = Some(temperature);
734        self
735    }
736
737    pub fn with_thinking_budget(mut self, budget: usize) -> Self {
738        self.thinking_budget = Some(budget);
739        self
740    }
741
742    /// Override the maximum number of tool execution rounds for this session.
743    ///
744    /// Useful when binding a markdown-defined subagent to a session —
745    /// pass the agent definition's `max_steps` value here to enforce its step budget.
746    pub fn with_max_tool_rounds(mut self, rounds: usize) -> Self {
747        self.max_tool_rounds = Some(rounds);
748        self
749    }
750
751    /// Override the maximum number of sibling parallel branches for this session.
752    pub fn with_max_parallel_tasks(mut self, tasks: usize) -> Self {
753        self.max_parallel_tasks = Some(tasks.max(1));
754        self
755    }
756
757    /// Override automatic subagent delegation for this session.
758    pub fn with_auto_delegation(mut self, config: crate::config::AutoDelegationConfig) -> Self {
759        self.auto_delegation = Some(config);
760        self
761    }
762
763    /// Enable or disable automatic subagent delegation for this session.
764    pub fn with_auto_delegation_enabled(mut self, enabled: bool) -> Self {
765        let mut config = self.auto_delegation.take().unwrap_or_default();
766        config.enabled = enabled;
767        self.auto_delegation = Some(config);
768        self
769    }
770
771    /// Enable or disable manual child-agent tools for this session.
772    ///
773    /// When false, the model-visible `task` tool
774    /// compatibility alias are not registered. Worker agents remain registered
775    /// for introspection and hosts that manage them directly. This is for cost
776    /// control or debugging; it is not a security sandbox for the parent agent.
777    pub fn with_manual_delegation_enabled(mut self, enabled: bool) -> Self {
778        if let Some(config) = &mut self.auto_delegation {
779            config.allow_manual_delegation = enabled;
780        }
781        self.manual_delegation_enabled = Some(enabled);
782        self
783    }
784
785    /// Globally enable or disable automatic parallel child-agent fan-out.
786    ///
787    /// Manual `task` fan-out calls remain available
788    /// when this is false.
789    pub fn with_auto_parallel_delegation(mut self, enabled: bool) -> Self {
790        if let Some(config) = &mut self.auto_delegation {
791            config.auto_parallel = enabled;
792        }
793        self.auto_parallel_delegation = Some(enabled);
794        self
795    }
796
797    /// Set slot-based system prompt customization for this session.
798    ///
799    /// Allows customizing role, guidelines, response style, output language,
800    /// and extra instructions without overriding the core agentic capabilities.
801    pub fn with_prompt_slots(mut self, slots: SystemPromptSlots) -> Self {
802        self.prompt_slots = Some(slots);
803        self
804    }
805
806    /// Pin user-facing replies to a BCP-47 language tag for this session.
807    ///
808    /// Merges into existing `prompt_slots` when present so hosts can set locale
809    /// without wiping other slot customizations.
810    pub fn with_output_language(mut self, language: impl Into<String>) -> Self {
811        let slots = self
812            .prompt_slots
813            .take()
814            .unwrap_or_default()
815            .with_output_language(language);
816        self.prompt_slots = Some(slots);
817        self
818    }
819
820    /// Replace the built-in hook engine with an external hook executor.
821    ///
822    /// All lifecycle events are forwarded to the executor instead of the
823    /// in-process `HookEngine`.
824    pub fn with_hook_executor(mut self, executor: Arc<dyn crate::hooks::HookExecutor>) -> Self {
825        self.hook_executor = Some(executor);
826        self
827    }
828
829    /// Isolate mutating writes in a git worktree. Non-git roots fail closed.
830    pub fn with_effect_isolation(mut self, enabled: bool) -> Self {
831        self.effect_isolation = enabled;
832        self
833    }
834
835    pub fn with_completion_waivers(
836        mut self,
837        waivers: Vec<crate::harness_loop::CompletionWaiverV1>,
838    ) -> Self {
839        self.completion_waivers = waivers;
840        self
841    }
842
843    pub fn with_plan_run(mut self, admission: crate::harness_loop::PlanRunAdmission) -> Self {
844        self.plan_run = admission;
845        self
846    }
847
848    pub fn with_path_rules(mut self, rules: Vec<crate::path_instructions::PathRule>) -> Self {
849        self.path_rules = rules;
850        self
851    }
852
853    pub fn with_verifier(mut self, enabled: bool) -> Self {
854        self.verifier_enabled = enabled;
855        self
856    }
857
858    /// Install a host-only [`CompletionAttestor`](crate::CompletionAttestor).
859    ///
860    /// Invoked with the live mutation digest and paths after they exist and
861    /// before the completion gate decides. The gate still requires a Passed,
862    /// digest-bound report — this is not an `Observe` / bypass mode (#160).
863    pub fn with_completion_attestor(
864        mut self,
865        attestor: std::sync::Arc<dyn crate::completion_attestor::CompletionAttestor>,
866    ) -> Self {
867        self.completion_attestor = Some(attestor);
868        self
869    }
870
871    /// Admit a Meta Harness compose recipe for this session's fact-log actor.
872    pub fn with_harness(mut self, harness: crate::meta_harness::HarnessComposeOptions) -> Self {
873        self.harness = Some(harness);
874        self
875    }
876
877    /// Install a host Moore-component registry for `host:<id>` mounts.
878    pub fn with_host_harness_registry(
879        mut self,
880        registry: std::sync::Arc<dyn crate::meta_harness::HostHarnessRegistry>,
881    ) -> Self {
882        self.host_harness_registry = Some(registry);
883        self
884    }
885
886    /// Install a full custom Meta Harness assembler (Rust embedders).
887    ///
888    /// Takes precedence over [`Self::with_harness`]. Kernel policy still
889    /// applies when the assembler returns a graph via
890    /// [`crate::meta_harness::admit_component_tree`] / `HarnessGraph`.
891    pub fn with_host_harness_assembler(
892        mut self,
893        assembler: std::sync::Arc<dyn crate::meta_harness::HostHarnessAssembler>,
894    ) -> Self {
895        self.host_harness_assembler = Some(assembler);
896        self
897    }
898
899    pub fn with_external_observations(
900        mut self,
901        observations: Vec<crate::external_observation::ExternalObservationV1>,
902    ) -> Self {
903        self.external_observations = observations;
904        self
905    }
906
907    pub fn with_outcome_ledger(mut self, ledger: crate::outcome_memory::OutcomeLedger) -> Self {
908        self.outcome_ledger = ledger;
909        self
910    }
911
912    pub fn with_read_only_session(mut self, read_only: bool) -> Self {
913        self.read_only_session = read_only;
914        self
915    }
916
917    pub(crate) fn session_id_hint(&self) -> String {
918        self.session_id
919            .clone()
920            .filter(|id| !id.trim().is_empty())
921            .unwrap_or_else(|| "session".to_string())
922    }
923
924    pub(crate) fn can_write_workspace(&self) -> bool {
925        !self.read_only_session
926    }
927}