1use crate::backend::{ExecutionBackend, ExecutionBackendError, ExecutionBackendFactory};
4use awaken_contract::contract::executor::LlmExecutor;
5use awaken_contract::contract::inference::ContextWindowPolicy;
6use awaken_contract::contract::stream_checkpoint::StreamCheckpointStore;
7use awaken_contract::contract::tool::Tool;
8use awaken_contract::registry_spec::{AgentSpec, RemoteEndpoint};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use crate::error::RuntimeError;
13use crate::execution::{SequentialToolExecutor, ToolExecutor};
14use crate::phase::ExecutionEnv;
15
16#[derive(Clone)]
21pub struct ResolvedAgent {
22 pub spec: Arc<AgentSpec>,
24 pub upstream_model: String,
26 pub tools: HashMap<String, Arc<dyn Tool>>,
27 pub llm_executor: Arc<dyn LlmExecutor>,
28 pub tool_executor: Arc<dyn ToolExecutor>,
29 pub context_summarizer: Option<Arc<dyn crate::context::ContextSummarizer>>,
32 pub background_manager: Option<Arc<crate::extensions::background::BackgroundTaskManager>>,
36 pub stream_checkpoint_store: Option<Arc<dyn StreamCheckpointStore>>,
42 pub env: ExecutionEnv,
44}
45
46impl ResolvedAgent {
47 pub fn new(
49 id: impl Into<String>,
50 upstream_model: impl Into<String>,
51 system_prompt: impl Into<String>,
52 llm_executor: Arc<dyn LlmExecutor>,
53 ) -> Self {
54 let upstream_model = upstream_model.into();
55 let spec = Arc::new(AgentSpec {
56 id: id.into(),
57 model_id: upstream_model.clone(),
58 system_prompt: system_prompt.into(),
59 max_rounds: 16,
60 max_continuation_retries: 2,
61 context_policy: None,
62 reasoning_effort: None,
63 plugin_ids: Vec::new(),
64 active_hook_filter: Default::default(),
65 allowed_tools: None,
66 excluded_tools: None,
67 endpoint: None,
68 delegates: Vec::new(),
69 sections: Default::default(),
70 registry: None,
71 });
72 Self {
73 spec,
74 upstream_model,
75 tools: HashMap::new(),
76 llm_executor,
77 tool_executor: Arc::new(SequentialToolExecutor),
78 context_summarizer: None,
79 background_manager: None,
80 stream_checkpoint_store: None,
81 env: ExecutionEnv::empty(),
82 }
83 }
84
85 #[must_use]
89 pub fn with_stream_checkpoint_store(mut self, store: Arc<dyn StreamCheckpointStore>) -> Self {
90 self.stream_checkpoint_store = Some(store);
91 self
92 }
93
94 pub fn id(&self) -> &str {
97 &self.spec.id
98 }
99
100 pub fn model_id(&self) -> &str {
101 &self.spec.model_id
102 }
103
104 pub fn system_prompt(&self) -> &str {
105 &self.spec.system_prompt
106 }
107
108 pub fn max_rounds(&self) -> usize {
109 self.spec.max_rounds
110 }
111
112 pub fn context_policy(&self) -> Option<&ContextWindowPolicy> {
113 self.spec.context_policy.as_ref()
114 }
115
116 pub fn max_continuation_retries(&self) -> usize {
117 self.spec.max_continuation_retries
118 }
119
120 #[must_use]
123 pub fn with_tool_executor(mut self, executor: Arc<dyn ToolExecutor>) -> Self {
124 self.tool_executor = executor;
125 self
126 }
127
128 #[must_use]
129 pub fn with_max_rounds(mut self, max_rounds: usize) -> Self {
130 let mut spec = (*self.spec).clone();
131 spec.max_rounds = max_rounds;
132 self.spec = Arc::new(spec);
133 self
134 }
135
136 #[must_use]
137 pub fn with_tool(mut self, tool: Arc<dyn Tool>) -> Self {
138 let desc = tool.descriptor();
139 self.tools.insert(desc.id, tool);
140 self
141 }
142
143 #[must_use]
144 pub fn with_tools(mut self, tools: Vec<Arc<dyn Tool>>) -> Self {
145 for tool in tools {
146 let desc = tool.descriptor();
147 self.tools.insert(desc.id, tool);
148 }
149 self
150 }
151
152 #[must_use]
153 pub fn with_context_policy(mut self, policy: ContextWindowPolicy) -> Self {
154 let mut spec = (*self.spec).clone();
155 spec.context_policy = Some(policy);
156 self.spec = Arc::new(spec);
157 self
158 }
159
160 #[must_use]
161 pub fn with_context_summarizer(
162 mut self,
163 summarizer: Arc<dyn crate::context::ContextSummarizer>,
164 ) -> Self {
165 self.context_summarizer = Some(summarizer);
166 self
167 }
168
169 #[must_use]
173 pub fn with_background_manager(
174 mut self,
175 manager: Arc<crate::extensions::background::BackgroundTaskManager>,
176 ) -> Self {
177 self.background_manager = Some(manager);
178 self
179 }
180
181 #[must_use]
182 pub fn with_max_continuation_retries(mut self, n: usize) -> Self {
183 let mut spec = (*self.spec).clone();
184 spec.max_continuation_retries = n;
185 self.spec = Arc::new(spec);
186 self
187 }
188
189 pub fn tool_descriptors(&self) -> Vec<awaken_contract::contract::tool::ToolDescriptor> {
190 let mut descs: Vec<_> = self.tools.values().map(|t| t.descriptor()).collect();
191 descs.sort_by(|a, b| a.id.cmp(&b.id));
192 descs
193 }
194}
195
196impl std::fmt::Debug for ResolvedAgent {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 f.debug_struct("ResolvedAgent")
199 .field("agent_id", &self.id())
200 .finish_non_exhaustive()
201 }
202}
203
204#[derive(Clone)]
206pub struct ResolvedBackendAgent {
207 pub spec: Arc<AgentSpec>,
208 target: ResolvedBackendTarget,
209 backend_cache: Arc<std::sync::Mutex<Option<Arc<dyn ExecutionBackend>>>>,
210}
211
212#[derive(Clone)]
213enum ResolvedBackendTarget {
214 Ready(Arc<dyn ExecutionBackend>),
215 Factory {
216 factory: Arc<dyn ExecutionBackendFactory>,
217 endpoint: RemoteEndpoint,
218 },
219}
220
221impl ResolvedBackendAgent {
222 #[must_use]
223 pub fn with_backend(spec: Arc<AgentSpec>, backend: Arc<dyn ExecutionBackend>) -> Self {
224 Self {
225 spec,
226 target: ResolvedBackendTarget::Ready(backend),
227 backend_cache: Arc::new(std::sync::Mutex::new(None)),
228 }
229 }
230
231 #[must_use]
232 pub fn with_factory(
233 spec: Arc<AgentSpec>,
234 factory: Arc<dyn ExecutionBackendFactory>,
235 endpoint: RemoteEndpoint,
236 ) -> Self {
237 Self {
238 spec,
239 target: ResolvedBackendTarget::Factory { factory, endpoint },
240 backend_cache: Arc::new(std::sync::Mutex::new(None)),
241 }
242 }
243
244 pub fn backend(&self) -> Result<Arc<dyn ExecutionBackend>, ExecutionBackendError> {
245 match &self.target {
246 ResolvedBackendTarget::Ready(backend) => Ok(backend.clone()),
247 ResolvedBackendTarget::Factory { factory, endpoint } => {
248 if let Some(backend) = self
249 .backend_cache
250 .lock()
251 .map_err(|_| {
252 ExecutionBackendError::ExecutionFailed("backend cache lock poisoned".into())
253 })?
254 .clone()
255 {
256 return Ok(backend);
257 }
258
259 let backend = factory.build(endpoint).map_err(|error| {
260 ExecutionBackendError::ExecutionFailed(format!(
261 "failed to build backend '{}': {error}",
262 factory.backend()
263 ))
264 })?;
265 *self.backend_cache.lock().map_err(|_| {
266 ExecutionBackendError::ExecutionFailed("backend cache lock poisoned".into())
267 })? = Some(backend.clone());
268 Ok(backend)
269 }
270 }
271 }
272}
273
274#[derive(Clone)]
276pub enum ResolvedExecution {
277 Local(Box<ResolvedAgent>),
278 NonLocal(ResolvedBackendAgent),
279}
280
281impl ResolvedExecution {
282 pub fn local(agent: ResolvedAgent) -> Self {
283 Self::Local(Box::new(agent))
284 }
285
286 pub fn spec(&self) -> &AgentSpec {
287 match self {
288 Self::Local(agent) => agent.spec.as_ref(),
289 Self::NonLocal(agent) => agent.spec.as_ref(),
290 }
291 }
292
293 pub fn as_local(&self) -> Option<&ResolvedAgent> {
294 match self {
295 Self::Local(agent) => Some(agent),
296 Self::NonLocal(_) => None,
297 }
298 }
299
300 pub fn into_local(self) -> Result<ResolvedAgent, RuntimeError> {
301 match self {
302 Self::Local(agent) => Ok(*agent),
303 Self::NonLocal(agent) => Err(RuntimeError::ResolveFailed {
304 message: format!(
305 "agent '{}' is endpoint-backed and cannot be resolved locally",
306 agent.spec.id
307 ),
308 }),
309 }
310 }
311}
312
313impl std::fmt::Debug for ResolvedExecution {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 match self {
316 Self::Local(agent) => f
317 .debug_tuple("ResolvedExecution::Local")
318 .field(agent)
319 .finish(),
320 Self::NonLocal(agent) => f
321 .debug_struct("ResolvedExecution::NonLocal")
322 .field("agent_id", &agent.spec.id)
323 .finish_non_exhaustive(),
324 }
325 }
326}
327
328pub trait AgentResolver: Send + Sync {
335 fn resolve(&self, agent_id: &str) -> Result<ResolvedAgent, RuntimeError>;
336
337 fn agent_ids(&self) -> Vec<String> {
341 Vec::new()
342 }
343}
344
345pub trait ExecutionResolver: AgentResolver {
347 fn resolve_execution(&self, agent_id: &str) -> Result<ResolvedExecution, RuntimeError>;
348}
349
350pub struct LocalExecutionResolver {
353 resolver: Arc<dyn AgentResolver>,
354}
355
356impl LocalExecutionResolver {
357 pub fn new(resolver: Arc<dyn AgentResolver>) -> Self {
358 Self { resolver }
359 }
360}
361
362impl AgentResolver for LocalExecutionResolver {
363 fn resolve(&self, agent_id: &str) -> Result<ResolvedAgent, RuntimeError> {
364 self.resolver.resolve(agent_id)
365 }
366
367 fn agent_ids(&self) -> Vec<String> {
368 self.resolver.agent_ids()
369 }
370}
371
372impl ExecutionResolver for LocalExecutionResolver {
373 fn resolve_execution(&self, agent_id: &str) -> Result<ResolvedExecution, RuntimeError> {
374 self.resolve(agent_id).map(ResolvedExecution::local)
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use async_trait::async_trait;
382 use awaken_contract::contract::executor::{InferenceExecutionError, InferenceRequest};
383 use awaken_contract::contract::inference::{StopReason, StreamResult, TokenUsage};
384 use awaken_contract::contract::tool::{
385 ToolCallContext, ToolDescriptor, ToolError, ToolOutput, ToolResult,
386 };
387 use serde_json::Value;
388
389 struct MockLlm;
390
391 #[async_trait]
392 impl LlmExecutor for MockLlm {
393 async fn execute(
394 &self,
395 _request: InferenceRequest,
396 ) -> Result<StreamResult, InferenceExecutionError> {
397 Ok(StreamResult {
398 content: vec![],
399 tool_calls: vec![],
400 usage: Some(TokenUsage::default()),
401 stop_reason: Some(StopReason::EndTurn),
402 has_incomplete_tool_calls: false,
403 })
404 }
405 fn name(&self) -> &str {
406 "mock"
407 }
408 }
409
410 struct TestTool {
411 id: String,
412 }
413
414 #[async_trait]
415 impl Tool for TestTool {
416 fn descriptor(&self) -> ToolDescriptor {
417 ToolDescriptor::new(&self.id, &self.id, format!("{} tool", self.id))
418 }
419 async fn execute(
420 &self,
421 args: Value,
422 _ctx: &ToolCallContext,
423 ) -> Result<ToolOutput, ToolError> {
424 Ok(ToolResult::success(&self.id, args).into())
425 }
426 }
427
428 fn mock_executor() -> Arc<dyn LlmExecutor> {
429 Arc::new(MockLlm)
430 }
431
432 #[test]
433 fn new_defaults() {
434 let agent = ResolvedAgent::new("agent-1", "model-1", "system prompt", mock_executor());
435 assert_eq!(agent.id(), "agent-1");
436 assert_eq!(agent.upstream_model, "model-1");
437 assert_eq!(agent.system_prompt(), "system prompt");
438 assert_eq!(agent.max_rounds(), 16);
439 assert!(agent.tools.is_empty());
440 assert!(agent.context_policy().is_none());
441 assert!(agent.context_summarizer.is_none());
442 assert!(agent.background_manager.is_none());
443 assert_eq!(agent.max_continuation_retries(), 2);
444 }
445
446 #[test]
447 fn with_background_manager_attaches_handle() {
448 let manager = Arc::new(crate::extensions::background::BackgroundTaskManager::new());
449 let agent = ResolvedAgent::new("a", "m", "s", mock_executor())
450 .with_background_manager(manager.clone());
451 assert!(Arc::ptr_eq(
452 agent.background_manager.as_ref().unwrap(),
453 &manager
454 ));
455 }
456
457 #[test]
458 fn with_max_rounds() {
459 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_max_rounds(100);
460 assert_eq!(agent.max_rounds(), 100);
461 }
462
463 #[test]
464 fn with_tool() {
465 let tool: Arc<dyn Tool> = Arc::new(TestTool { id: "echo".into() });
466 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_tool(tool);
467 assert_eq!(agent.tools.len(), 1);
468 assert!(agent.tools.contains_key("echo"));
469 }
470
471 #[test]
472 fn with_tools() {
473 let tools: Vec<Arc<dyn Tool>> = vec![
474 Arc::new(TestTool { id: "echo".into() }),
475 Arc::new(TestTool {
476 id: "search".into(),
477 }),
478 ];
479 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_tools(tools);
480 assert_eq!(agent.tools.len(), 2);
481 assert!(agent.tools.contains_key("echo"));
482 assert!(agent.tools.contains_key("search"));
483 }
484
485 #[test]
486 fn tool_descriptors() {
487 let tools: Vec<Arc<dyn Tool>> = vec![
488 Arc::new(TestTool { id: "echo".into() }),
489 Arc::new(TestTool {
490 id: "search".into(),
491 }),
492 ];
493 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_tools(tools);
494 let descriptors = agent.tool_descriptors();
495 assert_eq!(descriptors.len(), 2);
496 let ids: Vec<&str> = descriptors.iter().map(|d| d.id.as_str()).collect();
497 assert!(ids.contains(&"echo"));
498 assert!(ids.contains(&"search"));
499 }
500
501 #[test]
502 fn with_context_policy() {
503 use awaken_contract::contract::inference::ContextWindowPolicy;
504
505 let policy = ContextWindowPolicy {
506 max_context_tokens: 8000,
507 max_output_tokens: 2000,
508 min_recent_messages: 4,
509 enable_prompt_cache: true,
510 autocompact_threshold: Some(4096),
511 compaction_mode: Default::default(),
512 compaction_raw_suffix_messages: 3,
513 };
514 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_context_policy(policy);
515 assert!(agent.context_policy().is_some());
516 assert_eq!(agent.context_policy().unwrap().max_context_tokens, 8000);
517 }
518
519 #[test]
520 fn with_max_continuation_retries() {
521 let agent =
522 ResolvedAgent::new("a", "m", "s", mock_executor()).with_max_continuation_retries(5);
523 assert_eq!(agent.max_continuation_retries(), 5);
524 }
525
526 #[test]
527 fn model_id_equals_model_by_default() {
528 let agent = ResolvedAgent::new("a", "claude-3", "s", mock_executor());
529 assert_eq!(agent.model_id(), "claude-3");
530 assert_eq!(agent.upstream_model, "claude-3");
531 }
532
533 #[test]
534 fn clone_works() {
535 let agent = ResolvedAgent::new("a", "m", "s", mock_executor())
536 .with_max_rounds(50)
537 .with_max_continuation_retries(3);
538 let cloned = agent.clone();
539 assert_eq!(cloned.id(), "a");
540 assert_eq!(cloned.max_rounds(), 50);
541 assert_eq!(cloned.max_continuation_retries(), 3);
542 }
543
544 #[test]
545 fn with_tool_executor() {
546 let agent = ResolvedAgent::new("a", "m", "s", mock_executor())
547 .with_tool_executor(Arc::new(crate::execution::SequentialToolExecutor));
548 assert_eq!(agent.tool_executor.name(), "sequential");
549 }
550
551 #[test]
552 fn chained_builder() {
553 let agent = ResolvedAgent::new("agent", "model", "system", mock_executor())
554 .with_max_rounds(10)
555 .with_max_continuation_retries(0)
556 .with_tool(Arc::new(TestTool { id: "t1".into() }))
557 .with_tool(Arc::new(TestTool { id: "t2".into() }));
558
559 assert_eq!(agent.max_rounds(), 10);
560 assert_eq!(agent.max_continuation_retries(), 0);
561 assert_eq!(agent.tools.len(), 2);
562 }
563
564 #[test]
565 fn tool_descriptors_sorted_by_id() {
566 let tools: Vec<Arc<dyn Tool>> = vec![
567 Arc::new(TestTool { id: "zebra".into() }),
568 Arc::new(TestTool { id: "alpha".into() }),
569 Arc::new(TestTool {
570 id: "middle".into(),
571 }),
572 ];
573 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_tools(tools);
574 let descriptors = agent.tool_descriptors();
575 let ids: Vec<&str> = descriptors.iter().map(|d| d.id.as_str()).collect();
576 assert_eq!(ids, vec!["alpha", "middle", "zebra"]);
577 }
578
579 #[test]
580 fn tool_descriptors_empty() {
581 let agent = ResolvedAgent::new("a", "m", "s", mock_executor());
582 let descriptors = agent.tool_descriptors();
583 assert!(descriptors.is_empty());
584 }
585
586 #[test]
587 fn duplicate_tool_id_overwrites() {
588 let t1: Arc<dyn Tool> = Arc::new(TestTool { id: "echo".into() });
589 let t2: Arc<dyn Tool> = Arc::new(TestTool { id: "echo".into() });
590 let agent = ResolvedAgent::new("a", "m", "s", mock_executor())
591 .with_tool(t1)
592 .with_tool(t2);
593 assert_eq!(agent.tools.len(), 1, "duplicate tool ID should overwrite");
594 }
595
596 #[test]
597 fn with_tools_deduplicates_by_id() {
598 let tools: Vec<Arc<dyn Tool>> = vec![
599 Arc::new(TestTool { id: "echo".into() }),
600 Arc::new(TestTool { id: "echo".into() }),
601 Arc::new(TestTool {
602 id: "search".into(),
603 }),
604 ];
605 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_tools(tools);
606 assert_eq!(agent.tools.len(), 2);
607 }
608
609 #[test]
610 fn system_prompt_preserved_verbatim() {
611 let prompt = "You are a helpful assistant.\nBe concise.\nDo not hallucinate.";
612 let agent = ResolvedAgent::new("a", "m", prompt, mock_executor());
613 assert_eq!(agent.system_prompt(), prompt);
614 }
615
616 #[test]
617 fn with_context_summarizer() {
618 use crate::context::ContextSummarizer;
619 use crate::context::summarizer::SummarizationError;
620
621 struct MockSummarizer;
622 #[async_trait]
623 impl ContextSummarizer for MockSummarizer {
624 async fn summarize(
625 &self,
626 _transcript: &str,
627 _previous_summary: Option<&str>,
628 _executor: &dyn awaken_contract::contract::executor::LlmExecutor,
629 ) -> Result<String, SummarizationError> {
630 Ok("summary".into())
631 }
632 }
633
634 let agent = ResolvedAgent::new("a", "m", "s", mock_executor())
635 .with_context_summarizer(Arc::new(MockSummarizer));
636 assert!(agent.context_summarizer.is_some());
637 }
638
639 #[test]
640 fn default_max_continuation_retries() {
641 let agent = ResolvedAgent::new("a", "m", "s", mock_executor());
642 assert_eq!(agent.max_continuation_retries(), 2);
643 }
644
645 #[test]
646 fn zero_max_rounds() {
647 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_max_rounds(0);
648 assert_eq!(agent.max_rounds(), 0);
649 }
650
651 #[test]
652 fn builder_all_options_set() {
653 use awaken_contract::contract::inference::ContextWindowPolicy;
654
655 let policy = ContextWindowPolicy {
656 max_context_tokens: 16000,
657 max_output_tokens: 4000,
658 min_recent_messages: 8,
659 enable_prompt_cache: false,
660 autocompact_threshold: Some(8000),
661 compaction_mode: Default::default(),
662 compaction_raw_suffix_messages: 5,
663 };
664 let agent = ResolvedAgent::new("full-agent", "gpt-4", "Be helpful.", mock_executor())
665 .with_max_rounds(32)
666 .with_max_continuation_retries(10)
667 .with_tool(Arc::new(TestTool { id: "a".into() }))
668 .with_tool(Arc::new(TestTool { id: "b".into() }))
669 .with_tool(Arc::new(TestTool { id: "c".into() }))
670 .with_context_policy(policy)
671 .with_tool_executor(Arc::new(crate::execution::SequentialToolExecutor));
672
673 assert_eq!(agent.id(), "full-agent");
674 assert_eq!(agent.upstream_model, "gpt-4");
675 assert_eq!(agent.system_prompt(), "Be helpful.");
676 assert_eq!(agent.max_rounds(), 32);
677 assert_eq!(agent.max_continuation_retries(), 10);
678 assert_eq!(agent.tools.len(), 3);
679 assert!(agent.context_policy().is_some());
680 assert_eq!(agent.context_policy().unwrap().max_context_tokens, 16000);
681 assert_eq!(agent.tool_executor.name(), "sequential");
682 }
683
684 #[test]
685 fn empty_system_prompt() {
686 let agent = ResolvedAgent::new("a", "m", "", mock_executor());
687 assert_eq!(agent.system_prompt(), "");
688 }
689
690 #[test]
691 fn system_prompt_with_unicode() {
692 let prompt = "You are \u{1F916} a helpful assistant. \u{2764}";
693 let agent = ResolvedAgent::new("a", "m", prompt, mock_executor());
694 assert_eq!(agent.system_prompt(), prompt);
695 }
696
697 #[test]
698 fn tool_descriptors_single_tool() {
699 let agent = ResolvedAgent::new("a", "m", "s", mock_executor())
700 .with_tool(Arc::new(TestTool { id: "only".into() }));
701 let descs = agent.tool_descriptors();
702 assert_eq!(descs.len(), 1);
703 assert_eq!(descs[0].id, "only");
704 }
705
706 #[test]
707 fn tool_descriptors_many_tools_sorted() {
708 let ids = ["zeta", "beta", "alpha", "gamma", "delta"];
709 let tools: Vec<Arc<dyn Tool>> = ids
710 .iter()
711 .map(|id| Arc::new(TestTool { id: (*id).into() }) as Arc<dyn Tool>)
712 .collect();
713 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_tools(tools);
714 let descs = agent.tool_descriptors();
715 let sorted_ids: Vec<&str> = descs.iter().map(|d| d.id.as_str()).collect();
716 assert_eq!(sorted_ids, vec!["alpha", "beta", "delta", "gamma", "zeta"]);
717 }
718
719 #[test]
720 fn with_tools_then_with_tool_merges() {
721 let tools: Vec<Arc<dyn Tool>> = vec![
722 Arc::new(TestTool { id: "a".into() }),
723 Arc::new(TestTool { id: "b".into() }),
724 ];
725 let agent = ResolvedAgent::new("a", "m", "s", mock_executor())
726 .with_tools(tools)
727 .with_tool(Arc::new(TestTool { id: "c".into() }));
728 assert_eq!(agent.tools.len(), 3);
729 assert!(agent.tools.contains_key("a"));
730 assert!(agent.tools.contains_key("b"));
731 assert!(agent.tools.contains_key("c"));
732 }
733
734 #[test]
735 fn default_tool_executor_is_sequential() {
736 let agent = ResolvedAgent::new("a", "m", "s", mock_executor());
737 assert_eq!(agent.tool_executor.name(), "sequential");
738 assert!(agent.tool_executor.requires_incremental_state());
739 }
740
741 #[test]
742 fn model_id_and_upstream_model_independent_after_creation() {
743 let mut agent = ResolvedAgent::new("a", "original-model", "s", mock_executor());
744 agent.upstream_model = "resolved-model-name".into();
746 assert_eq!(agent.model_id(), "original-model");
747 assert_eq!(agent.upstream_model, "resolved-model-name");
748 }
749
750 #[test]
751 fn large_max_rounds() {
752 let agent = ResolvedAgent::new("a", "m", "s", mock_executor()).with_max_rounds(usize::MAX);
753 assert_eq!(agent.max_rounds(), usize::MAX);
754 }
755
756 #[test]
757 fn zero_continuation_retries_disables_recovery() {
758 let agent =
759 ResolvedAgent::new("a", "m", "s", mock_executor()).with_max_continuation_retries(0);
760 assert_eq!(agent.max_continuation_retries(), 0);
761 }
762}