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 agent = agent(fake.clone(), "").with_config(AgentConfig {
4327 max_tool_rounds: 10,
4328 options: ModelOptions {
4329 temperature: Some(0.2),
4330 max_tokens: Some(128),
4331 extra: Default::default(),
4332 structured: None,
4333 },
4334 ..Default::default()
4335 });
4336 agent.run("hi").await.unwrap();
4337
4338 let req = &fake.requests()[0];
4339 assert_eq!(req.options.temperature, Some(0.2));
4340 assert_eq!(req.options.max_tokens, Some(128));
4341 }
4342
4343 #[tokio::test]
4344 async fn request_options_replace_config_options() {
4345 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
4346 let mut agent = agent(fake.clone(), "").with_config(AgentConfig {
4347 options: ModelOptions {
4348 temperature: Some(0.2),
4349 max_tokens: Some(128),
4350 ..Default::default()
4351 },
4352 ..Default::default()
4353 });
4354 agent
4355 .run_request(RunRequest::text("hi").with_options(ModelOptions {
4356 max_tokens: Some(64),
4357 ..Default::default()
4358 }))
4359 .await
4360 .unwrap();
4361
4362 let req = &fake.requests()[0];
4363 assert_eq!(req.options.temperature, None);
4364 assert_eq!(req.options.max_tokens, Some(64));
4365 }
4366
4367 #[tokio::test]
4374 async fn cancelled_before_run_returns_cancelled() {
4375 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
4376 let mut agent = agent(fake.clone(), "");
4377 let token = CancellationToken::new();
4378 token.cancel();
4379
4380 let err = agent
4381 .run_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
4382 .await
4383 .unwrap_err();
4384 assert!(matches!(err, AgentError::Cancelled));
4385 assert_eq!(fake.requests().len(), 0); assert_eq!(agent.memory.context().await.unwrap().len(), 1); }
4388
4389 #[tokio::test]
4392 async fn pre_cancelled_token_rounds_consistent_across_paths() {
4393 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
4394 let token = CancellationToken::new();
4395 token.cancel();
4396
4397 let (mut run_agent, mut rx) = attach_channel(agent(fake.clone(), ""));
4399 let err = run_agent
4400 .run_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
4401 .await
4402 .unwrap_err();
4403 assert!(matches!(err, AgentError::Cancelled));
4404 drop(run_agent);
4405 let events = drain(&mut rx).await;
4406 let rounds_run = match react_event(&**events.last().unwrap()) {
4407 ReActEvent::RunEnded { summary, error, .. } => {
4408 assert_eq!(error, &Some(AgentError::Cancelled));
4409 summary.rounds
4410 }
4411 _ => panic!("expected RunEnded"),
4412 };
4413
4414 let (mut stream_agent, mut rx) = attach_channel(agent(fake, ""));
4416 let mut stream = stream_agent
4417 .run_stream_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
4418 .await
4419 .unwrap();
4420 let chunks: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
4421 assert!(chunks.contains(&MessageChunk::Cancelled));
4422 drop(stream);
4423 drop(stream_agent);
4424 let events = drain(&mut rx).await;
4425 let rounds_stream = match react_event(&**events.last().unwrap()) {
4426 ReActEvent::RunEnded { summary, error, .. } => {
4427 assert_eq!(error, &Some(AgentError::Cancelled));
4428 summary.rounds
4429 }
4430 _ => panic!("expected RunEnded"),
4431 };
4432
4433 assert_eq!(rounds_run, rounds_stream);
4434 assert_eq!(rounds_run, 1);
4435 }
4436
4437 #[tokio::test]
4442 async fn cancel_during_chat_drops_inflight() {
4443 struct PendingProvider;
4444 #[async_trait::async_trait]
4445 impl Provider for PendingProvider {
4446 async fn chat_with_context(
4447 &self,
4448 _request: ChatRequest,
4449 _context: &ProviderRequestContext,
4450 ) -> Result<ChatResponse, ProviderError> {
4451 std::future::pending().await }
4453 async fn stream_chat_with_context(
4454 &self,
4455 _request: ChatRequest,
4456 _context: &ProviderRequestContext,
4457 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
4458 {
4459 unreachable!("this test uses non-streaming path only")
4460 }
4461 }
4462
4463 let mut agent = ReActAgent::new(PendingProvider, ToolRegistry::new(), "");
4464 let token = CancellationToken::new();
4465 let result = tokio::select! {
4466 r = agent.run_request_with_context(RunRequest::text("hi"), cancellation_context(&token)) => r.map(|output| output.answer),
4467 _ = async {
4468 tokio::time::sleep(Duration::from_millis(20)).await;
4469 token.cancel();
4470 std::future::pending::<()>().await; } => unreachable!("cancellation branch only sends a signal"),
4472 };
4473 assert!(matches!(result, Err(AgentError::Cancelled)));
4474 assert_eq!(agent.memory.context().await.unwrap().len(), 1); }
4476
4477 #[tokio::test]
4482 async fn provider_error_propagates_from_both_paths() {
4483 struct FailProvider(ProviderError);
4484 #[async_trait::async_trait]
4485 impl Provider for FailProvider {
4486 async fn chat_with_context(
4487 &self,
4488 _request: ChatRequest,
4489 _context: &ProviderRequestContext,
4490 ) -> Result<ChatResponse, ProviderError> {
4491 Err(self.0.clone())
4492 }
4493 async fn stream_chat_with_context(
4494 &self,
4495 _request: ChatRequest,
4496 _context: &ProviderRequestContext,
4497 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
4498 {
4499 Err(self.0.clone())
4500 }
4501 }
4502
4503 let err = ProviderError::Timeout(TimeoutStage::Request);
4504 let mut agent = ReActAgent::new(FailProvider(err.clone()), ToolRegistry::new(), "");
4506 let got = agent.run("hi").await.unwrap_err();
4507 assert!(matches!(
4508 got,
4509 AgentError::Provider(ProviderError::Timeout(_))
4510 ));
4511 assert_eq!(agent.memory.context().await.unwrap().len(), 1); let mut agent = ReActAgent::new(FailProvider(err.clone()), ToolRegistry::new(), "");
4516 let mut stream = agent.run_stream("hi").await.unwrap();
4517 let item = stream.next().await.unwrap().unwrap_err();
4518 assert!(matches!(
4519 item,
4520 AgentError::Provider(ProviderError::Timeout(_))
4521 ));
4522 assert!(stream.next().await.is_none());
4523 drop(stream); assert_eq!(agent.memory.context().await.unwrap().len(), 1);
4525 }
4526
4527 #[tokio::test]
4528 async fn deadline_exceeded_during_chat_is_distinct_from_provider_timeout() {
4529 struct PendingProvider;
4530 #[async_trait::async_trait]
4531 impl Provider for PendingProvider {
4532 async fn chat_with_context(
4533 &self,
4534 _request: ChatRequest,
4535 _context: &ProviderRequestContext,
4536 ) -> Result<ChatResponse, ProviderError> {
4537 std::future::pending().await
4538 }
4539
4540 async fn stream_chat_with_context(
4541 &self,
4542 _request: ChatRequest,
4543 _context: &ProviderRequestContext,
4544 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
4545 {
4546 std::future::pending().await
4547 }
4548 }
4549
4550 let mut agent = ReActAgent::new(PendingProvider, ToolRegistry::new(), "");
4551 let err = agent
4552 .run_request_with_context(
4553 RunRequest::text("hi"),
4554 RunContext::new("deadline").with_timeout(Duration::from_millis(10)),
4555 )
4556 .await
4557 .unwrap_err();
4558 assert_eq!(err, AgentError::DeadlineExceeded);
4559 assert_eq!(agent.memory.context().await.unwrap().len(), 1);
4560 }
4561
4562 #[tokio::test]
4563 async fn streaming_deadline_exceeded_terminates_with_error_item() {
4564 struct PendingStreamProvider;
4565 #[async_trait::async_trait]
4566 impl Provider for PendingStreamProvider {
4567 async fn chat_with_context(
4568 &self,
4569 _request: ChatRequest,
4570 _context: &ProviderRequestContext,
4571 ) -> Result<ChatResponse, ProviderError> {
4572 unreachable!("this test uses streaming only")
4573 }
4574
4575 async fn stream_chat_with_context(
4576 &self,
4577 _request: ChatRequest,
4578 _context: &ProviderRequestContext,
4579 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
4580 {
4581 Ok(Box::pin(futures::stream::pending()))
4582 }
4583 }
4584
4585 let mut agent = ReActAgent::new(PendingStreamProvider, ToolRegistry::new(), "");
4586 let mut stream = agent
4587 .run_stream_request_with_context(
4588 RunRequest::text("hi"),
4589 RunContext::new("stream-deadline").with_timeout(Duration::from_millis(10)),
4590 )
4591 .await
4592 .unwrap();
4593 assert_eq!(
4594 stream.next().await.unwrap().unwrap_err(),
4595 AgentError::DeadlineExceeded
4596 );
4597 }
4598
4599 #[cfg(feature = "structured")]
4602 #[tokio::test]
4603 async fn structured_output_valid_answer_passes() {
4604 let schema = serde_json::json!({
4605 "type": "object",
4606 "properties": { "city": { "type": "string" } },
4607 "required": ["city"],
4608 });
4609 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4610 let mut agent = agent(fake.clone(), "").with_structured_output(schema);
4611 let answer = agent.run("Beijing weather").await.unwrap();
4612 assert_eq!(answer, r#"{"city":"Beijing"}"#);
4613 assert_eq!(fake.requests().len(), 1); }
4615
4616 #[cfg(feature = "structured")]
4619 #[tokio::test]
4620 async fn structured_output_retries_after_invalid_answer() {
4621 let schema = serde_json::json!({
4622 "type": "object",
4623 "properties": { "city": { "type": "string" } },
4624 "required": ["city"],
4625 });
4626 let fake = SharedFake::new([
4627 FakeReply::Text("Not JSON".into()),
4628 FakeReply::Text(r#"{"city":"Beijing"}"#.into()),
4629 ]);
4630 let mut agent = agent(fake.clone(), "").with_structured_output(schema);
4631 let answer = agent.run("Beijing weather").await.unwrap();
4632 assert_eq!(answer, r#"{"city":"Beijing"}"#);
4633 assert_eq!(fake.requests().len(), 2); let context = agent.memory.context().await.unwrap();
4637 assert!(context.iter().any(|m| matches!(
4638 m,
4639 Message::User(blocks) if blocks.iter().any(|b| matches!(b, ContentBlock::Text(t) if t.contains("JSON schema validation")))
4640 )));
4641 }
4642
4643 #[cfg(feature = "structured")]
4648 #[tokio::test]
4649 async fn structured_output_exhausts_retry_budget() {
4650 let schema = serde_json::json!({ "type": "object" });
4651 let fake = SharedFake::new([
4652 FakeReply::Text("bad1".into()),
4653 FakeReply::Text("bad2".into()),
4654 FakeReply::Text("bad3".into()),
4655 FakeReply::Text("bad4".into()),
4656 ]);
4657 let mut agent = agent(fake.clone(), "").with_config(AgentConfig {
4660 max_tool_rounds: 1,
4661 max_structured_retries: 3,
4662 ..Default::default()
4663 });
4664 agent = agent.with_structured_output(schema);
4665 let err = agent.run("hi").await.unwrap_err();
4666 assert!(matches!(err, AgentError::StructuredRetriesExhausted(3)));
4667 assert_eq!(fake.requests().len(), 4); }
4669
4670 #[cfg(feature = "structured")]
4674 #[tokio::test]
4675 async fn typed_output_parses_valid_answer() {
4676 #[derive(Debug, Deserialize, JsonSchema)]
4677 struct Weather {
4678 city: String,
4679 }
4680 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4681 let mut agent = agent(fake.clone(), "");
4682 let weather: Weather = agent.run_typed("Beijing weather").await.unwrap();
4683 assert_eq!(weather.city, "Beijing");
4684 assert_eq!(fake.requests().len(), 1); assert!(fake.requests()[0].options.structured.is_some());
4688 assert!(agent.config.options.structured.is_none());
4689 }
4690
4691 #[cfg(feature = "structured")]
4692 #[tokio::test]
4693 async fn typed_run_request_returns_value_and_output() {
4694 #[derive(Debug, Deserialize, JsonSchema, PartialEq)]
4695 struct Weather {
4696 city: String,
4697 }
4698 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4699 let mut agent = agent(fake.clone(), "");
4700 let typed = agent
4701 .run_typed_request_with_context::<Weather>(
4702 RunRequest::text("Beijing weather"),
4703 RunContext::new("typed-1"),
4704 )
4705 .await
4706 .unwrap();
4707
4708 assert_eq!(
4709 typed.value,
4710 Weather {
4711 city: "Beijing".into()
4712 }
4713 );
4714 assert_eq!(typed.output.run_id, "typed-1");
4715 assert_eq!(typed.output.answer, r#"{"city":"Beijing"}"#);
4716 assert_eq!(
4717 typed.output.final_message,
4718 Message::assistant(r#"{"city":"Beijing"}"#)
4719 );
4720 }
4721
4722 #[cfg(feature = "structured")]
4723 #[tokio::test]
4724 async fn typed_schema_overrides_request_and_config_schema() {
4725 #[derive(Debug, Deserialize, JsonSchema)]
4726 struct Weather {
4727 city: String,
4728 }
4729 let config_schema = serde_json::json!({
4730 "type": "object",
4731 "properties": { "config_only": { "type": "string" } },
4732 "required": ["config_only"],
4733 });
4734 let request_schema = serde_json::json!({
4735 "type": "object",
4736 "properties": { "request_only": { "type": "string" } },
4737 "required": ["request_only"],
4738 });
4739 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4740 let mut agent = agent(fake.clone(), "").with_structured_output(config_schema);
4741 let options = ModelOptions {
4742 structured: Some(request_schema),
4743 ..Default::default()
4744 };
4745
4746 let weather: Weather = agent
4747 .run_typed_request(RunRequest::text("Beijing weather").with_options(options))
4748 .await
4749 .unwrap()
4750 .value;
4751
4752 assert_eq!(weather.city, "Beijing");
4753 let requests = fake.requests();
4754 let structured = requests[0]
4755 .options
4756 .structured
4757 .as_ref()
4758 .expect("typed schema should be sent");
4759 let props = &structured["properties"];
4760 assert!(props.get("city").is_some());
4761 assert!(props.get("request_only").is_none());
4762 assert!(props.get("config_only").is_none());
4763 }
4764
4765 #[cfg(feature = "structured")]
4768 #[tokio::test]
4769 async fn typed_output_retries_then_parses() {
4770 #[derive(Debug, Deserialize, JsonSchema)]
4771 struct Weather {
4772 city: String,
4773 }
4774 let fake = SharedFake::new([
4775 FakeReply::Text("Not JSON".into()),
4776 FakeReply::Text(r#"{"city":"Beijing"}"#.into()),
4777 ]);
4778 let mut agent = agent(fake.clone(), "");
4779 let weather: Weather = agent.run_typed("Beijing weather").await.unwrap();
4780 assert_eq!(weather.city, "Beijing");
4781 assert_eq!(fake.requests().len(), 2);
4782 }
4783
4784 #[cfg(feature = "structured")]
4790 #[tokio::test]
4791 async fn typed_output_parse_failure_on_schema_mismatch() {
4792 fn string_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
4793 serde_json::from_value(serde_json::json!({ "type": "string" })).unwrap()
4794 }
4795 #[derive(Debug, Deserialize, JsonSchema)]
4796 #[allow(dead_code)] struct Weather {
4798 #[schemars(schema_with = "string_schema")]
4799 temperature: i32,
4800 }
4801 let fake = SharedFake::new([FakeReply::Text(r#"{"temperature":"30"}"#.into())]);
4802 let mut agent = agent(fake.clone(), "");
4803 let err: AgentError = agent
4804 .run_typed::<Weather>("Beijing weather")
4805 .await
4806 .unwrap_err();
4807 assert!(matches!(err, AgentError::StructuredParse(_)));
4808 }
4809
4810 #[cfg(feature = "structured")]
4814 #[tokio::test]
4815 async fn typed_agent_trait_generic_call() {
4816 #[derive(Debug, Deserialize, JsonSchema)]
4817 struct Weather {
4818 city: String,
4819 }
4820 async fn typed_run<A: TypedAgent + Send>(
4821 agent: &mut A,
4822 input: &str,
4823 ) -> Result<Weather, AgentError> {
4824 agent.run_typed(input).await
4825 }
4826
4827 let fake = SharedFake::new([FakeReply::Text(r#"{"city":"Beijing"}"#.into())]);
4828 let mut agent = agent(fake.clone(), "");
4829 let weather = typed_run(&mut agent, "Beijing weather").await.unwrap();
4830 assert_eq!(weather.city, "Beijing");
4831 }
4832
4833 #[cfg(feature = "structured")]
4837 #[tokio::test]
4838 async fn typed_output_agent_trait_run_returns_text() {
4839 #[derive(Debug, Deserialize, JsonSchema)]
4840 struct Weather {
4841 city: String,
4842 }
4843 let fake = SharedFake::new([
4844 FakeReply::Text(r#"{"city":"Beijing"}"#.into()),
4845 FakeReply::Text("Hello".into()),
4846 ]);
4847 let mut agent = agent(fake.clone(), "");
4848 let weather: Weather = agent.run_typed("Beijing weather").await.unwrap();
4849 assert_eq!(weather.city, "Beijing");
4850 let text = Agent::run(&mut agent, "Say hi").await.unwrap();
4853 assert_eq!(text, "Hello");
4854 }
4855
4856 #[cfg(feature = "structured")]
4860 #[tokio::test]
4861 async fn structured_output_stream_exhausts_retry_budget() {
4862 let schema = serde_json::json!({ "type": "object" });
4863 let fake = SharedFake::new([
4864 FakeReply::Text("bad1".into()),
4865 FakeReply::Text("bad2".into()),
4866 ]);
4867 let mut agent = agent(fake.clone(), "").with_config(AgentConfig {
4868 max_structured_retries: 1,
4869 ..Default::default()
4870 });
4871 agent = agent.with_structured_output(schema);
4872 let mut stream = agent.run_stream("hi").await.unwrap();
4873 let mut saw_err = false;
4874 while let Some(item) = stream.next().await {
4875 if let Err(e) = item {
4876 assert!(matches!(e, AgentError::StructuredRetriesExhausted(1)));
4877 saw_err = true;
4878 break;
4879 }
4880 }
4881 assert!(saw_err);
4882 }
4883
4884 #[tokio::test]
4889 async fn tool_round_atomic_under_cancel() {
4890 struct SlowTool {
4892 calls: Arc<AtomicUsize>,
4893 }
4894 #[async_trait::async_trait]
4895 impl Tool for SlowTool {
4896 fn schema(&self) -> ToolSchema {
4897 ToolSchema::new("slow", "Slow tool", serde_json::json!({}))
4898 }
4899 async fn call(
4900 &self,
4901 _arguments: serde_json::Value,
4902 _context: ToolContext<'_>,
4903 ) -> Result<ToolResult, ToolError> {
4904 self.calls.fetch_add(1, Ordering::Relaxed);
4905 tokio::time::sleep(Duration::from_millis(100)).await;
4906 Ok(ToolOutput::text("42").into())
4907 }
4908 }
4909
4910 let calls = Arc::new(AtomicUsize::new(0));
4911 let mut registry = ToolRegistry::new();
4912 registry.register(SlowTool {
4913 calls: calls.clone(),
4914 });
4915 let fake = SharedFake::new([
4916 FakeReply::ToolCalls {
4917 content: "".into(),
4918 calls: vec![call("c1", "slow", "{}")],
4919 },
4920 FakeReply::Text("42".into()),
4921 ]);
4922 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
4923 let token = CancellationToken::new();
4924
4925 let result = tokio::select! {
4928 r = agent.run_request_with_context(RunRequest::text("Compute"), cancellation_context(&token)) => r.map(|output| output.answer),
4929 _ = async {
4930 tokio::time::sleep(Duration::from_millis(50)).await;
4931 token.cancel();
4932 std::future::pending::<()>().await; } => unreachable!("cancellation branch only sends a signal"),
4934 };
4935 assert!(matches!(result, Err(AgentError::Cancelled)));
4936 assert_eq!(calls.load(Ordering::Relaxed), 1); assert_eq!(fake.requests().len(), 1); let ctx = agent.memory.context().await.unwrap();
4941 assert_eq!(ctx.len(), 3);
4942 assert!(matches!(
4943 &ctx[1],
4944 Message::Assistant { tool_calls, .. } if tool_calls.len() == 1
4945 ));
4946 assert!(matches!(&ctx[2], Message::ToolResult { content, .. } if content == "42"));
4947 }
4948
4949 #[tokio::test]
4955 async fn custom_tool_round_executor() {
4956 #[derive(Default)]
4959 struct DenyAlphaToolRoundExecutor {
4960 denied: bool,
4961 }
4962 #[async_trait::async_trait]
4963 impl ToolRoundExecutor for DenyAlphaToolRoundExecutor {
4964 async fn execute_round<'a>(
4965 &'a mut self,
4966 ctx: ToolRoundCtx<'a>,
4967 calls: Vec<ToolCall>,
4968 ) -> BoxStream<'a, ToolCallOutcome> {
4969 let mut outcomes = Vec::with_capacity(calls.len());
4970 for call in calls.into_iter().rev() {
4971 if call.name == "alpha" && !self.denied {
4972 self.denied = true;
4973 outcomes.push(ToolCallOutcome {
4977 call,
4978 content: "denied by policy".into(),
4979 memory_policy: ToolMemoryPolicy::Normal,
4980 effect: None,
4981 });
4982 } else {
4983 outcomes.push(ctx.run(call).await);
4984 }
4985 }
4986 Box::pin(futures::stream::iter(outcomes))
4987 }
4988 }
4989
4990 let (alpha_tool, alpha_calls) = FakeTool::new("alpha", "A");
4991 let (beta_tool, beta_calls) = FakeTool::new("beta", "B");
4992 let mut registry = ToolRegistry::new();
4993 registry.register(alpha_tool);
4994 registry.register(beta_tool);
4995 let fake = SharedFake::new([
4996 FakeReply::ToolCalls {
4997 content: "".into(),
4998 calls: vec![call("c1", "alpha", "{}"), call("c2", "beta", "{}")],
4999 },
5000 FakeReply::Text("done".into()),
5001 ]);
5002 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default())
5003 .with_tool_round_executor(DenyAlphaToolRoundExecutor::default());
5004
5005 let answer = agent.run("Compute").await.unwrap();
5006 assert_eq!(answer, "done");
5007 assert_eq!(alpha_calls.load(Ordering::Relaxed), 0);
5009 assert_eq!(beta_calls.load(Ordering::Relaxed), 1);
5010 let requests = fake.requests();
5013 assert_eq!(requests.len(), 2);
5014 assert_eq!(requests[1].messages[2], Message::tool_result("c2", "B"),);
5015 assert_eq!(
5016 requests[1].messages[3],
5017 Message::tool_result("c1", "denied by policy"),
5018 );
5019 }
5020
5021 #[tokio::test]
5025 async fn custom_tool_round_executor_streaming() {
5026 #[derive(Default)]
5027 struct ReversedToolRoundExecutor;
5028 #[async_trait::async_trait]
5029 impl ToolRoundExecutor for ReversedToolRoundExecutor {
5030 async fn execute_round<'a>(
5031 &'a mut self,
5032 ctx: ToolRoundCtx<'a>,
5033 calls: Vec<ToolCall>,
5034 ) -> BoxStream<'a, ToolCallOutcome> {
5035 let mut outcomes = Vec::with_capacity(calls.len());
5036 for call in calls.into_iter().rev() {
5037 outcomes.push(ctx.run(call).await);
5038 }
5039 Box::pin(futures::stream::iter(outcomes))
5040 }
5041 }
5042
5043 let (alpha_tool, _) = FakeTool::new("alpha", "A");
5044 let (beta_tool, _) = FakeTool::new("beta", "B");
5045 let mut registry = ToolRegistry::new();
5046 registry.register(alpha_tool);
5047 registry.register(beta_tool);
5048 let fake = SharedFake::new([
5049 FakeReply::ToolCalls {
5050 content: "".into(),
5051 calls: vec![call("c1", "alpha", "{}"), call("c2", "beta", "{}")],
5052 },
5053 FakeReply::Text("done".into()),
5054 ]);
5055 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default())
5056 .with_tool_round_executor(ReversedToolRoundExecutor);
5057
5058 let mut stream = agent.run_stream("Compute").await.unwrap();
5059 let mut results = Vec::new();
5060 let mut done = false;
5061 while let Some(item) = stream.next().await {
5062 match item.unwrap() {
5063 MessageChunk::ToolResult { id, content, .. } => {
5064 results.push((id, content));
5065 }
5066 MessageChunk::Done(_) => done = true,
5067 _ => {}
5068 }
5069 }
5070 assert!(done);
5071 assert_eq!(
5073 results,
5074 vec![
5075 ("c2".to_string(), "B".to_string()),
5076 ("c1".to_string(), "A".to_string())
5077 ]
5078 );
5079 }
5080
5081 #[tokio::test]
5082 async fn kernel_batches_effect_requests_from_same_round() {
5083 let mut registry = ToolRegistry::new();
5084 registry
5085 .register(EffectTool::new("read_a", "effect-a", "read A"))
5086 .register(EffectTool::new("read_b", "effect-b", "read B"));
5087 let fake = SharedFake::new([
5088 FakeReply::ToolCalls {
5089 content: "".into(),
5090 calls: vec![call("c1", "read_a", "{}"), call("c2", "read_b", "{}")],
5091 },
5092 FakeReply::Text("done".into()),
5093 ]);
5094 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
5095 let context = RunContext::new("kernel-batch-effects");
5096
5097 let action = agent
5098 .start(RunRequest::text("read both"), &context)
5099 .await
5100 .unwrap();
5101 let AgentAction::RequestModel { request } = action else {
5102 panic!("expected initial model request");
5103 };
5104 let action = agent
5105 .observe(
5106 Observation::Model(ModelObservation::new(
5107 request.id,
5108 fake.chat(request.chat).await.unwrap(),
5109 )),
5110 &context,
5111 )
5112 .await
5113 .unwrap();
5114 let AgentAction::RequestEffects { requests } = action else {
5115 panic!("expected batch effect request");
5116 };
5117 assert_eq!(
5118 requests
5119 .iter()
5120 .map(|request| request.id.as_str())
5121 .collect::<Vec<_>>(),
5122 vec!["effect-a", "effect-b"]
5123 );
5124 assert_eq!(
5125 requests
5126 .iter()
5127 .map(|request| request.source.tool_call_id.as_deref())
5128 .collect::<Vec<_>>(),
5129 vec![Some("c1"), Some("c2")]
5130 );
5131
5132 let action = agent
5135 .observe(
5136 Observation::Effects(vec![
5137 EffectObservation::succeeded("effect-b", "observed B"),
5138 EffectObservation::succeeded("effect-a", "observed A"),
5139 ]),
5140 &context,
5141 )
5142 .await
5143 .unwrap();
5144 let AgentAction::RequestModel { request } = action else {
5145 panic!("expected next model request");
5146 };
5147 assert!(matches!(
5148 &request.chat.messages[2],
5149 Message::ToolResult { id, content } if id == "c1" && content == "observed A"
5150 ));
5151 assert!(matches!(
5152 &request.chat.messages[3],
5153 Message::ToolResult { id, content } if id == "c2" && content == "observed B"
5154 ));
5155
5156 let action = agent
5157 .observe(
5158 Observation::Model(ModelObservation::new(
5159 request.id,
5160 fake.chat(request.chat).await.unwrap(),
5161 )),
5162 &context,
5163 )
5164 .await
5165 .unwrap();
5166 let AgentAction::Respond { output } = action else {
5167 panic!("expected final response");
5168 };
5169 assert_eq!(output.answer, "done");
5170 }
5171
5172 #[tokio::test]
5173 async fn kernel_records_mixed_outputs_and_effects_in_tool_call_order() {
5174 let mut registry = ToolRegistry::new();
5175 registry
5176 .register(FakeTool::new("before", "plain before").0)
5177 .register(EffectTool::new("read", "effect-read", "read"))
5178 .register(FakeTool::new("after", "plain after").0);
5179 let fake = SharedFake::new([
5180 FakeReply::ToolCalls {
5181 content: "".into(),
5182 calls: vec![
5183 call("c1", "before", "{}"),
5184 call("c2", "read", "{}"),
5185 call("c3", "after", "{}"),
5186 ],
5187 },
5188 FakeReply::Text("done".into()),
5189 ]);
5190 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
5191 let context = RunContext::new("kernel-mixed-effects");
5192
5193 let AgentAction::RequestModel { request } = agent
5194 .start(RunRequest::text("read with context"), &context)
5195 .await
5196 .unwrap()
5197 else {
5198 panic!("expected initial model request");
5199 };
5200 let AgentAction::RequestEffect { request } = agent
5201 .observe(
5202 Observation::Model(ModelObservation::new(
5203 request.id,
5204 fake.chat(request.chat).await.unwrap(),
5205 )),
5206 &context,
5207 )
5208 .await
5209 .unwrap()
5210 else {
5211 panic!("expected single effect request");
5212 };
5213 assert_eq!(request.id, "effect-read");
5214
5215 let action = agent
5216 .observe(
5217 Observation::Effect(EffectObservation::succeeded("effect-read", "observed read")),
5218 &context,
5219 )
5220 .await
5221 .unwrap();
5222 let AgentAction::RequestModel { request } = action else {
5223 panic!("expected next model request");
5224 };
5225 assert!(matches!(
5226 &request.chat.messages[2],
5227 Message::ToolResult { id, content } if id == "c1" && content == "plain before"
5228 ));
5229 assert!(matches!(
5230 &request.chat.messages[3],
5231 Message::ToolResult { id, content } if id == "c2" && content == "observed read"
5232 ));
5233 assert!(matches!(
5234 &request.chat.messages[4],
5235 Message::ToolResult { id, content } if id == "c3" && content == "plain after"
5236 ));
5237 }
5238
5239 #[tokio::test]
5240 async fn kernel_rejects_partial_effect_batch_observation() {
5241 let mut registry = ToolRegistry::new();
5242 registry
5243 .register(EffectTool::new("read_a", "effect-a", "read A"))
5244 .register(EffectTool::new("read_b", "effect-b", "read B"));
5245 let fake = SharedFake::new([FakeReply::ToolCalls {
5246 content: "".into(),
5247 calls: vec![call("c1", "read_a", "{}"), call("c2", "read_b", "{}")],
5248 }]);
5249 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
5250 let context = RunContext::new("kernel-batch-effects-partial");
5251
5252 let AgentAction::RequestModel { request } = agent
5253 .start(RunRequest::text("read both"), &context)
5254 .await
5255 .unwrap()
5256 else {
5257 panic!("expected initial model request");
5258 };
5259 let AgentAction::RequestEffects { .. } = agent
5260 .observe(
5261 Observation::Model(ModelObservation::new(
5262 request.id,
5263 fake.chat(request.chat).await.unwrap(),
5264 )),
5265 &context,
5266 )
5267 .await
5268 .unwrap()
5269 else {
5270 panic!("expected batch effect request");
5271 };
5272 let err = agent
5273 .observe(
5274 Observation::Effects(vec![EffectObservation::succeeded("effect-a", "observed A")]),
5275 &context,
5276 )
5277 .await
5278 .unwrap_err();
5279 assert!(
5280 matches!(err, AgentError::InvalidStep(message) if message.contains("count mismatch"))
5281 );
5282 }
5283
5284 #[tokio::test]
5285 async fn kernel_rejects_duplicate_effect_batch_observation_and_can_retry() {
5286 let mut registry = ToolRegistry::new();
5287 registry
5288 .register(EffectTool::new("read_a", "effect-a", "read A"))
5289 .register(EffectTool::new("read_b", "effect-b", "read B"));
5290 let fake = SharedFake::new([
5291 FakeReply::ToolCalls {
5292 content: "".into(),
5293 calls: vec![call("c1", "read_a", "{}"), call("c2", "read_b", "{}")],
5294 },
5295 FakeReply::Text("done".into()),
5296 ]);
5297 let mut agent = agent_with_registry(fake.clone(), registry, AgentConfig::default());
5298 let context = RunContext::new("kernel-batch-effects-duplicate");
5299
5300 let AgentAction::RequestModel { request } = agent
5301 .start(RunRequest::text("read both"), &context)
5302 .await
5303 .unwrap()
5304 else {
5305 panic!("expected initial model request");
5306 };
5307 let AgentAction::RequestEffects { .. } = agent
5308 .observe(
5309 Observation::Model(ModelObservation::new(
5310 request.id,
5311 fake.chat(request.chat).await.unwrap(),
5312 )),
5313 &context,
5314 )
5315 .await
5316 .unwrap()
5317 else {
5318 panic!("expected batch effect request");
5319 };
5320 let err = agent
5321 .observe(
5322 Observation::Effects(vec![
5323 EffectObservation::succeeded("effect-a", "observed A"),
5324 EffectObservation::succeeded("effect-a", "observed A again"),
5325 ]),
5326 &context,
5327 )
5328 .await
5329 .unwrap_err();
5330 assert!(matches!(err, AgentError::InvalidStep(message) if message.contains("duplicate")));
5331
5332 let action = agent
5333 .observe(
5334 Observation::Effects(vec![
5335 EffectObservation::succeeded("effect-b", "observed B"),
5336 EffectObservation::succeeded("effect-a", "observed A"),
5337 ]),
5338 &context,
5339 )
5340 .await
5341 .unwrap();
5342 let AgentAction::RequestModel { request } = action else {
5343 panic!("expected next model request");
5344 };
5345 assert!(matches!(
5346 &request.chat.messages[2],
5347 Message::ToolResult { id, content } if id == "c1" && content == "observed A"
5348 ));
5349 assert!(matches!(
5350 &request.chat.messages[3],
5351 Message::ToolResult { id, content } if id == "c2" && content == "observed B"
5352 ));
5353 }
5354
5355 #[tokio::test]
5359 async fn stream_cancel_mid_generation() {
5360 struct SlowStreamProvider;
5363 #[async_trait::async_trait]
5364 impl Provider for SlowStreamProvider {
5365 async fn chat_with_context(
5366 &self,
5367 _request: ChatRequest,
5368 _context: &ProviderRequestContext,
5369 ) -> Result<ChatResponse, ProviderError> {
5370 unreachable!("this test uses streaming path only")
5371 }
5372 async fn stream_chat_with_context(
5373 &self,
5374 _request: ChatRequest,
5375 _context: &ProviderRequestContext,
5376 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
5377 {
5378 Ok(Box::pin(async_stream::stream! {
5379 yield Ok(StreamEvent::Delta("d0".into()));
5380 tokio::time::sleep(Duration::from_millis(100)).await;
5381 yield Ok(StreamEvent::Delta("d1".into()));
5382 tokio::time::sleep(Duration::from_millis(100)).await;
5383 yield Ok(StreamEvent::Done {
5384 reason: FinishReason::Stop,
5385 usage: None,
5386 });
5387 }))
5388 }
5389 }
5390
5391 let mut agent = ReActAgent::new(SlowStreamProvider, ToolRegistry::new(), "");
5392 let token = CancellationToken::new();
5393 let mut stream = agent
5394 .run_stream_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
5395 .await
5396 .unwrap();
5397
5398 let mut first = Vec::new();
5401 tokio::select! {
5402 _ = async {
5403 while let Some(ev) = stream.next().await {
5404 let ev = ev.unwrap();
5405 if ev == MessageChunk::Cancelled {
5406 break;
5407 }
5408 first.push(ev);
5409 }
5410 } => {}
5411 _ = async {
5412 tokio::time::sleep(Duration::from_millis(50)).await;
5413 token.cancel();
5414 } => {}
5415 }
5416 assert_eq!(first, vec![MessageChunk::Delta("d0".into())]);
5417
5418 let rest: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
5421 assert_eq!(rest, vec![MessageChunk::Cancelled]);
5422 drop(stream); assert_eq!(agent.memory.context().await.unwrap().len(), 1);
5425 }
5426
5427 #[tokio::test]
5431 async fn cancelled_then_fresh_token_run_works() {
5432 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
5433 let mut agent = agent(fake, "");
5434
5435 let t1 = CancellationToken::new();
5436 t1.cancel();
5437 assert!(matches!(
5438 agent
5439 .run_request_with_context(RunRequest::text("one"), cancellation_context(&t1))
5440 .await,
5441 Err(AgentError::Cancelled)
5442 ));
5443
5444 let t2 = CancellationToken::new();
5445 assert_eq!(
5446 agent
5447 .run_request_with_context(RunRequest::text("two"), cancellation_context(&t2))
5448 .await
5449 .unwrap()
5450 .answer,
5451 "hi"
5452 );
5453 }
5454
5455 use crate::event_channel::{BroadcastEventChannel, EventReceiver};
5459 use crate::tool::RegistryError;
5460
5461 fn attach_channel(agent: ReActAgent) -> (ReActAgent, Box<dyn EventReceiver>) {
5465 let channel = BroadcastEventChannel::new(64);
5466 let rx = channel.subscribe();
5467 (agent.with_event_channel(channel), rx)
5468 }
5469
5470 async fn drain(rx: &mut Box<dyn EventReceiver>) -> Vec<Arc<dyn AgentEvent>> {
5472 let mut out = Vec::new();
5473 while let Some(ev) = rx.recv().await {
5474 out.push(ev);
5475 }
5476 out
5477 }
5478
5479 fn names(events: &[Arc<dyn AgentEvent>]) -> Vec<&'static str> {
5480 events.iter().map(|e| e.name()).collect()
5481 }
5482
5483 fn react_event(ev: &dyn AgentEvent) -> &ReActEvent {
5486 ev.as_any()
5487 .downcast_ref::<ReActEvent>()
5488 .expect("test event should be ReActEvent")
5489 }
5490
5491 #[tokio::test]
5495 async fn events_published_on_run() {
5496 let (calc, _calls) = FakeTool::new("calc", "42");
5497 let mut registry = ToolRegistry::new();
5498 registry.register(calc);
5499 let fake = SharedFake::new([
5500 FakeReply::ToolCalls {
5501 content: "".into(),
5502 calls: vec![call("c1", "calc", r#"{"a":1}"#)],
5503 },
5504 FakeReply::Text("The answer is 42".into()),
5505 ]);
5506 let (mut agent, mut rx) =
5507 attach_channel(agent_with_registry(fake, registry, AgentConfig::default()));
5508 let answer = agent.run("Compute").await.unwrap();
5509 assert_eq!(answer, "The answer is 42");
5510 drop(agent);
5511 let events = drain(&mut rx).await;
5512
5513 assert_eq!(
5514 names(&events),
5515 ["run.started", "tool.started", "tool.completed", "run.ended"]
5516 );
5517 match react_event(&*events[1]) {
5520 ReActEvent::ToolStarted {
5523 id,
5524 name,
5525 arguments,
5526 } => {
5527 assert_eq!(id, "c1");
5528 assert_eq!(name, "calc");
5529 assert_eq!(arguments, r#"{"a":1}"#);
5530 }
5531 _ => panic!("expected ToolStarted"),
5532 }
5533 match react_event(&*events[2]) {
5534 ReActEvent::ToolCompleted { result, .. } => {
5536 assert_eq!(result, &Ok(ToolOutput::text("42").into()));
5537 }
5538 _ => panic!("expected ToolCompleted"),
5539 }
5540 match react_event(&*events[3]) {
5541 ReActEvent::RunEnded { summary, error } => {
5544 assert_eq!(error, &None);
5545 assert_eq!(summary.rounds, 2);
5546 assert_eq!(summary.tool_calls, 1);
5547 }
5548 _ => panic!("expected RunEnded"),
5549 }
5550 }
5551
5552 #[tokio::test]
5553 async fn run_started_event_preserves_block_input() {
5554 let blocks = vec![ContentBlock::Text("look".into())];
5555 let fake = SharedFake::new([FakeReply::Text("done".into())]);
5556 let (mut agent, mut rx) = attach_channel(agent(fake, ""));
5557 agent
5558 .run_request(RunRequest::blocks(blocks.clone()))
5559 .await
5560 .unwrap();
5561 drop(agent);
5562 let events = drain(&mut rx).await;
5563
5564 match react_event(&*events[0]) {
5565 ReActEvent::RunStarted { input, .. } => {
5566 assert_eq!(input, &crate::UserInput::Blocks(blocks));
5567 }
5568 _ => panic!("expected RunStarted"),
5569 }
5570 }
5571
5572 #[tokio::test]
5576 async fn events_carry_tool_failure() {
5577 let fake = SharedFake::new([
5578 FakeReply::ToolCalls {
5579 content: "".into(),
5580 calls: vec![call("c1", "nope", "{}")],
5581 },
5582 FakeReply::Text("Got it".into()),
5583 ]);
5584 let (mut agent, mut rx) = attach_channel(agent(fake, ""));
5585 agent.run("Trigger").await.unwrap();
5586 drop(agent);
5587
5588 let events = drain(&mut rx).await;
5589 let completed = events
5590 .iter()
5591 .find_map(|e| match react_event(&**e) {
5592 ReActEvent::ToolCompleted { result, .. } => Some(result),
5593 _ => None,
5594 })
5595 .expect("expected ToolCompleted event");
5596 assert!(matches!(completed, Err(RegistryError::NotFound(n)) if n == "nope"));
5597 assert_eq!(
5598 completed.as_ref().unwrap_err().to_string(),
5599 "tool not found: nope"
5600 );
5601 }
5602
5603 #[tokio::test]
5606 async fn stream_events_include_delta_and_reasoning() {
5607 let (calc, _calls) = FakeTool::new("calc", "42");
5608 let mut registry = ToolRegistry::new();
5609 registry.register(calc);
5610 let fake = SharedFake::new([
5611 FakeReply::ToolCalls {
5612 content: "Thinking: ".into(),
5613 calls: vec![call("c1", "calc", "{}")],
5614 },
5615 FakeReply::TextWithReasoning {
5616 content: "The answer is 42".into(),
5617 reasoning: "Reasoning steps".into(),
5618 },
5619 ]);
5620 let (mut agent, mut rx) =
5621 attach_channel(agent_with_registry(fake, registry, AgentConfig::default()));
5622
5623 let answer: String = agent
5625 .run_stream("Compute")
5626 .await
5627 .unwrap()
5628 .map(|e| e.unwrap())
5629 .filter_map(|e| async move {
5630 match e {
5631 MessageChunk::Delta(d) => Some(d),
5632 _ => None,
5633 }
5634 })
5635 .collect()
5636 .await;
5637 assert_eq!(answer, "Thinking: The answer is 42");
5638 drop(agent);
5639
5640 let events = drain(&mut rx).await;
5641 assert_eq!(
5645 names(&events),
5646 [
5647 "run.started",
5648 "delta",
5649 "tool.started",
5650 "tool.completed",
5651 "delta",
5652 "reasoning",
5653 "run.ended",
5654 ]
5655 );
5656 let reasoning = events
5657 .iter()
5658 .find_map(|e| match react_event(&**e) {
5659 ReActEvent::Reasoning { text } => Some(text.as_str()),
5660 _ => None,
5661 })
5662 .expect("expected Reasoning event");
5663 assert_eq!(reasoning, "Reasoning steps");
5664 match react_event(&**events.last().unwrap()) {
5665 ReActEvent::RunEnded { summary, error } => {
5666 assert_eq!(error, &None);
5667 assert_eq!(summary.rounds, 2);
5668 assert_eq!(summary.tool_calls, 1);
5669 }
5670 _ => panic!("expected RunEnded"),
5671 }
5672 }
5673
5674 #[tokio::test]
5677 async fn cancelled_run_publishes_run_ended_with_error() {
5678 struct SlowStreamProvider;
5680 #[async_trait::async_trait]
5681 impl Provider for SlowStreamProvider {
5682 async fn chat_with_context(
5683 &self,
5684 _request: ChatRequest,
5685 _context: &ProviderRequestContext,
5686 ) -> Result<ChatResponse, ProviderError> {
5687 unreachable!("this test uses streaming path only")
5688 }
5689 async fn stream_chat_with_context(
5690 &self,
5691 _request: ChatRequest,
5692 _context: &ProviderRequestContext,
5693 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
5694 {
5695 Ok(Box::pin(async_stream::stream! {
5696 yield Ok(StreamEvent::Delta("d0".into()));
5697 tokio::time::sleep(Duration::from_millis(100)).await;
5698 yield Ok(StreamEvent::Delta("d1".into()));
5699 tokio::time::sleep(Duration::from_millis(100)).await;
5700 yield Ok(StreamEvent::Done {
5701 reason: FinishReason::Stop,
5702 usage: None,
5703 });
5704 }))
5705 }
5706 }
5707
5708 let (mut agent, mut rx) =
5709 attach_channel(ReActAgent::new(SlowStreamProvider, ToolRegistry::new(), ""));
5710 let token = CancellationToken::new();
5711 let mut stream = agent
5712 .run_stream_request_with_context(RunRequest::text("hi"), cancellation_context(&token))
5713 .await
5714 .unwrap();
5715
5716 tokio::select! {
5721 _ = async {
5722 while let Some(ev) = stream.next().await {
5723 let ev = ev.unwrap();
5724 if ev == MessageChunk::Cancelled {
5725 break;
5726 }
5727 }
5728 } => {}
5729 _ = async {
5730 tokio::time::sleep(Duration::from_millis(50)).await;
5731 token.cancel();
5732 } => {}
5733 }
5734 let rest: Vec<MessageChunk> = stream.by_ref().map(|e| e.unwrap()).collect().await;
5735 assert!(rest.contains(&MessageChunk::Cancelled));
5736 drop(stream);
5737 drop(agent);
5738
5739 let events = drain(&mut rx).await;
5740 assert!(matches!(names(&events).as_slice(), [.., "run.ended"]));
5741 match react_event(&**events.last().unwrap()) {
5742 ReActEvent::RunEnded { error, .. } => {
5743 assert_eq!(error, &Some(AgentError::Cancelled));
5744 }
5745 _ => panic!("expected RunEnded"),
5746 }
5747 }
5748
5749 #[cfg(feature = "tracing")]
5750 mod tracing_tests {
5751 use super::*;
5752
5753 use std::collections::HashMap;
5757 use std::sync::atomic::AtomicU64;
5758 use tracing::field::{Field, Visit};
5759 use tracing::subscriber::Subscriber;
5760 use tracing::{Event, Id, Level, Metadata};
5761
5762 #[derive(Debug, Clone, PartialEq, Eq)]
5765 struct SpanInfo {
5766 name: &'static str,
5767 level: Level,
5768 fields: Vec<(String, String)>,
5769 }
5770
5771 #[derive(Debug, Clone, PartialEq, Eq)]
5774 enum Op {
5775 Enter(String),
5776 Exit(String),
5777 Close(String),
5778 Record(String, String),
5779 }
5780
5781 #[derive(Debug, Default)]
5788 struct CollectSubscriber {
5789 spans: std::sync::Mutex<Vec<SpanInfo>>,
5790 ops: std::sync::Mutex<Vec<Op>>,
5791 names: std::sync::Mutex<HashMap<Id, String>>,
5792 next_id: AtomicU64,
5793 }
5794
5795 struct FieldCollector<'a>(&'a mut Vec<(String, String)>);
5798
5799 impl Visit for FieldCollector<'_> {
5800 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
5801 self.0
5802 .push((field.name().to_string(), format!("{value:?}")));
5803 }
5804 }
5805
5806 impl Subscriber for CollectSubscriber {
5807 fn enabled(&self, _metadata: &Metadata<'_>) -> bool {
5808 true
5809 }
5810
5811 fn new_span(&self, span: &tracing::span::Attributes<'_>) -> Id {
5812 let id = Id::from_u64(self.next_id.fetch_add(1, Ordering::Relaxed) + 1);
5815 let mut fields = Vec::new();
5816 span.record(&mut FieldCollector(&mut fields));
5817 self.spans.lock().unwrap().push(SpanInfo {
5818 name: span.metadata().name(),
5819 level: *span.metadata().level(),
5820 fields,
5821 });
5822 self.names
5823 .lock()
5824 .unwrap()
5825 .insert(id.clone(), span.metadata().name().to_string());
5826 id
5827 }
5828
5829 fn record(&self, id: &Id, values: &tracing::span::Record<'_>) {
5830 let name = self.names.lock().unwrap().get(id).cloned();
5831 let Some(name) = name else { return };
5832 let mut fields = Vec::new();
5833 values.record(&mut FieldCollector(&mut fields));
5834 let mut ops = self.ops.lock().unwrap();
5835 for (field, value) in fields {
5836 ops.push(Op::Record(name.clone(), format!("{field}={value}")));
5837 }
5838 }
5839
5840 fn enter(&self, id: &Id) {
5841 let name = self.names.lock().unwrap().get(id).cloned();
5842 if let Some(name) = name {
5843 self.ops.lock().unwrap().push(Op::Enter(name));
5844 }
5845 }
5846
5847 fn exit(&self, id: &Id) {
5848 let name = self.names.lock().unwrap().get(id).cloned();
5849 if let Some(name) = name {
5850 self.ops.lock().unwrap().push(Op::Exit(name));
5851 }
5852 }
5853
5854 fn try_close(&self, id: Id) -> bool {
5855 let name = self.names.lock().unwrap().get(&id).cloned();
5856 if let Some(name) = name {
5857 self.ops.lock().unwrap().push(Op::Close(name));
5858 }
5859 true
5860 }
5861
5862 fn clone_span(&self, id: &Id) -> Id {
5863 id.clone()
5864 }
5865
5866 fn event(&self, _event: &Event<'_>) {}
5867 fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
5868 }
5869
5870 fn collect_guard(sub: &Arc<CollectSubscriber>) -> tracing::dispatcher::DefaultGuard {
5873 tracing::dispatcher::set_default(&tracing::Dispatch::new(sub.clone()))
5874 }
5875
5876 fn enter_names(ops: &[Op]) -> Vec<String> {
5877 ops.iter()
5878 .filter_map(|op| match op {
5879 Op::Enter(name) => Some(name.clone()),
5880 _ => None,
5881 })
5882 .collect()
5883 }
5884
5885 fn records_of(ops: &[Op], span: &str) -> Vec<String> {
5886 ops.iter()
5887 .filter_map(|op| match op {
5888 Op::Record(s, kv) if s == span => Some(kv.clone()),
5889 _ => None,
5890 })
5891 .collect()
5892 }
5893
5894 fn assert_nesting_invariants(ops: &[Op]) {
5900 let mut stack: Vec<String> = Vec::new();
5901 for op in ops {
5902 match op {
5903 Op::Enter(name) => {
5904 if name != "agent.run" {
5905 assert!(
5906 stack.contains(&"agent.run".to_string()),
5907 "span {name} requires agent.run on the stack when entering (stack: {stack:?})"
5908 );
5909 }
5910 assert!(
5911 !stack.contains(name),
5912 "same span entered twice (double instrumenting): {name} (stack: {stack:?})"
5913 );
5914 stack.push(name.clone());
5915 }
5916 Op::Exit(name) => {
5917 assert_eq!(
5918 stack.pop().as_deref(),
5919 Some(name.as_str()),
5920 "exit must pair with enter: {name}"
5921 );
5922 }
5923 _ => {}
5924 }
5925 }
5926 }
5927
5928 #[tokio::test(flavor = "current_thread")]
5933 async fn trace_span_tree_non_stream() {
5934 let sub = Arc::new(CollectSubscriber::default());
5935 let _guard = collect_guard(&sub);
5936
5937 let (calc, _calls) = FakeTool::new("calc", "42");
5938 let mut registry = ToolRegistry::new();
5939 registry.register(calc);
5940 let fake = SharedFake::new([
5941 FakeReply::WithUsage {
5942 reply: Box::new(FakeReply::ToolCalls {
5943 content: "".into(),
5944 calls: vec![call("c1", "calc", "{}")],
5945 }),
5946 usage: Usage::new(10, 2),
5947 },
5948 FakeReply::text_with_usage("42", Usage::new(20, 5)),
5949 ]);
5950 let mut agent = agent_with_registry(fake, registry, AgentConfig::default());
5951 assert_eq!(agent.run("Compute").await.unwrap(), "42");
5952
5953 let ops = sub.ops.lock().unwrap().clone();
5954 assert_nesting_invariants(&ops);
5957 let enters = enter_names(&ops);
5964 let mut first_seen = Vec::new();
5965 for name in enters.iter() {
5966 if !first_seen.contains(name) {
5967 first_seen.push(name.clone());
5968 }
5969 }
5970 assert_eq!(first_seen, ["agent.run", "llm_request", "tool"]);
5971 assert!(enters.iter().filter(|n| *n == "llm_request").count() >= 2);
5972 assert!(enters.iter().filter(|n| *n == "tool").count() >= 1);
5973
5974 assert_eq!(
5977 records_of(&ops, "llm_request"),
5978 [
5979 "usage.prompt_tokens=10",
5980 "usage.completion_tokens=2",
5981 "usage.prompt_tokens=20",
5982 "usage.completion_tokens=5",
5983 ]
5984 );
5985
5986 let spans = sub.spans.lock().unwrap().clone();
5989 let run_span = spans.iter().find(|s| s.name == "agent.run").unwrap();
5990 assert_eq!(run_span.level, Level::INFO);
5991 assert_eq!(
5992 spans
5993 .iter()
5994 .find(|s| s.name == "llm_request")
5995 .unwrap()
5996 .level,
5997 Level::DEBUG
5998 );
5999 assert_eq!(
6000 spans.iter().find(|s| s.name == "tool").unwrap().level,
6001 Level::DEBUG
6002 );
6003
6004 let llm_rounds: Vec<u64> = spans
6007 .iter()
6008 .filter(|s| s.name == "llm_request")
6009 .map(|s| {
6010 s.fields
6011 .iter()
6012 .find(|(f, _)| f == "round")
6013 .map(|(_, v)| v.parse().unwrap())
6014 .unwrap()
6015 })
6016 .collect();
6017 assert_eq!(llm_rounds, vec![1, 2]);
6018
6019 let run_ids: Vec<String> = spans
6022 .iter()
6023 .map(|s| {
6024 s.fields
6025 .iter()
6026 .find(|(f, _)| f == "run.id")
6027 .map(|(_, v)| v.clone())
6028 .unwrap_or_else(|| panic!("span {} must carry a run.id field", s.name))
6029 })
6030 .collect();
6031 assert!(run_ids.iter().all(|id| id == &run_ids[0]));
6032 assert!(run_ids[0].starts_with("run-"));
6033 }
6034
6035 #[tokio::test(flavor = "current_thread")]
6039 async fn trace_span_tree_stream() {
6040 let sub = Arc::new(CollectSubscriber::default());
6041 let _guard = collect_guard(&sub);
6042
6043 let (calc, _calls) = FakeTool::new("calc", "42");
6044 let mut registry = ToolRegistry::new();
6045 registry.register(calc);
6046 let fake = SharedFake::new([
6047 FakeReply::WithUsage {
6048 reply: Box::new(FakeReply::ToolCalls {
6049 content: "".into(),
6050 calls: vec![call("c1", "calc", "{}")],
6051 }),
6052 usage: Usage::new(10, 2),
6053 },
6054 FakeReply::text_with_usage("42", Usage::new(20, 5)),
6055 ]);
6056 let mut agent = agent_with_registry(fake, registry, AgentConfig::default());
6057 agent
6058 .run_stream("Compute")
6059 .await
6060 .unwrap()
6061 .for_each(|_| async {})
6062 .await;
6063
6064 let ops = sub.ops.lock().unwrap().clone();
6065 assert_nesting_invariants(&ops);
6069 let enters = enter_names(&ops);
6074 let mut first_seen = Vec::new();
6075 for name in enters.iter() {
6076 if !first_seen.contains(name) {
6077 first_seen.push(name.clone());
6078 }
6079 }
6080 assert_eq!(first_seen, ["agent.run", "llm_request", "tool"]);
6081 assert!(enters.iter().filter(|n| *n == "agent.run").count() > 2);
6084
6085 assert_eq!(
6087 records_of(&ops, "llm_request"),
6088 [
6089 "usage.prompt_tokens=10",
6090 "usage.completion_tokens=2",
6091 "usage.prompt_tokens=20",
6092 "usage.completion_tokens=5",
6093 ]
6094 );
6095 }
6096
6097 #[tokio::test(flavor = "current_thread")]
6101 async fn trace_run_id_differs_between_runs() {
6102 let sub = Arc::new(CollectSubscriber::default());
6103 let _guard = collect_guard(&sub);
6104
6105 let fake =
6106 SharedFake::new([FakeReply::Text("hi".into()), FakeReply::Text("bye".into())]);
6107 let (mut agent, mut rx) = attach_channel(agent(fake, ""));
6108 agent.run("one").await.unwrap();
6109 agent.run("two").await.unwrap();
6110 drop(agent);
6111
6112 let spans = sub.spans.lock().unwrap().clone();
6113 let run_ids: Vec<String> = spans
6114 .iter()
6115 .filter(|s| s.name == "agent.run")
6116 .map(|s| {
6117 s.fields
6118 .iter()
6119 .find(|(f, _)| f == "run.id")
6120 .map(|(_, v)| v.clone())
6121 .unwrap()
6122 })
6123 .collect();
6124 assert_eq!(run_ids.len(), 2);
6125 assert_ne!(run_ids[0], run_ids[1]);
6126
6127 let events = drain(&mut rx).await;
6130 let event_ids: Vec<String> = events
6131 .iter()
6132 .filter_map(|e| match react_event(&**e) {
6133 ReActEvent::RunStarted { run_id, .. } => Some(run_id.clone()),
6134 _ => None,
6135 })
6136 .collect();
6137 assert_eq!(event_ids, run_ids);
6138 }
6139
6140 #[tokio::test(flavor = "current_thread")]
6143 async fn trace_run_error_recorded() {
6144 let sub = Arc::new(CollectSubscriber::default());
6145 let _guard = collect_guard(&sub);
6146
6147 let (calc, _calls) = FakeTool::new("calc", "42");
6148 let mut registry = ToolRegistry::new();
6149 registry.register(calc);
6150 let fake = SharedFake::new([
6151 FakeReply::ToolCalls {
6152 content: "".into(),
6153 calls: vec![call("c1", "calc", "{}")],
6154 },
6155 FakeReply::ToolCalls {
6156 content: "".into(),
6157 calls: vec![call("c2", "calc", "{}")],
6158 },
6159 ]);
6160 let mut agent = agent_with_registry(
6161 fake,
6162 registry,
6163 AgentConfig {
6164 max_tool_rounds: 2,
6165 ..Default::default()
6166 },
6167 );
6168 assert!(matches!(
6169 agent.run("Keep computing").await,
6170 Err(AgentError::TooManyToolRounds(2))
6171 ));
6172
6173 let ops = sub.ops.lock().unwrap().clone();
6174 let records = records_of(&ops, "agent.run");
6178 assert_eq!(records.len(), 1);
6179 assert!(
6180 records[0].starts_with("error=\"model requested tools for more than 2 rounds"),
6181 "unexpected records: {records:?}"
6182 );
6183 }
6184
6185 #[tokio::test(flavor = "current_thread")]
6188 async fn trace_tool_error_recorded() {
6189 struct FailingTool;
6190 #[async_trait::async_trait]
6191 impl Tool for FailingTool {
6192 fn schema(&self) -> ToolSchema {
6193 ToolSchema::new("boom", "Tool that always fails", serde_json::json!({}))
6194 }
6195 async fn call(
6196 &self,
6197 _arguments: serde_json::Value,
6198 _context: ToolContext<'_>,
6199 ) -> Result<ToolResult, ToolError> {
6200 Err(ToolError::Execution("internal error".into()))
6201 }
6202 }
6203
6204 let sub = Arc::new(CollectSubscriber::default());
6205 let _guard = collect_guard(&sub);
6206
6207 let mut registry = ToolRegistry::new();
6208 registry.register(FailingTool);
6209 let fake = SharedFake::new([
6210 FakeReply::ToolCalls {
6211 content: "".into(),
6212 calls: vec![call("c1", "boom", "{}")],
6213 },
6214 FakeReply::Text("Got it".into()),
6215 ]);
6216 let mut agent = agent_with_registry(fake, registry, AgentConfig::default());
6217 agent.run("Trigger failure").await.unwrap();
6218
6219 let ops = sub.ops.lock().unwrap().clone();
6220 let tool_errors = records_of(&ops, "tool");
6223 assert_eq!(tool_errors.len(), 1);
6224 assert!(tool_errors[0].contains("internal error"));
6225 assert!(records_of(&ops, "agent.run").is_empty());
6226 }
6227
6228 #[tokio::test(flavor = "current_thread")]
6232 async fn trace_llm_error_recorded() {
6233 let sub = Arc::new(CollectSubscriber::default());
6234 let _guard = collect_guard(&sub);
6235
6236 let fake = SharedFake::new([FakeReply::Text("hi".into())]);
6238 let mut agent = agent(fake, "");
6239 agent.run("one").await.unwrap();
6240 let err = agent.run("two").await.unwrap_err();
6241 assert!(matches!(err, AgentError::Provider(_)));
6242
6243 let ops = sub.ops.lock().unwrap().clone();
6244 let llm_records = records_of(&ops, "llm_request");
6248 assert!(
6249 llm_records.iter().any(|r| r.starts_with("error=")),
6250 "the failed round's llm span should have an error: {llm_records:?}"
6251 );
6252 assert!(
6254 records_of(&ops, "agent.run")
6255 .iter()
6256 .any(|r| r.starts_with("error="))
6257 );
6258 }
6259
6260 #[tokio::test(flavor = "current_thread")]
6264 async fn trace_stream_llm_error_recorded() {
6265 struct FailInStream;
6266 #[async_trait::async_trait]
6267 impl Provider for FailInStream {
6268 async fn chat_with_context(
6269 &self,
6270 _request: ChatRequest,
6271 _context: &ProviderRequestContext,
6272 ) -> Result<ChatResponse, ProviderError> {
6273 unreachable!("this test uses streaming path only")
6274 }
6275 async fn stream_chat_with_context(
6276 &self,
6277 _request: ChatRequest,
6278 _context: &ProviderRequestContext,
6279 ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError>
6280 {
6281 Ok(Box::pin(futures::stream::iter(vec![
6282 Ok(StreamEvent::Delta("hi".into())),
6283 Err(ProviderError::Protocol {
6284 message: "boom".into(),
6285 }),
6286 ])))
6287 }
6288 }
6289
6290 let sub = Arc::new(CollectSubscriber::default());
6291 let _guard = collect_guard(&sub);
6292
6293 let mut agent = ReActAgent::new(FailInStream, ToolRegistry::new(), "");
6294 let mut stream = agent.run_stream("two").await.unwrap();
6295 while stream.next().await.is_some() {}
6296
6297 let ops = sub.ops.lock().unwrap().clone();
6298 assert_nesting_invariants(&ops);
6299 assert!(
6300 records_of(&ops, "llm_request")
6301 .iter()
6302 .any(|r| r.contains("boom"))
6303 );
6304 assert!(
6305 records_of(&ops, "agent.run")
6306 .iter()
6307 .any(|r| r.contains("boom"))
6308 );
6309 }
6310 }
6311}