1use super::config::AgentConfig;
21#[cfg(feature = "structured")]
22use super::structured::{StructuredOutcome, StructuredValidator};
23#[cfg(feature = "structured")]
24use crate::agent::TypedAgent;
25use crate::agent::events::ReActEvent;
26use crate::agent::{
27 Agent, AgentAction, AgentError, AgentEvent, AgentKernel, MessageChunk, ModelObservation,
28 ModelRequest, Observation, RunSummary,
29};
30use crate::effect::{EffectObservation, EffectRequest};
31use crate::event_channel::EventChannel;
32use crate::memory::{Memory, WindowMemory};
33use crate::message::{Message, ToolCall};
34use crate::provider::FakeProvider;
35use crate::provider::{
36 ChatRequest, FinishReason, ModelOptions, Provider, ProviderError, ProviderRequestContext,
37 StreamEvent, Usage,
38};
39#[cfg(feature = "structured")]
40use crate::run::TypedRunOutput;
41use crate::run::{Artifact, RunContext, RunMetadata, RunOutput, RunRequest};
42use crate::tool::{SharedState, Tool, ToolMemoryPolicy, ToolOutput, ToolRegistry, ToolResult};
43use futures::StreamExt;
44use futures::stream::BoxStream;
45#[cfg(feature = "structured")]
46use schemars::JsonSchema;
47#[cfg(feature = "structured")]
48use serde::de::DeserializeOwned;
49use std::collections::HashMap;
50use std::collections::HashSet;
51use std::collections::VecDeque;
52use std::fmt;
53use std::future::Future;
54use std::pin::Pin;
55use std::sync::Arc;
56use std::time::Instant;
57#[cfg(feature = "tracing")]
58use tracing::Instrument;
59
60#[macro_export]
101macro_rules! react_agent {
102 ($provider:expr $(,)?) => {
104 $crate::agent::ReActAgent::new(
105 $provider,
106 $crate::tool::ToolRegistry::new(),
107 "",
108 )
109 };
110 ($provider:expr, $system:literal $(,)?) => {
111 $crate::agent::ReActAgent::new(
112 $provider,
113 $crate::tool::ToolRegistry::new(),
114 $system,
115 )
116 };
117 ($provider:expr, [$($tool:expr),* $(,)?] $(,)?) => {{
119 let mut __molo_registry = $crate::tool::ToolRegistry::new();
120 $(__molo_registry.register($tool);)*
121 $crate::agent::ReActAgent::new($provider, __molo_registry, "")
122 }};
123 ($provider:expr, [$($tool:expr),* $(,)?], $system:expr $(,)?) => {{
124 let mut __molo_registry = $crate::tool::ToolRegistry::new();
125 $(__molo_registry.register($tool);)*
126 $crate::agent::ReActAgent::new($provider, __molo_registry, $system)
127 }};
128 ($provider:expr, $registry:expr $(,)?) => {
130 $crate::agent::ReActAgent::new($provider, $registry, "")
131 };
132 ($provider:expr, $registry:expr, $system:expr $(,)?) => {
133 $crate::agent::ReActAgent::new($provider, $registry, $system)
134 };
135}
136
137pub struct ReActAgent {
221 provider: Box<dyn Provider>,
222 memory: Box<dyn Memory>,
223 registry: ToolRegistry,
224 system_prompt: String,
225 config: AgentConfig,
226 pub state: SharedState,
230 events: Option<Arc<dyn EventChannel>>,
233 executor: Box<dyn ToolRoundExecutor>,
238 kernel_state: Option<ReActKernelState>,
240}
241
242const MAX_ROUND_TEXT: usize = 4 << 20;
246
247const DEFAULT_MEMORY_TOKENS: usize = 128_000;
254
255pub struct ReActAgentBuilder {
283 provider: Box<dyn Provider>,
284 memory: Box<dyn Memory>,
285 tools: ToolRegistry,
286 system_prompt: String,
287 config: AgentConfig,
288 state: SharedState,
289 events: Option<Arc<dyn EventChannel>>,
290 executor: Box<dyn ToolRoundExecutor>,
291}
292
293impl ReActAgentBuilder {
294 pub fn new(provider: impl Provider + 'static) -> Self {
296 Self {
297 provider: Box::new(provider),
298 memory: Box::new(WindowMemory::new(DEFAULT_MEMORY_TOKENS)),
299 tools: ToolRegistry::new(),
300 system_prompt: String::new(),
301 config: AgentConfig::default(),
302 state: SharedState::default(),
303 events: None,
304 executor: Box::new(SerialToolRoundExecutor),
305 }
306 }
307
308 pub fn with_provider(mut self, provider: impl Provider + 'static) -> Self {
310 self.provider = Box::new(provider);
311 self
312 }
313
314 pub fn with_tools(mut self, tools: ToolRegistry) -> Self {
316 self.tools = tools;
317 self
318 }
319
320 pub fn with_tool(mut self, tool: impl Tool + 'static) -> Self {
322 self.tools.register(tool);
323 self
324 }
325
326 pub fn with_system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
328 self.system_prompt = system_prompt.into();
329 self
330 }
331
332 pub fn with_memory(mut self, memory: impl Memory + 'static) -> Self {
334 self.memory = Box::new(memory);
335 self
336 }
337
338 pub fn with_config(mut self, config: AgentConfig) -> Self {
340 self.config = config;
341 self
342 }
343
344 #[cfg(feature = "structured")]
346 pub fn with_structured_output(mut self, schema: serde_json::Value) -> Self {
347 self.config.options.structured = Some(schema);
348 self
349 }
350
351 pub fn with_state(mut self, state: SharedState) -> Self {
353 self.state = state;
354 self
355 }
356
357 pub fn with_event_channel(mut self, channel: impl EventChannel + 'static) -> Self {
359 self.events = Some(Arc::new(channel));
360 self
361 }
362
363 pub fn with_tool_round_executor(mut self, executor: impl ToolRoundExecutor + 'static) -> Self {
365 self.executor = Box::new(executor);
366 self
367 }
368
369 pub fn build(self) -> ReActAgent {
371 ReActAgent {
372 provider: self.provider,
373 memory: self.memory,
374 registry: self.tools,
375 system_prompt: self.system_prompt,
376 config: self.config,
377 state: self.state,
378 events: self.events,
379 executor: self.executor,
380 kernel_state: None,
381 }
382 }
383}
384
385impl fmt::Debug for ReActAgentBuilder {
386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387 let mut debug = f.debug_struct("ReActAgentBuilder");
388 debug
389 .field("provider", &"Box<dyn Provider>")
390 .field("memory", &"Box<dyn Memory>")
391 .field("tools", &self.tools)
392 .field("system_prompt", &self.system_prompt)
393 .field("config", &self.config)
394 .field("state", &self.state)
395 .field(
396 "events",
397 &match &self.events {
398 Some(_) => "Some<dyn EventChannel>",
399 None => "None",
400 },
401 );
402 debug.finish()
403 }
404}
405
406impl ReActAgent {
407 pub fn builder(provider: impl Provider + 'static) -> ReActAgentBuilder {
409 ReActAgentBuilder::new(provider)
410 }
411
412 pub fn new(
434 provider: impl Provider + 'static,
435 tools: ToolRegistry,
436 system_prompt: impl Into<String>,
437 ) -> Self {
438 Self {
439 provider: Box::new(provider),
440 memory: Box::new(WindowMemory::new(DEFAULT_MEMORY_TOKENS)),
441 registry: tools,
442 system_prompt: system_prompt.into(),
443 config: AgentConfig::default(),
444 state: SharedState::default(),
445 events: None,
446 executor: Box::new(SerialToolRoundExecutor),
447 kernel_state: None,
448 }
449 }
450
451 pub fn kernel(tools: ToolRegistry, system_prompt: impl Into<String>) -> Self {
458 Self::new(FakeProvider::new([]), tools, system_prompt)
459 }
460
461 pub fn with_memory(mut self, memory: impl Memory + 'static) -> Self {
495 self.memory = Box::new(memory);
496 self
497 }
498
499 pub fn with_config(mut self, config: AgentConfig) -> Self {
506 self.config = config;
507 self
508 }
509
510 #[cfg(feature = "structured")]
557 pub fn with_structured_output(mut self, schema: serde_json::Value) -> Self {
558 self.config.options.structured = Some(schema);
559 self
560 }
561
562 pub fn with_state(mut self, state: SharedState) -> Self {
566 self.state = state;
567 self
568 }
569
570 pub fn with_event_channel(mut self, channel: impl EventChannel + 'static) -> Self {
581 self.events = Some(Arc::new(channel));
582 self
583 }
584
585 pub fn with_tool_round_executor(mut self, executor: impl ToolRoundExecutor + 'static) -> Self {
652 self.executor = Box::new(executor);
653 self
654 }
655
656 fn publish<E: AgentEvent + 'static>(&self, make_event: impl FnOnce() -> Arc<E>) {
662 if let Some(pipe) = &self.events {
663 pipe.publish(make_event());
664 }
665 }
666
667 async fn run_rounds_with_context(
669 &mut self,
670 context: &RunContext,
671 run_id: &str,
672 counters: &mut RunCounters,
673 options: &ModelOptions,
674 schema: Option<&serde_json::Value>,
675 ) -> Result<FinalAnswer, AgentError> {
676 let schemas = self.registry.schemas();
677 #[cfg(feature = "structured")]
681 let mut validator = schema.map(|schema| {
682 StructuredValidator::new(schema.clone(), self.config.max_structured_retries)
683 });
684 #[cfg(not(feature = "structured"))]
685 let _ = schema;
686 let mut tool_rounds = 0usize;
693 loop {
694 if tool_rounds >= self.config.max_tool_rounds {
695 return Err(AgentError::TooManyToolRounds(self.config.max_tool_rounds));
696 }
697 counters.rounds += 1;
698 check_run_context(context)?;
702
703 let answer: Option<FinalAnswer> = async {
716 let llm_span = span_llm(run_id, counters.rounds);
717 let model_request_id = format!("{run_id}-model-{}", counters.rounds);
731 let provider_context =
732 ProviderRequestContext::from_run_context(model_request_id, context);
733 let chat = self.provider.chat_with_context(
734 ChatRequest {
735 messages: self.assemble_messages(self.memory.context().await?),
736 tools: schemas.clone(),
737 options: options.clone(),
738 },
739 &provider_context,
740 );
741 let response =
742 match run_until_context(context, instrument(chat, llm_span.clone())).await {
743 Ok(Ok(response)) => response,
744 Ok(Err(e)) => {
745 #[cfg(feature = "tracing")]
746 llm_span.record("error", e.to_string());
747 return Err(AgentError::Provider(e));
748 }
749 Err(e) => return Err(e),
750 };
751 if let Some(usage) = response.usage {
755 #[cfg(feature = "tracing")]
756 {
757 llm_span.record("usage.prompt_tokens", usage.prompt_tokens);
758 llm_span.record("usage.completion_tokens", usage.completion_tokens);
759 if let Some(cached) = usage.cached_tokens {
760 llm_span.record("usage.cached_tokens", cached);
761 }
762 }
763 counters.usage.record(usage);
764 } else {
765 counters.usage.omit();
766 }
767 let finish_reason = response.finish_reason.clone();
768
769 let Message::Assistant {
774 content,
775 reasoning,
776 tool_calls,
777 } = response.message
778 else {
779 return Err(AgentError::Provider(ProviderError::Protocol {
784 message: "provider returned a non-assistant message".into(),
785 }));
786 };
787 if content.len() + reasoning.as_deref().map_or(0, str::len) > MAX_ROUND_TEXT {
793 return Err(AgentError::Provider(ProviderError::ResponseTooLarge {
794 limit_bytes: MAX_ROUND_TEXT,
795 }));
796 }
797
798 let final_message = Message::Assistant {
803 content: content.clone(),
804 reasoning: reasoning.clone(),
805 tool_calls: Vec::new(),
806 };
807
808 if !content.is_empty() || reasoning.is_some() || !tool_calls.is_empty() {
809 self.memory
810 .record(Message::Assistant {
811 content: content.clone(),
812 reasoning,
813 tool_calls: tool_calls.clone(),
814 })
815 .await?;
816 }
817
818 if tool_calls.is_empty() {
819 #[cfg(feature = "structured")]
825 {
826 if let Some(validator) = &mut validator {
827 match validator.validate(&content) {
828 StructuredOutcome::Passed => {}
829 StructuredOutcome::Retry { message } => {
830 self.memory.record(message).await?;
831 return Ok::<Option<FinalAnswer>, AgentError>(None);
832 }
833 StructuredOutcome::Exhausted { max_retries } => {
834 return Err(AgentError::StructuredRetriesExhausted(
835 max_retries,
836 ));
837 }
838 }
839 }
840 }
841 return Ok::<Option<FinalAnswer>, AgentError>(Some(FinalAnswer {
842 answer: content,
843 final_message,
844 finish_reason: Some(finish_reason),
845 }));
846 }
847
848 counters.tool_calls_total += tool_calls.len();
849 tool_rounds += 1;
850 let ctx = ToolRoundCtx {
861 context,
862 round: counters.rounds,
863 registry: &self.registry,
864 state: &self.state,
865 events: &self.events,
866 };
867 let mut outcomes = self.executor.execute_round(ctx, tool_calls).await;
868 while let Some(outcome) = outcomes.next().await {
869 if let Some(effect) = outcome.effect_request() {
870 return Err(AgentError::EffectRequiresHarness(format!(
871 "{} ({})",
872 effect.description, effect.id
873 )));
874 }
875 record_tool_result(&mut self.memory, &outcome).await?;
880 }
881 Ok::<Option<FinalAnswer>, AgentError>(None)
882 }
883 .await?;
884 if let Some(answer) = answer {
885 return Ok(answer);
886 }
887 }
888 }
889
890 fn assemble_messages(&self, context: Vec<Message>) -> Vec<Message> {
899 let mut messages = Vec::with_capacity(context.len() + 1);
900 let system = self.assemble_system_prompt();
901 if !system.is_empty() {
902 messages.push(Message::system(&system));
903 }
904 messages.extend(context);
905 messages
906 }
907
908 fn assemble_system_prompt(&self) -> String {
910 self.system_prompt.clone()
911 }
912}
913
914pub struct ToolRoundCtx<'a> {
924 pub context: &'a RunContext,
927 pub round: usize,
929 pub registry: &'a ToolRegistry,
931 pub state: &'a SharedState,
933 pub events: &'a Option<Arc<dyn EventChannel>>,
935}
936
937impl fmt::Debug for ToolRoundCtx<'_> {
938 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
939 f.debug_struct("ToolRoundCtx")
942 .field("run_id", &self.context.run_id)
943 .field("round", &self.round)
944 .field("registry", &self.registry)
945 .field("state", &self.state)
946 .finish_non_exhaustive()
947 }
948}
949
950impl ToolRoundCtx<'_> {
951 pub async fn run(&self, call: ToolCall) -> ToolCallOutcome {
966 let tool_span = span_tool(&self.context.run_id, self.round, &call.name);
971 self.publish(|| {
974 Arc::new(ReActEvent::ToolStarted {
975 id: call.id.clone(),
976 name: call.name.clone(),
977 arguments: call.arguments.clone(),
978 })
979 });
980 let call_future = self.registry.call(&call, self.context, self.state);
984 let result = instrument(call_future, tool_span.clone()).await;
985 #[cfg(feature = "tracing")]
986 if let Err(e) = &result {
987 tool_span.record("error", e.to_string());
988 }
989 let outcome = match &result {
990 Ok(ToolResult::Output(output)) => ToolCallOutcome::output(call.clone(), output.clone()),
991 Ok(ToolResult::Effect(request)) => {
992 ToolCallOutcome::effect(call.clone(), request.clone())
993 }
994 Ok(other) => ToolCallOutcome::text(call.clone(), other.to_string()),
995 Err(e) => ToolCallOutcome::text(call.clone(), e.to_string()),
996 };
997 let publish_tool_completed = {
998 let id = call.id.clone();
999 let name = call.name.clone();
1000 move || Arc::new(ReActEvent::ToolCompleted { id, name, result })
1001 };
1002 self.publish(publish_tool_completed);
1003 outcome
1004 }
1005
1006 fn publish<E: AgentEvent + 'static>(&self, make_event: impl FnOnce() -> Arc<E>) {
1011 if let Some(pipe) = self.events {
1012 pipe.publish(make_event());
1013 }
1014 }
1015}
1016
1017#[async_trait::async_trait]
1098pub trait ToolRoundExecutor: Send + Sync {
1099 async fn execute_round<'a>(
1115 &'a mut self,
1116 ctx: ToolRoundCtx<'a>,
1117 calls: Vec<ToolCall>,
1118 ) -> BoxStream<'a, ToolCallOutcome>;
1119}
1120
1121#[derive(Debug, Default, Clone, Copy)]
1136pub struct SerialToolRoundExecutor;
1137
1138#[async_trait::async_trait]
1139impl ToolRoundExecutor for SerialToolRoundExecutor {
1140 async fn execute_round<'a>(
1141 &'a mut self,
1142 ctx: ToolRoundCtx<'a>,
1143 calls: Vec<ToolCall>,
1144 ) -> BoxStream<'a, ToolCallOutcome> {
1145 Box::pin(async_stream::stream! {
1146 for call in calls {
1147 yield ctx.run(call).await;
1148 }
1149 })
1150 }
1151}
1152
1153#[derive(Debug, Clone, PartialEq)]
1179pub struct ToolCallOutcome {
1180 pub call: ToolCall,
1183 pub content: String,
1185 pub memory_policy: ToolMemoryPolicy,
1187 pub effect: Option<EffectRequest>,
1190}
1191
1192impl ToolCallOutcome {
1193 pub fn text(call: ToolCall, content: impl Into<String>) -> Self {
1195 Self::output(call, ToolOutput::text(content))
1196 }
1197
1198 pub fn output(call: ToolCall, output: ToolOutput) -> Self {
1200 Self {
1201 call,
1202 content: output.content,
1203 memory_policy: output.memory_policy,
1204 effect: None,
1205 }
1206 }
1207
1208 pub fn effect(call: ToolCall, request: EffectRequest) -> Self {
1210 Self {
1211 call,
1212 content: String::new(),
1213 memory_policy: ToolMemoryPolicy::Normal,
1214 effect: Some(request),
1215 }
1216 }
1217
1218 pub fn effect_request(&self) -> Option<&EffectRequest> {
1220 self.effect.as_ref()
1221 }
1222}
1223
1224#[cfg(feature = "structured")]
1225impl ReActAgent {
1226 pub async fn run_typed<U>(&mut self, input: &str) -> Result<U, AgentError>
1280 where
1281 U: DeserializeOwned + JsonSchema + Send + Sync,
1282 {
1283 TypedAgent::run_typed(self, input).await
1286 }
1287}
1288
1289#[cfg(feature = "structured")]
1290#[async_trait::async_trait]
1291impl TypedAgent for ReActAgent {
1292 async fn run_typed_request_with_context<U>(
1293 &mut self,
1294 request: RunRequest,
1295 context: RunContext,
1296 ) -> Result<TypedRunOutput<U>, AgentError>
1297 where
1298 U: DeserializeOwned + JsonSchema + Send + Sync,
1299 {
1300 let schema = serde_json::to_value(schemars::schema_for!(U))
1301 .expect("schemars-generated schema always serializes (pure JSON value structure)");
1302 let output = self
1303 .run_request_inner(request, context, Some(&schema))
1304 .await?;
1305 let value = serde_json::from_str(&output.answer)
1306 .map_err(|e| AgentError::StructuredParse(e.to_string()))?;
1307 Ok(TypedRunOutput { value, output })
1308 }
1309}
1310
1311#[derive(Default)]
1318struct RunCounters {
1319 rounds: usize,
1322 tool_calls_total: usize,
1324 usage: UsageTotals,
1326}
1327
1328#[derive(Default, Clone, Copy)]
1333struct UsageTotals {
1334 total: Usage,
1337 omitted: bool,
1340 cache_omitted: bool,
1345}
1346
1347impl UsageTotals {
1348 fn record(&mut self, usage: Usage) {
1351 self.total += usage;
1352 self.cache_omitted |= usage.cached_tokens.is_none();
1353 }
1354
1355 fn omit(&mut self) {
1358 self.omitted = true;
1359 }
1360}
1361
1362struct FinalAnswer {
1363 answer: String,
1364 final_message: Message,
1365 finish_reason: Option<FinishReason>,
1366}
1367
1368struct RunExecution {
1369 answer: String,
1370 final_message: Message,
1371 summary: RunSummary,
1372 artifacts: Vec<Artifact>,
1373 metadata: RunMetadata,
1374}
1375
1376struct ReActKernelState {
1377 run_id: String,
1378 started_at: Instant,
1379 provider_model: Option<String>,
1380 counters: RunCounters,
1381 options: ModelOptions,
1382 schemas: Vec<crate::tool::ToolSchema>,
1383 #[cfg(feature = "structured")]
1384 validator: Option<StructuredValidator>,
1385 tool_rounds: usize,
1386 pending_tools: VecDeque<ToolCall>,
1387 pending_tool_results: VecDeque<PendingToolResult>,
1388 next_model_request: u64,
1389}
1390
1391#[derive(Debug)]
1392enum PendingToolResult {
1393 Outcome(ToolCallOutcome),
1394 Effect {
1395 effect_id: String,
1396 call: ToolCall,
1397 observation: Option<EffectObservation>,
1398 },
1399}
1400
1401impl PendingToolResult {
1402 fn effect_id(&self) -> Option<&str> {
1403 match self {
1404 Self::Outcome(_) => None,
1405 Self::Effect { effect_id, .. } => Some(effect_id),
1406 }
1407 }
1408}
1409
1410impl ReActKernelState {
1411 fn next_model_request(&mut self, messages: Vec<Message>) -> AgentAction {
1412 self.next_model_request += 1;
1413 AgentAction::RequestModel {
1414 request: ModelRequest::new(
1415 format!("{}-model-{}", self.run_id, self.next_model_request),
1416 ChatRequest {
1417 messages,
1418 tools: self.schemas.clone(),
1419 options: self.options.clone(),
1420 },
1421 ),
1422 }
1423 }
1424
1425 fn summary(&self, finish_reason: Option<FinishReason>) -> RunSummary {
1426 run_summary(
1427 &self.counters,
1428 finish_reason,
1429 self.started_at,
1430 self.provider_model.clone(),
1431 )
1432 }
1433}
1434
1435impl fmt::Debug for ReActAgent {
1436 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1437 let mut debug = f.debug_struct("ReActAgent");
1440 debug
1441 .field("provider", &"Box<dyn Provider>")
1442 .field("memory", &"Box<dyn Memory>")
1443 .field("tools", &self.registry)
1444 .field("system_prompt", &self.system_prompt)
1445 .field("config", &self.config)
1446 .field("state", &self.state)
1447 .field(
1448 "events",
1449 &match &self.events {
1450 Some(_) => "Some<dyn EventChannel>",
1451 None => "None",
1452 },
1453 );
1454 debug.finish()
1455 }
1456}
1457
1458#[async_trait::async_trait]
1459impl Agent for ReActAgent {
1460 async fn run_request_with_context(
1461 &mut self,
1462 request: RunRequest,
1463 context: RunContext,
1464 ) -> Result<RunOutput, AgentError> {
1465 self.run_request_inner(request, context, None).await
1466 }
1467
1468 async fn run_stream_request_with_context<'a>(
1469 &'a mut self,
1470 request: RunRequest,
1471 context: RunContext,
1472 ) -> Result<BoxStream<'a, Result<MessageChunk, AgentError>>, AgentError> {
1473 self.run_stream_request_inner(request, context).await
1474 }
1475}
1476
1477#[async_trait::async_trait]
1478impl AgentKernel for ReActAgent {
1479 async fn start(
1480 &mut self,
1481 request: RunRequest,
1482 context: &RunContext,
1483 ) -> Result<AgentAction, AgentError> {
1484 check_run_context(context)?;
1485 let run_id = context.run_id.clone();
1486 let input = request.input;
1487 self.memory.record(input.clone().into_message()).await?;
1488 self.publish(|| {
1489 Arc::new(ReActEvent::RunStarted {
1490 run_id: run_id.clone(),
1491 input,
1492 })
1493 });
1494
1495 let options = request
1496 .options
1497 .clone()
1498 .unwrap_or_else(|| self.config.options.clone());
1499 #[cfg(feature = "structured")]
1500 let validator = options.structured.as_ref().map(|schema| {
1501 StructuredValidator::new(schema.clone(), self.config.max_structured_retries)
1502 });
1503 let mut state = ReActKernelState {
1504 run_id,
1505 started_at: Instant::now(),
1506 provider_model: self.provider.model().map(str::to_string),
1507 counters: RunCounters::default(),
1508 options,
1509 schemas: self.registry.schemas(),
1510 #[cfg(feature = "structured")]
1511 validator,
1512 tool_rounds: 0,
1513 pending_tools: VecDeque::new(),
1514 pending_tool_results: VecDeque::new(),
1515 next_model_request: 0,
1516 };
1517 state.counters.rounds += 1;
1518 let messages = self.assemble_messages(self.memory.context().await?);
1519 let action = state.next_model_request(messages);
1520 self.kernel_state = Some(state);
1521 Ok(action)
1522 }
1523
1524 async fn observe(
1525 &mut self,
1526 observation: Observation,
1527 context: &RunContext,
1528 ) -> Result<AgentAction, AgentError> {
1529 check_run_context(context)?;
1530 match observation {
1531 Observation::Model(observation) => self.observe_model(observation, context).await,
1532 Observation::Effect(observation) => self.observe_effect(observation, context).await,
1533 Observation::Effects(observations) => self.observe_effects(observations, context).await,
1534 _ => Err(AgentError::InvalidStep("unsupported observation".into())),
1535 }
1536 }
1537}
1538
1539impl ReActAgent {
1540 async fn observe_model(
1541 &mut self,
1542 observation: ModelObservation,
1543 context: &RunContext,
1544 ) -> Result<AgentAction, AgentError> {
1545 let mut state = self.kernel_state.take().ok_or_else(|| {
1546 AgentError::InvalidStep("model observation without active run".into())
1547 })?;
1548 if !state.pending_tool_results.is_empty() || !state.pending_tools.is_empty() {
1549 self.kernel_state = Some(state);
1550 return Err(AgentError::InvalidStep(
1551 "model observation received while tool calls are pending".into(),
1552 ));
1553 }
1554
1555 let response = observation.response;
1556 if let Some(usage) = response.usage {
1557 state.counters.usage.record(usage);
1558 } else {
1559 state.counters.usage.omit();
1560 }
1561 let finish_reason = response.finish_reason.clone();
1562 let Message::Assistant {
1563 content,
1564 reasoning,
1565 tool_calls,
1566 } = response.message
1567 else {
1568 self.kernel_state = Some(state);
1569 return Err(AgentError::Provider(ProviderError::Protocol {
1570 message: "provider returned a non-assistant message".into(),
1571 }));
1572 };
1573 if content.len() + reasoning.as_deref().map_or(0, str::len) > MAX_ROUND_TEXT {
1574 self.kernel_state = Some(state);
1575 return Err(AgentError::Provider(ProviderError::ResponseTooLarge {
1576 limit_bytes: MAX_ROUND_TEXT,
1577 }));
1578 }
1579
1580 let final_message = Message::Assistant {
1581 content: content.clone(),
1582 reasoning: reasoning.clone(),
1583 tool_calls: Vec::new(),
1584 };
1585 if !content.is_empty() || reasoning.is_some() || !tool_calls.is_empty() {
1586 self.memory
1587 .record(Message::Assistant {
1588 content: content.clone(),
1589 reasoning,
1590 tool_calls: tool_calls.clone(),
1591 })
1592 .await?;
1593 }
1594
1595 if tool_calls.is_empty() {
1596 #[cfg(feature = "structured")]
1597 {
1598 if let Some(validator) = &mut state.validator {
1599 match validator.validate(&content) {
1600 StructuredOutcome::Passed => {}
1601 StructuredOutcome::Retry { message } => {
1602 self.memory.record(message).await?;
1603 state.counters.rounds += 1;
1604 let messages = self.assemble_messages(self.memory.context().await?);
1605 let action = state.next_model_request(messages);
1606 self.kernel_state = Some(state);
1607 return Ok(action);
1608 }
1609 StructuredOutcome::Exhausted { max_retries } => {
1610 self.kernel_state = None;
1611 return Err(AgentError::StructuredRetriesExhausted(max_retries));
1612 }
1613 }
1614 }
1615 }
1616 let summary = state.summary(Some(finish_reason));
1617 let output = RunOutput {
1618 run_id: state.run_id.clone(),
1619 answer: content,
1620 summary: summary.clone(),
1621 final_message,
1622 artifacts: Vec::new(),
1623 metadata: RunMetadata::new(),
1624 };
1625 publish_ended(&self.events, summary, None);
1626 self.kernel_state = None;
1627 return Ok(AgentAction::Respond { output });
1628 }
1629
1630 if state.tool_rounds >= self.config.max_tool_rounds {
1631 self.kernel_state = None;
1632 return Err(AgentError::TooManyToolRounds(self.config.max_tool_rounds));
1633 }
1634 state.tool_rounds += 1;
1635 state.counters.tool_calls_total += tool_calls.len();
1636 state.pending_tools = VecDeque::from(tool_calls);
1637 let action = self.process_kernel_pending_tools(state, context).await?;
1638 Ok(action)
1639 }
1640
1641 async fn observe_effect(
1642 &mut self,
1643 observation: EffectObservation,
1644 context: &RunContext,
1645 ) -> Result<AgentAction, AgentError> {
1646 let mut state = self.kernel_state.take().ok_or_else(|| {
1647 AgentError::InvalidStep("effect observation without active run".into())
1648 })?;
1649 let pending_effect_count = state
1650 .pending_tool_results
1651 .iter()
1652 .filter(|result| result.effect_id().is_some())
1653 .count();
1654 if pending_effect_count > 1 {
1655 self.kernel_state = Some(state);
1656 return Err(AgentError::InvalidStep(
1657 "single effect observation received while a batch is pending".into(),
1658 ));
1659 }
1660 let Some(expected_effect_id) = state
1661 .pending_tool_results
1662 .iter()
1663 .find_map(PendingToolResult::effect_id)
1664 .map(str::to_string)
1665 else {
1666 self.kernel_state = Some(state);
1667 return Err(AgentError::InvalidStep(
1668 "effect observation received with no pending effect".into(),
1669 ));
1670 };
1671 if observation.effect_id != expected_effect_id {
1672 self.kernel_state = Some(state);
1673 return Err(AgentError::InvalidStep(format!(
1674 "effect observation id mismatch: expected {expected_effect_id}, got {}",
1675 observation.effect_id
1676 )));
1677 }
1678
1679 if observation.output.observation_for_model.len() > MAX_ROUND_TEXT {
1680 self.kernel_state = Some(state);
1681 return Err(AgentError::Provider(ProviderError::ResponseTooLarge {
1682 limit_bytes: MAX_ROUND_TEXT,
1683 }));
1684 }
1685 let recorded = Self::mark_pending_effect_observed(
1686 &mut state.pending_tool_results,
1687 &expected_effect_id,
1688 observation,
1689 );
1690 debug_assert!(recorded);
1691 if let Err(error) = self.record_pending_tool_results(&mut state).await {
1692 self.kernel_state = Some(state);
1693 return Err(error);
1694 }
1695 self.process_kernel_pending_tools(state, context).await
1696 }
1697
1698 async fn observe_effects(
1699 &mut self,
1700 observations: Vec<EffectObservation>,
1701 context: &RunContext,
1702 ) -> Result<AgentAction, AgentError> {
1703 let mut state = self.kernel_state.take().ok_or_else(|| {
1704 AgentError::InvalidStep("effect observations without active run".into())
1705 })?;
1706 let pending_effect_ids = state
1707 .pending_tool_results
1708 .iter()
1709 .filter_map(PendingToolResult::effect_id)
1710 .map(str::to_string)
1711 .collect::<Vec<_>>();
1712 if pending_effect_ids.is_empty() {
1713 self.kernel_state = Some(state);
1714 return Err(AgentError::InvalidStep(
1715 "effect observations received with no pending effects".into(),
1716 ));
1717 }
1718 if observations.len() != pending_effect_ids.len() {
1719 let expected = pending_effect_ids.len();
1720 self.kernel_state = Some(state);
1721 return Err(AgentError::InvalidStep(format!(
1722 "effect observation count mismatch: expected {expected}, got {}",
1723 observations.len()
1724 )));
1725 }
1726
1727 let expected_ids = pending_effect_ids.iter().cloned().collect::<HashSet<_>>();
1728 let mut observations_by_id = HashMap::with_capacity(observations.len());
1729 for observation in observations {
1730 if observation.output.observation_for_model.len() > MAX_ROUND_TEXT {
1731 self.kernel_state = Some(state);
1732 return Err(AgentError::Provider(ProviderError::ResponseTooLarge {
1733 limit_bytes: MAX_ROUND_TEXT,
1734 }));
1735 }
1736 if !expected_ids.contains(&observation.effect_id) {
1737 let effect_id = observation.effect_id;
1738 self.kernel_state = Some(state);
1739 return Err(AgentError::InvalidStep(format!(
1740 "unexpected effect observation for {effect_id}"
1741 )));
1742 }
1743
1744 let effect_id = observation.effect_id.clone();
1745 if observations_by_id
1746 .insert(effect_id.clone(), observation)
1747 .is_some()
1748 {
1749 self.kernel_state = Some(state);
1750 return Err(AgentError::InvalidStep(format!(
1751 "duplicate effect observation for {effect_id}"
1752 )));
1753 }
1754 }
1755 for effect_id in &pending_effect_ids {
1756 if !observations_by_id.contains_key(effect_id) {
1757 self.kernel_state = Some(state);
1758 return Err(AgentError::InvalidStep(format!(
1759 "missing effect observation for {effect_id}"
1760 )));
1761 }
1762 }
1763
1764 for effect_id in pending_effect_ids {
1765 let observation = observations_by_id
1766 .remove(&effect_id)
1767 .expect("effect observation was prevalidated");
1768 let recorded = Self::mark_pending_effect_observed(
1769 &mut state.pending_tool_results,
1770 &effect_id,
1771 observation,
1772 );
1773 debug_assert!(recorded);
1774 }
1775 if let Err(error) = self.record_pending_tool_results(&mut state).await {
1776 self.kernel_state = Some(state);
1777 return Err(error);
1778 }
1779 self.process_kernel_pending_tools(state, context).await
1780 }
1781
1782 fn mark_pending_effect_observed(
1783 pending_tool_results: &mut VecDeque<PendingToolResult>,
1784 expected_effect_id: &str,
1785 observation: EffectObservation,
1786 ) -> bool {
1787 for result in pending_tool_results {
1788 let PendingToolResult::Effect {
1789 effect_id,
1790 observation: pending_observation,
1791 ..
1792 } = result
1793 else {
1794 continue;
1795 };
1796 if effect_id == expected_effect_id {
1797 *pending_observation = Some(observation);
1798 return true;
1799 }
1800 }
1801 false
1802 }
1803
1804 async fn record_pending_tool_results(
1805 &mut self,
1806 state: &mut ReActKernelState,
1807 ) -> Result<(), AgentError> {
1808 while let Some(outcome) =
1809 state
1810 .pending_tool_results
1811 .front()
1812 .and_then(|pending| match pending {
1813 PendingToolResult::Outcome(outcome) => Some(outcome.clone()),
1814 PendingToolResult::Effect {
1815 call,
1816 observation: Some(observation),
1817 ..
1818 } => Some(Self::effect_observation_outcome(
1819 call.clone(),
1820 observation.clone(),
1821 )),
1822 PendingToolResult::Effect {
1823 observation: None, ..
1824 } => None,
1825 })
1826 {
1827 record_tool_result(&mut self.memory, &outcome).await?;
1828 state.pending_tool_results.pop_front();
1829 }
1830 Ok(())
1831 }
1832
1833 fn effect_observation_outcome(
1834 call: ToolCall,
1835 observation: EffectObservation,
1836 ) -> ToolCallOutcome {
1837 ToolCallOutcome {
1838 call,
1839 content: observation.output.observation_for_model,
1840 memory_policy: observation.output.memory_policy,
1841 effect: None,
1842 }
1843 }
1844
1845 async fn process_kernel_pending_tools(
1846 &mut self,
1847 mut state: ReActKernelState,
1848 context: &RunContext,
1849 ) -> Result<AgentAction, AgentError> {
1850 let mut effects = Vec::new();
1851 let mut effect_ids = HashSet::new();
1852 while let Some(call) = state.pending_tools.pop_front() {
1853 let ctx = ToolRoundCtx {
1854 context,
1855 round: state.counters.rounds,
1856 registry: &self.registry,
1857 state: &self.state,
1858 events: &self.events,
1859 };
1860 let outcome = ctx.run(call).await;
1861 if let Some(effect) = outcome.effect_request().cloned() {
1862 if !effect_ids.insert(effect.id.clone()) {
1863 return Err(AgentError::InvalidStep(format!(
1864 "duplicate effect request id: {}",
1865 effect.id
1866 )));
1867 }
1868 state
1869 .pending_tool_results
1870 .push_back(PendingToolResult::Effect {
1871 effect_id: effect.id.clone(),
1872 call: outcome.call.clone(),
1873 observation: None,
1874 });
1875 effects.push(effect);
1876 continue;
1877 }
1878 state
1879 .pending_tool_results
1880 .push_back(PendingToolResult::Outcome(outcome));
1881 }
1882 if let Err(error) = self.record_pending_tool_results(&mut state).await {
1883 self.kernel_state = Some(state);
1884 return Err(error);
1885 }
1886 if !effects.is_empty() {
1887 self.kernel_state = Some(state);
1888 return if effects.len() == 1 {
1889 let request = effects
1890 .pop()
1891 .expect("single effect request must be present");
1892 Ok(AgentAction::RequestEffect { request })
1893 } else {
1894 Ok(AgentAction::RequestEffects { requests: effects })
1895 };
1896 }
1897
1898 check_run_context(context)?;
1899 state.counters.rounds += 1;
1900 let messages = self.assemble_messages(self.memory.context().await?);
1901 let action = state.next_model_request(messages);
1902 self.kernel_state = Some(state);
1903 Ok(action)
1904 }
1905}
1906
1907#[cfg(feature = "tracing")]
1919type TraceSpan = tracing::Span;
1920
1921#[cfg(not(feature = "tracing"))]
1922#[derive(Debug, Clone)]
1923struct TraceSpan;
1924
1925#[cfg(feature = "tracing")]
1926fn instrument<F>(future: F, span: TraceSpan) -> tracing::instrument::Instrumented<F>
1927where
1928 F: Future,
1929{
1930 future.instrument(span)
1931}
1932
1933#[cfg(not(feature = "tracing"))]
1934fn instrument<F>(future: F, _span: TraceSpan) -> F
1935where
1936 F: Future,
1937{
1938 future
1939}
1940
1941fn span_run(run_id: &str) -> TraceSpan {
1942 #[cfg(feature = "tracing")]
1943 {
1944 tracing::info_span!("agent.run", "run.id" = %run_id, error = tracing::field::Empty)
1945 }
1946 #[cfg(not(feature = "tracing"))]
1947 {
1948 let _ = run_id;
1949 TraceSpan
1950 }
1951}
1952
1953fn span_llm(run_id: &str, round: usize) -> TraceSpan {
1958 #[cfg(feature = "tracing")]
1959 {
1960 tracing::debug_span!(
1961 "llm_request",
1962 "run.id" = %run_id,
1963 round = round,
1964 usage.prompt_tokens = tracing::field::Empty,
1965 usage.completion_tokens = tracing::field::Empty,
1966 usage.cached_tokens = tracing::field::Empty,
1967 error = tracing::field::Empty,
1968 )
1969 }
1970 #[cfg(not(feature = "tracing"))]
1971 {
1972 let _ = (run_id, round);
1973 TraceSpan
1974 }
1975}
1976
1977fn span_tool(run_id: &str, round: usize, name: &str) -> TraceSpan {
1980 #[cfg(feature = "tracing")]
1981 {
1982 tracing::debug_span!(
1983 "tool",
1984 "run.id" = %run_id,
1985 round = round,
1986 name = %name,
1987 error = tracing::field::Empty,
1988 )
1989 }
1990 #[cfg(not(feature = "tracing"))]
1991 {
1992 let _ = (run_id, round, name);
1993 TraceSpan
1994 }
1995}
1996
1997fn publish_ended(
2006 events: &Option<Arc<dyn EventChannel>>,
2007 summary: RunSummary,
2008 error: Option<AgentError>,
2009) {
2010 if let Some(pipe) = events {
2011 pipe.publish(Arc::new(ReActEvent::RunEnded { summary, error }));
2012 }
2013}
2014
2015fn stream_end(
2026 events: &Option<Arc<dyn EventChannel>>,
2027 #[cfg_attr(not(feature = "tracing"), allow(unused_variables))] run_span: &TraceSpan,
2028 summary: RunSummary,
2029 error: AgentError,
2030) -> Result<MessageChunk, AgentError> {
2031 #[cfg(feature = "tracing")]
2036 if !matches!(error, AgentError::Cancelled) {
2037 run_span.record("error", error.to_string());
2038 }
2039 publish_ended(events, summary, Some(error.clone()));
2040 match error {
2041 AgentError::Cancelled => Ok(MessageChunk::Cancelled),
2042 e => Err(e),
2043 }
2044}
2045
2046fn run_summary(
2047 counters: &RunCounters,
2048 finish_reason: Option<FinishReason>,
2049 started_at: Instant,
2050 provider_model: Option<String>,
2051) -> RunSummary {
2052 run_summary_from_parts(
2053 counters.rounds,
2054 counters.tool_calls_total,
2055 counters.usage,
2056 finish_reason,
2057 started_at,
2058 provider_model,
2059 )
2060}
2061
2062fn run_summary_from_parts(
2063 rounds: usize,
2064 tool_calls: usize,
2065 usage: UsageTotals,
2066 finish_reason: Option<FinishReason>,
2067 started_at: Instant,
2068 provider_model: Option<String>,
2069) -> RunSummary {
2070 RunSummary {
2071 rounds,
2072 tool_calls,
2073 usage: usage.total,
2074 usage_omitted: usage.omitted,
2075 cache_omitted: usage.cache_omitted || usage.omitted,
2079 finish_reason,
2080 latency: started_at.elapsed(),
2081 provider_model,
2082 }
2083}
2084
2085fn check_run_context(context: &RunContext) -> Result<(), AgentError> {
2086 if context.is_cancelled() {
2087 Err(AgentError::Cancelled)
2088 } else if context.is_expired() {
2089 Err(AgentError::DeadlineExceeded)
2090 } else {
2091 Ok(())
2092 }
2093}
2094
2095async fn run_until_context<F>(context: &RunContext, future: F) -> Result<F::Output, AgentError>
2096where
2097 F: Future,
2098{
2099 check_run_context(context)?;
2100 match context.remaining() {
2101 Some(remaining) if remaining.is_zero() => Err(AgentError::DeadlineExceeded),
2102 Some(remaining) => {
2103 tokio::select! {
2104 _ = context.cancellation.cancelled() => Err(AgentError::Cancelled),
2105 _ = tokio::time::sleep(remaining) => Err(AgentError::DeadlineExceeded),
2106 output = future => Ok(output),
2107 }
2108 }
2109 None => context
2110 .cancellation
2111 .run_until_cancelled(future)
2112 .await
2113 .ok_or(AgentError::Cancelled),
2114 }
2115}
2116
2117async fn record_tool_result(
2134 memory: &mut Box<dyn Memory>,
2135 outcome: &ToolCallOutcome,
2136) -> Result<(), AgentError> {
2137 let message = Message::tool_result(outcome.call.id.clone(), outcome.content.clone());
2138 let record_result = if outcome.memory_policy.is_protected() {
2139 memory.record_protected(message).await
2140 } else {
2141 memory.record(message).await
2142 };
2143 match record_result {
2144 Ok(()) => Ok(()),
2145 Err(e) => {
2146 let fallback = Message::tool_result(
2147 outcome.call.id.clone(),
2148 format!("memory record failed: {e}"),
2149 );
2150 let _ = if outcome.memory_policy.is_protected() {
2151 memory.record_protected(fallback).await
2152 } else {
2153 memory.record(fallback).await
2154 };
2155 Err(e.into())
2156 }
2157 }
2158}
2159
2160impl ReActAgent {
2161 async fn run_request_inner(
2162 &mut self,
2163 request: RunRequest,
2164 context: RunContext,
2165 schema: Option<&serde_json::Value>,
2166 ) -> Result<RunOutput, AgentError> {
2167 let run_id = context.run_id.clone();
2173 let run_span = span_run(&run_id);
2174 let provider_model = self.provider.model().map(str::to_string);
2175 let started_at = Instant::now();
2176 let run_future = async {
2177 let input = request.input;
2183 self.memory.record(input.clone().into_message()).await?;
2184 self.publish(|| {
2185 Arc::new(ReActEvent::RunStarted {
2186 run_id: run_id.clone(),
2187 input,
2188 })
2189 });
2190
2191 let mut counters = RunCounters::default();
2195 let mut options = request
2196 .options
2197 .clone()
2198 .unwrap_or_else(|| self.config.options.clone());
2199 if let Some(schema) = schema {
2200 options.structured = Some(schema.clone());
2201 }
2202 let validation_schema = options.structured.as_ref();
2203 let result: Result<FinalAnswer, AgentError> = self
2204 .run_rounds_with_context(
2205 &context,
2206 &run_id,
2207 &mut counters,
2208 &options,
2209 validation_schema,
2210 )
2211 .await;
2212 let output_result = result.map(|final_answer| {
2213 let summary = run_summary(
2214 &counters,
2215 final_answer.finish_reason.clone(),
2216 started_at,
2217 provider_model.clone(),
2218 );
2219 RunExecution {
2220 answer: final_answer.answer,
2221 final_message: final_answer.final_message,
2222 summary,
2223 artifacts: Vec::new(),
2224 metadata: RunMetadata::new(),
2225 }
2226 });
2227 let summary = match &output_result {
2228 Ok(execution) => execution.summary.clone(),
2229 Err(_) => run_summary(&counters, None, started_at, provider_model.clone()),
2230 };
2231 publish_ended(&self.events, summary, output_result.as_ref().err().cloned());
2236 output_result
2237 };
2238 let result = instrument(run_future, run_span.clone()).await;
2239 #[cfg(feature = "tracing")]
2240 if let Err(e) = &result {
2241 run_span.record("error", e.to_string());
2242 }
2243 result.map(|execution| RunOutput {
2244 run_id,
2245 answer: execution.answer,
2246 summary: execution.summary,
2247 final_message: execution.final_message,
2248 artifacts: execution.artifacts,
2249 metadata: execution.metadata,
2250 })
2251 }
2252
2253 async fn run_stream_request_inner<'a>(
2254 &'a mut self,
2255 request: RunRequest,
2256 context: RunContext,
2257 ) -> Result<BoxStream<'a, Result<MessageChunk, AgentError>>, AgentError> {
2258 let run_id = context.run_id.clone();
2271 let run_span = span_run(&run_id);
2272 let stream_span = run_span.clone();
2273 let input = request.input;
2274 self.memory.record(input.clone().into_message()).await?;
2275 self.publish(|| {
2276 Arc::new(ReActEvent::RunStarted {
2277 run_id: run_id.clone(),
2278 input,
2279 })
2280 });
2281 let schemas = self.registry.schemas();
2282 let max_rounds = self.config.max_tool_rounds;
2283 let provider_model = self.provider.model().map(str::to_string);
2284 let started_at = Instant::now();
2285 let options = request
2286 .options
2287 .clone()
2288 .unwrap_or_else(|| self.config.options.clone());
2289 let validation_schema = options.structured.clone();
2290
2291 let context = context.clone();
2295 let stream = async_stream::stream! {
2296 let mut rounds = 0usize;
2297 let mut tool_calls_total = 0usize;
2302 let mut usage_totals = UsageTotals::default();
2303 #[cfg(feature = "structured")]
2307 let mut validator = validation_schema.as_ref().map(|schema| {
2308 StructuredValidator::new(schema.clone(), self.config.max_structured_retries)
2309 });
2310 #[cfg(not(feature = "structured"))]
2311 let _ = &validation_schema;
2312 let mut tool_rounds = 0usize;
2317 'rounds: loop {
2321 if tool_rounds >= max_rounds {
2324 let summary = run_summary_from_parts(
2325 rounds,
2326 tool_calls_total,
2327 usage_totals,
2328 None,
2329 started_at,
2330 provider_model.clone(),
2331 );
2332 yield stream_end(&self.events, &run_span, summary,
2333 AgentError::TooManyToolRounds(max_rounds));
2334 break;
2335 }
2336
2337 rounds += 1;
2342
2343 if let Err(e) = check_run_context(&context) {
2347 usage_totals.omit();
2350 let summary = run_summary_from_parts(
2351 rounds,
2352 tool_calls_total,
2353 usage_totals,
2354 None,
2355 started_at,
2356 provider_model.clone(),
2357 );
2358 yield stream_end(&self.events, &run_span, summary, e);
2359 break;
2360 }
2361
2362 let messages = match self.memory.context().await {
2366 Ok(messages) => messages,
2367 Err(e) => {
2368 usage_totals.omit();
2371 let summary = run_summary_from_parts(
2372 rounds,
2373 tool_calls_total,
2374 usage_totals,
2375 None,
2376 started_at,
2377 provider_model.clone(),
2378 );
2379 yield stream_end(&self.events, &run_span, summary,
2380 AgentError::Memory(e));
2381 break;
2382 }
2383 };
2384 let llm_span = span_llm(&run_id, rounds);
2392 let model_request_id = format!("{run_id}-model-{rounds}");
2393 let provider_context =
2394 ProviderRequestContext::from_run_context(model_request_id, &context);
2395 let stream_chat = self.provider.stream_chat_with_context(
2396 ChatRequest {
2397 messages: self.assemble_messages(messages),
2398 tools: schemas.clone(),
2399 options: options.clone(),
2400 },
2401 &provider_context,
2402 );
2403 let mut provider_stream = match run_until_context(
2404 &context,
2405 instrument(stream_chat, llm_span.clone()),
2406 )
2407 .await
2408 {
2409 Ok(Ok(stream)) => stream,
2410 Ok(Err(e)) => {
2411 #[cfg(feature = "tracing")]
2414 llm_span.record("error", e.to_string());
2415 usage_totals.omit();
2418 let summary = run_summary_from_parts(
2419 rounds,
2420 tool_calls_total,
2421 usage_totals,
2422 None,
2423 started_at,
2424 provider_model.clone(),
2425 );
2426 yield stream_end(&self.events, &run_span, summary,
2427 AgentError::Provider(e));
2428 break;
2429 }
2430 Err(e) => {
2431 usage_totals.omit();
2434 let summary = run_summary_from_parts(
2435 rounds,
2436 tool_calls_total,
2437 usage_totals,
2438 None,
2439 started_at,
2440 provider_model.clone(),
2441 );
2442 yield stream_end(&self.events, &run_span, summary, e);
2443 break;
2444 }
2445 };
2446
2447 let mut text = String::new();
2459 let mut reasoning = String::new();
2460 let mut calls = Vec::new();
2461 let mut round_finish_reason = None::<FinishReason>;
2462 let mut round_usage_reported = false;
2466 loop {
2467 let next = run_until_context(
2473 &context,
2474 instrument(provider_stream.next(), llm_span.clone()),
2475 )
2476 .await;
2477 let Some(event) = (match next {
2478 Ok(event) => event,
2479 Err(e) => {
2480 usage_totals.omit();
2483 let summary = run_summary_from_parts(
2484 rounds,
2485 tool_calls_total,
2486 usage_totals,
2487 None,
2488 started_at,
2489 provider_model.clone(),
2490 );
2491 yield stream_end(&self.events, &run_span, summary, e);
2492 break 'rounds;
2493 }
2494 }) else {
2495 break;
2498 };
2499 match event {
2500 Ok(StreamEvent::Delta(delta)) => {
2501 if text.len() + delta.len() > MAX_ROUND_TEXT {
2506 usage_totals.omit();
2510 let summary = run_summary_from_parts(
2511 rounds,
2512 tool_calls_total,
2513 usage_totals,
2514 None,
2515 started_at,
2516 provider_model.clone(),
2517 );
2518 yield stream_end(&self.events, &run_span, summary,
2519 AgentError::Provider(ProviderError::ResponseTooLarge {
2520 limit_bytes: MAX_ROUND_TEXT,
2521 }));
2522 break 'rounds;
2523 }
2524 text.push_str(&delta);
2525 self.publish(|| Arc::new(ReActEvent::Delta { text: delta.clone() }));
2526 yield Ok(MessageChunk::Delta(delta));
2527 }
2528 Ok(StreamEvent::Reasoning(chunk)) => {
2529 if reasoning.len() + chunk.len() > MAX_ROUND_TEXT {
2532 usage_totals.omit();
2536 let summary = run_summary_from_parts(
2537 rounds,
2538 tool_calls_total,
2539 usage_totals,
2540 None,
2541 started_at,
2542 provider_model.clone(),
2543 );
2544 yield stream_end(&self.events, &run_span, summary,
2545 AgentError::Provider(ProviderError::ResponseTooLarge {
2546 limit_bytes: MAX_ROUND_TEXT,
2547 }));
2548 break 'rounds;
2549 }
2550 reasoning.push_str(&chunk);
2551 self.publish(move || Arc::new(ReActEvent::Reasoning { text: chunk }));
2552 }
2553 Ok(StreamEvent::ToolCall { id, name, arguments }) => {
2554 calls.push(ToolCall {
2555 id: id.clone(),
2556 name: name.clone(),
2557 arguments: arguments.clone(),
2558 });
2559 yield Ok(MessageChunk::ToolCall { id, name, arguments });
2560 }
2561 Ok(StreamEvent::Done { reason, usage }) => {
2562 if let Some(usage) = usage {
2570 #[cfg(feature = "tracing")]
2571 {
2572 llm_span.record("usage.prompt_tokens", usage.prompt_tokens);
2573 llm_span.record("usage.completion_tokens", usage.completion_tokens);
2574 if let Some(cached) = usage.cached_tokens {
2575 llm_span.record("usage.cached_tokens", cached);
2576 }
2577 }
2578 usage_totals.record(usage);
2579 round_usage_reported = true;
2580 } else {
2581 usage_totals.omit();
2582 }
2583 round_finish_reason = Some(reason);
2584 break;
2591 }
2592 Err(e) => {
2593 #[cfg(feature = "tracing")]
2598 llm_span.record("error", e.to_string());
2599 usage_totals.omit();
2602 let summary = run_summary_from_parts(
2603 rounds,
2604 tool_calls_total,
2605 usage_totals,
2606 None,
2607 started_at,
2608 provider_model.clone(),
2609 );
2610 yield stream_end(&self.events, &run_span, summary,
2611 AgentError::Provider(e));
2612 break 'rounds;
2613 }
2614 Ok(_) => {}
2615 }
2616 }
2617
2618 if !round_usage_reported {
2622 usage_totals.omit();
2623 }
2624
2625 if !text.is_empty() || !reasoning.is_empty() || !calls.is_empty() {
2628 let message = Message::Assistant {
2629 content: text.clone(),
2630 reasoning: (!reasoning.is_empty()).then_some(reasoning),
2631 tool_calls: calls.clone(),
2632 };
2633 match self.memory.record(message).await {
2634 Ok(()) => {}
2635 Err(e) => {
2636 let summary = run_summary_from_parts(
2637 rounds,
2638 tool_calls_total,
2639 usage_totals,
2640 None,
2641 started_at,
2642 provider_model.clone(),
2643 );
2644 yield stream_end(&self.events, &run_span, summary,
2645 AgentError::Memory(e));
2646 break;
2647 }
2648 }
2649 }
2650
2651 if calls.is_empty() {
2652 #[cfg(feature = "structured")]
2660 {
2661 if let Some(validator) = &mut validator {
2662 match validator.validate(&text) {
2663 StructuredOutcome::Passed => {}
2664 StructuredOutcome::Retry { message } => {
2665 match self.memory.record(message).await {
2668 Ok(()) => continue 'rounds,
2669 Err(e) => {
2670 let summary = run_summary_from_parts(
2671 rounds,
2672 tool_calls_total,
2673 usage_totals,
2674 None,
2675 started_at,
2676 provider_model.clone(),
2677 );
2678 yield stream_end(&self.events, &run_span, summary,
2679 AgentError::Memory(e));
2680 break;
2681 }
2682 }
2683 }
2684 StructuredOutcome::Exhausted { max_retries } => {
2685 let summary = run_summary_from_parts(
2686 rounds,
2687 tool_calls_total,
2688 usage_totals,
2689 None,
2690 started_at,
2691 provider_model.clone(),
2692 );
2693 yield stream_end(&self.events, &run_span, summary,
2694 AgentError::StructuredRetriesExhausted(max_retries));
2695 break 'rounds;
2696 }
2697 }
2698 }
2699 }
2700 let summary = run_summary_from_parts(
2703 rounds,
2704 tool_calls_total,
2705 usage_totals,
2706 round_finish_reason,
2707 started_at,
2708 provider_model.clone(),
2709 );
2710 publish_ended(&self.events, summary.clone(), None);
2711 yield Ok(MessageChunk::Done(summary));
2712 break;
2713 }
2714
2715 tool_calls_total += calls.len();
2720 tool_rounds += 1;
2721 let ctx = ToolRoundCtx {
2722 context: &context,
2723 round: rounds,
2724 registry: &self.registry,
2725 state: &self.state,
2726 events: &self.events,
2727 };
2728 let mut outcomes = self.executor.execute_round(ctx, calls).await;
2729 while let Some(outcome) = outcomes.next().await {
2730 if let Some(effect) = outcome.effect_request() {
2731 let summary = run_summary_from_parts(
2732 rounds,
2733 tool_calls_total,
2734 usage_totals,
2735 None,
2736 started_at,
2737 provider_model.clone(),
2738 );
2739 yield stream_end(
2740 &self.events,
2741 &run_span,
2742 summary,
2743 AgentError::EffectRequiresHarness(format!(
2744 "{} ({})",
2745 effect.description, effect.id
2746 )),
2747 );
2748 break 'rounds;
2749 }
2750 yield Ok(MessageChunk::ToolResult {
2751 id: outcome.call.id.clone(),
2752 name: outcome.call.name.clone(),
2753 content: outcome.content.clone(),
2754 });
2755 if let Err(e) = record_tool_result(&mut self.memory, &outcome).await {
2760 let summary = run_summary_from_parts(
2761 rounds,
2762 tool_calls_total,
2763 usage_totals,
2764 None,
2765 started_at,
2766 provider_model.clone(),
2767 );
2768 yield stream_end(&self.events, &run_span, summary, e);
2769 break 'rounds;
2775 }
2776 }
2777 }
2778 };
2779 Ok(Box::pin(SpanStream {
2780 stream: Box::pin(stream),
2781 span: stream_span,
2782 }))
2783 }
2784}
2785
2786struct SpanStream<S> {
2799 stream: Pin<Box<S>>,
2800 #[cfg_attr(not(feature = "tracing"), allow(dead_code))]
2801 span: TraceSpan,
2802}
2803
2804impl<S: futures::Stream> futures::Stream for SpanStream<S> {
2805 type Item = S::Item;
2806
2807 fn poll_next(
2808 mut self: Pin<&mut Self>,
2809 cx: &mut std::task::Context<'_>,
2810 ) -> std::task::Poll<Option<Self::Item>> {
2811 #[cfg(feature = "tracing")]
2815 let span = self.span.clone();
2816 #[cfg(feature = "tracing")]
2817 let _enter = span.enter();
2818 self.stream.as_mut().poll_next(cx)
2822 }
2823}
2824
2825#[cfg(test)]
2826mod tests {
2827 use super::*;
2828 use crate::CancellationToken;
2829 use crate::effect::{EffectKind, EffectObservation, EffectRequest};
2830 use crate::memory::MemoryError;
2831 use crate::message::ContentBlock;
2832 use crate::provider::{
2833 ChatResponse, FakeProvider, FakeReply, FinishReason, ModelOptions, ProviderError,
2834 StreamEvent, TimeoutStage,
2835 };
2836 use crate::tool::{Tool, ToolContext, ToolError, ToolOutput, ToolResult, ToolSchema};
2837 use futures::StreamExt;
2838 #[cfg(feature = "structured")]
2839 use serde::Deserialize;
2840 use std::sync::Arc;
2841 use std::sync::atomic::{AtomicUsize, Ordering};
2842 use std::time::Duration;
2843
2844 #[derive(Clone)]
2849 struct SharedFake(Arc<FakeProvider>);
2850
2851 impl SharedFake {
2852 fn new(replies: impl IntoIterator<Item = FakeReply>) -> Self {
2853 Self(Arc::new(FakeProvider::new(replies)))
2854 }
2855
2856 fn requests(&self) -> Vec<ChatRequest> {
2857 self.0.requests()
2858 }
2859 }
2860
2861 #[async_trait::async_trait]
2862 impl Provider for SharedFake {
2863 fn model(&self) -> Option<&str> {
2864 self.0.model()
2865 }
2866
2867 async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, ProviderError> {
2868 self.0.chat(request).await
2869 }
2870
2871 async fn chat_with_context(
2872 &self,
2873 request: ChatRequest,
2874 context: &ProviderRequestContext,
2875 ) -> Result<ChatResponse, ProviderError> {
2876 self.0.chat_with_context(request, context).await
2877 }
2878
2879 async fn stream_chat(
2880 &self,
2881 request: ChatRequest,
2882 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError> {
2883 self.0.stream_chat(request).await
2884 }
2885
2886 async fn stream_chat_with_context(
2887 &self,
2888 request: ChatRequest,
2889 context: &ProviderRequestContext,
2890 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError> {
2891 self.0.stream_chat_with_context(request, context).await
2892 }
2893 }
2894
2895 #[derive(Debug, Clone)]
2897 struct FakeTool {
2898 name: &'static str,
2899 result: &'static str,
2900 calls: Arc<AtomicUsize>,
2901 }
2902
2903 impl FakeTool {
2904 fn new(name: &'static str, result: &'static str) -> (Self, Arc<AtomicUsize>) {
2905 let calls = Arc::new(AtomicUsize::new(0));
2906 (
2907 Self {
2908 name,
2909 result,
2910 calls: calls.clone(),
2911 },
2912 calls,
2913 )
2914 }
2915 }
2916
2917 #[async_trait::async_trait]
2918 impl Tool for FakeTool {
2919 fn schema(&self) -> ToolSchema {
2920 ToolSchema::new(self.name, "Test tool", serde_json::json!({}))
2921 }
2922
2923 async fn call(
2924 &self,
2925 _arguments: serde_json::Value,
2926 _context: ToolContext<'_>,
2927 ) -> Result<ToolResult, ToolError> {
2928 self.calls.fetch_add(1, Ordering::Relaxed);
2929 Ok(ToolOutput::text(self.result).into())
2930 }
2931 }
2932
2933 #[derive(Debug, Clone)]
2934 struct EffectTool {
2935 name: &'static str,
2936 effect_id: &'static str,
2937 description: &'static str,
2938 }
2939
2940 impl EffectTool {
2941 fn new(name: &'static str, effect_id: &'static str, description: &'static str) -> Self {
2942 Self {
2943 name,
2944 effect_id,
2945 description,
2946 }
2947 }
2948 }
2949
2950 #[async_trait::async_trait]
2951 impl Tool for EffectTool {
2952 fn schema(&self) -> ToolSchema {
2953 ToolSchema::new(self.name, "Effect tool", serde_json::json!({}))
2954 }
2955
2956 async fn call(
2957 &self,
2958 _arguments: serde_json::Value,
2959 _context: ToolContext<'_>,
2960 ) -> Result<ToolResult, ToolError> {
2961 Ok(ToolResult::Effect(
2962 EffectRequest::new(
2963 EffectKind::Custom("test.effect".into()),
2964 self.description,
2965 serde_json::json!({}),
2966 )
2967 .with_id(self.effect_id),
2968 ))
2969 }
2970 }
2971
2972 fn call(id: &str, name: &str, arguments: &str) -> ToolCall {
2973 ToolCall {
2974 id: id.into(),
2975 name: name.into(),
2976 arguments: arguments.into(),
2977 }
2978 }
2979
2980 fn done_summary(chunk: &MessageChunk) -> Option<&RunSummary> {
2981 match chunk {
2982 MessageChunk::Done(summary) => Some(summary),
2983 _ => None,
2984 }
2985 }
2986
2987 fn assert_done_summary(
2988 chunk: &MessageChunk,
2989 rounds: usize,
2990 tool_calls: usize,
2991 usage: Usage,
2992 usage_omitted: bool,
2993 ) {
2994 assert_done_summary_with_finish(
2995 chunk,
2996 rounds,
2997 tool_calls,
2998 usage,
2999 usage_omitted,
3000 Some(FinishReason::Stop),
3001 );
3002 }
3003
3004 fn assert_done_summary_with_finish(
3005 chunk: &MessageChunk,
3006 rounds: usize,
3007 tool_calls: usize,
3008 usage: Usage,
3009 usage_omitted: bool,
3010 finish_reason: Option<FinishReason>,
3011 ) {
3012 let summary = done_summary(chunk).expect("expected Done chunk");
3013 assert_eq!(summary.rounds, rounds);
3014 assert_eq!(summary.tool_calls, tool_calls);
3015 assert_eq!(summary.usage, usage);
3016 assert_eq!(summary.usage_omitted, usage_omitted);
3017 assert_eq!(summary.finish_reason, finish_reason);
3018 assert_eq!(summary.provider_model, None);
3019 }
3020
3021 fn agent(fake: SharedFake, system_prompt: &str) -> ReActAgent {
3024 ReActAgent::new(fake, ToolRegistry::new(), system_prompt)
3025 }
3026
3027 fn agent_with_registry(
3029 fake: SharedFake,
3030 registry: ToolRegistry,
3031 config: AgentConfig,
3032 ) -> ReActAgent {
3033 ReActAgent::new(fake, registry, "").with_config(config)
3034 }
3035
3036 fn cancellation_context(token: &CancellationToken) -> RunContext {
3037 RunContext::generated().with_cancellation(token.clone())
3038 }
3039
3040 #[tokio::test]
3041 async fn builder_assembles_agent_components() {
3042 let fake = SharedFake::new([FakeReply::Text("built".into())]);
3043 let (tool, _calls) = FakeTool::new("builder_tool", "unused");
3044 let mut agent = ReActAgent::builder(fake.clone())
3045 .with_tool(tool)
3046 .with_system_prompt("Builder system")
3047 .with_config(AgentConfig {
3048 options: ModelOptions {
3049 temperature: Some(0.4),
3050 ..Default::default()
3051 },
3052 ..Default::default()
3053 })
3054 .build();
3055
3056 assert_eq!(agent.run("hi").await.unwrap(), "built");
3057 let request = &fake.requests()[0];
3058 assert_eq!(request.messages[0], Message::system("Builder system"));
3059 assert_eq!(request.tools.len(), 1);
3060 assert_eq!(request.tools[0].name, "builder_tool");
3061 assert_eq!(request.options.temperature, Some(0.4));
3062 }
3063
3064 #[tokio::test]
3065 async fn direct_answer() {
3066 let fake = SharedFake::new([FakeReply::Text("Hello".into())]);
3067 let mut agent = agent(fake.clone(), "");
3068
3069 let answer = agent.run("Are you there").await.unwrap();
3070 assert_eq!(answer, "Hello");
3071
3072 let requests = fake.requests();
3073 assert_eq!(requests.len(), 1);
3074 assert_eq!(requests[0].messages.len(), 1);
3075 assert_eq!(requests[0].messages[0], Message::user("Are you there"));
3076 }
3077
3078 #[tokio::test]
3079 async fn run_request_returns_structured_output() {
3080 let fake = SharedFake::new([FakeReply::text_with_usage("Hello", Usage::new(5, 2))]);
3081 let mut agent = agent(fake.clone(), "");
3082 let output = agent
3083 .run_request_with_context(RunRequest::text("Are you there"), RunContext::new("r1"))
3084 .await
3085 .unwrap();
3086
3087 assert_eq!(output.run_id, "r1");
3088 assert_eq!(output.answer, "Hello");
3089 assert_eq!(output.final_message, Message::assistant("Hello"));
3090 assert!(output.artifacts.is_empty());
3091 assert!(output.metadata.is_empty());
3092 assert_eq!(output.summary.rounds, 1);
3093 assert_eq!(output.summary.tool_calls, 0);
3094 assert_eq!(output.summary.usage, Usage::new(5, 2));
3095 assert_eq!(output.summary.finish_reason, Some(FinishReason::Stop));
3096 assert_eq!(output.summary.provider_model, None);
3097 }
3098
3099 #[tokio::test]
3102 async fn run_request_carries_reported_cache_breakdown() {
3103 let fake = SharedFake::new([FakeReply::text_with_usage(
3104 "Hello",
3105 Usage::new(5, 2).with_cached_tokens(4),
3106 )]);
3107 let mut agent = agent(fake.clone(), "");
3108 let output = agent
3109 .run_request_with_context(RunRequest::text("hi"), RunContext::new("r1"))
3110 .await
3111 .unwrap();
3112
3113 assert_eq!(output.summary.usage.cached_tokens, Some(4));
3114 assert!(!output.summary.usage_omitted);
3115 assert!(!output.summary.cache_omitted);
3116 }
3117
3118 #[tokio::test]
3122 async fn run_request_tracks_omitted_cache_breakdown() {
3123 let fake = SharedFake::new([FakeReply::text_with_usage("Hello", Usage::new(5, 2))]);
3124 let mut agent = agent(fake.clone(), "");
3125 let output = agent
3126 .run_request_with_context(RunRequest::text("hi"), RunContext::new("r1"))
3127 .await
3128 .unwrap();
3129
3130 assert_eq!(output.summary.usage.cached_tokens, None);
3133 assert!(!output.summary.usage_omitted);
3134 assert!(output.summary.cache_omitted);
3135 }
3136
3137 #[tokio::test]
3138 async fn run_request_blocks_are_recorded_as_user_blocks() {
3139 let blocks = vec![ContentBlock::Text("What is this?".into())];
3140 let fake = SharedFake::new([FakeReply::Text("A block".into())]);
3141 let mut agent = agent(fake.clone(), "");
3142
3143 agent
3144 .run_request(RunRequest::blocks(blocks.clone()))
3145 .await
3146 .unwrap();
3147
3148 let requests = fake.requests();
3149 assert_eq!(requests[0].messages[0], Message::user_blocks(blocks));
3150 }
3151
3152 #[tokio::test]
3153 async fn run_summary_carries_provider_model_when_available() {
3154 #[derive(Clone)]
3155 struct NamedProvider(SharedFake);
3156
3157 #[async_trait::async_trait]
3158 impl Provider for NamedProvider {
3159 fn model(&self) -> Option<&str> {
3160 Some("named-model")
3161 }
3162
3163 async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, ProviderError> {
3164 self.0.chat(request).await
3165 }
3166
3167 async fn chat_with_context(
3168 &self,
3169 request: ChatRequest,
3170 context: &ProviderRequestContext,
3171 ) -> Result<ChatResponse, ProviderError> {
3172 self.0.chat_with_context(request, context).await
3173 }
3174
3175 async fn stream_chat(
3176 &self,
3177 request: ChatRequest,
3178 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
3179 {
3180 self.0.stream_chat(request).await
3181 }
3182
3183 async fn stream_chat_with_context(
3184 &self,
3185 request: ChatRequest,
3186 context: &ProviderRequestContext,
3187 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
3188 {
3189 self.0.stream_chat_with_context(request, context).await
3190 }
3191 }
3192
3193 let mut agent = ReActAgent::new(
3194 NamedProvider(SharedFake::new([FakeReply::Text("hi".into())])),
3195 ToolRegistry::new(),
3196 "",
3197 );
3198 let output = agent.run_request(RunRequest::text("hi")).await.unwrap();
3199 assert_eq!(output.summary.provider_model, Some("named-model".into()));
3200 }
3201
3202 #[tokio::test]
3203 async fn single_tool_round() {
3204 let (calc, calls) = FakeTool::new("calc", "42");
3205 let mut registry = ToolRegistry::new();
3206 registry.register(calc);
3207 let fake = SharedFake::new([
3208 FakeReply::ToolCalls {
3209 content: "".into(),
3210 calls: vec![call("c1", "calc", r#"{"a":1}"#)],
3211 },
3212 FakeReply::Text("The answer is 42".into()),
3213 ]);
3214 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
3215
3216 let answer = agent.run("Compute 1+1").await.unwrap();
3217 assert_eq!(answer, "The answer is 42");
3218 assert_eq!(calls.load(Ordering::Relaxed), 1);
3219
3220 let requests = fake.requests();
3223 assert_eq!(requests.len(), 2);
3224 assert!(requests[1].messages.iter().any(|m| matches!(
3225 m,
3226 Message::ToolResult { id, content } if id == "c1" && content == "42"
3227 )));
3228 }
3229
3230 #[tokio::test]
3231 async fn multiple_tools_same_round() {
3232 let (t1, calls1) = FakeTool::new("t1", "one");
3233 let (t2, calls2) = FakeTool::new("t2", "two");
3234 let mut registry = ToolRegistry::new();
3235 registry.register(t1).register(t2);
3236 let fake = SharedFake::new([
3237 FakeReply::ToolCalls {
3238 content: "".into(),
3239 calls: vec![call("c1", "t1", "{}"), call("c2", "t2", "{}")],
3240 },
3241 FakeReply::Text("done".into()),
3242 ]);
3243 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
3244
3245 let answer = agent.run("Run them all").await.unwrap();
3246 assert_eq!(answer, "done");
3247 assert_eq!(calls1.load(Ordering::Relaxed), 1);
3248 assert_eq!(calls2.load(Ordering::Relaxed), 1);
3249
3250 let requests = fake.requests();
3253 let assistant = requests[1]
3254 .messages
3255 .iter()
3256 .find_map(|m| match m {
3257 Message::Assistant { tool_calls, .. } => Some(tool_calls),
3258 _ => None,
3259 })
3260 .expect("second round should contain Assistant");
3261 assert_eq!(assistant.len(), 2);
3262
3263 let results: Vec<&str> = requests[1]
3264 .messages
3265 .iter()
3266 .filter_map(|m| match m {
3267 Message::ToolResult { content, .. } => Some(content.as_str()),
3268 _ => None,
3269 })
3270 .collect();
3271 assert_eq!(results, vec!["one", "two"]);
3272 }
3273
3274 #[tokio::test]
3277 async fn empty_assistant_not_recorded() {
3278 let fake = SharedFake::new([FakeReply::Text("".into())]);
3279 let mut agent = agent(fake.clone(), "");
3280
3281 let answer = agent.run("hi").await.unwrap();
3282 assert_eq!(answer, "");
3283
3284 let requests = fake.requests();
3285 assert_eq!(requests.len(), 1);
3286 assert_eq!(requests[0].messages.len(), 1); }
3288
3289 #[tokio::test]
3293 async fn default_memory_is_bounded_window() {
3294 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
3295 let mut agent = agent(fake, "");
3296
3297 agent
3301 .memory
3302 .record(Message::user("x".repeat(600_000)))
3303 .await
3304 .unwrap();
3305 agent
3306 .memory
3307 .record(Message::assistant("first-round reply"))
3308 .await
3309 .unwrap();
3310 agent
3311 .memory
3312 .record(Message::user("second round"))
3313 .await
3314 .unwrap();
3315 agent
3316 .memory
3317 .record(Message::assistant("second-round reply"))
3318 .await
3319 .unwrap();
3320
3321 let ctx = agent.memory.context().await.unwrap();
3322 assert_eq!(
3323 ctx,
3324 vec![
3325 Message::user("second round"),
3326 Message::assistant("second-round reply")
3327 ]
3328 );
3329 }
3330
3331 #[tokio::test]
3334 async fn too_many_tool_rounds() {
3335 let (calc, _calls) = FakeTool::new("calc", "42");
3336 let mut registry = ToolRegistry::new();
3337 registry.register(calc);
3338 let fake = SharedFake::new([
3339 FakeReply::ToolCalls {
3340 content: "".into(),
3341 calls: vec![call("c1", "calc", "{}")],
3342 },
3343 FakeReply::ToolCalls {
3344 content: "".into(),
3345 calls: vec![call("c2", "calc", "{}")],
3346 },
3347 ]);
3348 let mut agent = agent_with_registry(
3349 fake.clone(),
3350 registry,
3351 AgentConfig {
3352 max_tool_rounds: 2,
3353 ..Default::default()
3354 },
3355 );
3356
3357 let err = agent.run("Keep computing").await.unwrap_err();
3358 assert!(matches!(err, AgentError::TooManyToolRounds(2)));
3359 assert_eq!(fake.requests().len(), 2); }
3361
3362 #[tokio::test]
3365 async fn system_prompt_assembled_every_request() {
3366 let fake = SharedFake::new([
3367 FakeReply::Text("Hello".into()),
3368 FakeReply::Text("Goodbye".into()),
3369 ]);
3370 let mut agent = agent(fake.clone(), "You are an assistant");
3371
3372 agent.run("Are you there").await.unwrap();
3373 agent.run("Any more?").await.unwrap();
3374
3375 let requests = fake.requests();
3376 assert_eq!(requests.len(), 2);
3377 for request in &requests {
3380 let systems = request
3381 .messages
3382 .iter()
3383 .filter(|m| matches!(m, Message::System(_)))
3384 .count();
3385 assert_eq!(systems, 1);
3386 assert_eq!(request.messages[0], Message::system("You are an assistant"));
3387 }
3388 assert_eq!(requests[1].messages.len(), 4); assert_eq!(requests[1].messages[3], Message::user("Any more?"));
3390 }
3391
3392 #[tokio::test]
3395 async fn macro_arms_without_system_prompt() {
3396 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
3398 let mut agent = crate::react_agent!(fake.clone());
3399 assert_eq!(agent.run("hi").await.unwrap(), "hi");
3400 assert_eq!(fake.requests()[0].messages.len(), 1); let (t1, calls1) = FakeTool::new("t1", "one");
3404 let fake = SharedFake::new([
3405 FakeReply::ToolCalls {
3406 content: "".into(),
3407 calls: vec![call("c1", "t1", "{}")],
3408 },
3409 FakeReply::Text("done".into()),
3410 ]);
3411 let mut agent = crate::react_agent!(fake.clone(), [t1]);
3412 assert_eq!(agent.run("x").await.unwrap(), "done");
3413 assert_eq!(calls1.load(Ordering::Relaxed), 1);
3414 assert_eq!(fake.requests()[1].messages.len(), 3); let (t2, _calls2) = FakeTool::new("t2", "two");
3418 let mut registry = ToolRegistry::new();
3419 registry.register(t2);
3420 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
3421 let mut agent = crate::react_agent!(fake.clone(), registry);
3422 assert_eq!(agent.run("hi").await.unwrap(), "hi");
3423 assert_eq!(fake.requests()[0].messages.len(), 1); }
3425
3426 #[tokio::test]
3429 async fn macro_three_arms() {
3430 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
3432 let mut agent = crate::react_agent!(fake.clone(), "");
3433 assert_eq!(agent.run("hi").await.unwrap(), "hi");
3434 assert_eq!(fake.requests()[0].messages.len(), 1);
3435
3436 let (t1, calls1) = FakeTool::new("t1", "one");
3439 let (t2, calls2) = FakeTool::new("t2", "two");
3440 let fake = SharedFake::new([
3441 FakeReply::ToolCalls {
3442 content: "".into(),
3443 calls: vec![call("c1", "t1", "{}")],
3444 },
3445 FakeReply::Text("done".into()),
3446 ]);
3447 let mut agent = crate::react_agent!(fake.clone(), [t1, t2], "");
3448 assert_eq!(agent.run("x").await.unwrap(), "done");
3449 assert_eq!(calls1.load(Ordering::Relaxed), 1);
3450 assert_eq!(calls2.load(Ordering::Relaxed), 0);
3451
3452 let (t3, calls3) = FakeTool::new("t3", "three");
3454 let mut registry = ToolRegistry::new();
3455 registry.register(t3);
3456 let fake = SharedFake::new([
3457 FakeReply::ToolCalls {
3458 content: "".into(),
3459 calls: vec![call("c1", "t3", "{}")],
3460 },
3461 FakeReply::Text("done".into()),
3462 ]);
3463 let mut agent = crate::react_agent!(fake, registry, "");
3464 assert_eq!(agent.run("x").await.unwrap(), "done");
3465 assert_eq!(calls3.load(Ordering::Relaxed), 1);
3466 }
3467
3468 #[test]
3471 fn macro_all_arms_compile() {
3472 struct Echo;
3473 #[async_trait::async_trait]
3474 impl Tool for Echo {
3475 fn schema(&self) -> ToolSchema {
3476 ToolSchema::new("echo", "Echo", serde_json::json!({}))
3477 }
3478 async fn call(
3479 &self,
3480 _arguments: serde_json::Value,
3481 _context: ToolContext<'_>,
3482 ) -> Result<ToolResult, ToolError> {
3483 Ok(ToolOutput::text("echo").into())
3484 }
3485 }
3486
3487 fn fake() -> SharedFake {
3488 SharedFake::new([FakeReply::Text("hi".into())])
3489 }
3490
3491 let a1 = crate::react_agent!(fake()); let a2 = crate::react_agent!(fake(), "You are an assistant"); let a3 = crate::react_agent!(fake(), [Echo]); let a4 = crate::react_agent!(fake(), [Echo], "You are an assistant"); let mut registry = ToolRegistry::new();
3496 registry.register(Echo);
3497 let a5 = crate::react_agent!(fake(), registry.clone()); let a6 = crate::react_agent!(fake(), registry, "You are an assistant"); let _ = (a1, a2, a3, a4, a5, a6);
3500 }
3501
3502 #[tokio::test]
3504 async fn empty_system_prompt_skips_system_message() {
3505 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
3506 let mut agent = agent(fake.clone(), "");
3507
3508 agent.run("hi").await.unwrap();
3509
3510 let requests = fake.requests();
3511 assert_eq!(requests[0].messages.len(), 1);
3512 assert_eq!(requests[0].messages[0], Message::user("hi"));
3513 }
3514
3515 #[tokio::test]
3518 async fn tool_failure_returns_text_and_continues() {
3519 struct FailingTool;
3521 #[async_trait::async_trait]
3522 impl Tool for FailingTool {
3523 fn schema(&self) -> ToolSchema {
3524 ToolSchema::new("boom", "Tool that always fails", serde_json::json!({}))
3525 }
3526 async fn call(
3527 &self,
3528 _arguments: serde_json::Value,
3529 _context: ToolContext<'_>,
3530 ) -> Result<ToolResult, ToolError> {
3531 Err(ToolError::Execution("internal error".into()))
3532 }
3533 }
3534
3535 let mut registry = ToolRegistry::new();
3536 registry.register(FailingTool);
3537 let fake = SharedFake::new([
3538 FakeReply::ToolCalls {
3539 content: "".into(),
3540 calls: vec![call("c1", "boom", "{}")],
3541 },
3542 FakeReply::Text("Got it".into()),
3543 ]);
3544 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
3545
3546 let answer = agent.run("Trigger failure").await.unwrap();
3547 assert_eq!(answer, "Got it");
3548
3549 let requests = fake.requests();
3552 assert!(requests[1].messages.iter().any(|m| matches!(
3553 m,
3554 Message::ToolResult { content, .. } if content.contains("internal error")
3555 )));
3556 }
3557
3558 #[tokio::test]
3563 async fn stream_too_many_tool_rounds_terminates() {
3564 let (calc, _calls) = FakeTool::new("calc", "42");
3565 let mut registry = ToolRegistry::new();
3566 registry.register(calc);
3567 let fake = SharedFake::new([
3568 FakeReply::ToolCalls {
3569 content: "".into(),
3570 calls: vec![call("c1", "calc", "{}")],
3571 },
3572 FakeReply::ToolCalls {
3573 content: "".into(),
3574 calls: vec![call("c2", "calc", "{}")],
3575 },
3576 ]);
3577 let mut agent = agent_with_registry(
3578 fake.clone(),
3579 registry,
3580 AgentConfig {
3581 max_tool_rounds: 2,
3582 ..Default::default()
3583 },
3584 );
3585
3586 let mut stream = agent.run_stream("Keep computing").await.unwrap();
3587 let chunks: Vec<Result<MessageChunk, AgentError>> = stream.by_ref().collect().await;
3588 assert_eq!(chunks.len(), 5);
3593 assert!(matches!(
3594 chunks.last(),
3595 Some(Err(AgentError::TooManyToolRounds(2)))
3596 ));
3597 assert_eq!(fake.requests().len(), 2);
3598 }
3599
3600 #[tokio::test]
3603 async fn stream_entry_record_user_failure_returns_error() {
3604 struct FailingUserMemory;
3605 #[async_trait::async_trait]
3606 impl Memory for FailingUserMemory {
3607 async fn record(&mut self, _message: Message) -> Result<(), MemoryError> {
3608 Err(MemoryError::Storage("disk full".into()))
3609 }
3610 async fn context(&self) -> Result<Vec<Message>, MemoryError> {
3611 Ok(Vec::new())
3612 }
3613 }
3614
3615 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
3616 let mut agent = agent(fake.clone(), "").with_memory(FailingUserMemory);
3617 let err = match agent.run_stream("hi").await {
3618 Err(e) => e,
3619 Ok(_) => panic!("expected input recording failure to return Err directly"),
3620 };
3621 assert!(matches!(err, AgentError::Memory(MemoryError::Storage(_))));
3622 }
3623
3624 #[tokio::test]
3627 async fn stream_context_failure_terminates_with_err() {
3628 struct FailingContextMemory;
3629 #[async_trait::async_trait]
3630 impl Memory for FailingContextMemory {
3631 async fn record(&mut self, _message: Message) -> Result<(), MemoryError> {
3632 Ok(())
3633 }
3634 async fn context(&self) -> Result<Vec<Message>, MemoryError> {
3635 Err(MemoryError::Storage("disk full".into()))
3636 }
3637 }
3638
3639 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
3640 let mut agent = agent(fake.clone(), "").with_memory(FailingContextMemory);
3641 let mut stream = agent.run_stream("hi").await.unwrap();
3642 let chunks: Vec<Result<MessageChunk, AgentError>> = stream.by_ref().collect().await;
3643 assert_eq!(chunks.len(), 1);
3644 assert!(matches!(
3645 chunks[0],
3646 Err(AgentError::Memory(MemoryError::Storage(_)))
3647 ));
3648 }
3649
3650 #[tokio::test]
3653 async fn stream_assistant_record_failure_terminates_with_err() {
3654 struct FailingAssistantMemory;
3655 #[async_trait::async_trait]
3656 impl Memory for FailingAssistantMemory {
3657 async fn record(&mut self, message: Message) -> Result<(), MemoryError> {
3658 if matches!(message, Message::Assistant { .. }) {
3659 Err(MemoryError::Storage("disk full".into()))
3660 } else {
3661 Ok(())
3662 }
3663 }
3664 async fn context(&self) -> Result<Vec<Message>, MemoryError> {
3665 Ok(Vec::new())
3666 }
3667 }
3668
3669 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
3670 let mut agent = agent(fake.clone(), "").with_memory(FailingAssistantMemory);
3671 let mut stream = agent.run_stream("hi").await.unwrap();
3672 let chunks: Vec<Result<MessageChunk, AgentError>> = stream.by_ref().collect().await;
3673 assert_eq!(chunks.len(), 2); assert!(matches!(
3675 chunks.last(),
3676 Some(Err(AgentError::Memory(MemoryError::Storage(_))))
3677 ));
3678 }
3679
3680 #[tokio::test]
3683 async fn stream_tool_failure_returns_text_and_continues() {
3684 struct FailingTool;
3685 #[async_trait::async_trait]
3686 impl Tool for FailingTool {
3687 fn schema(&self) -> ToolSchema {
3688 ToolSchema::new("boom", "Tool that always fails", serde_json::json!({}))
3689 }
3690 async fn call(
3691 &self,
3692 _arguments: serde_json::Value,
3693 _context: ToolContext<'_>,
3694 ) -> Result<ToolResult, ToolError> {
3695 Err(ToolError::Execution("internal error".into()))
3696 }
3697 }
3698
3699 let mut registry = ToolRegistry::new();
3700 registry.register(FailingTool);
3701 let fake = SharedFake::new([
3702 FakeReply::ToolCalls {
3703 content: "".into(),
3704 calls: vec![call("c1", "boom", "{}")],
3705 },
3706 FakeReply::Text("Got it".into()),
3707 ]);
3708 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
3709
3710 let mut stream = agent.run_stream("Trigger failure").await.unwrap();
3711 let chunks: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
3712 assert!(matches!(
3713 &chunks[1],
3714 MessageChunk::ToolResult { content, .. } if content.contains("internal error")
3715 ));
3716 assert_done_summary(chunks.last().unwrap(), 2, 1, Usage::default(), true);
3719 }
3720
3721 #[tokio::test]
3724 async fn stream_multiple_tools_same_round() {
3725 let (calc_a, _calls) = FakeTool::new("calc_a", "A");
3726 let (calc_b, _calls) = FakeTool::new("calc_b", "B");
3727 let mut registry = ToolRegistry::new();
3728 registry.register(calc_a).register(calc_b);
3729 let fake = SharedFake::new([
3730 FakeReply::ToolCalls {
3731 content: "".into(),
3732 calls: vec![call("c1", "calc_a", "{}"), call("c2", "calc_b", "{}")],
3733 },
3734 FakeReply::Text("Done".into()),
3735 ]);
3736 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
3737
3738 let mut stream = agent.run_stream("Compute").await.unwrap();
3739 let chunks: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
3740 assert_eq!(chunks.len(), 6);
3741 assert_eq!(
3742 &chunks[..5],
3743 &[
3744 MessageChunk::ToolCall {
3745 id: "c1".into(),
3746 name: "calc_a".into(),
3747 arguments: "{}".into()
3748 },
3749 MessageChunk::ToolCall {
3750 id: "c2".into(),
3751 name: "calc_b".into(),
3752 arguments: "{}".into()
3753 },
3754 MessageChunk::ToolResult {
3755 id: "c1".into(),
3756 name: "calc_a".into(),
3757 content: "A".into()
3758 },
3759 MessageChunk::ToolResult {
3760 id: "c2".into(),
3761 name: "calc_b".into(),
3762 content: "B".into()
3763 },
3764 MessageChunk::Delta("Done".into()),
3765 ]
3766 );
3767 assert_done_summary(&chunks[5], 2, 2, Usage::default(), true);
3768 }
3769
3770 #[tokio::test]
3774 async fn run_and_stream_error_semantics_equivalent() {
3775 let script = |fake: &SharedFake| {
3776 let (calc, _calls) = FakeTool::new("calc", "42");
3777 let mut registry = ToolRegistry::new();
3778 registry.register(calc);
3779 agent_with_registry(
3780 fake.clone(),
3781 registry,
3782 AgentConfig {
3783 max_tool_rounds: 1,
3784 ..Default::default()
3785 },
3786 )
3787 };
3788
3789 let fake = SharedFake::new([
3791 FakeReply::ToolCalls {
3792 content: "".into(),
3793 calls: vec![call("c1", "calc", "{}")],
3794 },
3795 FakeReply::ToolCalls {
3796 content: "".into(),
3797 calls: vec![call("c2", "calc", "{}")],
3798 },
3799 ]);
3800 let mut agent = script(&fake);
3801 let run_err = agent.run("Compute").await.unwrap_err();
3802
3803 let fake2 = SharedFake::new([
3805 FakeReply::ToolCalls {
3806 content: "".into(),
3807 calls: vec![call("c1", "calc", "{}")],
3808 },
3809 FakeReply::ToolCalls {
3810 content: "".into(),
3811 calls: vec![call("c2", "calc", "{}")],
3812 },
3813 ]);
3814 let mut agent = script(&fake2);
3815 let mut stream = agent.run_stream("Compute").await.unwrap();
3816 let chunks: Vec<Result<MessageChunk, AgentError>> = stream.by_ref().collect().await;
3817 let stream_err = chunks.into_iter().find_map(|e| e.err());
3818
3819 assert_eq!(run_err, AgentError::TooManyToolRounds(1));
3821 assert_eq!(stream_err, Some(AgentError::TooManyToolRounds(1)));
3822 assert_eq!(fake.requests().len(), fake2.requests().len());
3823 }
3824
3825 #[tokio::test]
3828 async fn run_id_differs_across_instances() {
3829 let id_a = RunContext::generated().run_id;
3830 let id_b = RunContext::generated().run_id;
3831 assert_ne!(
3832 id_a, id_b,
3833 "back-to-back instances must not collide on run_id"
3834 );
3835 assert!(id_a.starts_with("run-") && id_b.starts_with("run-"));
3837 }
3838
3839 #[tokio::test]
3842 async fn run_context_failure_returns_memory_error() {
3843 struct FailingContextMemory;
3844 #[async_trait::async_trait]
3845 impl Memory for FailingContextMemory {
3846 async fn record(&mut self, _message: Message) -> Result<(), MemoryError> {
3847 Ok(())
3848 }
3849 async fn context(&self) -> Result<Vec<Message>, MemoryError> {
3850 Err(MemoryError::Storage("disk full".into()))
3851 }
3852 }
3853
3854 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
3855 let mut agent = agent(fake.clone(), "").with_memory(FailingContextMemory);
3856 let err = agent.run("hi").await.unwrap_err();
3857 assert!(matches!(err, AgentError::Memory(MemoryError::Storage(_))));
3858 assert!(fake.requests().is_empty());
3860 }
3861
3862 #[tokio::test]
3869 async fn stream_empty_provider_stream_yields_empty_answer() {
3870 struct EmptyStreamProvider;
3871 #[async_trait::async_trait]
3872 impl Provider for EmptyStreamProvider {
3873 async fn chat_with_context(
3874 &self,
3875 _r: ChatRequest,
3876 _context: &ProviderRequestContext,
3877 ) -> Result<ChatResponse, ProviderError> {
3878 unreachable!("this test uses streaming only")
3879 }
3880 async fn stream_chat_with_context(
3881 &self,
3882 _r: ChatRequest,
3883 _context: &ProviderRequestContext,
3884 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
3885 {
3886 Ok(Box::pin(futures::stream::empty()))
3887 }
3888 }
3889
3890 let mut agent = ReActAgent::new(EmptyStreamProvider, ToolRegistry::new(), "");
3891 let chunks: Vec<Result<MessageChunk, AgentError>> = {
3892 let mut stream = agent.run_stream("hi").await.unwrap();
3893 stream.by_ref().collect().await
3894 };
3895 assert_eq!(chunks.len(), 1);
3897 let chunk = chunks[0].as_ref().unwrap();
3898 assert_done_summary_with_finish(chunk, 1, 0, Usage::default(), true, None);
3899 assert_eq!(
3902 agent.memory.context().await.unwrap(),
3903 vec![Message::user("hi")]
3904 );
3905 }
3906
3907 #[tokio::test]
3911 async fn stream_event_order() {
3912 let (calc, _calls) = FakeTool::new("calc", "42");
3913 let mut registry = ToolRegistry::new();
3914 registry.register(calc);
3915 let fake = SharedFake::new([
3916 FakeReply::ToolCalls {
3917 content: "Thinking: ".into(),
3918 calls: vec![call("c1", "calc", "{}")],
3919 },
3920 FakeReply::TextWithReasoning {
3921 content: "The answer is 42".into(),
3922 reasoning: "Reasoning steps".into(),
3923 },
3924 ]);
3925 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
3926
3927 let mut stream = agent.run_stream("Compute").await.unwrap();
3928 let events: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
3929 assert_eq!(events.len(), 5);
3930 assert_eq!(
3931 &events[..4],
3932 &[
3933 MessageChunk::Delta("Thinking: ".into()),
3934 MessageChunk::ToolCall {
3935 id: "c1".into(),
3936 name: "calc".into(),
3937 arguments: "{}".into()
3938 },
3939 MessageChunk::ToolResult {
3940 id: "c1".into(),
3941 name: "calc".into(),
3942 content: "42".into()
3943 },
3944 MessageChunk::Delta("The answer is 42".into()),
3945 ]
3946 );
3947 assert_done_summary(&events[4], 2, 1, Usage::default(), true);
3948 }
3949
3950 #[tokio::test]
3953 async fn stream_pure_tool_round_no_delta() {
3954 let (calc, _calls) = FakeTool::new("calc", "42");
3955 let mut registry = ToolRegistry::new();
3956 registry.register(calc);
3957 let fake = SharedFake::new([
3958 FakeReply::ToolCalls {
3959 content: "".into(),
3960 calls: vec![call("c1", "calc", "{}")],
3961 },
3962 FakeReply::Text("42".into()),
3963 ]);
3964 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
3965
3966 let mut stream = agent.run_stream("Compute").await.unwrap();
3967 let events: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
3968 assert_eq!(events.len(), 4);
3969 assert_eq!(
3970 &events[..3],
3971 &[
3972 MessageChunk::ToolCall {
3973 id: "c1".into(),
3974 name: "calc".into(),
3975 arguments: "{}".into()
3976 },
3977 MessageChunk::ToolResult {
3978 id: "c1".into(),
3979 name: "calc".into(),
3980 content: "42".into()
3981 },
3982 MessageChunk::Delta("42".into()),
3983 ]
3984 );
3985 assert_done_summary(&events[3], 2, 1, Usage::default(), true);
3986 }
3987
3988 #[tokio::test]
3992 async fn stream_done_summary_accumulates_usage() {
3993 let (calc, _calls) = FakeTool::new("calc", "42");
3994 let mut registry = ToolRegistry::new();
3995 registry.register(calc);
3996 let fake = SharedFake::new([
3997 FakeReply::WithUsage {
3998 reply: Box::new(FakeReply::ToolCalls {
3999 content: "".into(),
4000 calls: vec![call("c1", "calc", "{}")],
4001 }),
4002 usage: Usage::new(10, 2),
4003 },
4004 FakeReply::text_with_usage("42", Usage::new(20, 5)),
4005 ]);
4006 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
4007
4008 let mut stream = agent.run_stream("Compute").await.unwrap();
4009 let events: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
4010
4011 assert_done_summary(events.last().unwrap(), 2, 1, Usage::new(30, 7), false);
4014 }
4015
4016 #[tokio::test]
4019 async fn summary_tracks_omitted_usage_rounds() {
4020 let (calc, _calls) = FakeTool::new("calc", "42");
4021 let mut registry = ToolRegistry::new();
4022 registry.register(calc);
4023 let fake = SharedFake::new([
4027 FakeReply::ToolCalls {
4028 content: "".into(),
4029 calls: vec![call("c1", "calc", "{}")],
4030 },
4031 FakeReply::text_with_usage("42", Usage::new(20, 5)),
4032 ]);
4033 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
4034
4035 let mut stream = agent.run_stream("Compute").await.unwrap();
4036 let events: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
4037
4038 assert_done_summary(events.last().unwrap(), 2, 1, Usage::new(20, 5), true);
4039 }
4040
4041 #[tokio::test]
4045 async fn stream_done_summary_accumulates_cache_counts() {
4046 let (calc, _calls) = FakeTool::new("calc", "42");
4047 let mut registry = ToolRegistry::new();
4048 registry.register(calc);
4049 let fake = SharedFake::new([
4053 FakeReply::WithUsage {
4054 reply: Box::new(FakeReply::ToolCalls {
4055 content: "".into(),
4056 calls: vec![call("c1", "calc", "{}")],
4057 }),
4058 usage: Usage::new(10, 2).with_cached_tokens(4),
4059 },
4060 FakeReply::text_with_usage("42", Usage::new(20, 5)),
4061 ]);
4062 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
4063
4064 let mut stream = agent.run_stream("Compute").await.unwrap();
4065 let events: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
4066
4067 let summary = done_summary(events.last().unwrap()).expect("expected Done chunk");
4068 assert_eq!(summary.usage, Usage::new(30, 7).with_cached_tokens(4));
4069 assert!(!summary.usage_omitted);
4070 assert!(summary.cache_omitted);
4071
4072 let (calc, _calls) = FakeTool::new("calc", "42");
4074 let mut registry = ToolRegistry::new();
4075 registry.register(calc);
4076 let fake = SharedFake::new([
4077 FakeReply::WithUsage {
4078 reply: Box::new(FakeReply::ToolCalls {
4079 content: "".into(),
4080 calls: vec![call("c1", "calc", "{}")],
4081 }),
4082 usage: Usage::new(10, 2).with_cached_tokens(4),
4083 },
4084 FakeReply::text_with_usage("42", Usage::new(20, 5).with_cached_tokens(6)),
4085 ]);
4086 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
4087
4088 let mut stream = agent.run_stream("Compute").await.unwrap();
4089 let events: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
4090
4091 let summary = done_summary(events.last().unwrap()).expect("expected Done chunk");
4092 assert_eq!(summary.usage, Usage::new(30, 7).with_cached_tokens(10));
4093 assert!(!summary.usage_omitted);
4094 assert!(!summary.cache_omitted);
4095 }
4096
4097 #[tokio::test]
4100 async fn run_and_stream_same_semantics() {
4101 let script = [
4102 FakeReply::ToolCalls {
4103 content: "".into(),
4104 calls: vec![call("c1", "calc", "{}")],
4105 },
4106 FakeReply::Text("42".into()),
4107 ];
4108 let (calc, _calls) = FakeTool::new("calc", "42");
4109
4110 let mut registry = ToolRegistry::new();
4112 registry.register(calc.clone());
4113 let fake1 = SharedFake::new(script.clone());
4114 let mut agent1 = agent_with_registry(fake1.clone(), registry, AgentConfig::default());
4115 let answer1 = agent1.run("Compute").await.unwrap();
4116
4117 let mut registry = ToolRegistry::new();
4119 registry.register(calc);
4120 let fake2 = SharedFake::new(script);
4121 let mut agent2 = agent_with_registry(fake2.clone(), registry, AgentConfig::default());
4122 let events: Vec<MessageChunk> = agent2
4123 .run_stream("Compute")
4124 .await
4125 .unwrap()
4126 .map(|e| e.unwrap())
4127 .collect()
4128 .await;
4129 let answer2: String = events
4131 .iter()
4132 .filter_map(|e| match e {
4133 MessageChunk::Delta(d) => Some(d.as_str()),
4134 _ => None,
4135 })
4136 .collect();
4137 assert_eq!(answer1, answer2);
4138 assert_eq!(answer1, "42");
4139
4140 assert_eq!(fake1.requests(), fake2.requests());
4143 }
4144
4145 #[tokio::test]
4152 async fn stream_error_terminates_without_done() {
4153 struct FailInStream;
4154 #[async_trait::async_trait]
4155 impl Provider for FailInStream {
4156 async fn chat_with_context(
4157 &self,
4158 _request: ChatRequest,
4159 _context: &ProviderRequestContext,
4160 ) -> Result<ChatResponse, ProviderError> {
4161 unreachable!("this test uses streaming path only")
4162 }
4163 async fn stream_chat_with_context(
4164 &self,
4165 _request: ChatRequest,
4166 _context: &ProviderRequestContext,
4167 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
4168 {
4169 Ok(Box::pin(futures::stream::iter(vec![
4170 Ok(StreamEvent::Delta("hi".into())),
4171 Err(ProviderError::Protocol {
4172 message: "boom".into(),
4173 }),
4174 ])))
4175 }
4176 }
4177
4178 let mut agent = ReActAgent::new(FailInStream, ToolRegistry::new(), "");
4179
4180 let mut stream = agent.run_stream("two").await.unwrap();
4181 assert_eq!(
4182 stream.next().await.unwrap().unwrap(),
4183 MessageChunk::Delta("hi".into())
4184 );
4185 assert!(matches!(
4186 stream.next().await.unwrap(),
4187 Err(AgentError::Provider(ProviderError::Protocol { message: m })) if m == "boom"
4188 ));
4189 assert!(stream.next().await.is_none()); }
4191
4192 #[tokio::test]
4195 async fn script_exhausted_fails_explicitly() {
4196 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
4197 let mut agent = agent(fake, "");
4198 agent.run("one").await.unwrap();
4199
4200 let err = agent.run("two").await.unwrap_err();
4201 assert!(
4202 matches!(err, AgentError::Provider(ProviderError::Protocol { message: m }) if m.contains("exhausted"))
4203 );
4204 }
4205
4206 #[tokio::test]
4210 async fn shared_state_flows_to_tools() {
4211 struct CounterTool;
4212 #[async_trait::async_trait]
4213 impl Tool for CounterTool {
4214 fn schema(&self) -> ToolSchema {
4215 ToolSchema::new("counter", "Count", serde_json::json!({}))
4216 }
4217 async fn call(
4218 &self,
4219 _arguments: serde_json::Value,
4220 context: ToolContext<'_>,
4221 ) -> Result<ToolResult, ToolError> {
4222 let state = context.state;
4223 state.with_mut::<usize>(|n| *n += 1);
4224 Ok(ToolOutput::text(format!("count={}", state.get::<usize>().unwrap_or(0))).into())
4225 }
4226 }
4227
4228 let mut registry = ToolRegistry::new();
4229 registry.register(CounterTool);
4230 let fake = SharedFake::new([
4231 FakeReply::ToolCalls {
4232 content: "".into(),
4233 calls: vec![call("c1", "counter", "{}"), call("c2", "counter", "{}")],
4234 },
4235 FakeReply::Text("done".into()),
4236 ]);
4237 let state = SharedState::new();
4238 state.insert(0usize);
4239 let mut agent = ReActAgent::new(fake, registry, "").with_state(state.clone());
4240
4241 agent.run("Count").await.unwrap();
4242
4243 assert_eq!(state.get::<usize>(), Some(2));
4247 }
4248
4249 #[tokio::test]
4252 async fn memory_error_passthrough() {
4253 struct FailingMemory;
4254 #[async_trait::async_trait]
4255 impl Memory for FailingMemory {
4256 async fn record(&mut self, _message: Message) -> Result<(), MemoryError> {
4257 Err(MemoryError::Storage("disk full".into()))
4258 }
4259 async fn context(&self) -> Result<Vec<Message>, MemoryError> {
4260 Ok(Vec::new())
4261 }
4262 }
4263
4264 let mut agent = ReActAgent::new(
4265 FakeProvider::new([FakeReply::Text("hi".into())]),
4266 ToolRegistry::new(),
4267 "",
4268 )
4269 .with_memory(FailingMemory);
4270 let err = agent.run("hi").await.unwrap_err();
4271 assert!(matches!(err, AgentError::Memory(MemoryError::Storage(_))));
4272 }
4273
4274 #[tokio::test]
4282 async fn stream_tool_result_record_failure_terminates_stream() {
4283 struct FailingToolResultMemory;
4284 #[async_trait::async_trait]
4285 impl Memory for FailingToolResultMemory {
4286 async fn record(&mut self, message: Message) -> Result<(), MemoryError> {
4287 if matches!(message, Message::ToolResult { .. }) {
4288 Err(MemoryError::Storage("disk full".into()))
4289 } else {
4290 Ok(())
4291 }
4292 }
4293 async fn context(&self) -> Result<Vec<Message>, MemoryError> {
4294 Ok(Vec::new())
4295 }
4296 }
4297
4298 let (calc, _calls) = FakeTool::new("calc", "42");
4299 let mut registry = ToolRegistry::new();
4300 registry.register(calc);
4301 let fake = SharedFake::new([
4302 FakeReply::ToolCalls {
4303 content: "".into(),
4304 calls: vec![call("c1", "calc", "{}")],
4305 },
4306 FakeReply::Text("42".into()),
4307 ]);
4308 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default())
4309 .with_memory(FailingToolResultMemory);
4310
4311 let mut stream = agent.run_stream("Compute").await.unwrap();
4312 let chunks: Vec<Result<MessageChunk, AgentError>> = stream.by_ref().collect().await;
4313 assert_eq!(chunks.len(), 3);
4315 assert!(matches!(
4316 chunks[2],
4317 Err(AgentError::Memory(MemoryError::Storage(_)))
4318 ));
4319 }
4320
4321 #[tokio::test]
4324 async fn config_options_forwarded_to_chat_request() {
4325 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
4326 let mut options = ModelOptions {
4327 temperature: Some(0.2),
4328 max_tokens: Some(128),
4329 ..Default::default()
4330 };
4331 options.headers.insert("x-tenant".into(), "acme".into());
4332 let mut agent = agent(fake.clone(), "").with_config(AgentConfig {
4333 max_tool_rounds: 10,
4334 options,
4335 ..Default::default()
4336 });
4337 agent.run("hi").await.unwrap();
4338
4339 let req = &fake.requests()[0];
4340 assert_eq!(req.options.temperature, Some(0.2));
4341 assert_eq!(req.options.max_tokens, Some(128));
4342 assert_eq!(
4343 req.options.headers.get("x-tenant").map(String::as_str),
4344 Some("acme")
4345 );
4346 }
4347
4348 #[tokio::test]
4349 async fn request_options_replace_config_options() {
4350 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
4351 let mut agent = agent(fake.clone(), "").with_config(AgentConfig {
4352 options: ModelOptions {
4353 temperature: Some(0.2),
4354 max_tokens: Some(128),
4355 ..Default::default()
4356 },
4357 ..Default::default()
4358 });
4359 agent
4360 .run_request(RunRequest::text("hi").with_options(ModelOptions {
4361 max_tokens: Some(64),
4362 ..Default::default()
4363 }))
4364 .await
4365 .unwrap();
4366
4367 let req = &fake.requests()[0];
4368 assert_eq!(req.options.temperature, None);
4369 assert_eq!(req.options.max_tokens, Some(64));
4370 }
4371
4372 #[tokio::test]
4379 async fn cancelled_before_run_returns_cancelled() {
4380 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
4381 let mut agent = agent(fake.clone(), "");
4382 let token = CancellationToken::new();
4383 token.cancel();
4384
4385 let err = agent
4386 .run_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
4387 .await
4388 .unwrap_err();
4389 assert!(matches!(err, AgentError::Cancelled));
4390 assert_eq!(fake.requests().len(), 0); assert_eq!(agent.memory.context().await.unwrap().len(), 1); }
4393
4394 #[tokio::test]
4397 async fn pre_cancelled_token_rounds_consistent_across_paths() {
4398 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
4399 let token = CancellationToken::new();
4400 token.cancel();
4401
4402 let (mut run_agent, mut rx) = attach_channel(agent(fake.clone(), ""));
4404 let err = run_agent
4405 .run_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
4406 .await
4407 .unwrap_err();
4408 assert!(matches!(err, AgentError::Cancelled));
4409 drop(run_agent);
4410 let events = drain(&mut rx).await;
4411 let rounds_run = match react_event(&**events.last().unwrap()) {
4412 ReActEvent::RunEnded { summary, error, .. } => {
4413 assert_eq!(error, &Some(AgentError::Cancelled));
4414 summary.rounds
4415 }
4416 _ => panic!("expected RunEnded"),
4417 };
4418
4419 let (mut stream_agent, mut rx) = attach_channel(agent(fake, ""));
4421 let mut stream = stream_agent
4422 .run_stream_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
4423 .await
4424 .unwrap();
4425 let chunks: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
4426 assert!(chunks.contains(&MessageChunk::Cancelled));
4427 drop(stream);
4428 drop(stream_agent);
4429 let events = drain(&mut rx).await;
4430 let rounds_stream = match react_event(&**events.last().unwrap()) {
4431 ReActEvent::RunEnded { summary, error, .. } => {
4432 assert_eq!(error, &Some(AgentError::Cancelled));
4433 summary.rounds
4434 }
4435 _ => panic!("expected RunEnded"),
4436 };
4437
4438 assert_eq!(rounds_run, rounds_stream);
4439 assert_eq!(rounds_run, 1);
4440 }
4441
4442 #[tokio::test]
4447 async fn cancel_during_chat_drops_inflight() {
4448 struct PendingProvider;
4449 #[async_trait::async_trait]
4450 impl Provider for PendingProvider {
4451 async fn chat_with_context(
4452 &self,
4453 _request: ChatRequest,
4454 _context: &ProviderRequestContext,
4455 ) -> Result<ChatResponse, ProviderError> {
4456 std::future::pending().await }
4458 async fn stream_chat_with_context(
4459 &self,
4460 _request: ChatRequest,
4461 _context: &ProviderRequestContext,
4462 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
4463 {
4464 unreachable!("this test uses non-streaming path only")
4465 }
4466 }
4467
4468 let mut agent = ReActAgent::new(PendingProvider, ToolRegistry::new(), "");
4469 let token = CancellationToken::new();
4470 let result = tokio::select! {
4471 r = agent.run_request_with_context(RunRequest::text("hi"), cancellation_context(&token)) => r.map(|output| output.answer),
4472 _ = async {
4473 tokio::time::sleep(Duration::from_millis(20)).await;
4474 token.cancel();
4475 std::future::pending::<()>().await; } => unreachable!("cancellation branch only sends a signal"),
4477 };
4478 assert!(matches!(result, Err(AgentError::Cancelled)));
4479 assert_eq!(agent.memory.context().await.unwrap().len(), 1); }
4481
4482 #[tokio::test]
4487 async fn provider_error_propagates_from_both_paths() {
4488 struct FailProvider(ProviderError);
4489 #[async_trait::async_trait]
4490 impl Provider for FailProvider {
4491 async fn chat_with_context(
4492 &self,
4493 _request: ChatRequest,
4494 _context: &ProviderRequestContext,
4495 ) -> Result<ChatResponse, ProviderError> {
4496 Err(self.0.clone())
4497 }
4498 async fn stream_chat_with_context(
4499 &self,
4500 _request: ChatRequest,
4501 _context: &ProviderRequestContext,
4502 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
4503 {
4504 Err(self.0.clone())
4505 }
4506 }
4507
4508 let err = ProviderError::Timeout(TimeoutStage::Request);
4509 let mut agent = ReActAgent::new(FailProvider(err.clone()), ToolRegistry::new(), "");
4511 let got = agent.run("hi").await.unwrap_err();
4512 assert!(matches!(
4513 got,
4514 AgentError::Provider(ProviderError::Timeout(_))
4515 ));
4516 assert_eq!(agent.memory.context().await.unwrap().len(), 1); let mut agent = ReActAgent::new(FailProvider(err.clone()), ToolRegistry::new(), "");
4521 let mut stream = agent.run_stream("hi").await.unwrap();
4522 let item = stream.next().await.unwrap().unwrap_err();
4523 assert!(matches!(
4524 item,
4525 AgentError::Provider(ProviderError::Timeout(_))
4526 ));
4527 assert!(stream.next().await.is_none());
4528 drop(stream); assert_eq!(agent.memory.context().await.unwrap().len(), 1);
4530 }
4531
4532 #[tokio::test]
4533 async fn deadline_exceeded_during_chat_is_distinct_from_provider_timeout() {
4534 struct PendingProvider;
4535 #[async_trait::async_trait]
4536 impl Provider for PendingProvider {
4537 async fn chat_with_context(
4538 &self,
4539 _request: ChatRequest,
4540 _context: &ProviderRequestContext,
4541 ) -> Result<ChatResponse, ProviderError> {
4542 std::future::pending().await
4543 }
4544
4545 async fn stream_chat_with_context(
4546 &self,
4547 _request: ChatRequest,
4548 _context: &ProviderRequestContext,
4549 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
4550 {
4551 std::future::pending().await
4552 }
4553 }
4554
4555 let mut agent = ReActAgent::new(PendingProvider, ToolRegistry::new(), "");
4556 let err = agent
4557 .run_request_with_context(
4558 RunRequest::text("hi"),
4559 RunContext::new("deadline").with_timeout(Duration::from_millis(10)),
4560 )
4561 .await
4562 .unwrap_err();
4563 assert_eq!(err, AgentError::DeadlineExceeded);
4564 assert_eq!(agent.memory.context().await.unwrap().len(), 1);
4565 }
4566
4567 #[tokio::test]
4568 async fn streaming_deadline_exceeded_terminates_with_error_item() {
4569 struct PendingStreamProvider;
4570 #[async_trait::async_trait]
4571 impl Provider for PendingStreamProvider {
4572 async fn chat_with_context(
4573 &self,
4574 _request: ChatRequest,
4575 _context: &ProviderRequestContext,
4576 ) -> Result<ChatResponse, ProviderError> {
4577 unreachable!("this test uses streaming only")
4578 }
4579
4580 async fn stream_chat_with_context(
4581 &self,
4582 _request: ChatRequest,
4583 _context: &ProviderRequestContext,
4584 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
4585 {
4586 Ok(Box::pin(futures::stream::pending()))
4587 }
4588 }
4589
4590 let mut agent = ReActAgent::new(PendingStreamProvider, ToolRegistry::new(), "");
4591 let mut stream = agent
4592 .run_stream_request_with_context(
4593 RunRequest::text("hi"),
4594 RunContext::new("stream-deadline").with_timeout(Duration::from_millis(10)),
4595 )
4596 .await
4597 .unwrap();
4598 assert_eq!(
4599 stream.next().await.unwrap().unwrap_err(),
4600 AgentError::DeadlineExceeded
4601 );
4602 }
4603
4604 #[cfg(feature = "structured")]
4607 #[tokio::test]
4608 async fn structured_output_valid_answer_passes() {
4609 let schema = serde_json::json!({
4610 "type": "object",
4611 "properties": { "city": { "type": "string" } },
4612 "required": ["city"],
4613 });
4614 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4615 let mut agent = agent(fake.clone(), "").with_structured_output(schema);
4616 let answer = agent.run("Beijing weather").await.unwrap();
4617 assert_eq!(answer, r#"{"city":"Beijing"}"#);
4618 assert_eq!(fake.requests().len(), 1); }
4620
4621 #[cfg(feature = "structured")]
4624 #[tokio::test]
4625 async fn structured_output_retries_after_invalid_answer() {
4626 let schema = serde_json::json!({
4627 "type": "object",
4628 "properties": { "city": { "type": "string" } },
4629 "required": ["city"],
4630 });
4631 let fake = SharedFake::new([
4632 FakeReply::Text("Not JSON".into()),
4633 FakeReply::Text(r#"{"city":"Beijing"}"#.into()),
4634 ]);
4635 let mut agent = agent(fake.clone(), "").with_structured_output(schema);
4636 let answer = agent.run("Beijing weather").await.unwrap();
4637 assert_eq!(answer, r#"{"city":"Beijing"}"#);
4638 assert_eq!(fake.requests().len(), 2); let context = agent.memory.context().await.unwrap();
4642 assert!(context.iter().any(|m| matches!(
4643 m,
4644 Message::User(blocks) if blocks.iter().any(|b| matches!(b, ContentBlock::Text(t) if t.contains("JSON schema validation")))
4645 )));
4646 }
4647
4648 #[cfg(feature = "structured")]
4653 #[tokio::test]
4654 async fn structured_output_exhausts_retry_budget() {
4655 let schema = serde_json::json!({ "type": "object" });
4656 let fake = SharedFake::new([
4657 FakeReply::Text("bad1".into()),
4658 FakeReply::Text("bad2".into()),
4659 FakeReply::Text("bad3".into()),
4660 FakeReply::Text("bad4".into()),
4661 ]);
4662 let mut agent = agent(fake.clone(), "").with_config(AgentConfig {
4665 max_tool_rounds: 1,
4666 max_structured_retries: 3,
4667 ..Default::default()
4668 });
4669 agent = agent.with_structured_output(schema);
4670 let err = agent.run("hi").await.unwrap_err();
4671 assert!(matches!(err, AgentError::StructuredRetriesExhausted(3)));
4672 assert_eq!(fake.requests().len(), 4); }
4674
4675 #[cfg(feature = "structured")]
4679 #[tokio::test]
4680 async fn typed_output_parses_valid_answer() {
4681 #[derive(Debug, Deserialize, JsonSchema)]
4682 struct Weather {
4683 city: String,
4684 }
4685 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4686 let mut agent = agent(fake.clone(), "");
4687 let weather: Weather = agent.run_typed("Beijing weather").await.unwrap();
4688 assert_eq!(weather.city, "Beijing");
4689 assert_eq!(fake.requests().len(), 1); assert!(fake.requests()[0].options.structured.is_some());
4693 assert!(agent.config.options.structured.is_none());
4694 }
4695
4696 #[cfg(feature = "structured")]
4697 #[tokio::test]
4698 async fn typed_run_request_returns_value_and_output() {
4699 #[derive(Debug, Deserialize, JsonSchema, PartialEq)]
4700 struct Weather {
4701 city: String,
4702 }
4703 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4704 let mut agent = agent(fake.clone(), "");
4705 let typed = agent
4706 .run_typed_request_with_context::<Weather>(
4707 RunRequest::text("Beijing weather"),
4708 RunContext::new("typed-1"),
4709 )
4710 .await
4711 .unwrap();
4712
4713 assert_eq!(
4714 typed.value,
4715 Weather {
4716 city: "Beijing".into()
4717 }
4718 );
4719 assert_eq!(typed.output.run_id, "typed-1");
4720 assert_eq!(typed.output.answer, r#"{"city":"Beijing"}"#);
4721 assert_eq!(
4722 typed.output.final_message,
4723 Message::assistant(r#"{"city":"Beijing"}"#)
4724 );
4725 }
4726
4727 #[cfg(feature = "structured")]
4728 #[tokio::test]
4729 async fn typed_schema_overrides_request_and_config_schema() {
4730 #[derive(Debug, Deserialize, JsonSchema)]
4731 struct Weather {
4732 city: String,
4733 }
4734 let config_schema = serde_json::json!({
4735 "type": "object",
4736 "properties": { "config_only": { "type": "string" } },
4737 "required": ["config_only"],
4738 });
4739 let request_schema = serde_json::json!({
4740 "type": "object",
4741 "properties": { "request_only": { "type": "string" } },
4742 "required": ["request_only"],
4743 });
4744 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4745 let mut agent = agent(fake.clone(), "").with_structured_output(config_schema);
4746 let options = ModelOptions {
4747 structured: Some(request_schema),
4748 ..Default::default()
4749 };
4750
4751 let weather: Weather = agent
4752 .run_typed_request(RunRequest::text("Beijing weather").with_options(options))
4753 .await
4754 .unwrap()
4755 .value;
4756
4757 assert_eq!(weather.city, "Beijing");
4758 let requests = fake.requests();
4759 let structured = requests[0]
4760 .options
4761 .structured
4762 .as_ref()
4763 .expect("typed schema should be sent");
4764 let props = &structured["properties"];
4765 assert!(props.get("city").is_some());
4766 assert!(props.get("request_only").is_none());
4767 assert!(props.get("config_only").is_none());
4768 }
4769
4770 #[cfg(feature = "structured")]
4773 #[tokio::test]
4774 async fn typed_output_retries_then_parses() {
4775 #[derive(Debug, Deserialize, JsonSchema)]
4776 struct Weather {
4777 city: String,
4778 }
4779 let fake = SharedFake::new([
4780 FakeReply::Text("Not JSON".into()),
4781 FakeReply::Text(r#"{"city":"Beijing"}"#.into()),
4782 ]);
4783 let mut agent = agent(fake.clone(), "");
4784 let weather: Weather = agent.run_typed("Beijing weather").await.unwrap();
4785 assert_eq!(weather.city, "Beijing");
4786 assert_eq!(fake.requests().len(), 2);
4787 }
4788
4789 #[cfg(feature = "structured")]
4795 #[tokio::test]
4796 async fn typed_output_parse_failure_on_schema_mismatch() {
4797 fn string_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
4798 serde_json::from_value(serde_json::json!({ "type": "string" })).unwrap()
4799 }
4800 #[derive(Debug, Deserialize, JsonSchema)]
4801 #[allow(dead_code)] struct Weather {
4803 #[schemars(schema_with = "string_schema")]
4804 temperature: i32,
4805 }
4806 let fake = SharedFake::new([FakeReply::Text(r#"{"temperature":"30"}"#.into())]);
4807 let mut agent = agent(fake.clone(), "");
4808 let err: AgentError = agent
4809 .run_typed::<Weather>("Beijing weather")
4810 .await
4811 .unwrap_err();
4812 assert!(matches!(err, AgentError::StructuredParse(_)));
4813 }
4814
4815 #[cfg(feature = "structured")]
4819 #[tokio::test]
4820 async fn typed_agent_trait_generic_call() {
4821 #[derive(Debug, Deserialize, JsonSchema)]
4822 struct Weather {
4823 city: String,
4824 }
4825 async fn typed_run<A: TypedAgent + Send>(
4826 agent: &mut A,
4827 input: &str,
4828 ) -> Result<Weather, AgentError> {
4829 agent.run_typed(input).await
4830 }
4831
4832 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4833 let mut agent = agent(fake.clone(), "");
4834 let weather = typed_run(&mut agent, "Beijing weather").await.unwrap();
4835 assert_eq!(weather.city, "Beijing");
4836 }
4837
4838 #[cfg(feature = "structured")]
4842 #[tokio::test]
4843 async fn typed_output_agent_trait_run_returns_text() {
4844 #[derive(Debug, Deserialize, JsonSchema)]
4845 struct Weather {
4846 city: String,
4847 }
4848 let fake = SharedFake::new([
4849 FakeReply::Text(r#"{"city":"Beijing"}"#.into()),
4850 FakeReply::Text("Hello".into()),
4851 ]);
4852 let mut agent = agent(fake.clone(), "");
4853 let weather: Weather = agent.run_typed("Beijing weather").await.unwrap();
4854 assert_eq!(weather.city, "Beijing");
4855 let text = Agent::run(&mut agent, "Say hi").await.unwrap();
4858 assert_eq!(text, "Hello");
4859 }
4860
4861 #[cfg(feature = "structured")]
4865 #[tokio::test]
4866 async fn structured_output_stream_exhausts_retry_budget() {
4867 let schema = serde_json::json!({ "type": "object" });
4868 let fake = SharedFake::new([
4869 FakeReply::Text("bad1".into()),
4870 FakeReply::Text("bad2".into()),
4871 ]);
4872 let mut agent = agent(fake.clone(), "").with_config(AgentConfig {
4873 max_structured_retries: 1,
4874 ..Default::default()
4875 });
4876 agent = agent.with_structured_output(schema);
4877 let mut stream = agent.run_stream("hi").await.unwrap();
4878 let mut saw_err = false;
4879 while let Some(item) = stream.next().await {
4880 if let Err(e) = item {
4881 assert!(matches!(e, AgentError::StructuredRetriesExhausted(1)));
4882 saw_err = true;
4883 break;
4884 }
4885 }
4886 assert!(saw_err);
4887 }
4888
4889 #[tokio::test]
4894 async fn tool_round_atomic_under_cancel() {
4895 struct SlowTool {
4897 calls: Arc<AtomicUsize>,
4898 }
4899 #[async_trait::async_trait]
4900 impl Tool for SlowTool {
4901 fn schema(&self) -> ToolSchema {
4902 ToolSchema::new("slow", "Slow tool", serde_json::json!({}))
4903 }
4904 async fn call(
4905 &self,
4906 _arguments: serde_json::Value,
4907 _context: ToolContext<'_>,
4908 ) -> Result<ToolResult, ToolError> {
4909 self.calls.fetch_add(1, Ordering::Relaxed);
4910 tokio::time::sleep(Duration::from_millis(100)).await;
4911 Ok(ToolOutput::text("42").into())
4912 }
4913 }
4914
4915 let calls = Arc::new(AtomicUsize::new(0));
4916 let mut registry = ToolRegistry::new();
4917 registry.register(SlowTool {
4918 calls: calls.clone(),
4919 });
4920 let fake = SharedFake::new([
4921 FakeReply::ToolCalls {
4922 content: "".into(),
4923 calls: vec![call("c1", "slow", "{}")],
4924 },
4925 FakeReply::Text("42".into()),
4926 ]);
4927 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
4928 let token = CancellationToken::new();
4929
4930 let result = tokio::select! {
4933 r = agent.run_request_with_context(RunRequest::text("Compute"), cancellation_context(&token)) => r.map(|output| output.answer),
4934 _ = async {
4935 tokio::time::sleep(Duration::from_millis(50)).await;
4936 token.cancel();
4937 std::future::pending::<()>().await; } => unreachable!("cancellation branch only sends a signal"),
4939 };
4940 assert!(matches!(result, Err(AgentError::Cancelled)));
4941 assert_eq!(calls.load(Ordering::Relaxed), 1); assert_eq!(fake.requests().len(), 1); let ctx = agent.memory.context().await.unwrap();
4946 assert_eq!(ctx.len(), 3);
4947 assert!(matches!(
4948 &ctx[1],
4949 Message::Assistant { tool_calls, .. } if tool_calls.len() == 1
4950 ));
4951 assert!(matches!(&ctx[2], Message::ToolResult { content, .. } if content == "42"));
4952 }
4953
4954 #[tokio::test]
4960 async fn custom_tool_round_executor() {
4961 #[derive(Default)]
4964 struct DenyAlphaToolRoundExecutor {
4965 denied: bool,
4966 }
4967 #[async_trait::async_trait]
4968 impl ToolRoundExecutor for DenyAlphaToolRoundExecutor {
4969 async fn execute_round<'a>(
4970 &'a mut self,
4971 ctx: ToolRoundCtx<'a>,
4972 calls: Vec<ToolCall>,
4973 ) -> BoxStream<'a, ToolCallOutcome> {
4974 let mut outcomes = Vec::with_capacity(calls.len());
4975 for call in calls.into_iter().rev() {
4976 if call.name == "alpha" && !self.denied {
4977 self.denied = true;
4978 outcomes.push(ToolCallOutcome {
4982 call,
4983 content: "denied by policy".into(),
4984 memory_policy: ToolMemoryPolicy::Normal,
4985 effect: None,
4986 });
4987 } else {
4988 outcomes.push(ctx.run(call).await);
4989 }
4990 }
4991 Box::pin(futures::stream::iter(outcomes))
4992 }
4993 }
4994
4995 let (alpha_tool, alpha_calls) = FakeTool::new("alpha", "A");
4996 let (beta_tool, beta_calls) = FakeTool::new("beta", "B");
4997 let mut registry = ToolRegistry::new();
4998 registry.register(alpha_tool);
4999 registry.register(beta_tool);
5000 let fake = SharedFake::new([
5001 FakeReply::ToolCalls {
5002 content: "".into(),
5003 calls: vec![call("c1", "alpha", "{}"), call("c2", "beta", "{}")],
5004 },
5005 FakeReply::Text("done".into()),
5006 ]);
5007 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default())
5008 .with_tool_round_executor(DenyAlphaToolRoundExecutor::default());
5009
5010 let answer = agent.run("Compute").await.unwrap();
5011 assert_eq!(answer, "done");
5012 assert_eq!(alpha_calls.load(Ordering::Relaxed), 0);
5014 assert_eq!(beta_calls.load(Ordering::Relaxed), 1);
5015 let requests = fake.requests();
5018 assert_eq!(requests.len(), 2);
5019 assert_eq!(requests[1].messages[2], Message::tool_result("c2", "B"),);
5020 assert_eq!(
5021 requests[1].messages[3],
5022 Message::tool_result("c1", "denied by policy"),
5023 );
5024 }
5025
5026 #[tokio::test]
5030 async fn custom_tool_round_executor_streaming() {
5031 #[derive(Default)]
5032 struct ReversedToolRoundExecutor;
5033 #[async_trait::async_trait]
5034 impl ToolRoundExecutor for ReversedToolRoundExecutor {
5035 async fn execute_round<'a>(
5036 &'a mut self,
5037 ctx: ToolRoundCtx<'a>,
5038 calls: Vec<ToolCall>,
5039 ) -> BoxStream<'a, ToolCallOutcome> {
5040 let mut outcomes = Vec::with_capacity(calls.len());
5041 for call in calls.into_iter().rev() {
5042 outcomes.push(ctx.run(call).await);
5043 }
5044 Box::pin(futures::stream::iter(outcomes))
5045 }
5046 }
5047
5048 let (alpha_tool, _) = FakeTool::new("alpha", "A");
5049 let (beta_tool, _) = FakeTool::new("beta", "B");
5050 let mut registry = ToolRegistry::new();
5051 registry.register(alpha_tool);
5052 registry.register(beta_tool);
5053 let fake = SharedFake::new([
5054 FakeReply::ToolCalls {
5055 content: "".into(),
5056 calls: vec![call("c1", "alpha", "{}"), call("c2", "beta", "{}")],
5057 },
5058 FakeReply::Text("done".into()),
5059 ]);
5060 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default())
5061 .with_tool_round_executor(ReversedToolRoundExecutor);
5062
5063 let mut stream = agent.run_stream("Compute").await.unwrap();
5064 let mut results = Vec::new();
5065 let mut done = false;
5066 while let Some(item) = stream.next().await {
5067 match item.unwrap() {
5068 MessageChunk::ToolResult { id, content, .. } => {
5069 results.push((id, content));
5070 }
5071 MessageChunk::Done(_) => done = true,
5072 _ => {}
5073 }
5074 }
5075 assert!(done);
5076 assert_eq!(
5078 results,
5079 vec![
5080 ("c2".to_string(), "B".to_string()),
5081 ("c1".to_string(), "A".to_string())
5082 ]
5083 );
5084 }
5085
5086 #[tokio::test]
5087 async fn kernel_batches_effect_requests_from_same_round() {
5088 let mut registry = ToolRegistry::new();
5089 registry
5090 .register(EffectTool::new("read_a", "effect-a", "read A"))
5091 .register(EffectTool::new("read_b", "effect-b", "read B"));
5092 let fake = SharedFake::new([
5093 FakeReply::ToolCalls {
5094 content: "".into(),
5095 calls: vec![call("c1", "read_a", "{}"), call("c2", "read_b", "{}")],
5096 },
5097 FakeReply::Text("done".into()),
5098 ]);
5099 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
5100 let context = RunContext::new("kernel-batch-effects");
5101
5102 let action = agent
5103 .start(RunRequest::text("read both"), &context)
5104 .await
5105 .unwrap();
5106 let AgentAction::RequestModel { request } = action else {
5107 panic!("expected initial model request");
5108 };
5109 let action = agent
5110 .observe(
5111 Observation::Model(ModelObservation::new(
5112 request.id,
5113 fake.chat(request.chat).await.unwrap(),
5114 )),
5115 &context,
5116 )
5117 .await
5118 .unwrap();
5119 let AgentAction::RequestEffects { requests } = action else {
5120 panic!("expected batch effect request");
5121 };
5122 assert_eq!(
5123 requests
5124 .iter()
5125 .map(|request| request.id.as_str())
5126 .collect::<Vec<_>>(),
5127 vec!["effect-a", "effect-b"]
5128 );
5129 assert_eq!(
5130 requests
5131 .iter()
5132 .map(|request| request.source.tool_call_id.as_deref())
5133 .collect::<Vec<_>>(),
5134 vec![Some("c1"), Some("c2")]
5135 );
5136
5137 let action = agent
5140 .observe(
5141 Observation::Effects(vec![
5142 EffectObservation::succeeded("effect-b", "observed B"),
5143 EffectObservation::succeeded("effect-a", "observed A"),
5144 ]),
5145 &context,
5146 )
5147 .await
5148 .unwrap();
5149 let AgentAction::RequestModel { request } = action else {
5150 panic!("expected next model request");
5151 };
5152 assert!(matches!(
5153 &request.chat.messages[2],
5154 Message::ToolResult { id, content } if id == "c1" && content == "observed A"
5155 ));
5156 assert!(matches!(
5157 &request.chat.messages[3],
5158 Message::ToolResult { id, content } if id == "c2" && content == "observed B"
5159 ));
5160
5161 let action = agent
5162 .observe(
5163 Observation::Model(ModelObservation::new(
5164 request.id,
5165 fake.chat(request.chat).await.unwrap(),
5166 )),
5167 &context,
5168 )
5169 .await
5170 .unwrap();
5171 let AgentAction::Respond { output } = action else {
5172 panic!("expected final response");
5173 };
5174 assert_eq!(output.answer, "done");
5175 }
5176
5177 #[tokio::test]
5178 async fn kernel_records_mixed_outputs_and_effects_in_tool_call_order() {
5179 let mut registry = ToolRegistry::new();
5180 registry
5181 .register(FakeTool::new("before", "plain before").0)
5182 .register(EffectTool::new("read", "effect-read", "read"))
5183 .register(FakeTool::new("after", "plain after").0);
5184 let fake = SharedFake::new([
5185 FakeReply::ToolCalls {
5186 content: "".into(),
5187 calls: vec![
5188 call("c1", "before", "{}"),
5189 call("c2", "read", "{}"),
5190 call("c3", "after", "{}"),
5191 ],
5192 },
5193 FakeReply::Text("done".into()),
5194 ]);
5195 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
5196 let context = RunContext::new("kernel-mixed-effects");
5197
5198 let AgentAction::RequestModel { request } = agent
5199 .start(RunRequest::text("read with context"), &context)
5200 .await
5201 .unwrap()
5202 else {
5203 panic!("expected initial model request");
5204 };
5205 let AgentAction::RequestEffect { request } = agent
5206 .observe(
5207 Observation::Model(ModelObservation::new(
5208 request.id,
5209 fake.chat(request.chat).await.unwrap(),
5210 )),
5211 &context,
5212 )
5213 .await
5214 .unwrap()
5215 else {
5216 panic!("expected single effect request");
5217 };
5218 assert_eq!(request.id, "effect-read");
5219
5220 let action = agent
5221 .observe(
5222 Observation::Effect(EffectObservation::succeeded("effect-read", "observed read")),
5223 &context,
5224 )
5225 .await
5226 .unwrap();
5227 let AgentAction::RequestModel { request } = action else {
5228 panic!("expected next model request");
5229 };
5230 assert!(matches!(
5231 &request.chat.messages[2],
5232 Message::ToolResult { id, content } if id == "c1" && content == "plain before"
5233 ));
5234 assert!(matches!(
5235 &request.chat.messages[3],
5236 Message::ToolResult { id, content } if id == "c2" && content == "observed read"
5237 ));
5238 assert!(matches!(
5239 &request.chat.messages[4],
5240 Message::ToolResult { id, content } if id == "c3" && content == "plain after"
5241 ));
5242 }
5243
5244 #[tokio::test]
5245 async fn kernel_rejects_partial_effect_batch_observation() {
5246 let mut registry = ToolRegistry::new();
5247 registry
5248 .register(EffectTool::new("read_a", "effect-a", "read A"))
5249 .register(EffectTool::new("read_b", "effect-b", "read B"));
5250 let fake = SharedFake::new([FakeReply::ToolCalls {
5251 content: "".into(),
5252 calls: vec![call("c1", "read_a", "{}"), call("c2", "read_b", "{}")],
5253 }]);
5254 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
5255 let context = RunContext::new("kernel-batch-effects-partial");
5256
5257 let AgentAction::RequestModel { request } = agent
5258 .start(RunRequest::text("read both"), &context)
5259 .await
5260 .unwrap()
5261 else {
5262 panic!("expected initial model request");
5263 };
5264 let AgentAction::RequestEffects { .. } = agent
5265 .observe(
5266 Observation::Model(ModelObservation::new(
5267 request.id,
5268 fake.chat(request.chat).await.unwrap(),
5269 )),
5270 &context,
5271 )
5272 .await
5273 .unwrap()
5274 else {
5275 panic!("expected batch effect request");
5276 };
5277 let err = agent
5278 .observe(
5279 Observation::Effects(vec![EffectObservation::succeeded("effect-a", "observed A")]),
5280 &context,
5281 )
5282 .await
5283 .unwrap_err();
5284 assert!(
5285 matches!(err, AgentError::InvalidStep(message) if message.contains("count mismatch"))
5286 );
5287 }
5288
5289 #[tokio::test]
5290 async fn kernel_rejects_duplicate_effect_batch_observation_and_can_retry() {
5291 let mut registry = ToolRegistry::new();
5292 registry
5293 .register(EffectTool::new("read_a", "effect-a", "read A"))
5294 .register(EffectTool::new("read_b", "effect-b", "read B"));
5295 let fake = SharedFake::new([
5296 FakeReply::ToolCalls {
5297 content: "".into(),
5298 calls: vec![call("c1", "read_a", "{}"), call("c2", "read_b", "{}")],
5299 },
5300 FakeReply::Text("done".into()),
5301 ]);
5302 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
5303 let context = RunContext::new("kernel-batch-effects-duplicate");
5304
5305 let AgentAction::RequestModel { request } = agent
5306 .start(RunRequest::text("read both"), &context)
5307 .await
5308 .unwrap()
5309 else {
5310 panic!("expected initial model request");
5311 };
5312 let AgentAction::RequestEffects { .. } = agent
5313 .observe(
5314 Observation::Model(ModelObservation::new(
5315 request.id,
5316 fake.chat(request.chat).await.unwrap(),
5317 )),
5318 &context,
5319 )
5320 .await
5321 .unwrap()
5322 else {
5323 panic!("expected batch effect request");
5324 };
5325 let err = agent
5326 .observe(
5327 Observation::Effects(vec![
5328 EffectObservation::succeeded("effect-a", "observed A"),
5329 EffectObservation::succeeded("effect-a", "observed A again"),
5330 ]),
5331 &context,
5332 )
5333 .await
5334 .unwrap_err();
5335 assert!(matches!(err, AgentError::InvalidStep(message) if message.contains("duplicate")));
5336
5337 let action = agent
5338 .observe(
5339 Observation::Effects(vec![
5340 EffectObservation::succeeded("effect-b", "observed B"),
5341 EffectObservation::succeeded("effect-a", "observed A"),
5342 ]),
5343 &context,
5344 )
5345 .await
5346 .unwrap();
5347 let AgentAction::RequestModel { request } = action else {
5348 panic!("expected next model request");
5349 };
5350 assert!(matches!(
5351 &request.chat.messages[2],
5352 Message::ToolResult { id, content } if id == "c1" && content == "observed A"
5353 ));
5354 assert!(matches!(
5355 &request.chat.messages[3],
5356 Message::ToolResult { id, content } if id == "c2" && content == "observed B"
5357 ));
5358 }
5359
5360 #[tokio::test]
5364 async fn stream_cancel_mid_generation() {
5365 struct SlowStreamProvider;
5368 #[async_trait::async_trait]
5369 impl Provider for SlowStreamProvider {
5370 async fn chat_with_context(
5371 &self,
5372 _request: ChatRequest,
5373 _context: &ProviderRequestContext,
5374 ) -> Result<ChatResponse, ProviderError> {
5375 unreachable!("this test uses streaming path only")
5376 }
5377 async fn stream_chat_with_context(
5378 &self,
5379 _request: ChatRequest,
5380 _context: &ProviderRequestContext,
5381 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
5382 {
5383 Ok(Box::pin(async_stream::stream! {
5384 yield Ok(StreamEvent::Delta("d0".into()));
5385 tokio::time::sleep(Duration::from_millis(100)).await;
5386 yield Ok(StreamEvent::Delta("d1".into()));
5387 tokio::time::sleep(Duration::from_millis(100)).await;
5388 yield Ok(StreamEvent::Done {
5389 reason: FinishReason::Stop,
5390 usage: None,
5391 });
5392 }))
5393 }
5394 }
5395
5396 let mut agent = ReActAgent::new(SlowStreamProvider, ToolRegistry::new(), "");
5397 let token = CancellationToken::new();
5398 let mut stream = agent
5399 .run_stream_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
5400 .await
5401 .unwrap();
5402
5403 let mut first = Vec::new();
5406 tokio::select! {
5407 _ = async {
5408 while let Some(ev) = stream.next().await {
5409 let ev = ev.unwrap();
5410 if ev == MessageChunk::Cancelled {
5411 break;
5412 }
5413 first.push(ev);
5414 }
5415 } => {}
5416 _ = async {
5417 tokio::time::sleep(Duration::from_millis(50)).await;
5418 token.cancel();
5419 } => {}
5420 }
5421 assert_eq!(first, vec![MessageChunk::Delta("d0".into())]);
5422
5423 let rest: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
5426 assert_eq!(rest, vec![MessageChunk::Cancelled]);
5427 drop(stream); assert_eq!(agent.memory.context().await.unwrap().len(), 1);
5430 }
5431
5432 #[tokio::test]
5436 async fn cancelled_then_fresh_token_run_works() {
5437 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
5438 let mut agent = agent(fake, "");
5439
5440 let t1 = CancellationToken::new();
5441 t1.cancel();
5442 assert!(matches!(
5443 agent
5444 .run_request_with_context(RunRequest::text("one"), cancellation_context(&t1))
5445 .await,
5446 Err(AgentError::Cancelled)
5447 ));
5448
5449 let t2 = CancellationToken::new();
5450 assert_eq!(
5451 agent
5452 .run_request_with_context(RunRequest::text("two"), cancellation_context(&t2))
5453 .await
5454 .unwrap()
5455 .answer,
5456 "hi"
5457 );
5458 }
5459
5460 use crate::event_channel::{BroadcastEventChannel, EventReceiver};
5464 use crate::tool::RegistryError;
5465
5466 fn attach_channel(agent: ReActAgent) -> (ReActAgent, Box<dyn EventReceiver>) {
5470 let channel = BroadcastEventChannel::new(64);
5471 let rx = channel.subscribe();
5472 (agent.with_event_channel(channel), rx)
5473 }
5474
5475 async fn drain(rx: &mut Box<dyn EventReceiver>) -> Vec<Arc<dyn AgentEvent>> {
5477 let mut out = Vec::new();
5478 while let Some(ev) = rx.recv().await {
5479 out.push(ev);
5480 }
5481 out
5482 }
5483
5484 fn names(events: &[Arc<dyn AgentEvent>]) -> Vec<&'static str> {
5485 events.iter().map(|e| e.name()).collect()
5486 }
5487
5488 fn react_event(ev: &dyn AgentEvent) -> &ReActEvent {
5491 ev.as_any()
5492 .downcast_ref::<ReActEvent>()
5493 .expect("test event should be ReActEvent")
5494 }
5495
5496 #[tokio::test]
5500 async fn events_published_on_run() {
5501 let (calc, _calls) = FakeTool::new("calc", "42");
5502 let mut registry = ToolRegistry::new();
5503 registry.register(calc);
5504 let fake = SharedFake::new([
5505 FakeReply::ToolCalls {
5506 content: "".into(),
5507 calls: vec![call("c1", "calc", r#"{"a":1}"#)],
5508 },
5509 FakeReply::Text("The answer is 42".into()),
5510 ]);
5511 let (mut agent, mut rx) =
5512 attach_channel(agent_with_registry(fake, registry, AgentConfig::default()));
5513 let answer = agent.run("Compute").await.unwrap();
5514 assert_eq!(answer, "The answer is 42");
5515 drop(agent);
5516 let events = drain(&mut rx).await;
5517
5518 assert_eq!(
5519 names(&events),
5520 ["run.started", "tool.started", "tool.completed", "run.ended"]
5521 );
5522 match react_event(&*events[1]) {
5525 ReActEvent::ToolStarted {
5528 id,
5529 name,
5530 arguments,
5531 } => {
5532 assert_eq!(id, "c1");
5533 assert_eq!(name, "calc");
5534 assert_eq!(arguments, r#"{"a":1}"#);
5535 }
5536 _ => panic!("expected ToolStarted"),
5537 }
5538 match react_event(&*events[2]) {
5539 ReActEvent::ToolCompleted { result, .. } => {
5541 assert_eq!(result, &Ok(ToolOutput::text("42").into()));
5542 }
5543 _ => panic!("expected ToolCompleted"),
5544 }
5545 match react_event(&*events[3]) {
5546 ReActEvent::RunEnded { summary, error } => {
5549 assert_eq!(error, &None);
5550 assert_eq!(summary.rounds, 2);
5551 assert_eq!(summary.tool_calls, 1);
5552 }
5553 _ => panic!("expected RunEnded"),
5554 }
5555 }
5556
5557 #[tokio::test]
5558 async fn run_started_event_preserves_block_input() {
5559 let blocks = vec![ContentBlock::Text("look".into())];
5560 let fake = SharedFake::new([FakeReply::Text("done".into())]);
5561 let (mut agent, mut rx) = attach_channel(agent(fake, ""));
5562 agent
5563 .run_request(RunRequest::blocks(blocks.clone()))
5564 .await
5565 .unwrap();
5566 drop(agent);
5567 let events = drain(&mut rx).await;
5568
5569 match react_event(&*events[0]) {
5570 ReActEvent::RunStarted { input, .. } => {
5571 assert_eq!(input, &crate::UserInput::Blocks(blocks));
5572 }
5573 _ => panic!("expected RunStarted"),
5574 }
5575 }
5576
5577 #[tokio::test]
5581 async fn events_carry_tool_failure() {
5582 let fake = SharedFake::new([
5583 FakeReply::ToolCalls {
5584 content: "".into(),
5585 calls: vec![call("c1", "nope", "{}")],
5586 },
5587 FakeReply::Text("Got it".into()),
5588 ]);
5589 let (mut agent, mut rx) = attach_channel(agent(fake, ""));
5590 agent.run("Trigger").await.unwrap();
5591 drop(agent);
5592
5593 let events = drain(&mut rx).await;
5594 let completed = events
5595 .iter()
5596 .find_map(|e| match react_event(&**e) {
5597 ReActEvent::ToolCompleted { result, .. } => Some(result),
5598 _ => None,
5599 })
5600 .expect("expected ToolCompleted event");
5601 assert!(matches!(completed, Err(RegistryError::NotFound(n)) if n == "nope"));
5602 assert_eq!(
5603 completed.as_ref().unwrap_err().to_string(),
5604 "tool not found: nope"
5605 );
5606 }
5607
5608 #[tokio::test]
5611 async fn stream_events_include_delta_and_reasoning() {
5612 let (calc, _calls) = FakeTool::new("calc", "42");
5613 let mut registry = ToolRegistry::new();
5614 registry.register(calc);
5615 let fake = SharedFake::new([
5616 FakeReply::ToolCalls {
5617 content: "Thinking: ".into(),
5618 calls: vec![call("c1", "calc", "{}")],
5619 },
5620 FakeReply::TextWithReasoning {
5621 content: "The answer is 42".into(),
5622 reasoning: "Reasoning steps".into(),
5623 },
5624 ]);
5625 let (mut agent, mut rx) =
5626 attach_channel(agent_with_registry(fake, registry, AgentConfig::default()));
5627
5628 let answer: String = agent
5630 .run_stream("Compute")
5631 .await
5632 .unwrap()
5633 .map(|e| e.unwrap())
5634 .filter_map(|e| async move {
5635 match e {
5636 MessageChunk::Delta(d) => Some(d),
5637 _ => None,
5638 }
5639 })
5640 .collect()
5641 .await;
5642 assert_eq!(answer, "Thinking: The answer is 42");
5643 drop(agent);
5644
5645 let events = drain(&mut rx).await;
5646 assert_eq!(
5650 names(&events),
5651 [
5652 "run.started",
5653 "delta",
5654 "tool.started",
5655 "tool.completed",
5656 "delta",
5657 "reasoning",
5658 "run.ended",
5659 ]
5660 );
5661 let reasoning = events
5662 .iter()
5663 .find_map(|e| match react_event(&**e) {
5664 ReActEvent::Reasoning { text } => Some(text.as_str()),
5665 _ => None,
5666 })
5667 .expect("expected Reasoning event");
5668 assert_eq!(reasoning, "Reasoning steps");
5669 match react_event(&**events.last().unwrap()) {
5670 ReActEvent::RunEnded { summary, error } => {
5671 assert_eq!(error, &None);
5672 assert_eq!(summary.rounds, 2);
5673 assert_eq!(summary.tool_calls, 1);
5674 }
5675 _ => panic!("expected RunEnded"),
5676 }
5677 }
5678
5679 #[tokio::test]
5682 async fn cancelled_run_publishes_run_ended_with_error() {
5683 struct SlowStreamProvider;
5685 #[async_trait::async_trait]
5686 impl Provider for SlowStreamProvider {
5687 async fn chat_with_context(
5688 &self,
5689 _request: ChatRequest,
5690 _context: &ProviderRequestContext,
5691 ) -> Result<ChatResponse, ProviderError> {
5692 unreachable!("this test uses streaming path only")
5693 }
5694 async fn stream_chat_with_context(
5695 &self,
5696 _request: ChatRequest,
5697 _context: &ProviderRequestContext,
5698 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
5699 {
5700 Ok(Box::pin(async_stream::stream! {
5701 yield Ok(StreamEvent::Delta("d0".into()));
5702 tokio::time::sleep(Duration::from_millis(100)).await;
5703 yield Ok(StreamEvent::Delta("d1".into()));
5704 tokio::time::sleep(Duration::from_millis(100)).await;
5705 yield Ok(StreamEvent::Done {
5706 reason: FinishReason::Stop,
5707 usage: None,
5708 });
5709 }))
5710 }
5711 }
5712
5713 let (mut agent, mut rx) =
5714 attach_channel(ReActAgent::new(SlowStreamProvider, ToolRegistry::new(), ""));
5715 let token = CancellationToken::new();
5716 let mut stream = agent
5717 .run_stream_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
5718 .await
5719 .unwrap();
5720
5721 tokio::select! {
5726 _ = async {
5727 while let Some(ev) = stream.next().await {
5728 let ev = ev.unwrap();
5729 if ev == MessageChunk::Cancelled {
5730 break;
5731 }
5732 }
5733 } => {}
5734 _ = async {
5735 tokio::time::sleep(Duration::from_millis(50)).await;
5736 token.cancel();
5737 } => {}
5738 }
5739 let rest: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
5740 assert!(rest.contains(&MessageChunk::Cancelled));
5741 drop(stream);
5742 drop(agent);
5743
5744 let events = drain(&mut rx).await;
5745 assert!(matches!(names(&events).as_slice(), [.., "run.ended"]));
5746 match react_event(&**events.last().unwrap()) {
5747 ReActEvent::RunEnded { error, .. } => {
5748 assert_eq!(error, &Some(AgentError::Cancelled));
5749 }
5750 _ => panic!("expected RunEnded"),
5751 }
5752 }
5753
5754 #[cfg(feature = "tracing")]
5755 mod tracing_tests {
5756 use super::*;
5757
5758 use std::collections::HashMap;
5762 use std::sync::atomic::AtomicU64;
5763 use tracing::field::{Field, Visit};
5764 use tracing::subscriber::Subscriber;
5765 use tracing::{Event, Id, Level, Metadata};
5766
5767 #[derive(Debug, Clone, PartialEq, Eq)]
5770 struct SpanInfo {
5771 name: &'static str,
5772 level: Level,
5773 fields: Vec<(String, String)>,
5774 }
5775
5776 #[derive(Debug, Clone, PartialEq, Eq)]
5779 enum Op {
5780 Enter(String),
5781 Exit(String),
5782 Close(String),
5783 Record(String, String),
5784 }
5785
5786 #[derive(Debug, Default)]
5793 struct CollectSubscriber {
5794 spans: std::sync::Mutex<Vec<SpanInfo>>,
5795 ops: std::sync::Mutex<Vec<Op>>,
5796 names: std::sync::Mutex<HashMap<Id, String>>,
5797 next_id: AtomicU64,
5798 }
5799
5800 struct FieldCollector<'a>(&'a mut Vec<(String, String)>);
5803
5804 impl Visit for FieldCollector<'_> {
5805 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
5806 self.0
5807 .push((field.name().to_string(), format!("{value:?}")));
5808 }
5809 }
5810
5811 impl Subscriber for CollectSubscriber {
5812 fn enabled(&self, _metadata: &Metadata<'_>) -> bool {
5813 true
5814 }
5815
5816 fn new_span(&self, span: &tracing::span::Attributes<'_>) -> Id {
5817 let id = Id::from_u64(self.next_id.fetch_add(1, Ordering::Relaxed) + 1);
5820 let mut fields = Vec::new();
5821 span.record(&mut FieldCollector(&mut fields));
5822 self.spans.lock().unwrap().push(SpanInfo {
5823 name: span.metadata().name(),
5824 level: *span.metadata().level(),
5825 fields,
5826 });
5827 self.names
5828 .lock()
5829 .unwrap()
5830 .insert(id.clone(), span.metadata().name().to_string());
5831 id
5832 }
5833
5834 fn record(&self, id: &Id, values: &tracing::span::Record<'_>) {
5835 let name = self.names.lock().unwrap().get(id).cloned();
5836 let Some(name) = name else { return };
5837 let mut fields = Vec::new();
5838 values.record(&mut FieldCollector(&mut fields));
5839 let mut ops = self.ops.lock().unwrap();
5840 for (field, value) in fields {
5841 ops.push(Op::Record(name.clone(), format!("{field}={value}")));
5842 }
5843 }
5844
5845 fn enter(&self, id: &Id) {
5846 let name = self.names.lock().unwrap().get(id).cloned();
5847 if let Some(name) = name {
5848 self.ops.lock().unwrap().push(Op::Enter(name));
5849 }
5850 }
5851
5852 fn exit(&self, id: &Id) {
5853 let name = self.names.lock().unwrap().get(id).cloned();
5854 if let Some(name) = name {
5855 self.ops.lock().unwrap().push(Op::Exit(name));
5856 }
5857 }
5858
5859 fn try_close(&self, id: Id) -> bool {
5860 let name = self.names.lock().unwrap().get(&id).cloned();
5861 if let Some(name) = name {
5862 self.ops.lock().unwrap().push(Op::Close(name));
5863 }
5864 true
5865 }
5866
5867 fn clone_span(&self, id: &Id) -> Id {
5868 id.clone()
5869 }
5870
5871 fn event(&self, _event: &Event<'_>) {}
5872 fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
5873 }
5874
5875 fn collect_guard(sub: &Arc<CollectSubscriber>) -> tracing::dispatcher::DefaultGuard {
5878 tracing::dispatcher::set_default(&tracing::Dispatch::new(sub.clone()))
5879 }
5880
5881 fn enter_names(ops: &[Op]) -> Vec<String> {
5882 ops.iter()
5883 .filter_map(|op| match op {
5884 Op::Enter(name) => Some(name.clone()),
5885 _ => None,
5886 })
5887 .collect()
5888 }
5889
5890 fn records_of(ops: &[Op], span: &str) -> Vec<String> {
5891 ops.iter()
5892 .filter_map(|op| match op {
5893 Op::Record(s, kv) if s == span => Some(kv.clone()),
5894 _ => None,
5895 })
5896 .collect()
5897 }
5898
5899 fn assert_nesting_invariants(ops: &[Op]) {
5905 let mut stack: Vec<String> = Vec::new();
5906 for op in ops {
5907 match op {
5908 Op::Enter(name) => {
5909 if name != "agent.run" {
5910 assert!(
5911 stack.contains(&"agent.run".to_string()),
5912 "span {name} requires agent.run on the stack when entering (stack: {stack:?})"
5913 );
5914 }
5915 assert!(
5916 !stack.contains(name),
5917 "same span entered twice (double instrumenting): {name} (stack: {stack:?})"
5918 );
5919 stack.push(name.clone());
5920 }
5921 Op::Exit(name) => {
5922 assert_eq!(
5923 stack.pop().as_deref(),
5924 Some(name.as_str()),
5925 "exit must pair with enter: {name}"
5926 );
5927 }
5928 _ => {}
5929 }
5930 }
5931 }
5932
5933 #[tokio::test(flavor = "current_thread")]
5938 async fn trace_span_tree_non_stream() {
5939 let sub = Arc::new(CollectSubscriber::default());
5940 let _guard = collect_guard(&sub);
5941
5942 let (calc, _calls) = FakeTool::new("calc", "42");
5943 let mut registry = ToolRegistry::new();
5944 registry.register(calc);
5945 let fake = SharedFake::new([
5946 FakeReply::WithUsage {
5947 reply: Box::new(FakeReply::ToolCalls {
5948 content: "".into(),
5949 calls: vec![call("c1", "calc", "{}")],
5950 }),
5951 usage: Usage::new(10, 2),
5952 },
5953 FakeReply::text_with_usage("42", Usage::new(20, 5)),
5954 ]);
5955 let mut agent = agent_with_registry(fake, registry, AgentConfig::default());
5956 assert_eq!(agent.run("Compute").await.unwrap(), "42");
5957
5958 let ops = sub.ops.lock().unwrap().clone();
5959 assert_nesting_invariants(&ops);
5962 let enters = enter_names(&ops);
5969 let mut first_seen = Vec::new();
5970 for name in enters.iter() {
5971 if !first_seen.contains(name) {
5972 first_seen.push(name.clone());
5973 }
5974 }
5975 assert_eq!(first_seen, ["agent.run", "llm_request", "tool"]);
5976 assert!(enters.iter().filter(|n| *n == "llm_request").count() >= 2);
5977 assert!(enters.iter().filter(|n| *n == "tool").count() >= 1);
5978
5979 assert_eq!(
5982 records_of(&ops, "llm_request"),
5983 [
5984 "usage.prompt_tokens=10",
5985 "usage.completion_tokens=2",
5986 "usage.prompt_tokens=20",
5987 "usage.completion_tokens=5",
5988 ]
5989 );
5990
5991 let spans = sub.spans.lock().unwrap().clone();
5994 let run_span = spans.iter().find(|s| s.name == "agent.run").unwrap();
5995 assert_eq!(run_span.level, Level::INFO);
5996 assert_eq!(
5997 spans
5998 .iter()
5999 .find(|s| s.name == "llm_request")
6000 .unwrap()
6001 .level,
6002 Level::DEBUG
6003 );
6004 assert_eq!(
6005 spans.iter().find(|s| s.name == "tool").unwrap().level,
6006 Level::DEBUG
6007 );
6008
6009 let llm_rounds: Vec<u64> = spans
6012 .iter()
6013 .filter(|s| s.name == "llm_request")
6014 .map(|s| {
6015 s.fields
6016 .iter()
6017 .find(|(f, _)| f == "round")
6018 .map(|(_, v)| v.parse().unwrap())
6019 .unwrap()
6020 })
6021 .collect();
6022 assert_eq!(llm_rounds, vec![1, 2]);
6023
6024 let run_ids: Vec<String> = spans
6027 .iter()
6028 .map(|s| {
6029 s.fields
6030 .iter()
6031 .find(|(f, _)| f == "run.id")
6032 .map(|(_, v)| v.clone())
6033 .unwrap_or_else(|| panic!("span {} must carry a run.id field", s.name))
6034 })
6035 .collect();
6036 assert!(run_ids.iter().all(|id| id == &run_ids[0]));
6037 assert!(run_ids[0].starts_with("run-"));
6038 }
6039
6040 #[tokio::test(flavor = "current_thread")]
6044 async fn trace_span_tree_stream() {
6045 let sub = Arc::new(CollectSubscriber::default());
6046 let _guard = collect_guard(&sub);
6047
6048 let (calc, _calls) = FakeTool::new("calc", "42");
6049 let mut registry = ToolRegistry::new();
6050 registry.register(calc);
6051 let fake = SharedFake::new([
6052 FakeReply::WithUsage {
6053 reply: Box::new(FakeReply::ToolCalls {
6054 content: "".into(),
6055 calls: vec![call("c1", "calc", "{}")],
6056 }),
6057 usage: Usage::new(10, 2),
6058 },
6059 FakeReply::text_with_usage("42", Usage::new(20, 5)),
6060 ]);
6061 let mut agent = agent_with_registry(fake, registry, AgentConfig::default());
6062 agent
6063 .run_stream("Compute")
6064 .await
6065 .unwrap()
6066 .for_each(|_| async {})
6067 .await;
6068
6069 let ops = sub.ops.lock().unwrap().clone();
6070 assert_nesting_invariants(&ops);
6074 let enters = enter_names(&ops);
6079 let mut first_seen = Vec::new();
6080 for name in enters.iter() {
6081 if !first_seen.contains(name) {
6082 first_seen.push(name.clone());
6083 }
6084 }
6085 assert_eq!(first_seen, ["agent.run", "llm_request", "tool"]);
6086 assert!(enters.iter().filter(|n| *n == "agent.run").count() > 2);
6089
6090 assert_eq!(
6092 records_of(&ops, "llm_request"),
6093 [
6094 "usage.prompt_tokens=10",
6095 "usage.completion_tokens=2",
6096 "usage.prompt_tokens=20",
6097 "usage.completion_tokens=5",
6098 ]
6099 );
6100 }
6101
6102 #[tokio::test(flavor = "current_thread")]
6106 async fn trace_run_id_differs_between_runs() {
6107 let sub = Arc::new(CollectSubscriber::default());
6108 let _guard = collect_guard(&sub);
6109
6110 let fake =
6111 SharedFake::new([FakeReply::Text("hi".into()), FakeReply::Text("bye".into())]);
6112 let (mut agent, mut rx) = attach_channel(agent(fake, ""));
6113 agent.run("one").await.unwrap();
6114 agent.run("two").await.unwrap();
6115 drop(agent);
6116
6117 let spans = sub.spans.lock().unwrap().clone();
6118 let run_ids: Vec<String> = spans
6119 .iter()
6120 .filter(|s| s.name == "agent.run")
6121 .map(|s| {
6122 s.fields
6123 .iter()
6124 .find(|(f, _)| f == "run.id")
6125 .map(|(_, v)| v.clone())
6126 .unwrap()
6127 })
6128 .collect();
6129 assert_eq!(run_ids.len(), 2);
6130 assert_ne!(run_ids[0], run_ids[1]);
6131
6132 let events = drain(&mut rx).await;
6135 let event_ids: Vec<String> = events
6136 .iter()
6137 .filter_map(|e| match react_event(&**e) {
6138 ReActEvent::RunStarted { run_id, .. } => Some(run_id.clone()),
6139 _ => None,
6140 })
6141 .collect();
6142 assert_eq!(event_ids, run_ids);
6143 }
6144
6145 #[tokio::test(flavor = "current_thread")]
6148 async fn trace_run_error_recorded() {
6149 let sub = Arc::new(CollectSubscriber::default());
6150 let _guard = collect_guard(&sub);
6151
6152 let (calc, _calls) = FakeTool::new("calc", "42");
6153 let mut registry = ToolRegistry::new();
6154 registry.register(calc);
6155 let fake = SharedFake::new([
6156 FakeReply::ToolCalls {
6157 content: "".into(),
6158 calls: vec![call("c1", "calc", "{}")],
6159 },
6160 FakeReply::ToolCalls {
6161 content: "".into(),
6162 calls: vec![call("c2", "calc", "{}")],
6163 },
6164 ]);
6165 let mut agent = agent_with_registry(
6166 fake,
6167 registry,
6168 AgentConfig {
6169 max_tool_rounds: 2,
6170 ..Default::default()
6171 },
6172 );
6173 assert!(matches!(
6174 agent.run("Keep computing").await,
6175 Err(AgentError::TooManyToolRounds(2))
6176 ));
6177
6178 let ops = sub.ops.lock().unwrap().clone();
6179 let records = records_of(&ops, "agent.run");
6183 assert_eq!(records.len(), 1);
6184 assert!(
6185 records[0].starts_with("error=\"model requested tools for more than 2 rounds"),
6186 "unexpected records: {records:?}"
6187 );
6188 }
6189
6190 #[tokio::test(flavor = "current_thread")]
6193 async fn trace_tool_error_recorded() {
6194 struct FailingTool;
6195 #[async_trait::async_trait]
6196 impl Tool for FailingTool {
6197 fn schema(&self) -> ToolSchema {
6198 ToolSchema::new("boom", "Tool that always fails", serde_json::json!({}))
6199 }
6200 async fn call(
6201 &self,
6202 _arguments: serde_json::Value,
6203 _context: ToolContext<'_>,
6204 ) -> Result<ToolResult, ToolError> {
6205 Err(ToolError::Execution("internal error".into()))
6206 }
6207 }
6208
6209 let sub = Arc::new(CollectSubscriber::default());
6210 let _guard = collect_guard(&sub);
6211
6212 let mut registry = ToolRegistry::new();
6213 registry.register(FailingTool);
6214 let fake = SharedFake::new([
6215 FakeReply::ToolCalls {
6216 content: "".into(),
6217 calls: vec![call("c1", "boom", "{}")],
6218 },
6219 FakeReply::Text("Got it".into()),
6220 ]);
6221 let mut agent = agent_with_registry(fake, registry, AgentConfig::default());
6222 agent.run("Trigger failure").await.unwrap();
6223
6224 let ops = sub.ops.lock().unwrap().clone();
6225 let tool_errors = records_of(&ops, "tool");
6228 assert_eq!(tool_errors.len(), 1);
6229 assert!(tool_errors[0].contains("internal error"));
6230 assert!(records_of(&ops, "agent.run").is_empty());
6231 }
6232
6233 #[tokio::test(flavor = "current_thread")]
6237 async fn trace_llm_error_recorded() {
6238 let sub = Arc::new(CollectSubscriber::default());
6239 let _guard = collect_guard(&sub);
6240
6241 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
6243 let mut agent = agent(fake, "");
6244 agent.run("one").await.unwrap();
6245 let err = agent.run("two").await.unwrap_err();
6246 assert!(matches!(err, AgentError::Provider(_)));
6247
6248 let ops = sub.ops.lock().unwrap().clone();
6249 let llm_records = records_of(&ops, "llm_request");
6253 assert!(
6254 llm_records.iter().any(|r| r.starts_with("error=")),
6255 "the failed round's llm span should have an error: {llm_records:?}"
6256 );
6257 assert!(
6259 records_of(&ops, "agent.run")
6260 .iter()
6261 .any(|r| r.starts_with("error="))
6262 );
6263 }
6264
6265 #[tokio::test(flavor = "current_thread")]
6269 async fn trace_stream_llm_error_recorded() {
6270 struct FailInStream;
6271 #[async_trait::async_trait]
6272 impl Provider for FailInStream {
6273 async fn chat_with_context(
6274 &self,
6275 _request: ChatRequest,
6276 _context: &ProviderRequestContext,
6277 ) -> Result<ChatResponse, ProviderError> {
6278 unreachable!("this test uses streaming path only")
6279 }
6280 async fn stream_chat_with_context(
6281 &self,
6282 _request: ChatRequest,
6283 _context: &ProviderRequestContext,
6284 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
6285 {
6286 Ok(Box::pin(futures::stream::iter(vec![
6287 Ok(StreamEvent::Delta("hi".into())),
6288 Err(ProviderError::Protocol {
6289 message: "boom".into(),
6290 }),
6291 ])))
6292 }
6293 }
6294
6295 let sub = Arc::new(CollectSubscriber::default());
6296 let _guard = collect_guard(&sub);
6297
6298 let mut agent = ReActAgent::new(FailInStream, ToolRegistry::new(), "");
6299 let mut stream = agent.run_stream("two").await.unwrap();
6300 while stream.next().await.is_some() {}
6301
6302 let ops = sub.ops.lock().unwrap().clone();
6303 assert_nesting_invariants(&ops);
6304 assert!(
6305 records_of(&ops, "llm_request")
6306 .iter()
6307 .any(|r| r.contains("boom"))
6308 );
6309 assert!(
6310 records_of(&ops, "agent.run")
6311 .iter()
6312 .any(|r| r.contains("boom"))
6313 );
6314 }
6315 }
6316}