1use std::sync::Arc;
13
14use async_trait::async_trait;
15use chrono::Utc;
16
17use crate::agent::{Agent, AgentStatus};
18use crate::atoms::{
19 ActAtom, ActInput, Atom, AtomContext, InputAtom, InputAtomInput, ReasonAtom, ReasonInput,
20};
21use crate::capabilities::{AgentCapabilityConfig, Capability, CapabilityRegistry};
22use crate::driver_registry::{DriverId, DriverRegistry};
23use crate::error::Result;
24use crate::events::{Event, EventData, EventRequest, OUTPUT_MESSAGE_COMPLETED};
25use crate::in_memory::{
26 InMemoryAgentStore, InMemoryEventEmitter, InMemoryHarnessStore, InMemoryMessageRetriever,
27 InMemoryProviderStore, InMemorySessionStore,
28};
29use crate::llmsim_driver::{LlmSimConfig, LlmSimDriver};
30use crate::message::Message;
31use crate::message_retriever::{InputMessage, MessageRetriever};
32use crate::session::{Session, SessionStatus};
33use crate::tool_types::ToolCall;
34use crate::tools::{Tool, ToolRegistry, ToolRegistryBuilder};
35use crate::traits::{EventEmitter, ResolvedModel};
36use crate::turn::{TurnAction, TurnContext, TurnOutcome, TurnStateMachine, TurnStopReason};
37use crate::typed_id::{AgentId, HarnessId, SessionId, TurnId};
38
39#[derive(Clone)]
49struct BridgingEventEmitter {
50 inner: InMemoryEventEmitter,
51 message_retriever: InMemoryMessageRetriever,
52}
53
54impl BridgingEventEmitter {
55 fn new(message_retriever: InMemoryMessageRetriever) -> Self {
56 Self {
57 inner: InMemoryEventEmitter::new(),
58 message_retriever,
59 }
60 }
61
62 async fn events(&self) -> Vec<Event> {
63 self.inner.events().await
64 }
65
66 async fn events_by_type(&self, event_type: &str) -> Vec<Event> {
67 self.inner.events_by_type(event_type).await
68 }
69
70 async fn event_count(&self) -> usize {
71 self.inner.event_count().await
72 }
73
74 async fn clear(&self) {
75 self.inner.clear().await;
76 }
77}
78
79#[async_trait]
80impl EventEmitter for BridgingEventEmitter {
81 async fn emit(&self, request: EventRequest) -> Result<Event> {
82 if request.data.event_type() == OUTPUT_MESSAGE_COMPLETED
84 && let EventData::OutputMessageCompleted(data) = &request.data
85 {
86 let _ = self
88 .message_retriever
89 .store(request.session_id, data.message.clone())
90 .await;
91 }
92
93 self.inner.emit(request).await
95 }
96}
97
98#[derive(Debug, Clone)]
104pub struct TurnResult {
105 pub response: String,
107 pub iterations: usize,
109 pub tool_calls_count: usize,
111 pub success: bool,
113 pub error: Option<String>,
115 pub stop_reason: TurnStopReason,
117 pub turn_id: TurnId,
119}
120
121impl TurnResult {
122 pub fn contains(&self, text: &str) -> bool {
124 self.response.contains(text)
125 }
126
127 fn from_outcome(outcome: TurnOutcome, turn_id: TurnId) -> Self {
129 let stop_reason = outcome.stop_reason();
130 match outcome {
131 TurnOutcome::Success {
132 response,
133 iterations,
134 tool_calls_count,
135 ..
136 } => Self {
137 response,
138 iterations,
139 tool_calls_count,
140 success: true,
141 error: None,
142 stop_reason,
143 turn_id,
144 },
145 TurnOutcome::Failed {
146 error, iterations, ..
147 } => Self {
148 response: String::new(),
149 iterations,
150 tool_calls_count: 0,
151 success: false,
152 error: Some(error),
153 stop_reason,
154 turn_id,
155 },
156 TurnOutcome::MaxIterationsReached {
157 response,
158 iterations,
159 tool_calls_count,
160 } => Self {
161 response,
162 iterations,
163 tool_calls_count,
164 success: true, error: None,
166 stop_reason,
167 turn_id,
168 },
169 TurnOutcome::Sealed {
173 reason,
174 response,
175 iterations,
176 tool_calls_count,
177 } => Self {
178 response,
179 iterations,
180 tool_calls_count,
181 success: false,
182 error: Some(format!("turn sealed: {reason}")),
183 stop_reason,
184 turn_id,
185 },
186 }
187 }
188}
189
190pub struct InMemoryAgenticLoopBuilder {
196 agent_name: String,
197 system_prompt: String,
198 model: Option<ResolvedModel>,
199 driver_registry: Option<DriverRegistry>,
200 llm_sim_config: Option<LlmSimConfig>,
201 tools: Vec<Box<dyn Tool>>,
202 capabilities: Vec<Box<dyn Capability>>,
203 max_iterations: usize,
204 parallel_tool_calls: Option<bool>,
205 reasoning_effort_handle: Option<crate::traits::ReasoningEffortHandle>,
206}
207
208impl Default for InMemoryAgenticLoopBuilder {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214impl InMemoryAgenticLoopBuilder {
215 pub fn new() -> Self {
217 Self {
218 agent_name: "Test Agent".to_string(),
219 system_prompt: "You are a helpful assistant.".to_string(),
220 model: None,
221 driver_registry: None,
222 llm_sim_config: Some(LlmSimConfig::default()),
223 tools: vec![],
224 capabilities: vec![],
225 max_iterations: 10,
226 parallel_tool_calls: None,
227 reasoning_effort_handle: None,
228 }
229 }
230
231 pub fn reasoning_effort_handle(mut self, handle: crate::traits::ReasoningEffortHandle) -> Self {
236 self.reasoning_effort_handle = Some(handle);
237 self
238 }
239
240 pub fn agent_name(mut self, name: impl Into<String>) -> Self {
242 self.agent_name = name.into();
243 self
244 }
245
246 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
248 self.system_prompt = prompt.into();
249 self
250 }
251
252 pub fn with_simulated_response(mut self, response: impl Into<String>) -> Self {
254 self.llm_sim_config = Some(LlmSimConfig::fixed(response));
255 self.model = None;
256 self.driver_registry = None;
257 self
258 }
259
260 pub fn with_llm_sim(mut self, config: LlmSimConfig) -> Self {
262 self.llm_sim_config = Some(config);
263 self.model = None;
264 self.driver_registry = None;
265 self
266 }
267
268 pub fn model(mut self, model: ResolvedModel) -> Self {
290 self.model = Some(model);
291 self.llm_sim_config = None;
292 self
293 }
294
295 pub fn driver_registry(mut self, driver_registry: DriverRegistry) -> Self {
312 self.driver_registry = Some(driver_registry);
313 self.llm_sim_config = None;
314 self
315 }
316
317 pub fn tool<T: Tool + 'static>(mut self, tool: T) -> Self {
319 self.tools.push(Box::new(tool));
320 self
321 }
322
323 pub fn capability<C: Capability + 'static>(mut self, capability: C) -> Self {
339 self.capabilities.push(Box::new(capability));
340 self
341 }
342
343 pub fn max_iterations(mut self, max: usize) -> Self {
345 self.max_iterations = max;
346 self
347 }
348
349 pub fn parallel_tool_calls(mut self, parallel_tool_calls: Option<bool>) -> Self {
355 self.parallel_tool_calls = parallel_tool_calls;
356 self
357 }
358
359 pub async fn build(self) -> Result<InMemoryAgenticLoop> {
361 let harness_store = InMemoryHarnessStore::new();
363 let agent_store = InMemoryAgentStore::new();
364 let session_store = InMemorySessionStore::new();
365 let message_retriever = InMemoryMessageRetriever::new();
366 let event_emitter = BridgingEventEmitter::new(message_retriever.clone());
367
368 let agent_capability_configs: Vec<AgentCapabilityConfig> = self
370 .capabilities
371 .iter()
372 .map(|cap| AgentCapabilityConfig::new(cap.id()))
373 .collect();
374
375 let harness_id = HarnessId::new();
377 let now = Utc::now();
378 let harness = crate::harness::Harness {
379 id: harness_id,
380 name: "in-memory".to_string(),
381 display_name: Some("In-Memory Harness".to_string()),
382 description: None,
383 system_prompt: Some(self.system_prompt.clone()),
384 parent_harness_id: None,
385 default_model_id: None,
386 tags: vec![],
387 capabilities: vec![],
388 mcp_servers: Default::default(),
389 initial_files: vec![],
390 network_access: None,
391 parallel_tool_calls: None,
392 embedder_metadata: Default::default(),
393 is_built_in: false,
394 status: crate::harness::HarnessStatus::Active,
395 created_at: now,
396 updated_at: now,
397 archived_at: None,
398 deleted_at: None,
399 };
400 harness_store.add_harness(harness).await;
401
402 let explicit_tool_definitions: Vec<crate::tool_types::ToolDefinition> =
407 self.tools.iter().map(|tool| tool.to_definition()).collect();
408
409 let agent_id = AgentId::new();
411 let agent = Agent {
412 public_id: agent_id,
413 internal_id: agent_id.uuid(),
414 name: "in-memory".to_string(),
415 display_name: Some(self.agent_name),
416 description: None,
417 system_prompt: self.system_prompt,
418 default_model_id: None,
419 harness_id,
420 default_version_id: None,
421 forked_from_agent_id: None,
422 forked_from_version_id: None,
423 root_agent_id: None,
424 tags: vec![],
425 capabilities: agent_capability_configs,
426 mcp_servers: Default::default(),
427 initial_files: vec![],
428 network_access: None,
429 max_iterations: None,
430 parallel_tool_calls: self.parallel_tool_calls,
431 tools: explicit_tool_definitions,
432 status: AgentStatus::Active,
433 created_at: now,
434 updated_at: now,
435 archived_at: None,
436 deleted_at: None,
437 usage: None,
438 };
439 agent_store.add_agent(agent).await;
440
441 let session_id = SessionId::new();
443 let session = Session {
444 id: session_id,
445 workspace_id: crate::WorkspaceId::from_uuid((session_id).uuid()),
446 organization_id: crate::DEFAULT_ORG_PUBLIC_ID.to_string(),
447 harness_id,
448 agent_id: Some(agent_id),
449 agent_version_id: None,
450 agent_identity_id: None,
451 owner_principal_id: crate::PrincipalId::from_seed(1),
452 resolved_owner_user_id: None,
453 owner: None,
454 effective_owner: None,
455 title: Some("In-Memory Session".to_string()),
456 goal: None,
457 locale: None,
458 preview: None,
459 output_preview: None,
460 tags: vec![],
461 model_id: None,
462 capabilities: vec![],
463 tools: vec![],
464 mcp_servers: Default::default(),
465 system_prompt: None,
466 initial_files: vec![],
467 hints: None,
468 network_access: None,
469 max_iterations: None,
470 parallel_tool_calls: None,
471 status: SessionStatus::Started,
472 created_at: now,
473 updated_at: now,
474 started_at: None,
475 finished_at: None,
476 usage: None,
477 is_pinned: None,
478 active_schedule_count: None,
479 features: vec![],
480 parent_session_id: None,
481 forked_from_session_id: None,
482 forked_from_sequence: None,
483 blueprint_id: None,
484 blueprint_config: None,
485 };
486 session_store.add_session(session).await;
487
488 let configured_model = self.model.as_ref().map(|m| m.model.clone());
491
492 let provider_store = InMemoryProviderStore::new();
494 let driver_registry =
495 if let (Some(model), Some(registry)) = (self.model, self.driver_registry) {
496 provider_store.set_default_model(model).await;
498 registry
499 } else {
500 let config = self.llm_sim_config.unwrap_or_default();
502 let model = ResolvedModel {
503 model: "llmsim-model".to_string(),
504 provider_type: DriverId::LlmSim,
505 api_key: Some("fake-key".to_string()),
506 base_url: None,
507 provider_metadata: None,
508 };
509 provider_store.set_default_model(model).await;
510
511 let driver = LlmSimDriver::new(config);
515 let mut registry = DriverRegistry::new();
516 registry.register(DriverId::LlmSim, move |_config| Box::new(driver.clone()));
517 registry
518 };
519
520 let configured_model_ref = configured_model.as_deref();
524 let mut tool_builder = ToolRegistryBuilder::new();
525 for capability in &self.capabilities {
526 let effective: &dyn crate::Capability = capability
527 .resolve_for_model(configured_model_ref)
528 .unwrap_or_else(|| capability.as_ref());
529 for tool in effective.tools() {
530 tool_builder = tool_builder.tool_boxed(tool);
531 }
532 }
533
534 for tool in self.tools {
536 tool_builder = tool_builder.tool_boxed(tool);
537 }
538 let tool_registry = tool_builder.build();
539
540 let mut capability_registry = CapabilityRegistry::new();
542 for capability in self.capabilities {
543 capability_registry.register_boxed(capability);
544 }
545
546 let input_atom = InputAtom::new(message_retriever.clone());
547 let mut reason_atom = ReasonAtom::new(
548 harness_store.clone(),
549 agent_store.clone(),
550 session_store.clone(),
551 message_retriever.clone(),
552 provider_store.clone(),
553 capability_registry,
554 driver_registry,
555 event_emitter.clone(),
556 );
557 let mut act_atom = ActAtom::new(tool_registry.clone(), event_emitter.clone())
558 .with_tool_registry(Arc::new(tool_registry.clone()));
559 if let Some(handle) = &self.reasoning_effort_handle {
560 reason_atom = reason_atom.with_reasoning_effort_handle(handle.clone());
561 act_atom = act_atom.with_reasoning_effort_handle(handle.clone());
562 }
563
564 Ok(InMemoryAgenticLoop {
565 harness_id,
566 agent_id,
567 session_id,
568 harness_store,
569 agent_store,
570 session_store,
571 message_retriever,
572 provider_store,
573 event_emitter,
574 tool_registry,
575 input_atom: Arc::new(input_atom),
576 reason_atom: Arc::new(reason_atom),
577 act_atom: Arc::new(act_atom),
578 max_iterations: self.max_iterations,
579 reasoning_effort_handle: self.reasoning_effort_handle,
580 })
581 }
582}
583
584pub struct InMemoryAgenticLoop {
617 harness_id: HarnessId,
618 agent_id: AgentId,
619 session_id: SessionId,
620 #[allow(dead_code)]
621 harness_store: InMemoryHarnessStore,
622 #[allow(dead_code)]
623 agent_store: InMemoryAgentStore,
624 #[allow(dead_code)]
625 session_store: InMemorySessionStore,
626 message_retriever: InMemoryMessageRetriever,
627 #[allow(dead_code)]
628 provider_store: InMemoryProviderStore,
629 event_emitter: BridgingEventEmitter,
630 tool_registry: ToolRegistry,
631 input_atom: Arc<InputAtom<InMemoryMessageRetriever>>,
632 reason_atom: Arc<ReasonAtom>,
633 act_atom: Arc<ActAtom<ToolRegistry, BridgingEventEmitter>>,
634 max_iterations: usize,
635 reasoning_effort_handle: Option<crate::traits::ReasoningEffortHandle>,
636}
637
638impl InMemoryAgenticLoop {
639 pub fn builder() -> InMemoryAgenticLoopBuilder {
641 InMemoryAgenticLoopBuilder::new()
642 }
643
644 pub fn agent_id(&self) -> AgentId {
646 self.agent_id
647 }
648
649 pub fn session_id(&self) -> SessionId {
651 self.session_id
652 }
653
654 pub async fn run_turn(&self, input: impl Into<InputMessage>) -> Result<TurnResult> {
687 if let Some(handle) = &self.reasoning_effort_handle {
691 handle.set(None);
692 }
693
694 let message = self
696 .message_retriever
697 .add(self.session_id, input.into())
698 .await?;
699
700 let turn_context = TurnContext::new(self.session_id, message.id, self.agent_id, 0);
702 let mut state_machine = TurnStateMachine::new(turn_context, self.max_iterations);
703
704 let mut last_reason_result: Option<crate::atoms::ReasonResult> = None;
706 let mut previous_response_id: Option<String> = None;
708
709 loop {
711 match state_machine.next_action() {
712 TurnAction::ExecuteInput => {
713 let base_context = AtomContext::new(
714 state_machine.context().session_id,
715 state_machine.context().turn_id,
716 state_machine.context().input_message_id,
717 );
718 self.input_atom
719 .execute(InputAtomInput {
720 context: base_context,
721 })
722 .await?;
723 state_machine.on_input_completed();
724 }
725
726 TurnAction::ExecuteReason => {
727 let base_context = AtomContext::new(
728 state_machine.context().session_id,
729 state_machine.context().turn_id,
730 state_machine.context().input_message_id,
731 );
732 let reason_result = self
733 .reason_atom
734 .execute(ReasonInput {
735 context: base_context.next_exec(),
736 harness_id: self.harness_id,
737 agent_id: Some(self.agent_id),
738 org_id: 0,
739 mcp_tool_definitions: vec![],
740 previous_response_id: previous_response_id.take(),
741 iteration: state_machine.current_iteration() as u32 + 1,
742 })
743 .await?;
744
745 let tool_call_count = reason_result.tool_calls.len();
746 previous_response_id = reason_result.response_id.clone();
747 state_machine.on_reason_completed(
750 reason_result.text.clone(),
751 tool_call_count,
752 reason_result.success,
753 reason_result.error.clone(),
754 reason_result.finish_reason.clone(),
755 false,
756 );
757
758 if reason_result.has_tool_calls {
760 last_reason_result = Some(reason_result);
761 }
762 }
763
764 TurnAction::ExecuteAct => {
765 let reason_result = last_reason_result
766 .take()
767 .expect("ExecuteAct requires prior ReasonResult with tool calls");
768 let base_context = AtomContext::new(
769 state_machine.context().session_id,
770 state_machine.context().turn_id,
771 state_machine.context().input_message_id,
772 );
773 self.act_atom
774 .execute(ActInput {
775 org_id: Some(0),
776 context: base_context.next_exec(),
777 harness_id: self.harness_id,
778 agent_id: Some(self.agent_id),
779 tool_calls: reason_result.tool_calls,
780 tool_definitions: reason_result.tool_definitions,
781 locale: reason_result.locale,
782 blueprint_id: None,
783 network_access: reason_result.network_access,
784 parallel_tool_calls: reason_result.parallel_tool_calls,
787 })
788 .await?;
789 state_machine.on_act_completed();
790 }
791
792 TurnAction::Complete(outcome) => {
793 return Ok(TurnResult::from_outcome(
794 outcome,
795 state_machine.context().turn_id,
796 ));
797 }
798 }
799 }
800 }
801
802 pub async fn run_conversation(&self, messages: &[&str]) -> Result<Vec<TurnResult>> {
804 let mut results = Vec::with_capacity(messages.len());
805 for msg in messages {
806 results.push(self.run_turn(*msg).await?);
807 }
808 Ok(results)
809 }
810
811 pub async fn messages(&self) -> Result<Vec<Message>> {
813 self.message_retriever.load(self.session_id).await
814 }
815
816 pub async fn events(&self) -> Vec<Event> {
818 self.event_emitter.events().await
819 }
820
821 pub async fn events_by_type(&self, event_type: &str) -> Vec<Event> {
823 self.event_emitter.events_by_type(event_type).await
824 }
825
826 pub async fn message_count(&self) -> Result<usize> {
828 self.message_retriever.count(self.session_id).await
829 }
830
831 pub async fn event_count(&self) -> usize {
833 self.event_emitter.event_count().await
834 }
835
836 pub async fn clear_events(&self) {
838 self.event_emitter.clear().await;
839 }
840
841 pub async fn clear_messages(&self) {
843 self.message_retriever.clear_session(self.session_id).await;
844 }
845
846 pub async fn reset(&self) {
848 self.clear_messages().await;
849 self.clear_events().await;
850 }
851
852 pub async fn conversation_string(&self) -> Result<String> {
854 let messages = self.messages().await?;
855 let mut result = String::new();
856 for msg in messages {
857 let role = format!("{:?}", msg.role);
858 let text = msg.text().unwrap_or("[non-text content]");
859 result.push_str(&format!("[{}] {}\n", role, text));
860 }
861 Ok(result)
862 }
863
864 pub fn message_retriever(&self) -> &InMemoryMessageRetriever {
866 &self.message_retriever
867 }
868
869 pub fn tool_registry(&self) -> &ToolRegistry {
871 &self.tool_registry
872 }
873}
874
875impl InMemoryAgenticLoop {
880 pub async fn with_fixed_response(response: impl Into<String>) -> Result<Self> {
890 Self::builder()
891 .with_simulated_response(response)
892 .build()
893 .await
894 }
895
896 pub async fn with_echo() -> Result<Self> {
906 Self::builder()
907 .with_llm_sim(LlmSimConfig::echo())
908 .build()
909 .await
910 }
911
912 pub async fn with_sequence(responses: Vec<impl Into<String>>) -> Result<Self> {
928 let responses: Vec<String> = responses.into_iter().map(|s| s.into()).collect();
929 Self::builder()
930 .with_llm_sim(LlmSimConfig::sequence(responses))
931 .build()
932 .await
933 }
934
935 pub async fn with_tool_calls(
955 response: impl Into<String>,
956 tool_calls: Vec<ToolCall>,
957 ) -> Result<Self> {
958 Self::builder()
959 .with_llm_sim(LlmSimConfig::fixed(response).with_tool_calls(tool_calls))
960 .build()
961 .await
962 }
963}
964
965#[cfg(test)]
970mod tests {
971 use super::*;
972
973 #[tokio::test]
974 async fn test_simple_turn() {
975 let runner = InMemoryAgenticLoop::with_fixed_response("Hello from the assistant!")
976 .await
977 .unwrap();
978
979 let result = runner.run_turn("Hi there").await.unwrap();
980
981 assert!(result.success);
982 assert_eq!(result.response, "Hello from the assistant!");
983 assert_eq!(result.iterations, 1);
984 assert_eq!(result.tool_calls_count, 0);
985 }
986
987 #[tokio::test]
988 async fn test_echo_turn() {
989 let runner = InMemoryAgenticLoop::with_echo().await.unwrap();
990
991 let result = runner.run_turn("Test message").await.unwrap();
992
993 assert!(result.success);
994 assert!(result.response.contains("Test message"));
995 }
996
997 #[tokio::test]
998 async fn test_sequence_turns() {
999 let runner = InMemoryAgenticLoop::with_sequence(vec!["First", "Second", "Third"])
1000 .await
1001 .unwrap();
1002
1003 let r1 = runner.run_turn("msg1").await.unwrap();
1004 let r2 = runner.run_turn("msg2").await.unwrap();
1005 let r3 = runner.run_turn("msg3").await.unwrap();
1006
1007 assert_eq!(r1.response, "First");
1008 assert_eq!(r2.response, "Second");
1009 assert_eq!(r3.response, "Third");
1010 }
1011
1012 #[tokio::test]
1013 async fn test_conversation() {
1014 let runner = InMemoryAgenticLoop::with_sequence(vec!["Hello!", "How can I help?"])
1015 .await
1016 .unwrap();
1017
1018 let results = runner
1019 .run_conversation(&["Hi", "I need help"])
1020 .await
1021 .unwrap();
1022
1023 assert_eq!(results.len(), 2);
1024 assert_eq!(results[0].response, "Hello!");
1025 assert_eq!(results[1].response, "How can I help?");
1026
1027 let messages = runner.messages().await.unwrap();
1029 assert_eq!(messages.len(), 4); }
1031
1032 #[tokio::test]
1033 async fn test_events_captured() {
1034 let runner = InMemoryAgenticLoop::with_fixed_response("Response")
1035 .await
1036 .unwrap();
1037
1038 runner.run_turn("Test").await.unwrap();
1039
1040 let events = runner.events().await;
1041 assert!(!events.is_empty());
1042
1043 let reason_events = runner.events_by_type("reason.started").await;
1045 assert_eq!(reason_events.len(), 1);
1046 }
1047
1048 #[tokio::test]
1049 async fn test_reset() {
1050 let runner = InMemoryAgenticLoop::with_fixed_response("Response")
1051 .await
1052 .unwrap();
1053
1054 runner.run_turn("Test").await.unwrap();
1055 assert!(runner.message_count().await.unwrap() > 0);
1056 assert!(runner.event_count().await > 0);
1057
1058 runner.reset().await;
1059 assert_eq!(runner.message_count().await.unwrap(), 0);
1060 assert_eq!(runner.event_count().await, 0);
1061 }
1062
1063 #[tokio::test]
1064 async fn test_builder_with_custom_config() {
1065 let runner = InMemoryAgenticLoop::builder()
1066 .agent_name("Custom Agent")
1067 .system_prompt("You are a custom assistant.")
1068 .with_simulated_response("Custom response")
1069 .max_iterations(5)
1070 .build()
1071 .await
1072 .unwrap();
1073
1074 let result = runner.run_turn("Test").await.unwrap();
1075 assert_eq!(result.response, "Custom response");
1076 }
1077
1078 #[tokio::test]
1079 async fn test_conversation_string() {
1080 let runner = InMemoryAgenticLoop::with_fixed_response("Hello!")
1081 .await
1082 .unwrap();
1083
1084 runner.run_turn("Hi").await.unwrap();
1085
1086 let conv = runner.conversation_string().await.unwrap();
1087 assert!(conv.contains("[User]"));
1088 assert!(conv.contains("[Agent]"));
1089 assert!(conv.contains("Hi"));
1090 assert!(conv.contains("Hello!"));
1091 }
1092}