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)]
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}
80
81impl std::fmt::Debug for SessionOptions {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("SessionOptions")
84 .field("model", &self.model)
85 .field("agent_dirs", &self.agent_dirs)
86 .field("queue_config", &self.queue_config)
87 .field("security_provider", &self.security_provider.is_some())
88 .field("context_providers", &self.context_providers.len())
89 .field("confirmation_manager", &self.confirmation_manager.is_some())
90 .field("permission_checker", &self.permission_checker.is_some())
91 .field("planning_enabled", &self.planning_enabled)
92 .field("goal_tracking", &self.goal_tracking)
93 .field("skill_registry", &self.skill_registry.as_ref().map(|r| format!("{} skills", r.len())))
94 .finish()
95 }
96}
97
98impl SessionOptions {
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn with_model(mut self, model: impl Into<String>) -> Self {
104 self.model = Some(model.into());
105 self
106 }
107
108 pub fn with_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
109 self.agent_dirs.push(dir.into());
110 self
111 }
112
113 pub fn with_queue_config(mut self, config: SessionQueueConfig) -> Self {
114 self.queue_config = Some(config);
115 self
116 }
117
118 pub fn with_default_security(mut self) -> Self {
120 self.security_provider = Some(Arc::new(crate::security::DefaultSecurityProvider::new()));
121 self
122 }
123
124 pub fn with_security_provider(mut self, provider: Arc<dyn crate::security::SecurityProvider>) -> Self {
126 self.security_provider = Some(provider);
127 self
128 }
129
130 pub fn with_fs_context(mut self, root_path: impl Into<PathBuf>) -> Self {
132 let config = crate::context::FileSystemContextConfig::new(root_path);
133 self.context_providers.push(Arc::new(crate::context::FileSystemContextProvider::new(config)));
134 self
135 }
136
137 pub fn with_context_provider(mut self, provider: Arc<dyn crate::context::ContextProvider>) -> Self {
139 self.context_providers.push(provider);
140 self
141 }
142
143 pub fn with_confirmation_manager(mut self, manager: Arc<dyn crate::hitl::ConfirmationProvider>) -> Self {
145 self.confirmation_manager = Some(manager);
146 self
147 }
148
149 pub fn with_permission_checker(mut self, checker: Arc<dyn crate::permissions::PermissionChecker>) -> Self {
151 self.permission_checker = Some(checker);
152 self
153 }
154
155 pub fn with_planning(mut self, enabled: bool) -> Self {
157 self.planning_enabled = enabled;
158 self
159 }
160
161 pub fn with_goal_tracking(mut self, enabled: bool) -> Self {
163 self.goal_tracking = enabled;
164 self
165 }
166
167 pub fn with_builtin_skills(mut self) -> Self {
169 self.skill_registry = Some(Arc::new(crate::skills::SkillRegistry::with_builtins()));
170 self
171 }
172
173 pub fn with_skill_registry(mut self, registry: Arc<crate::skills::SkillRegistry>) -> Self {
175 self.skill_registry = Some(registry);
176 self
177 }
178
179 pub fn with_skills_from_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
181 let registry = self.skill_registry.unwrap_or_else(|| Arc::new(crate::skills::SkillRegistry::new()));
182 let _ = registry.load_from_dir(dir);
183 self.skill_registry = Some(registry);
184 self
185 }
186}
187
188impl Default for SessionOptions {
189 fn default() -> Self {
190 Self {
191 model: None,
192 agent_dirs: Vec::new(),
193 queue_config: None,
194 security_provider: None,
195 context_providers: Vec::new(),
196 confirmation_manager: None,
197 permission_checker: None,
198 planning_enabled: false,
199 goal_tracking: false,
200 skill_registry: None,
201 }
202 }
203}
204
205pub struct Agent {
214 llm_client: Arc<dyn LlmClient>,
215 code_config: CodeConfig,
216 config: AgentConfig,
217}
218
219impl std::fmt::Debug for Agent {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 f.debug_struct("Agent").finish()
222 }
223}
224
225impl Agent {
226 pub async fn new(config_source: impl Into<String>) -> Result<Self> {
230 let source = config_source.into();
231 let path = Path::new(&source);
232
233 let config = if path.extension().is_some() && path.exists() {
234 CodeConfig::from_file(path)
235 .with_context(|| format!("Failed to load config: {}", path.display()))?
236 } else {
237 CodeConfig::from_hcl(&source)
239 .context("Failed to parse config as HCL string")?
240 };
241
242 Self::from_config(config).await
243 }
244
245 pub async fn create(config_source: impl Into<String>) -> Result<Self> {
250 Self::new(config_source).await
251 }
252
253 pub async fn from_config(config: CodeConfig) -> Result<Self> {
255 let llm_config = config
256 .default_llm_config()
257 .context("default_model must be set in 'provider/model' format with a valid API key")?;
258 let llm_client = crate::llm::create_client_with_config(llm_config);
259
260 let agent_config = AgentConfig {
261 max_tool_rounds: config
262 .max_tool_rounds
263 .unwrap_or(AgentConfig::default().max_tool_rounds),
264 ..AgentConfig::default()
265 };
266
267 Ok(Agent {
268 llm_client,
269 code_config: config,
270 config: agent_config,
271 })
272 }
273
274 pub fn session(
279 &self,
280 workspace: impl Into<String>,
281 options: Option<SessionOptions>,
282 ) -> Result<AgentSession> {
283 let opts = options.unwrap_or_default();
284
285 let llm_client = if let Some(ref model) = opts.model {
286 let (provider_name, model_id) = model
287 .split_once('/')
288 .context("model format must be 'provider/model' (e.g., 'openai/gpt-4o')")?;
289
290 let llm_config = self
291 .code_config
292 .llm_config(provider_name, model_id)
293 .with_context(|| {
294 format!("provider '{provider_name}' or model '{model_id}' not found in config")
295 })?;
296
297 crate::llm::create_client_with_config(llm_config)
298 } else {
299 self.llm_client.clone()
300 };
301
302 self.build_session(workspace.into(), llm_client, &opts)
303 }
304
305 fn build_session(
306 &self,
307 workspace: String,
308 llm_client: Arc<dyn LlmClient>,
309 opts: &SessionOptions,
310 ) -> Result<AgentSession> {
311 let canonical =
312 std::fs::canonicalize(&workspace).unwrap_or_else(|_| PathBuf::from(&workspace));
313
314 let tool_executor = Arc::new(ToolExecutor::new(canonical.display().to_string()));
315 let tool_defs = tool_executor.definitions();
316
317 let mut system_prompt = self.config.system_prompt.clone();
319 if let Some(ref registry) = opts.skill_registry {
320 let skill_prompt = registry.to_system_prompt();
321 if !skill_prompt.is_empty() {
322 system_prompt = match system_prompt {
323 Some(existing) => Some(format!("{}\n\n{}", existing, skill_prompt)),
324 None => Some(skill_prompt),
325 };
326 }
327 }
328
329 let config = AgentConfig {
330 system_prompt,
331 tools: tool_defs,
332 permission_checker: opts.permission_checker.clone(),
333 confirmation_manager: opts.confirmation_manager.clone(),
334 context_providers: opts.context_providers.clone(),
335 planning_enabled: opts.planning_enabled,
336 goal_tracking: opts.goal_tracking,
337 skill_registry: opts.skill_registry.clone(),
338 ..self.config.clone()
339 };
340
341 let command_queue = if let Some(ref queue_config) = opts.queue_config {
343 let (event_tx, _) = broadcast::channel(256);
344 let session_id = uuid::Uuid::new_v4().to_string();
345 let rt = tokio::runtime::Handle::try_current();
346 match rt {
347 Ok(handle) => {
348 let queue = tokio::task::block_in_place(|| {
350 handle.block_on(SessionLaneQueue::new(
351 &session_id,
352 queue_config.clone(),
353 event_tx,
354 ))
355 });
356 match queue {
357 Ok(q) => {
358 let q = Arc::new(q);
360 let q2 = Arc::clone(&q);
361 tokio::task::block_in_place(|| {
362 handle.block_on(async { q2.start().await.ok() })
363 });
364 Some(q)
365 }
366 Err(e) => {
367 tracing::warn!("Failed to create session lane queue: {}", e);
368 None
369 }
370 }
371 }
372 Err(_) => {
373 tracing::warn!(
374 "No async runtime available for queue creation — queue disabled"
375 );
376 None
377 }
378 }
379 } else {
380 None
381 };
382
383 let mut tool_context = ToolContext::new(canonical.clone());
385 if let Some(ref search_config) = self.code_config.search {
386 tool_context = tool_context.with_search_config(search_config.clone());
387 }
388
389 Ok(AgentSession {
390 llm_client,
391 tool_executor,
392 tool_context,
393 config,
394 workspace: canonical,
395 history: RwLock::new(Vec::new()),
396 command_queue,
397 })
398 }
399}
400
401pub struct AgentSession {
410 llm_client: Arc<dyn LlmClient>,
411 tool_executor: Arc<ToolExecutor>,
412 tool_context: ToolContext,
413 config: AgentConfig,
414 workspace: PathBuf,
415 history: RwLock<Vec<Message>>,
417 command_queue: Option<Arc<SessionLaneQueue>>,
419}
420
421impl std::fmt::Debug for AgentSession {
422 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423 f.debug_struct("AgentSession")
424 .field("workspace", &self.workspace.display().to_string())
425 .finish()
426 }
427}
428
429impl AgentSession {
430 fn build_agent_loop(&self) -> AgentLoop {
435 let mut agent_loop = AgentLoop::new(
436 self.llm_client.clone(),
437 self.tool_executor.clone(),
438 self.tool_context.clone(),
439 self.config.clone(),
440 );
441 if let Some(ref queue) = self.command_queue {
442 agent_loop = agent_loop.with_queue(Arc::clone(queue));
443 }
444 agent_loop
445 }
446
447 pub async fn send(&self, prompt: &str, history: Option<&[Message]>) -> Result<AgentResult> {
453 let agent_loop = self.build_agent_loop();
454
455 let use_internal = history.is_none();
456 let effective_history = match history {
457 Some(h) => h.to_vec(),
458 None => self.history.read().unwrap().clone(),
459 };
460
461 let result = agent_loop.execute(&effective_history, prompt, None).await?;
462
463 if use_internal {
466 *self.history.write().unwrap() = result.messages.clone();
467 }
468
469 Ok(result)
470 }
471
472 pub async fn stream(
479 &self,
480 prompt: &str,
481 history: Option<&[Message]>,
482 ) -> Result<(mpsc::Receiver<AgentEvent>, JoinHandle<()>)> {
483 let (tx, rx) = mpsc::channel(256);
484 let agent_loop = self.build_agent_loop();
485 let effective_history = match history {
486 Some(h) => h.to_vec(),
487 None => self.history.read().unwrap().clone(),
488 };
489 let prompt = prompt.to_string();
490
491 let handle = tokio::spawn(async move {
492 let _ = agent_loop
493 .execute(&effective_history, &prompt, Some(tx))
494 .await;
495 });
496
497 Ok((rx, handle))
498 }
499
500 pub fn history(&self) -> Vec<Message> {
502 self.history.read().unwrap().clone()
503 }
504
505 pub async fn read_file(&self, path: &str) -> Result<String> {
507 let args = serde_json::json!({ "file_path": path });
508 let result = self.tool_executor.execute("read", &args).await?;
509 Ok(result.output)
510 }
511
512 pub async fn bash(&self, command: &str) -> Result<String> {
514 let args = serde_json::json!({ "command": command });
515 let result = self.tool_executor.execute("bash", &args).await?;
516 Ok(result.output)
517 }
518
519 pub async fn glob(&self, pattern: &str) -> Result<Vec<String>> {
521 let args = serde_json::json!({ "pattern": pattern });
522 let result = self.tool_executor.execute("glob", &args).await?;
523 let files: Vec<String> = result
524 .output
525 .lines()
526 .filter(|l| !l.is_empty())
527 .map(|l| l.to_string())
528 .collect();
529 Ok(files)
530 }
531
532 pub async fn grep(&self, pattern: &str) -> Result<String> {
534 let args = serde_json::json!({ "pattern": pattern });
535 let result = self.tool_executor.execute("grep", &args).await?;
536 Ok(result.output)
537 }
538
539 pub async fn tool(&self, name: &str, args: serde_json::Value) -> Result<ToolCallResult> {
541 let result = self.tool_executor.execute(name, &args).await?;
542 Ok(ToolCallResult {
543 name: name.to_string(),
544 output: result.output,
545 exit_code: result.exit_code,
546 })
547 }
548
549 pub fn has_queue(&self) -> bool {
555 self.command_queue.is_some()
556 }
557
558 pub async fn set_lane_handler(&self, lane: SessionLane, config: LaneHandlerConfig) {
562 if let Some(ref queue) = self.command_queue {
563 queue.set_lane_handler(lane, config).await;
564 }
565 }
566
567 pub async fn complete_external_task(&self, task_id: &str, result: ExternalTaskResult) -> bool {
571 if let Some(ref queue) = self.command_queue {
572 queue.complete_external_task(task_id, result).await
573 } else {
574 false
575 }
576 }
577
578 pub async fn pending_external_tasks(&self) -> Vec<ExternalTask> {
580 if let Some(ref queue) = self.command_queue {
581 queue.pending_external_tasks().await
582 } else {
583 Vec::new()
584 }
585 }
586
587 pub async fn queue_stats(&self) -> SessionQueueStats {
589 if let Some(ref queue) = self.command_queue {
590 queue.stats().await
591 } else {
592 SessionQueueStats::default()
593 }
594 }
595
596 pub async fn queue_metrics(&self) -> Option<MetricsSnapshot> {
598 if let Some(ref queue) = self.command_queue {
599 queue.metrics_snapshot().await
600 } else {
601 None
602 }
603 }
604
605 pub async fn dead_letters(&self) -> Vec<DeadLetter> {
607 if let Some(ref queue) = self.command_queue {
608 queue.dead_letters().await
609 } else {
610 Vec::new()
611 }
612 }
613}
614
615#[cfg(test)]
620mod tests {
621 use super::*;
622 use crate::config::{ModelConfig, ModelModalities, ProviderConfig};
623
624 fn test_config() -> CodeConfig {
625 CodeConfig {
626 default_model: Some("anthropic/claude-sonnet-4-20250514".to_string()),
627 providers: vec![
628 ProviderConfig {
629 name: "anthropic".to_string(),
630 api_key: Some("test-key".to_string()),
631 base_url: None,
632 models: vec![ModelConfig {
633 id: "claude-sonnet-4-20250514".to_string(),
634 name: "Claude Sonnet 4".to_string(),
635 family: "claude-sonnet".to_string(),
636 api_key: None,
637 base_url: None,
638 attachment: false,
639 reasoning: false,
640 tool_call: true,
641 temperature: true,
642 release_date: None,
643 modalities: ModelModalities::default(),
644 cost: Default::default(),
645 limit: Default::default(),
646 }],
647 },
648 ProviderConfig {
649 name: "openai".to_string(),
650 api_key: Some("test-openai-key".to_string()),
651 base_url: None,
652 models: vec![ModelConfig {
653 id: "gpt-4o".to_string(),
654 name: "GPT-4o".to_string(),
655 family: "gpt-4".to_string(),
656 api_key: None,
657 base_url: None,
658 attachment: false,
659 reasoning: false,
660 tool_call: true,
661 temperature: true,
662 release_date: None,
663 modalities: ModelModalities::default(),
664 cost: Default::default(),
665 limit: Default::default(),
666 }],
667 },
668 ],
669 ..Default::default()
670 }
671 }
672
673 #[tokio::test]
674 async fn test_from_config() {
675 let agent = Agent::from_config(test_config()).await;
676 assert!(agent.is_ok());
677 }
678
679 #[tokio::test]
680 async fn test_session_default() {
681 let agent = Agent::from_config(test_config()).await.unwrap();
682 let session = agent.session("/tmp/test-workspace", None);
683 assert!(session.is_ok());
684 let debug = format!("{:?}", session.unwrap());
685 assert!(debug.contains("AgentSession"));
686 }
687
688 #[tokio::test]
689 async fn test_session_with_model_override() {
690 let agent = Agent::from_config(test_config()).await.unwrap();
691 let opts = SessionOptions::new().with_model("openai/gpt-4o");
692 let session = agent.session("/tmp/test-workspace", Some(opts));
693 assert!(session.is_ok());
694 }
695
696 #[tokio::test]
697 async fn test_session_with_invalid_model_format() {
698 let agent = Agent::from_config(test_config()).await.unwrap();
699 let opts = SessionOptions::new().with_model("gpt-4o");
700 let session = agent.session("/tmp/test-workspace", Some(opts));
701 assert!(session.is_err());
702 }
703
704 #[tokio::test]
705 async fn test_session_with_model_not_found() {
706 let agent = Agent::from_config(test_config()).await.unwrap();
707 let opts = SessionOptions::new().with_model("openai/nonexistent");
708 let session = agent.session("/tmp/test-workspace", Some(opts));
709 assert!(session.is_err());
710 }
711
712 #[tokio::test]
713 async fn test_new_with_hcl_string() {
714 let hcl = r#"
715 default_model = "anthropic/claude-sonnet-4-20250514"
716 providers {
717 name = "anthropic"
718 api_key = "test-key"
719 models {
720 id = "claude-sonnet-4-20250514"
721 name = "Claude Sonnet 4"
722 }
723 }
724 "#;
725 let agent = Agent::new(hcl).await;
726 assert!(agent.is_ok());
727 }
728
729 #[tokio::test]
730 async fn test_create_alias_hcl() {
731 let hcl = r#"
732 default_model = "anthropic/claude-sonnet-4-20250514"
733 providers {
734 name = "anthropic"
735 api_key = "test-key"
736 models {
737 id = "claude-sonnet-4-20250514"
738 name = "Claude Sonnet 4"
739 }
740 }
741 "#;
742 let agent = Agent::create(hcl).await;
743 assert!(agent.is_ok());
744 }
745
746 #[tokio::test]
747 async fn test_create_and_new_produce_same_result() {
748 let hcl = r#"
749 default_model = "anthropic/claude-sonnet-4-20250514"
750 providers {
751 name = "anthropic"
752 api_key = "test-key"
753 models {
754 id = "claude-sonnet-4-20250514"
755 name = "Claude Sonnet 4"
756 }
757 }
758 "#;
759 let agent_new = Agent::new(hcl).await;
760 let agent_create = Agent::create(hcl).await;
761 assert!(agent_new.is_ok());
762 assert!(agent_create.is_ok());
763
764 let session_new = agent_new.unwrap().session("/tmp/test-ws-new", None);
766 let session_create = agent_create.unwrap().session("/tmp/test-ws-create", None);
767 assert!(session_new.is_ok());
768 assert!(session_create.is_ok());
769 }
770
771 #[test]
772 fn test_from_config_requires_default_model() {
773 let rt = tokio::runtime::Runtime::new().unwrap();
774 let config = CodeConfig {
775 providers: vec![ProviderConfig {
776 name: "anthropic".to_string(),
777 api_key: Some("test-key".to_string()),
778 base_url: None,
779 models: vec![],
780 }],
781 ..Default::default()
782 };
783 let result = rt.block_on(Agent::from_config(config));
784 assert!(result.is_err());
785 }
786
787 #[tokio::test]
788 async fn test_history_empty_on_new_session() {
789 let agent = Agent::from_config(test_config()).await.unwrap();
790 let session = agent.session("/tmp/test-workspace", None).unwrap();
791 assert!(session.history().is_empty());
792 }
793
794
795 #[tokio::test]
796 async fn test_session_options_with_agent_dir() {
797 let opts = SessionOptions::new()
798 .with_agent_dir("/tmp/agents")
799 .with_agent_dir("/tmp/more-agents");
800 assert_eq!(opts.agent_dirs.len(), 2);
801 assert_eq!(opts.agent_dirs[0], PathBuf::from("/tmp/agents"));
802 assert_eq!(opts.agent_dirs[1], PathBuf::from("/tmp/more-agents"));
803 }
804
805
806
807
808 #[test]
813 fn test_session_options_with_queue_config() {
814 let qc = SessionQueueConfig::default().with_lane_features();
815 let opts = SessionOptions::new().with_queue_config(qc.clone());
816 assert!(opts.queue_config.is_some());
817
818 let config = opts.queue_config.unwrap();
819 assert!(config.enable_dlq);
820 assert!(config.enable_metrics);
821 assert!(config.enable_alerts);
822 assert_eq!(config.default_timeout_ms, Some(60_000));
823 }
824
825 #[tokio::test(flavor = "multi_thread")]
826 async fn test_session_with_queue_config() {
827 let agent = Agent::from_config(test_config()).await.unwrap();
828 let qc = SessionQueueConfig::default();
829 let opts = SessionOptions::new().with_queue_config(qc);
830 let session = agent.session("/tmp/test-workspace-queue", Some(opts));
831 assert!(session.is_ok());
832 let session = session.unwrap();
833 assert!(session.has_queue());
834 }
835
836 #[tokio::test]
837 async fn test_session_without_queue_config() {
838 let agent = Agent::from_config(test_config()).await.unwrap();
839 let session = agent.session("/tmp/test-workspace-noqueue", None).unwrap();
840 assert!(!session.has_queue());
841 }
842
843 #[tokio::test]
844 async fn test_session_queue_stats_without_queue() {
845 let agent = Agent::from_config(test_config()).await.unwrap();
846 let session = agent.session("/tmp/test-workspace-stats", None).unwrap();
847 let stats = session.queue_stats().await;
848 assert_eq!(stats.total_pending, 0);
850 assert_eq!(stats.total_active, 0);
851 }
852
853 #[tokio::test(flavor = "multi_thread")]
854 async fn test_session_queue_stats_with_queue() {
855 let agent = Agent::from_config(test_config()).await.unwrap();
856 let qc = SessionQueueConfig::default();
857 let opts = SessionOptions::new().with_queue_config(qc);
858 let session = agent
859 .session("/tmp/test-workspace-qstats", Some(opts))
860 .unwrap();
861 let stats = session.queue_stats().await;
862 assert_eq!(stats.total_pending, 0);
864 assert_eq!(stats.total_active, 0);
865 }
866
867 #[tokio::test(flavor = "multi_thread")]
868 async fn test_session_pending_external_tasks_empty() {
869 let agent = Agent::from_config(test_config()).await.unwrap();
870 let qc = SessionQueueConfig::default();
871 let opts = SessionOptions::new().with_queue_config(qc);
872 let session = agent
873 .session("/tmp/test-workspace-ext", Some(opts))
874 .unwrap();
875 let tasks = session.pending_external_tasks().await;
876 assert!(tasks.is_empty());
877 }
878
879 #[tokio::test(flavor = "multi_thread")]
880 async fn test_session_dead_letters_empty() {
881 let agent = Agent::from_config(test_config()).await.unwrap();
882 let qc = SessionQueueConfig::default().with_dlq(Some(100));
883 let opts = SessionOptions::new().with_queue_config(qc);
884 let session = agent
885 .session("/tmp/test-workspace-dlq", Some(opts))
886 .unwrap();
887 let dead = session.dead_letters().await;
888 assert!(dead.is_empty());
889 }
890
891 #[tokio::test(flavor = "multi_thread")]
892 async fn test_session_queue_metrics_disabled() {
893 let agent = Agent::from_config(test_config()).await.unwrap();
894 let qc = SessionQueueConfig::default();
896 let opts = SessionOptions::new().with_queue_config(qc);
897 let session = agent
898 .session("/tmp/test-workspace-nomet", Some(opts))
899 .unwrap();
900 let metrics = session.queue_metrics().await;
901 assert!(metrics.is_none());
902 }
903
904 #[tokio::test(flavor = "multi_thread")]
905 async fn test_session_queue_metrics_enabled() {
906 let agent = Agent::from_config(test_config()).await.unwrap();
907 let qc = SessionQueueConfig::default().with_metrics();
908 let opts = SessionOptions::new().with_queue_config(qc);
909 let session = agent
910 .session("/tmp/test-workspace-met", Some(opts))
911 .unwrap();
912 let metrics = session.queue_metrics().await;
913 assert!(metrics.is_some());
914 }
915
916 #[tokio::test(flavor = "multi_thread")]
917 async fn test_session_set_lane_handler() {
918 let agent = Agent::from_config(test_config()).await.unwrap();
919 let qc = SessionQueueConfig::default();
920 let opts = SessionOptions::new().with_queue_config(qc);
921 let session = agent
922 .session("/tmp/test-workspace-handler", Some(opts))
923 .unwrap();
924
925 session
927 .set_lane_handler(
928 SessionLane::Execute,
929 LaneHandlerConfig {
930 mode: crate::queue::TaskHandlerMode::External,
931 timeout_ms: 30_000,
932 },
933 )
934 .await;
935
936 }
939}