1use crate::agent::{AgentConfig, AgentEvent, AgentLoop, AgentResult};
20use crate::config::CodeConfig;
21use crate::error::Result;
22use crate::llm::{LlmClient, Message};
23use crate::queue::{
24 ExternalTask, ExternalTaskResult, LaneHandlerConfig, SessionLane, SessionQueueConfig,
25 SessionQueueStats,
26};
27use crate::session_lane_queue::SessionLaneQueue;
28use crate::tools::{ToolContext, ToolExecutor};
29use a3s_lane::{DeadLetter, MetricsSnapshot};
30use anyhow::Context;
31use std::path::{Path, PathBuf};
32use std::sync::{Arc, RwLock};
33use tokio::sync::{broadcast, mpsc};
34use tokio::task::JoinHandle;
35
36#[derive(Debug, Clone)]
42pub struct ToolCallResult {
43 pub name: String,
44 pub output: String,
45 pub exit_code: i32,
46}
47
48#[derive(Clone, Default)]
54pub struct SessionOptions {
55 pub model: Option<String>,
57 pub agent_dirs: Vec<PathBuf>,
60 pub queue_config: Option<SessionQueueConfig>,
65 pub security_provider: Option<Arc<dyn crate::security::SecurityProvider>>,
67 pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
69 pub confirmation_manager: Option<Arc<dyn crate::hitl::ConfirmationProvider>>,
71 pub permission_checker: Option<Arc<dyn crate::permissions::PermissionChecker>>,
73 pub planning_enabled: bool,
75 pub goal_tracking: bool,
77 pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
79 pub memory_store: Option<Arc<dyn crate::memory::MemoryStore>>,
81 pub(crate) file_memory_dir: Option<PathBuf>,
83 pub session_store: Option<Arc<dyn crate::store::SessionStore>>,
85 pub session_id: Option<String>,
87 pub auto_save: bool,
89 pub max_parse_retries: Option<u32>,
92 pub tool_timeout_ms: Option<u64>,
95 pub circuit_breaker_threshold: Option<u32>,
99 pub sandbox_config: Option<crate::sandbox::SandboxConfig>,
105}
106
107impl std::fmt::Debug for SessionOptions {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.debug_struct("SessionOptions")
110 .field("model", &self.model)
111 .field("agent_dirs", &self.agent_dirs)
112 .field("queue_config", &self.queue_config)
113 .field("security_provider", &self.security_provider.is_some())
114 .field("context_providers", &self.context_providers.len())
115 .field("confirmation_manager", &self.confirmation_manager.is_some())
116 .field("permission_checker", &self.permission_checker.is_some())
117 .field("planning_enabled", &self.planning_enabled)
118 .field("goal_tracking", &self.goal_tracking)
119 .field(
120 "skill_registry",
121 &self
122 .skill_registry
123 .as_ref()
124 .map(|r| format!("{} skills", r.len())),
125 )
126 .field("memory_store", &self.memory_store.is_some())
127 .field("session_store", &self.session_store.is_some())
128 .field("session_id", &self.session_id)
129 .field("auto_save", &self.auto_save)
130 .field("max_parse_retries", &self.max_parse_retries)
131 .field("tool_timeout_ms", &self.tool_timeout_ms)
132 .field("circuit_breaker_threshold", &self.circuit_breaker_threshold)
133 .field("sandbox_config", &self.sandbox_config)
134 .finish()
135 }
136}
137
138impl SessionOptions {
139 pub fn new() -> Self {
140 Self::default()
141 }
142
143 pub fn with_model(mut self, model: impl Into<String>) -> Self {
144 self.model = Some(model.into());
145 self
146 }
147
148 pub fn with_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
149 self.agent_dirs.push(dir.into());
150 self
151 }
152
153 pub fn with_queue_config(mut self, config: SessionQueueConfig) -> Self {
154 self.queue_config = Some(config);
155 self
156 }
157
158 pub fn with_default_security(mut self) -> Self {
160 self.security_provider = Some(Arc::new(crate::security::DefaultSecurityProvider::new()));
161 self
162 }
163
164 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 pub fn with_fs_context(mut self, root_path: impl Into<PathBuf>) -> Self {
175 let config = crate::context::FileSystemContextConfig::new(root_path);
176 self.context_providers
177 .push(Arc::new(crate::context::FileSystemContextProvider::new(
178 config,
179 )));
180 self
181 }
182
183 pub fn with_context_provider(
185 mut self,
186 provider: Arc<dyn crate::context::ContextProvider>,
187 ) -> Self {
188 self.context_providers.push(provider);
189 self
190 }
191
192 pub fn with_confirmation_manager(
194 mut self,
195 manager: Arc<dyn crate::hitl::ConfirmationProvider>,
196 ) -> Self {
197 self.confirmation_manager = Some(manager);
198 self
199 }
200
201 pub fn with_permission_checker(
203 mut self,
204 checker: Arc<dyn crate::permissions::PermissionChecker>,
205 ) -> Self {
206 self.permission_checker = Some(checker);
207 self
208 }
209
210 pub fn with_permissive_policy(self) -> Self {
217 self.with_permission_checker(Arc::new(crate::permissions::PermissionPolicy::permissive()))
218 }
219
220 pub fn with_planning(mut self, enabled: bool) -> Self {
222 self.planning_enabled = enabled;
223 self
224 }
225
226 pub fn with_goal_tracking(mut self, enabled: bool) -> Self {
228 self.goal_tracking = enabled;
229 self
230 }
231
232 pub fn with_builtin_skills(mut self) -> Self {
234 self.skill_registry = Some(Arc::new(crate::skills::SkillRegistry::with_builtins()));
235 self
236 }
237
238 pub fn with_skill_registry(mut self, registry: Arc<crate::skills::SkillRegistry>) -> Self {
240 self.skill_registry = Some(registry);
241 self
242 }
243
244 pub fn with_skills_from_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
246 let registry = self
247 .skill_registry
248 .unwrap_or_else(|| Arc::new(crate::skills::SkillRegistry::new()));
249 let _ = registry.load_from_dir(dir);
250 self.skill_registry = Some(registry);
251 self
252 }
253
254 pub fn with_memory(mut self, store: Arc<dyn crate::memory::MemoryStore>) -> Self {
256 self.memory_store = Some(store);
257 self
258 }
259
260 pub fn with_file_memory(mut self, dir: impl Into<PathBuf>) -> Self {
266 self.file_memory_dir = Some(dir.into());
267 self
268 }
269
270 pub fn with_session_store(mut self, store: Arc<dyn crate::store::SessionStore>) -> Self {
272 self.session_store = Some(store);
273 self
274 }
275
276 pub fn with_file_session_store(mut self, dir: impl Into<PathBuf>) -> Self {
278 let dir = dir.into();
279 match tokio::runtime::Handle::try_current() {
280 Ok(handle) => {
281 match tokio::task::block_in_place(|| {
282 handle.block_on(crate::store::FileSessionStore::new(dir))
283 }) {
284 Ok(store) => {
285 self.session_store =
286 Some(Arc::new(store) as Arc<dyn crate::store::SessionStore>);
287 }
288 Err(e) => {
289 tracing::warn!("Failed to create file session store: {}", e);
290 }
291 }
292 }
293 Err(_) => {
294 tracing::warn!(
295 "No async runtime available for file session store — persistence disabled"
296 );
297 }
298 }
299 self
300 }
301
302 pub fn with_session_id(mut self, id: impl Into<String>) -> Self {
304 self.session_id = Some(id.into());
305 self
306 }
307
308 pub fn with_auto_save(mut self, enabled: bool) -> Self {
310 self.auto_save = enabled;
311 self
312 }
313
314 pub fn with_parse_retries(mut self, max: u32) -> Self {
320 self.max_parse_retries = Some(max);
321 self
322 }
323
324 pub fn with_tool_timeout(mut self, timeout_ms: u64) -> Self {
330 self.tool_timeout_ms = Some(timeout_ms);
331 self
332 }
333
334 pub fn with_circuit_breaker(mut self, threshold: u32) -> Self {
340 self.circuit_breaker_threshold = Some(threshold);
341 self
342 }
343
344 pub fn with_resilience_defaults(self) -> Self {
350 self.with_parse_retries(2)
351 .with_tool_timeout(120_000)
352 .with_circuit_breaker(3)
353 }
354
355 pub fn with_sandbox(mut self, config: crate::sandbox::SandboxConfig) -> Self {
374 self.sandbox_config = Some(config);
375 self
376 }
377}
378
379pub struct Agent {
388 llm_client: Arc<dyn LlmClient>,
389 code_config: CodeConfig,
390 config: AgentConfig,
391}
392
393impl std::fmt::Debug for Agent {
394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 f.debug_struct("Agent").finish()
396 }
397}
398
399impl Agent {
400 pub async fn new(config_source: impl Into<String>) -> Result<Self> {
404 let source = config_source.into();
405 let path = Path::new(&source);
406
407 let config = if path.extension().is_some() && path.exists() {
408 CodeConfig::from_file(path)
409 .with_context(|| format!("Failed to load config: {}", path.display()))?
410 } else {
411 CodeConfig::from_hcl(&source).context("Failed to parse config as HCL string")?
413 };
414
415 Self::from_config(config).await
416 }
417
418 pub async fn create(config_source: impl Into<String>) -> Result<Self> {
423 Self::new(config_source).await
424 }
425
426 pub async fn from_config(config: CodeConfig) -> Result<Self> {
428 let llm_config = config
429 .default_llm_config()
430 .context("default_model must be set in 'provider/model' format with a valid API key")?;
431 let llm_client = crate::llm::create_client_with_config(llm_config);
432
433 let agent_config = AgentConfig {
434 max_tool_rounds: config
435 .max_tool_rounds
436 .unwrap_or(AgentConfig::default().max_tool_rounds),
437 ..AgentConfig::default()
438 };
439
440 Ok(Agent {
441 llm_client,
442 code_config: config,
443 config: agent_config,
444 })
445 }
446
447 pub fn session(
452 &self,
453 workspace: impl Into<String>,
454 options: Option<SessionOptions>,
455 ) -> Result<AgentSession> {
456 let opts = options.unwrap_or_default();
457
458 let llm_client = if let Some(ref model) = opts.model {
459 let (provider_name, model_id) = model
460 .split_once('/')
461 .context("model format must be 'provider/model' (e.g., 'openai/gpt-4o')")?;
462
463 let llm_config = self
464 .code_config
465 .llm_config(provider_name, model_id)
466 .with_context(|| {
467 format!("provider '{provider_name}' or model '{model_id}' not found in config")
468 })?;
469
470 crate::llm::create_client_with_config(llm_config)
471 } else {
472 self.llm_client.clone()
473 };
474
475 self.build_session(workspace.into(), llm_client, &opts)
476 }
477
478 pub fn resume_session(
486 &self,
487 session_id: &str,
488 options: SessionOptions,
489 ) -> Result<AgentSession> {
490 let store = options.session_store.as_ref().ok_or_else(|| {
491 crate::error::CodeError::Session(
492 "resume_session requires a session_store in SessionOptions".to_string(),
493 )
494 })?;
495
496 let data = match tokio::runtime::Handle::try_current() {
498 Ok(handle) => tokio::task::block_in_place(|| handle.block_on(store.load(session_id)))
499 .map_err(|e| {
500 crate::error::CodeError::Session(format!(
501 "Failed to load session {}: {}",
502 session_id, e
503 ))
504 })?,
505 Err(_) => {
506 return Err(crate::error::CodeError::Session(
507 "No async runtime available for session resume".to_string(),
508 ))
509 }
510 };
511
512 let data = data.ok_or_else(|| {
513 crate::error::CodeError::Session(format!("Session not found: {}", session_id))
514 })?;
515
516 let mut opts = options;
518 opts.session_id = Some(data.id.clone());
519
520 let llm_client = if let Some(ref model) = opts.model {
521 let (provider_name, model_id) = model
522 .split_once('/')
523 .context("model format must be 'provider/model'")?;
524 let llm_config = self
525 .code_config
526 .llm_config(provider_name, model_id)
527 .with_context(|| {
528 format!("provider '{provider_name}' or model '{model_id}' not found")
529 })?;
530 crate::llm::create_client_with_config(llm_config)
531 } else {
532 self.llm_client.clone()
533 };
534
535 let session = self.build_session(data.config.workspace.clone(), llm_client, &opts)?;
536
537 *session.history.write().unwrap() = data.messages;
539
540 Ok(session)
541 }
542
543 fn build_session(
544 &self,
545 workspace: String,
546 llm_client: Arc<dyn LlmClient>,
547 opts: &SessionOptions,
548 ) -> Result<AgentSession> {
549 let canonical =
550 std::fs::canonicalize(&workspace).unwrap_or_else(|_| PathBuf::from(&workspace));
551
552 let tool_executor = Arc::new(ToolExecutor::new(canonical.display().to_string()));
553 let tool_defs = tool_executor.definitions();
554
555 let mut system_prompt = self.config.system_prompt.clone();
557 if let Some(ref registry) = opts.skill_registry {
558 let skill_prompt = registry.to_system_prompt();
559 if !skill_prompt.is_empty() {
560 system_prompt = match system_prompt {
561 Some(existing) => Some(format!("{}\n\n{}", existing, skill_prompt)),
562 None => Some(skill_prompt),
563 };
564 }
565 }
566
567 let base = self.config.clone();
568 let config = AgentConfig {
569 system_prompt,
570 tools: tool_defs,
571 permission_checker: opts.permission_checker.clone(),
572 confirmation_manager: opts.confirmation_manager.clone(),
573 context_providers: opts.context_providers.clone(),
574 planning_enabled: opts.planning_enabled,
575 goal_tracking: opts.goal_tracking,
576 skill_registry: opts.skill_registry.clone(),
577 max_parse_retries: opts.max_parse_retries.unwrap_or(base.max_parse_retries),
578 tool_timeout_ms: opts.tool_timeout_ms.or(base.tool_timeout_ms),
579 circuit_breaker_threshold: opts
580 .circuit_breaker_threshold
581 .unwrap_or(base.circuit_breaker_threshold),
582 ..base
583 };
584
585 let command_queue = if let Some(ref queue_config) = opts.queue_config {
587 let (event_tx, _) = broadcast::channel(256);
588 let session_id = uuid::Uuid::new_v4().to_string();
589 let rt = tokio::runtime::Handle::try_current();
590
591 match rt {
592 Ok(handle) => {
593 let queue = tokio::task::block_in_place(|| {
595 handle.block_on(SessionLaneQueue::new(
596 &session_id,
597 queue_config.clone(),
598 event_tx,
599 ))
600 });
601 match queue {
602 Ok(q) => {
603 let q = Arc::new(q);
605 let q2 = Arc::clone(&q);
606 tokio::task::block_in_place(|| {
607 handle.block_on(async { q2.start().await.ok() })
608 });
609 Some(q)
610 }
611 Err(e) => {
612 tracing::warn!("Failed to create session lane queue: {}", e);
613 None
614 }
615 }
616 }
617 Err(_) => {
618 tracing::warn!(
619 "No async runtime available for queue creation — queue disabled"
620 );
621 None
622 }
623 }
624 } else {
625 None
626 };
627
628 let mut tool_context = ToolContext::new(canonical.clone());
630 if let Some(ref search_config) = self.code_config.search {
631 tool_context = tool_context.with_search_config(search_config.clone());
632 }
633
634 #[cfg(feature = "sandbox")]
636 if let Some(ref sandbox_cfg) = opts.sandbox_config {
637 let handle: Arc<dyn crate::sandbox::BashSandbox> =
638 Arc::new(crate::sandbox::BoxSandboxHandle::new(
639 sandbox_cfg.clone(),
640 canonical.display().to_string(),
641 ));
642 tool_executor.registry().set_sandbox(Arc::clone(&handle));
645 tool_context = tool_context.with_sandbox(handle);
646 }
647 #[cfg(not(feature = "sandbox"))]
648 if opts.sandbox_config.is_some() {
649 tracing::warn!(
650 "sandbox_config is set but the `sandbox` Cargo feature is not enabled \
651 — bash commands will run locally"
652 );
653 }
654
655 let memory = {
657 let store = if let Some(ref store) = opts.memory_store {
658 Some(Arc::clone(store))
659 } else if let Some(ref dir) = opts.file_memory_dir {
660 match tokio::runtime::Handle::try_current() {
661 Ok(handle) => {
662 let dir = dir.clone();
663 match tokio::task::block_in_place(|| {
664 handle.block_on(crate::memory::FileMemoryStore::new(dir))
665 }) {
666 Ok(store) => {
667 Some(Arc::new(store) as Arc<dyn crate::memory::MemoryStore>)
668 }
669 Err(e) => {
670 tracing::warn!("Failed to create file memory store: {}", e);
671 None
672 }
673 }
674 }
675 Err(_) => {
676 tracing::warn!(
677 "No async runtime available for file memory store — memory disabled"
678 );
679 None
680 }
681 }
682 } else {
683 None
684 };
685 store.map(crate::memory::AgentMemory::new)
686 };
687
688 let session_id = opts
689 .session_id
690 .clone()
691 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
692
693 Ok(AgentSession {
694 llm_client,
695 tool_executor,
696 tool_context,
697 config,
698 workspace: canonical,
699 session_id,
700 history: RwLock::new(Vec::new()),
701 command_queue,
702 memory,
703 session_store: opts.session_store.clone(),
704 auto_save: opts.auto_save,
705 })
706 }
707}
708
709pub struct AgentSession {
718 llm_client: Arc<dyn LlmClient>,
719 tool_executor: Arc<ToolExecutor>,
720 tool_context: ToolContext,
721 config: AgentConfig,
722 workspace: PathBuf,
723 session_id: String,
725 history: RwLock<Vec<Message>>,
727 command_queue: Option<Arc<SessionLaneQueue>>,
729 memory: Option<crate::memory::AgentMemory>,
731 session_store: Option<Arc<dyn crate::store::SessionStore>>,
733 auto_save: bool,
735}
736
737impl std::fmt::Debug for AgentSession {
738 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
739 f.debug_struct("AgentSession")
740 .field("session_id", &self.session_id)
741 .field("workspace", &self.workspace.display().to_string())
742 .field("auto_save", &self.auto_save)
743 .finish()
744 }
745}
746
747impl AgentSession {
748 fn build_agent_loop(&self) -> AgentLoop {
752 let mut agent_loop = AgentLoop::new(
753 self.llm_client.clone(),
754 self.tool_executor.clone(),
755 self.tool_context.clone(),
756 self.config.clone(),
757 );
758 if let Some(ref queue) = self.command_queue {
759 agent_loop = agent_loop.with_queue(Arc::clone(queue));
760 }
761 agent_loop
762 }
763
764 pub async fn send(&self, prompt: &str, history: Option<&[Message]>) -> Result<AgentResult> {
770 let agent_loop = self.build_agent_loop();
771
772 let use_internal = history.is_none();
773 let effective_history = match history {
774 Some(h) => h.to_vec(),
775 None => self.history.read().unwrap().clone(),
776 };
777
778 let result = agent_loop.execute(&effective_history, prompt, None).await?;
779
780 if use_internal {
783 *self.history.write().unwrap() = result.messages.clone();
784
785 if self.auto_save {
787 if let Err(e) = self.save().await {
788 tracing::warn!("Auto-save failed for session {}: {}", self.session_id, e);
789 }
790 }
791 }
792
793 Ok(result)
794 }
795
796 pub async fn send_with_attachments(
801 &self,
802 prompt: &str,
803 attachments: &[crate::llm::Attachment],
804 history: Option<&[Message]>,
805 ) -> Result<AgentResult> {
806 let use_internal = history.is_none();
810 let mut effective_history = match history {
811 Some(h) => h.to_vec(),
812 None => self.history.read().unwrap().clone(),
813 };
814 effective_history.push(Message::user_with_attachments(prompt, attachments));
815
816 let agent_loop = self.build_agent_loop();
817 let result = agent_loop
818 .execute_from_messages(effective_history, None, None)
819 .await?;
820
821 if use_internal {
822 *self.history.write().unwrap() = result.messages.clone();
823 if self.auto_save {
824 if let Err(e) = self.save().await {
825 tracing::warn!("Auto-save failed for session {}: {}", self.session_id, e);
826 }
827 }
828 }
829
830 Ok(result)
831 }
832
833 pub async fn stream_with_attachments(
838 &self,
839 prompt: &str,
840 attachments: &[crate::llm::Attachment],
841 history: Option<&[Message]>,
842 ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
843 let (tx, rx) = mpsc::channel(256);
844 let mut effective_history = match history {
845 Some(h) => h.to_vec(),
846 None => self.history.read().unwrap().clone(),
847 };
848 effective_history.push(Message::user_with_attachments(prompt, attachments));
849
850 let agent_loop = self.build_agent_loop();
851 let handle = tokio::spawn(async move {
852 let _ = agent_loop
853 .execute_from_messages(effective_history, None, Some(tx))
854 .await;
855 });
856
857 Ok((rx, handle))
858 }
859
860 pub async fn stream(
867 &self,
868 prompt: &str,
869 history: Option<&[Message]>,
870 ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
871 let (tx, rx) = mpsc::channel(256);
872 let agent_loop = self.build_agent_loop();
873 let effective_history = match history {
874 Some(h) => h.to_vec(),
875 None => self.history.read().unwrap().clone(),
876 };
877 let prompt = prompt.to_string();
878
879 let handle = tokio::spawn(async move {
880 let _ = agent_loop
881 .execute(&effective_history, &prompt, Some(tx))
882 .await;
883 });
884
885 Ok((rx, handle))
886 }
887
888 pub fn history(&self) -> Vec<Message> {
890 self.history.read().unwrap().clone()
891 }
892
893 pub fn memory(&self) -> Option<&crate::memory::AgentMemory> {
895 self.memory.as_ref()
896 }
897
898 pub fn session_id(&self) -> &str {
900 &self.session_id
901 }
902
903 pub async fn save(&self) -> Result<()> {
907 let store = match &self.session_store {
908 Some(s) => s,
909 None => return Ok(()),
910 };
911
912 let history = self.history.read().unwrap().clone();
913 let now = chrono::Utc::now().timestamp();
914
915 let data = crate::store::SessionData {
916 id: self.session_id.clone(),
917 config: crate::session::SessionConfig {
918 name: String::new(),
919 workspace: self.workspace.display().to_string(),
920 system_prompt: self.config.system_prompt.clone(),
921 max_context_length: 200_000,
922 auto_compact: false,
923 auto_compact_threshold: crate::session::DEFAULT_AUTO_COMPACT_THRESHOLD,
924 storage_type: crate::config::StorageBackend::File,
925 queue_config: None,
926 confirmation_policy: None,
927 permission_policy: None,
928 parent_id: None,
929 security_config: None,
930 hook_engine: None,
931 planning_enabled: self.config.planning_enabled,
932 goal_tracking: self.config.goal_tracking,
933 },
934 state: crate::session::SessionState::Active,
935 messages: history,
936 context_usage: crate::session::ContextUsage::default(),
937 total_usage: crate::llm::TokenUsage::default(),
938 total_cost: 0.0,
939 model_name: None,
940 cost_records: Vec::new(),
941 tool_names: crate::store::SessionData::tool_names_from_definitions(&self.config.tools),
942 thinking_enabled: false,
943 thinking_budget: None,
944 created_at: now,
945 updated_at: now,
946 llm_config: None,
947 tasks: Vec::new(),
948 parent_id: None,
949 };
950
951 store.save(&data).await?;
952 tracing::debug!("Session {} saved", self.session_id);
953 Ok(())
954 }
955
956 pub async fn read_file(&self, path: &str) -> Result<String> {
958 let args = serde_json::json!({ "file_path": path });
959 let result = self.tool_executor.execute("read", &args).await?;
960 Ok(result.output)
961 }
962
963 pub async fn bash(&self, command: &str) -> Result<String> {
968 let args = serde_json::json!({ "command": command });
969 let result = self
970 .tool_executor
971 .execute_with_context("bash", &args, &self.tool_context)
972 .await?;
973 Ok(result.output)
974 }
975
976 pub async fn glob(&self, pattern: &str) -> Result<Vec<String>> {
978 let args = serde_json::json!({ "pattern": pattern });
979 let result = self.tool_executor.execute("glob", &args).await?;
980 let files: Vec<String> = result
981 .output
982 .lines()
983 .filter(|l| !l.is_empty())
984 .map(|l| l.to_string())
985 .collect();
986 Ok(files)
987 }
988
989 pub async fn grep(&self, pattern: &str) -> Result<String> {
991 let args = serde_json::json!({ "pattern": pattern });
992 let result = self.tool_executor.execute("grep", &args).await?;
993 Ok(result.output)
994 }
995
996 pub async fn tool(&self, name: &str, args: serde_json::Value) -> Result<ToolCallResult> {
998 let result = self.tool_executor.execute(name, &args).await?;
999 Ok(ToolCallResult {
1000 name: name.to_string(),
1001 output: result.output,
1002 exit_code: result.exit_code,
1003 })
1004 }
1005
1006 pub fn has_queue(&self) -> bool {
1012 self.command_queue.is_some()
1013 }
1014
1015 pub async fn set_lane_handler(&self, lane: SessionLane, config: LaneHandlerConfig) {
1019 if let Some(ref queue) = self.command_queue {
1020 queue.set_lane_handler(lane, config).await;
1021 }
1022 }
1023
1024 pub async fn complete_external_task(&self, task_id: &str, result: ExternalTaskResult) -> bool {
1028 if let Some(ref queue) = self.command_queue {
1029 queue.complete_external_task(task_id, result).await
1030 } else {
1031 false
1032 }
1033 }
1034
1035 pub async fn pending_external_tasks(&self) -> Vec<ExternalTask> {
1037 if let Some(ref queue) = self.command_queue {
1038 queue.pending_external_tasks().await
1039 } else {
1040 Vec::new()
1041 }
1042 }
1043
1044 pub async fn queue_stats(&self) -> SessionQueueStats {
1046 if let Some(ref queue) = self.command_queue {
1047 queue.stats().await
1048 } else {
1049 SessionQueueStats::default()
1050 }
1051 }
1052
1053 pub async fn queue_metrics(&self) -> Option<MetricsSnapshot> {
1055 if let Some(ref queue) = self.command_queue {
1056 queue.metrics_snapshot().await
1057 } else {
1058 None
1059 }
1060 }
1061
1062 pub async fn dead_letters(&self) -> Vec<DeadLetter> {
1064 if let Some(ref queue) = self.command_queue {
1065 queue.dead_letters().await
1066 } else {
1067 Vec::new()
1068 }
1069 }
1070}
1071
1072#[cfg(test)]
1077mod tests {
1078 use super::*;
1079 use crate::config::{ModelConfig, ModelModalities, ProviderConfig};
1080 use crate::store::SessionStore;
1081
1082 fn test_config() -> CodeConfig {
1083 CodeConfig {
1084 default_model: Some("anthropic/claude-sonnet-4-20250514".to_string()),
1085 providers: vec![
1086 ProviderConfig {
1087 name: "anthropic".to_string(),
1088 api_key: Some("test-key".to_string()),
1089 base_url: None,
1090 models: vec![ModelConfig {
1091 id: "claude-sonnet-4-20250514".to_string(),
1092 name: "Claude Sonnet 4".to_string(),
1093 family: "claude-sonnet".to_string(),
1094 api_key: None,
1095 base_url: None,
1096 attachment: false,
1097 reasoning: false,
1098 tool_call: true,
1099 temperature: true,
1100 release_date: None,
1101 modalities: ModelModalities::default(),
1102 cost: Default::default(),
1103 limit: Default::default(),
1104 }],
1105 },
1106 ProviderConfig {
1107 name: "openai".to_string(),
1108 api_key: Some("test-openai-key".to_string()),
1109 base_url: None,
1110 models: vec![ModelConfig {
1111 id: "gpt-4o".to_string(),
1112 name: "GPT-4o".to_string(),
1113 family: "gpt-4".to_string(),
1114 api_key: None,
1115 base_url: None,
1116 attachment: false,
1117 reasoning: false,
1118 tool_call: true,
1119 temperature: true,
1120 release_date: None,
1121 modalities: ModelModalities::default(),
1122 cost: Default::default(),
1123 limit: Default::default(),
1124 }],
1125 },
1126 ],
1127 ..Default::default()
1128 }
1129 }
1130
1131 #[tokio::test]
1132 async fn test_from_config() {
1133 let agent = Agent::from_config(test_config()).await;
1134 assert!(agent.is_ok());
1135 }
1136
1137 #[tokio::test]
1138 async fn test_session_default() {
1139 let agent = Agent::from_config(test_config()).await.unwrap();
1140 let session = agent.session("/tmp/test-workspace", None);
1141 assert!(session.is_ok());
1142 let debug = format!("{:?}", session.unwrap());
1143 assert!(debug.contains("AgentSession"));
1144 }
1145
1146 #[tokio::test]
1147 async fn test_session_with_model_override() {
1148 let agent = Agent::from_config(test_config()).await.unwrap();
1149 let opts = SessionOptions::new().with_model("openai/gpt-4o");
1150 let session = agent.session("/tmp/test-workspace", Some(opts));
1151 assert!(session.is_ok());
1152 }
1153
1154 #[tokio::test]
1155 async fn test_session_with_invalid_model_format() {
1156 let agent = Agent::from_config(test_config()).await.unwrap();
1157 let opts = SessionOptions::new().with_model("gpt-4o");
1158 let session = agent.session("/tmp/test-workspace", Some(opts));
1159 assert!(session.is_err());
1160 }
1161
1162 #[tokio::test]
1163 async fn test_session_with_model_not_found() {
1164 let agent = Agent::from_config(test_config()).await.unwrap();
1165 let opts = SessionOptions::new().with_model("openai/nonexistent");
1166 let session = agent.session("/tmp/test-workspace", Some(opts));
1167 assert!(session.is_err());
1168 }
1169
1170 #[tokio::test]
1171 async fn test_new_with_hcl_string() {
1172 let hcl = r#"
1173 default_model = "anthropic/claude-sonnet-4-20250514"
1174 providers {
1175 name = "anthropic"
1176 api_key = "test-key"
1177 models {
1178 id = "claude-sonnet-4-20250514"
1179 name = "Claude Sonnet 4"
1180 }
1181 }
1182 "#;
1183 let agent = Agent::new(hcl).await;
1184 assert!(agent.is_ok());
1185 }
1186
1187 #[tokio::test]
1188 async fn test_create_alias_hcl() {
1189 let hcl = r#"
1190 default_model = "anthropic/claude-sonnet-4-20250514"
1191 providers {
1192 name = "anthropic"
1193 api_key = "test-key"
1194 models {
1195 id = "claude-sonnet-4-20250514"
1196 name = "Claude Sonnet 4"
1197 }
1198 }
1199 "#;
1200 let agent = Agent::create(hcl).await;
1201 assert!(agent.is_ok());
1202 }
1203
1204 #[tokio::test]
1205 async fn test_create_and_new_produce_same_result() {
1206 let hcl = r#"
1207 default_model = "anthropic/claude-sonnet-4-20250514"
1208 providers {
1209 name = "anthropic"
1210 api_key = "test-key"
1211 models {
1212 id = "claude-sonnet-4-20250514"
1213 name = "Claude Sonnet 4"
1214 }
1215 }
1216 "#;
1217 let agent_new = Agent::new(hcl).await;
1218 let agent_create = Agent::create(hcl).await;
1219 assert!(agent_new.is_ok());
1220 assert!(agent_create.is_ok());
1221
1222 let session_new = agent_new.unwrap().session("/tmp/test-ws-new", None);
1224 let session_create = agent_create.unwrap().session("/tmp/test-ws-create", None);
1225 assert!(session_new.is_ok());
1226 assert!(session_create.is_ok());
1227 }
1228
1229 #[test]
1230 fn test_from_config_requires_default_model() {
1231 let rt = tokio::runtime::Runtime::new().unwrap();
1232 let config = CodeConfig {
1233 providers: vec![ProviderConfig {
1234 name: "anthropic".to_string(),
1235 api_key: Some("test-key".to_string()),
1236 base_url: None,
1237 models: vec![],
1238 }],
1239 ..Default::default()
1240 };
1241 let result = rt.block_on(Agent::from_config(config));
1242 assert!(result.is_err());
1243 }
1244
1245 #[tokio::test]
1246 async fn test_history_empty_on_new_session() {
1247 let agent = Agent::from_config(test_config()).await.unwrap();
1248 let session = agent.session("/tmp/test-workspace", None).unwrap();
1249 assert!(session.history().is_empty());
1250 }
1251
1252 #[tokio::test]
1253 async fn test_session_options_with_agent_dir() {
1254 let opts = SessionOptions::new()
1255 .with_agent_dir("/tmp/agents")
1256 .with_agent_dir("/tmp/more-agents");
1257 assert_eq!(opts.agent_dirs.len(), 2);
1258 assert_eq!(opts.agent_dirs[0], PathBuf::from("/tmp/agents"));
1259 assert_eq!(opts.agent_dirs[1], PathBuf::from("/tmp/more-agents"));
1260 }
1261
1262 #[test]
1267 fn test_session_options_with_queue_config() {
1268 let qc = SessionQueueConfig::default().with_lane_features();
1269 let opts = SessionOptions::new().with_queue_config(qc.clone());
1270 assert!(opts.queue_config.is_some());
1271
1272 let config = opts.queue_config.unwrap();
1273 assert!(config.enable_dlq);
1274 assert!(config.enable_metrics);
1275 assert!(config.enable_alerts);
1276 assert_eq!(config.default_timeout_ms, Some(60_000));
1277 }
1278
1279 #[tokio::test(flavor = "multi_thread")]
1280 async fn test_session_with_queue_config() {
1281 let agent = Agent::from_config(test_config()).await.unwrap();
1282 let qc = SessionQueueConfig::default();
1283 let opts = SessionOptions::new().with_queue_config(qc);
1284 let session = agent.session("/tmp/test-workspace-queue", Some(opts));
1285 assert!(session.is_ok());
1286 let session = session.unwrap();
1287 assert!(session.has_queue());
1288 }
1289
1290 #[tokio::test]
1291 async fn test_session_without_queue_config() {
1292 let agent = Agent::from_config(test_config()).await.unwrap();
1293 let session = agent.session("/tmp/test-workspace-noqueue", None).unwrap();
1294 assert!(!session.has_queue());
1295 }
1296
1297 #[tokio::test]
1298 async fn test_session_queue_stats_without_queue() {
1299 let agent = Agent::from_config(test_config()).await.unwrap();
1300 let session = agent.session("/tmp/test-workspace-stats", None).unwrap();
1301 let stats = session.queue_stats().await;
1302 assert_eq!(stats.total_pending, 0);
1304 assert_eq!(stats.total_active, 0);
1305 }
1306
1307 #[tokio::test(flavor = "multi_thread")]
1308 async fn test_session_queue_stats_with_queue() {
1309 let agent = Agent::from_config(test_config()).await.unwrap();
1310 let qc = SessionQueueConfig::default();
1311 let opts = SessionOptions::new().with_queue_config(qc);
1312 let session = agent
1313 .session("/tmp/test-workspace-qstats", Some(opts))
1314 .unwrap();
1315 let stats = session.queue_stats().await;
1316 assert_eq!(stats.total_pending, 0);
1318 assert_eq!(stats.total_active, 0);
1319 }
1320
1321 #[tokio::test(flavor = "multi_thread")]
1322 async fn test_session_pending_external_tasks_empty() {
1323 let agent = Agent::from_config(test_config()).await.unwrap();
1324 let qc = SessionQueueConfig::default();
1325 let opts = SessionOptions::new().with_queue_config(qc);
1326 let session = agent
1327 .session("/tmp/test-workspace-ext", Some(opts))
1328 .unwrap();
1329 let tasks = session.pending_external_tasks().await;
1330 assert!(tasks.is_empty());
1331 }
1332
1333 #[tokio::test(flavor = "multi_thread")]
1334 async fn test_session_dead_letters_empty() {
1335 let agent = Agent::from_config(test_config()).await.unwrap();
1336 let qc = SessionQueueConfig::default().with_dlq(Some(100));
1337 let opts = SessionOptions::new().with_queue_config(qc);
1338 let session = agent
1339 .session("/tmp/test-workspace-dlq", Some(opts))
1340 .unwrap();
1341 let dead = session.dead_letters().await;
1342 assert!(dead.is_empty());
1343 }
1344
1345 #[tokio::test(flavor = "multi_thread")]
1346 async fn test_session_queue_metrics_disabled() {
1347 let agent = Agent::from_config(test_config()).await.unwrap();
1348 let qc = SessionQueueConfig::default();
1350 let opts = SessionOptions::new().with_queue_config(qc);
1351 let session = agent
1352 .session("/tmp/test-workspace-nomet", Some(opts))
1353 .unwrap();
1354 let metrics = session.queue_metrics().await;
1355 assert!(metrics.is_none());
1356 }
1357
1358 #[tokio::test(flavor = "multi_thread")]
1359 async fn test_session_queue_metrics_enabled() {
1360 let agent = Agent::from_config(test_config()).await.unwrap();
1361 let qc = SessionQueueConfig::default().with_metrics();
1362 let opts = SessionOptions::new().with_queue_config(qc);
1363 let session = agent
1364 .session("/tmp/test-workspace-met", Some(opts))
1365 .unwrap();
1366 let metrics = session.queue_metrics().await;
1367 assert!(metrics.is_some());
1368 }
1369
1370 #[tokio::test(flavor = "multi_thread")]
1371 async fn test_session_set_lane_handler() {
1372 let agent = Agent::from_config(test_config()).await.unwrap();
1373 let qc = SessionQueueConfig::default();
1374 let opts = SessionOptions::new().with_queue_config(qc);
1375 let session = agent
1376 .session("/tmp/test-workspace-handler", Some(opts))
1377 .unwrap();
1378
1379 session
1381 .set_lane_handler(
1382 SessionLane::Execute,
1383 LaneHandlerConfig {
1384 mode: crate::queue::TaskHandlerMode::External,
1385 timeout_ms: 30_000,
1386 },
1387 )
1388 .await;
1389
1390 }
1393
1394 #[tokio::test(flavor = "multi_thread")]
1399 async fn test_session_has_id() {
1400 let agent = Agent::from_config(test_config()).await.unwrap();
1401 let session = agent.session("/tmp/test-ws-id", None).unwrap();
1402 assert!(!session.session_id().is_empty());
1404 assert_eq!(session.session_id().len(), 36); }
1406
1407 #[tokio::test(flavor = "multi_thread")]
1408 async fn test_session_explicit_id() {
1409 let agent = Agent::from_config(test_config()).await.unwrap();
1410 let opts = SessionOptions::new().with_session_id("my-session-42");
1411 let session = agent.session("/tmp/test-ws-eid", Some(opts)).unwrap();
1412 assert_eq!(session.session_id(), "my-session-42");
1413 }
1414
1415 #[tokio::test(flavor = "multi_thread")]
1416 async fn test_session_save_no_store() {
1417 let agent = Agent::from_config(test_config()).await.unwrap();
1418 let session = agent.session("/tmp/test-ws-save", None).unwrap();
1419 session.save().await.unwrap();
1421 }
1422
1423 #[tokio::test(flavor = "multi_thread")]
1424 async fn test_session_save_and_load() {
1425 let store = Arc::new(crate::store::MemorySessionStore::new());
1426 let agent = Agent::from_config(test_config()).await.unwrap();
1427
1428 let opts = SessionOptions::new()
1429 .with_session_store(store.clone())
1430 .with_session_id("persist-test");
1431 let session = agent.session("/tmp/test-ws-persist", Some(opts)).unwrap();
1432
1433 session.save().await.unwrap();
1435
1436 assert!(store.exists("persist-test").await.unwrap());
1438
1439 let data = store.load("persist-test").await.unwrap().unwrap();
1440 assert_eq!(data.id, "persist-test");
1441 assert!(data.messages.is_empty());
1442 }
1443
1444 #[tokio::test(flavor = "multi_thread")]
1445 async fn test_session_save_with_history() {
1446 let store = Arc::new(crate::store::MemorySessionStore::new());
1447 let agent = Agent::from_config(test_config()).await.unwrap();
1448
1449 let opts = SessionOptions::new()
1450 .with_session_store(store.clone())
1451 .with_session_id("history-test");
1452 let session = agent.session("/tmp/test-ws-hist", Some(opts)).unwrap();
1453
1454 {
1456 let mut h = session.history.write().unwrap();
1457 h.push(Message::user("Hello"));
1458 h.push(Message::user("How are you?"));
1459 }
1460
1461 session.save().await.unwrap();
1462
1463 let data = store.load("history-test").await.unwrap().unwrap();
1464 assert_eq!(data.messages.len(), 2);
1465 }
1466
1467 #[tokio::test(flavor = "multi_thread")]
1468 async fn test_resume_session() {
1469 let store = Arc::new(crate::store::MemorySessionStore::new());
1470 let agent = Agent::from_config(test_config()).await.unwrap();
1471
1472 let opts = SessionOptions::new()
1474 .with_session_store(store.clone())
1475 .with_session_id("resume-test");
1476 let session = agent.session("/tmp/test-ws-resume", Some(opts)).unwrap();
1477 {
1478 let mut h = session.history.write().unwrap();
1479 h.push(Message::user("What is Rust?"));
1480 h.push(Message::user("Tell me more"));
1481 }
1482 session.save().await.unwrap();
1483
1484 let opts2 = SessionOptions::new().with_session_store(store.clone());
1486 let resumed = agent.resume_session("resume-test", opts2).unwrap();
1487
1488 assert_eq!(resumed.session_id(), "resume-test");
1489 let history = resumed.history();
1490 assert_eq!(history.len(), 2);
1491 assert_eq!(history[0].text(), "What is Rust?");
1492 }
1493
1494 #[tokio::test(flavor = "multi_thread")]
1495 async fn test_resume_session_not_found() {
1496 let store = Arc::new(crate::store::MemorySessionStore::new());
1497 let agent = Agent::from_config(test_config()).await.unwrap();
1498
1499 let opts = SessionOptions::new().with_session_store(store.clone());
1500 let result = agent.resume_session("nonexistent", opts);
1501 assert!(result.is_err());
1502 assert!(result.unwrap_err().to_string().contains("not found"));
1503 }
1504
1505 #[tokio::test(flavor = "multi_thread")]
1506 async fn test_resume_session_no_store() {
1507 let agent = Agent::from_config(test_config()).await.unwrap();
1508 let opts = SessionOptions::new();
1509 let result = agent.resume_session("any-id", opts);
1510 assert!(result.is_err());
1511 assert!(result.unwrap_err().to_string().contains("session_store"));
1512 }
1513
1514 #[tokio::test(flavor = "multi_thread")]
1515 async fn test_file_session_store_persistence() {
1516 let dir = tempfile::TempDir::new().unwrap();
1517 let store = Arc::new(
1518 crate::store::FileSessionStore::new(dir.path())
1519 .await
1520 .unwrap(),
1521 );
1522 let agent = Agent::from_config(test_config()).await.unwrap();
1523
1524 let opts = SessionOptions::new()
1526 .with_session_store(store.clone())
1527 .with_session_id("file-persist");
1528 let session = agent
1529 .session("/tmp/test-ws-file-persist", Some(opts))
1530 .unwrap();
1531 {
1532 let mut h = session.history.write().unwrap();
1533 h.push(Message::user("test message"));
1534 }
1535 session.save().await.unwrap();
1536
1537 let store2 = Arc::new(
1539 crate::store::FileSessionStore::new(dir.path())
1540 .await
1541 .unwrap(),
1542 );
1543 let data = store2.load("file-persist").await.unwrap().unwrap();
1544 assert_eq!(data.messages.len(), 1);
1545 }
1546
1547 #[tokio::test(flavor = "multi_thread")]
1548 async fn test_session_options_builders() {
1549 let opts = SessionOptions::new()
1550 .with_session_id("test-id")
1551 .with_auto_save(true);
1552 assert_eq!(opts.session_id, Some("test-id".to_string()));
1553 assert!(opts.auto_save);
1554 }
1555
1556 #[test]
1561 fn test_session_options_with_sandbox_sets_config() {
1562 use crate::sandbox::SandboxConfig;
1563 let cfg = SandboxConfig {
1564 image: "ubuntu:22.04".into(),
1565 memory_mb: 1024,
1566 ..SandboxConfig::default()
1567 };
1568 let opts = SessionOptions::new().with_sandbox(cfg);
1569 assert!(opts.sandbox_config.is_some());
1570 let sc = opts.sandbox_config.unwrap();
1571 assert_eq!(sc.image, "ubuntu:22.04");
1572 assert_eq!(sc.memory_mb, 1024);
1573 }
1574
1575 #[test]
1576 fn test_session_options_default_has_no_sandbox() {
1577 let opts = SessionOptions::default();
1578 assert!(opts.sandbox_config.is_none());
1579 }
1580
1581 #[tokio::test]
1582 async fn test_session_debug_includes_sandbox_config() {
1583 use crate::sandbox::SandboxConfig;
1584 let opts = SessionOptions::new().with_sandbox(SandboxConfig::default());
1585 let debug = format!("{:?}", opts);
1586 assert!(debug.contains("sandbox_config"));
1587 }
1588
1589 #[tokio::test]
1590 async fn test_session_build_with_sandbox_config_no_feature_warn() {
1591 let agent = Agent::from_config(test_config()).await.unwrap();
1594 let opts = SessionOptions::new().with_sandbox(crate::sandbox::SandboxConfig::default());
1595 let session = agent.session("/tmp/test-sandbox-session", Some(opts));
1597 assert!(session.is_ok());
1598 }
1599}