1pub mod output_mode;
64pub mod streamed;
65
66pub use output_mode::OutputMode;
67
68use std::collections::{BTreeMap, BTreeSet};
69
70use serde::{Deserialize, Serialize};
71
72use rig_core::{
73 OneOrMany,
74 message::{AssistantContent, ToolCall, ToolChoice, ToolResult, ToolResultContent, UserContent},
75};
76
77use crate::{
78 agent::hook::{InvalidToolCallAction, InvalidToolCallContext, RetryRequest},
79 agent::prompt_request::{
80 CompletionCall, PromptResponse, TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER,
81 assistant_text_from_choice, build_full_history, build_history_for_request,
82 invalid_tool_retry_user_message, is_empty_assistant_turn, tool_result_message,
83 },
84 completion::{Message, PromptError, Usage},
85 json_utils,
86};
87
88pub use streamed::{
89 PartialStreamedTurn, StreamedInvalidToolCall, StreamedResolution, StreamedTurn,
90 StreamedTurnAssembler, StreamedTurnEvent,
91};
92
93fn unknown_tool_call_error(
99 tool_name: String,
100 available_tools: Vec<String>,
101 allowed_tools: Vec<String>,
102 chat_history: Vec<Message>,
103) -> PromptError {
104 PromptError::UnknownToolCall {
105 tool_name,
106 available_tools,
107 allowed_tools,
108 chat_history: Box::new(chat_history),
109 }
110}
111
112pub(crate) const DEFAULT_OUTPUT_RETRIES: usize = 1;
116
117#[derive(Debug, Clone)]
122pub enum AgentRunStep {
123 CallModel {
126 prompt: Message,
128 history: Vec<Message>,
131 turn: usize,
133 },
134 CallTools {
137 calls: Vec<PendingToolCall>,
139 },
140 Done(PromptResponse),
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146#[non_exhaustive]
147pub struct PendingToolCall {
148 pub tool_call: ToolCall,
150 pub preresolved_result: Option<UserContent>,
154 #[serde(default)]
159 pub internal_call_id: Option<String>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164#[non_exhaustive]
165pub struct ModelTurn {
166 pub message_id: Option<String>,
168 pub choice: OneOrMany<AssistantContent>,
170 pub usage: Usage,
172 pub executable_tool_names: BTreeSet<String>,
174 pub allowed_tool_names: BTreeSet<String>,
176}
177
178impl ModelTurn {
179 pub fn new(
182 message_id: Option<String>,
183 choice: OneOrMany<AssistantContent>,
184 usage: Usage,
185 executable_tool_names: BTreeSet<String>,
186 allowed_tool_names: BTreeSet<String>,
187 ) -> Self {
188 Self {
189 message_id,
190 choice,
191 usage,
192 executable_tool_names,
193 allowed_tool_names,
194 }
195 }
196}
197
198#[derive(Debug)]
204pub enum ModelTurnOutcome {
205 Continue {
213 response_hook_suppressed: bool,
215 },
216 NeedsResolution(InvalidToolCallContext),
221 TurnRetried,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228struct ResolvingState {
229 message_id: Option<String>,
230 original_choice: OneOrMany<AssistantContent>,
233 items: Vec<AssistantContent>,
235 next_index: usize,
237 executable_tool_names: BTreeSet<String>,
238 allowed_tool_names: BTreeSet<String>,
239 skipped: BTreeMap<String, UserContent>,
241 recovered: bool,
242 any_skipped: bool,
243 has_tool_calls: bool,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
247struct TurnState {
248 message_id: Option<String>,
249 items: Vec<AssistantContent>,
250 has_tool_calls: bool,
251 skipped: BTreeMap<String, UserContent>,
252 #[serde(default)]
255 internal_call_ids: Vec<(String, String)>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
259enum RunState {
260 PreparingRequest,
262 AwaitingModel,
264 ResolvingToolCalls(Box<ResolvingState>),
267 AwaitingAdvance(Box<TurnState>),
270 ExecutingTools(Vec<PendingToolCall>),
274 Done(Box<PromptResponse>),
276 Failed,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct AgentRun {
284 max_turns: usize,
285 max_invalid_tool_call_retries: usize,
286 tool_choice: Option<ToolChoice>,
287 #[serde(default)]
291 output_tool_name: Option<String>,
292 #[serde(default)]
295 output_schema: Option<serde_json::Value>,
296 #[serde(default)]
300 max_output_retries: usize,
301 #[serde(default)]
302 output_retries: usize,
303 chat_history: Option<Vec<Message>>,
304 new_messages: Vec<Message>,
305 current_turn: usize,
306 usage: Usage,
307 completion_calls: Vec<CompletionCall>,
308 completion_call_index: usize,
309 invalid_tool_call_retries: usize,
310 #[serde(default)]
313 rollback_pending: bool,
314 #[serde(default)]
318 streamed_completion_call_recorded: bool,
319 state: RunState,
320}
321
322impl AgentRun {
323 pub fn new(prompt: impl Into<Message>) -> Self {
326 Self {
327 max_turns: 1,
328 max_invalid_tool_call_retries: 0,
329 tool_choice: None,
330 output_tool_name: None,
331 output_schema: None,
332 max_output_retries: 0,
333 output_retries: 0,
334 chat_history: None,
335 new_messages: vec![prompt.into()],
336 current_turn: 0,
337 usage: Usage::new(),
338 completion_calls: Vec::new(),
339 completion_call_index: 0,
340 invalid_tool_call_retries: 0,
341 rollback_pending: false,
342 streamed_completion_call_recorded: false,
343 state: RunState::PreparingRequest,
344 }
345 }
346
347 pub fn with_history(mut self, history: Vec<Message>) -> Self {
349 self.chat_history = Some(history);
350 self
351 }
352
353 pub fn max_turns(mut self, max_turns: usize) -> Self {
358 self.max_turns = max_turns;
359 self
360 }
361
362 pub fn with_output_validation(
367 mut self,
368 output_schema: Option<serde_json::Value>,
369 max_output_retries: usize,
370 ) -> Self {
371 self.output_schema = output_schema;
372 self.max_output_retries = max_output_retries;
373 self
374 }
375
376 fn missing_required_output_fields(&self, args: &serde_json::Value) -> Vec<String> {
382 let Some(required) = self
383 .output_schema
384 .as_ref()
385 .and_then(|schema| schema.get("required"))
386 .and_then(|required| required.as_array())
387 else {
388 return Vec::new();
389 };
390 let object = args.as_object();
391 required
392 .iter()
393 .filter_map(|field| field.as_str())
394 .filter(|field| object.is_none_or(|object| !object.contains_key(*field)))
395 .map(str::to_owned)
396 .collect()
397 }
398
399 fn text_satisfies_output_schema(&self, text: &str) -> bool {
403 serde_json::from_str::<serde_json::Value>(text.trim())
404 .ok()
405 .is_some_and(|value| self.missing_required_output_fields(&value).is_empty())
406 }
407
408 fn can_reprompt_for_output(&self) -> bool {
412 self.output_retries < self.max_output_retries && self.current_turn < self.max_turns
413 }
414
415 fn reprompt_for_output(&mut self) -> Result<AgentRunStep, PromptError> {
420 self.output_retries += 1;
421 self.state = RunState::PreparingRequest;
422 self.next_step()
423 }
424
425 pub fn max_invalid_tool_call_retries(mut self, retries: usize) -> Self {
429 self.max_invalid_tool_call_retries = retries;
430 self
431 }
432
433 pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
437 self.tool_choice = Some(tool_choice);
438 self
439 }
440
441 pub fn with_output_tool_name(mut self, name: impl Into<String>) -> Self {
445 self.output_tool_name = Some(name.into());
446 self
447 }
448
449 pub(crate) fn set_output_tool_name(&mut self, name: Option<String>) {
453 if self.output_tool_name.is_none() {
457 self.output_tool_name = name;
458 }
459 }
460
461 pub(crate) fn output_tool_name(&self) -> Option<&str> {
465 self.output_tool_name.as_deref()
466 }
467
468 pub fn usage(&self) -> Usage {
470 self.usage
471 }
472
473 pub fn turn(&self) -> usize {
475 self.current_turn
476 }
477
478 pub fn completion_calls(&self) -> &[CompletionCall] {
480 &self.completion_calls
481 }
482
483 pub fn messages(&self) -> &[Message] {
486 &self.new_messages
487 }
488
489 pub(crate) fn accepted_turn_choice(&self) -> Option<OneOrMany<AssistantContent>> {
491 let RunState::AwaitingAdvance(turn) = &self.state else {
492 return None;
493 };
494
495 OneOrMany::from_iter_optional(turn.items.clone())
496 }
497
498 pub fn retry_model_turn(&mut self, request: RetryRequest) -> Result<(), PromptError> {
512 let turn = match std::mem::replace(&mut self.state, RunState::Failed) {
513 RunState::AwaitingAdvance(turn) => turn,
514 other => {
515 self.state = other;
516 return Err(self.protocol_violation(
517 "retry_model_turn called without an accepted turn awaiting advancement",
518 ));
519 }
520 };
521
522 if turn.has_tool_calls {
523 return Err(PromptError::prompt_cancelled(
524 self.full_history(),
525 "model-turn retry does not support tool-bearing model turns; use tool-call hooks instead",
526 ));
527 }
528
529 match request {
530 RetryRequest::Repeat => {}
531 RetryRequest::Feedback(feedback) => {
532 let Some(content) = OneOrMany::from_iter_optional(turn.items) else {
533 return Err(PromptError::prompt_cancelled(
534 self.full_history(),
535 "model-turn retry lost the rejected assistant content",
536 ));
537 };
538 if !is_empty_assistant_turn(&content) {
539 self.new_messages.push(Message::Assistant {
540 id: turn.message_id,
541 content,
542 });
543 }
544 self.new_messages.push(Message::user(feedback));
545 }
546 }
547
548 self.state = RunState::PreparingRequest;
549 Ok(())
550 }
551
552 pub fn full_history(&self) -> Vec<Message> {
554 build_full_history(self.chat_history.as_deref(), self.new_messages.clone())
555 }
556
557 pub fn is_done(&self) -> bool {
559 matches!(self.state, RunState::Done(_))
560 }
561
562 pub fn response(&self) -> Option<&PromptResponse> {
567 match &self.state {
568 RunState::Done(response) => Some(response),
569 _ => None,
570 }
571 }
572
573 pub fn cancel_error(&self, reason: impl Into<String>) -> PromptError {
576 PromptError::prompt_cancelled(self.full_history(), reason)
577 }
578
579 pub fn pending_invalid_tool_call(&self) -> Option<InvalidToolCallContext> {
583 let RunState::ResolvingToolCalls(resolving) = &self.state else {
584 return None;
585 };
586 let AssistantContent::ToolCall(tool_call) = resolving.items.get(resolving.next_index)?
587 else {
588 return None;
589 };
590 if resolving
591 .allowed_tool_names
592 .contains(&tool_call.function.name)
593 {
594 return None;
595 }
596
597 Some(InvalidToolCallContext {
598 tool_name: tool_call.function.name.clone(),
599 tool_call_id: Some(tool_call.id.clone()),
600 internal_call_id: None,
601 args: Some(json_utils::serialize_json_value(
602 &tool_call.function.arguments,
603 )),
604 available_tools: resolving.executable_tool_names.iter().cloned().collect(),
605 allowed_tools: resolving.allowed_tool_names.iter().cloned().collect(),
606 tool_choice: self.tool_choice.clone(),
607 chat_history: self.diagnostic_history(resolving),
608 is_streaming: false,
609 })
610 }
611
612 pub fn next_step(&mut self) -> Result<AgentRunStep, PromptError> {
620 match std::mem::replace(&mut self.state, RunState::Failed) {
621 RunState::PreparingRequest => {
622 let Some((prompt_ref, history_for_turn)) = self.new_messages.split_last() else {
623 return Err(PromptError::prompt_cancelled(
624 self.full_history(),
625 "prompt loop lost its pending prompt",
626 ));
627 };
628 let prompt = prompt_ref.clone();
629
630 if self.current_turn >= self.max_turns {
631 return Err(PromptError::MaxTurnsError {
632 max_turns: self.max_turns,
633 chat_history: self.full_history().into(),
634 prompt: prompt.into(),
635 });
636 }
637
638 let history =
639 build_history_for_request(self.chat_history.as_deref(), history_for_turn);
640 self.current_turn += 1;
641 self.rollback_pending = false;
642 self.streamed_completion_call_recorded = false;
643 self.state = RunState::AwaitingModel;
644 Ok(AgentRunStep::CallModel {
645 prompt,
646 history,
647 turn: self.current_turn,
648 })
649 }
650 RunState::AwaitingAdvance(turn_state) => {
651 let TurnState {
652 message_id,
653 items,
654 has_tool_calls,
655 skipped,
656 mut internal_call_ids,
657 } = *turn_state;
658 let Some(choice) = OneOrMany::from_iter_optional(items.clone()) else {
659 return Err(PromptError::prompt_cancelled(
660 self.full_history(),
661 "model turn lost its assistant content",
662 ));
663 };
664
665 if has_tool_calls
670 && let Some(output_tool_name) = self.output_tool_name.clone()
671 && let Some(tool_call) = items.iter().find_map(|item| match item {
672 AssistantContent::ToolCall(tc) if tc.function.name == output_tool_name => {
673 Some(tc)
674 }
675 _ => None,
676 })
677 {
678 let output_tool_calls = items
679 .iter()
680 .filter(|item| {
681 matches!(
682 item,
683 AssistantContent::ToolCall(tc)
684 if tc.function.name == output_tool_name
685 )
686 })
687 .count();
688 let args = tool_call.function.arguments.clone();
689 let tool_call_id = tool_call.id.clone();
690 let output = json_utils::serialize_json_value(&args);
691
692 let missing = self.missing_required_output_fields(&args);
696 if !missing.is_empty() && self.can_reprompt_for_output() {
697 self.new_messages.push(Message::Assistant {
698 id: message_id,
699 content: choice.clone(),
700 });
701 let feedback = format!(
702 "The `{output_tool_name}` arguments were missing required field(s): \
703 {}. Call `{output_tool_name}` again with every required field.",
704 missing.join(", ")
705 );
706 if let Some(user_message) =
707 invalid_tool_retry_user_message(&choice, &tool_call_id, feedback)
708 {
709 self.new_messages.push(user_message);
710 }
711 return self.reprompt_for_output();
712 }
713
714 let mut final_items: Vec<AssistantContent> = items
720 .iter()
721 .filter(|item| !matches!(item, AssistantContent::ToolCall(_)))
722 .cloned()
723 .collect();
724 final_items.push(AssistantContent::text(output.clone()));
725 let final_content = OneOrMany::from_iter_optional(final_items);
726 if let Some(content) = final_content.clone() {
727 self.new_messages.push(Message::Assistant {
728 id: message_id,
729 content,
730 });
731 }
732
733 let mut response = PromptResponse::new(output, self.usage)
734 .with_messages(self.new_messages.clone())
735 .with_completion_calls(self.completion_calls.clone())
736 .with_output_tool_calls(output_tool_calls);
737 if let Some(content) = final_content {
738 response = response.with_content(content);
739 }
740 self.state = RunState::Done(Box::new(response.clone()));
741 return Ok(AgentRunStep::Done(response));
742 }
743
744 if !is_empty_assistant_turn(&choice) {
745 self.new_messages.push(Message::Assistant {
746 id: message_id,
747 content: choice.clone(),
748 });
749 }
750
751 if has_tool_calls {
752 self.output_retries = 0;
757 let calls: Vec<PendingToolCall> = items
758 .iter()
759 .filter_map(|item| match item {
760 AssistantContent::ToolCall(tool_call) => {
761 let internal_call_id = internal_call_ids
765 .iter()
766 .position(|(id, _)| *id == tool_call.id)
767 .map(|index| internal_call_ids.remove(index).1);
768 Some(PendingToolCall {
769 tool_call: tool_call.clone(),
770 preresolved_result: skipped.get(&tool_call.id).cloned(),
771 internal_call_id,
772 })
773 }
774 _ => None,
775 })
776 .collect();
777 self.state = RunState::ExecutingTools(calls.clone());
778 Ok(AgentRunStep::CallTools { calls })
779 } else {
780 if let Some(output_tool_name) = self.output_tool_name.clone()
791 && !is_empty_assistant_turn(&choice)
792 && self.can_reprompt_for_output()
793 && !self.text_satisfies_output_schema(&assistant_text_from_choice(&choice))
794 {
795 let feedback = format!(
796 "Provide your final answer by calling the `{output_tool_name}` tool \
797 with the structured result as its arguments, not as plain text."
798 );
799 self.new_messages.push(Message::user(feedback));
800 return self.reprompt_for_output();
801 }
802
803 let response =
804 PromptResponse::new(assistant_text_from_choice(&choice), self.usage)
805 .with_messages(self.new_messages.clone())
806 .with_completion_calls(self.completion_calls.clone())
807 .with_content(choice.clone());
808 self.state = RunState::Done(Box::new(response.clone()));
809 Ok(AgentRunStep::Done(response))
810 }
811 }
812 RunState::ExecutingTools(calls) => {
813 let step = AgentRunStep::CallTools {
816 calls: calls.clone(),
817 };
818 self.state = RunState::ExecutingTools(calls);
819 Ok(step)
820 }
821 RunState::Done(response) => {
822 let step = AgentRunStep::Done((*response).clone());
823 self.state = RunState::Done(response);
824 Ok(step)
825 }
826 state @ (RunState::AwaitingModel | RunState::ResolvingToolCalls(_)) => {
827 let reason = match &state {
828 RunState::AwaitingModel => {
829 "next_step called while a model response is pending; feed it via model_response first"
830 }
831 _ => {
832 "next_step called while an invalid tool-call resolution is pending; answer it via resolve_invalid_tool_call first"
833 }
834 };
835 self.state = state;
836 Err(self.protocol_violation(reason))
837 }
838 RunState::Failed => Err(self.protocol_violation(
839 "next_step called after the run already failed or was misdriven",
840 )),
841 }
842 }
843
844 pub fn model_response(&mut self, turn: ModelTurn) -> Result<ModelTurnOutcome, PromptError> {
850 if !matches!(self.state, RunState::AwaitingModel) {
851 return Err(
852 self.protocol_violation("model_response called without a pending CallModel step")
853 );
854 }
855 if self.streamed_completion_call_recorded {
856 return Err(self.protocol_violation(
857 "model_response called after record_streamed_completion_call for the same turn; feed streamed turns via streamed_turn",
858 ));
859 }
860
861 self.record_completion_call(turn.usage);
862
863 let items: Vec<AssistantContent> = turn.choice.iter().cloned().collect();
864 let has_tool_calls = items
865 .iter()
866 .any(|item| matches!(item, AssistantContent::ToolCall(_)));
867
868 self.state = RunState::ResolvingToolCalls(Box::new(ResolvingState {
869 message_id: turn.message_id,
870 original_choice: turn.choice,
871 items,
872 next_index: 0,
873 executable_tool_names: turn.executable_tool_names,
874 allowed_tool_names: turn.allowed_tool_names,
875 skipped: BTreeMap::new(),
876 recovered: false,
877 any_skipped: false,
878 has_tool_calls,
879 }));
880
881 self.advance_resolution()
882 }
883
884 fn record_completion_call(&mut self, usage: Usage) -> CompletionCall {
891 let call = CompletionCall::new(self.completion_call_index, usage);
892 self.completion_call_index += 1;
893 self.completion_calls.push(call);
894 self.usage += usage;
895 call
896 }
897
898 fn finalize_turn(
903 &mut self,
904 message_id: Option<String>,
905 items: Vec<AssistantContent>,
906 has_tool_calls: bool,
907 skipped: BTreeMap<String, UserContent>,
908 internal_call_ids: Vec<(String, String)>,
909 ) {
910 self.state = RunState::AwaitingAdvance(Box::new(TurnState {
911 message_id,
912 items,
913 has_tool_calls,
914 skipped,
915 internal_call_ids,
916 }));
917 }
918
919 pub fn resolve_invalid_tool_call(
935 &mut self,
936 action: InvalidToolCallAction,
937 ) -> Result<ModelTurnOutcome, PromptError> {
938 let mut resolving = match std::mem::replace(&mut self.state, RunState::Failed) {
941 RunState::ResolvingToolCalls(resolving) => resolving,
942 other => {
943 self.state = other;
944 return Err(self.protocol_violation(
945 "resolve_invalid_tool_call called without a pending invalid tool call",
946 ));
947 }
948 };
949 let tool_call = match resolving.items.get(resolving.next_index) {
950 Some(AssistantContent::ToolCall(tool_call))
951 if !resolving
952 .allowed_tool_names
953 .contains(&tool_call.function.name) =>
954 {
955 tool_call.clone()
956 }
957 _ => {
958 self.state = RunState::ResolvingToolCalls(resolving);
959 return Err(self.protocol_violation(
960 "resolve_invalid_tool_call called without a pending invalid tool call",
961 ));
962 }
963 };
964
965 let diagnostic_history = self.diagnostic_history(&resolving);
966 let executable_tool_names: Vec<String> =
967 resolving.executable_tool_names.iter().cloned().collect();
968 let allowed_tool_names: Vec<String> =
969 resolving.allowed_tool_names.iter().cloned().collect();
970
971 match action {
972 InvalidToolCallAction::Fail => Err(unknown_tool_call_error(
973 tool_call.function.name,
974 executable_tool_names,
975 allowed_tool_names,
976 diagnostic_history,
977 )),
978 InvalidToolCallAction::Retry { feedback } => {
979 if self.invalid_tool_call_retries >= self.max_invalid_tool_call_retries {
980 return Err(unknown_tool_call_error(
981 tool_call.function.name,
982 executable_tool_names,
983 allowed_tool_names,
984 diagnostic_history,
985 ));
986 }
987 self.invalid_tool_call_retries += 1;
988
989 self.new_messages.push(Message::Assistant {
990 id: resolving.message_id.clone(),
991 content: resolving.original_choice.clone(),
992 });
993 let Some(user_message) = invalid_tool_retry_user_message(
994 &resolving.original_choice,
995 &tool_call.id,
996 feedback,
997 ) else {
998 return Err(PromptError::prompt_cancelled(
999 diagnostic_history,
1000 "invalid tool call retry produced no retry messages",
1001 ));
1002 };
1003 self.new_messages.push(user_message);
1004 self.state = RunState::PreparingRequest;
1005 Ok(ModelTurnOutcome::TurnRetried)
1006 }
1007 InvalidToolCallAction::Repair { tool_name } => {
1008 if !allowed_tool_names.contains(&tool_name) {
1009 return Err(unknown_tool_call_error(
1010 tool_name,
1011 executable_tool_names,
1012 allowed_tool_names,
1013 diagnostic_history,
1014 ));
1015 }
1016 if let Some(AssistantContent::ToolCall(tool_call)) =
1017 resolving.items.get_mut(resolving.next_index)
1018 {
1019 tool_call.function.name = tool_name;
1020 }
1021 resolving.recovered = true;
1022 self.state = RunState::ResolvingToolCalls(resolving);
1023 self.advance_resolution()
1024 }
1025 InvalidToolCallAction::Stop { reason } => {
1026 self.state = RunState::Failed;
1027 Err(PromptError::prompt_cancelled(diagnostic_history, reason))
1028 }
1029 InvalidToolCallAction::Skip { reason } => {
1030 if matches!(self.tool_choice, Some(ToolChoice::None)) {
1031 return Err(unknown_tool_call_error(
1032 tool_call.function.name,
1033 executable_tool_names,
1034 allowed_tool_names,
1035 diagnostic_history,
1036 ));
1037 }
1038 let user_content = if let Some(call_id) = tool_call.call_id.clone() {
1039 UserContent::tool_result_with_call_id(
1040 tool_call.id.clone(),
1041 call_id,
1042 OneOrMany::one(reason.into()),
1043 )
1044 } else {
1045 UserContent::tool_result(tool_call.id.clone(), OneOrMany::one(reason.into()))
1046 };
1047 resolving.skipped.insert(tool_call.id.clone(), user_content);
1048 resolving.recovered = true;
1049 resolving.any_skipped = true;
1050 resolving.next_index += 1;
1051 self.state = RunState::ResolvingToolCalls(resolving);
1052 self.advance_resolution()
1053 }
1054 }
1055 }
1056
1057 pub(crate) fn ignore_invalid_tool_call(&mut self) -> Result<ModelTurnOutcome, PromptError> {
1066 let mut resolving = match std::mem::replace(&mut self.state, RunState::Failed) {
1067 RunState::ResolvingToolCalls(resolving) => resolving,
1068 other => {
1069 self.state = other;
1070 return Err(self.protocol_violation(
1071 "ignore_invalid_tool_call called without a pending invalid tool call",
1072 ));
1073 }
1074 };
1075
1076 match resolving.items.get(resolving.next_index) {
1077 Some(AssistantContent::ToolCall(tool_call))
1078 if !resolving
1079 .allowed_tool_names
1080 .contains(&tool_call.function.name) => {}
1081 _ => {
1082 self.state = RunState::ResolvingToolCalls(resolving);
1083 return Err(self.protocol_violation(
1084 "ignore_invalid_tool_call called without a pending invalid tool call",
1085 ));
1086 }
1087 }
1088
1089 resolving.items.remove(resolving.next_index);
1090 resolving.has_tool_calls = resolving
1091 .items
1092 .iter()
1093 .any(|item| matches!(item, AssistantContent::ToolCall(_)));
1094 if resolving.items.is_empty() {
1095 resolving.items.push(AssistantContent::text(""));
1096 }
1097 self.state = RunState::ResolvingToolCalls(resolving);
1098 self.advance_resolution()
1099 }
1100
1101 pub fn tool_results(&mut self, results: Vec<UserContent>) -> Result<(), PromptError> {
1109 let RunState::ExecutingTools(pending) = &self.state else {
1110 return Err(
1111 self.protocol_violation("tool_results called without a pending CallTools step")
1112 );
1113 };
1114 let mut unanswered: Vec<String> = pending
1117 .iter()
1118 .map(|call| call.tool_call.id.clone())
1119 .collect();
1120
1121 if results.is_empty() {
1122 self.state = RunState::Failed;
1123 return Err(PromptError::prompt_cancelled(
1124 self.full_history(),
1125 "tool execution produced no tool results",
1126 ));
1127 }
1128 for result in &results {
1129 let UserContent::ToolResult(tool_result) = result else {
1130 return Err(self.protocol_violation(
1131 "tool_results received content that is not a tool result",
1132 ));
1133 };
1134 let Some(index) = unanswered.iter().position(|id| *id == tool_result.id) else {
1135 return Err(self.protocol_violation(&format!(
1136 "tool_results received a result for unknown or already-answered tool call id `{}`",
1137 tool_result.id
1138 )));
1139 };
1140 unanswered.swap_remove(index);
1141 }
1142 if !unanswered.is_empty() {
1143 return Err(self.protocol_violation(&format!(
1144 "tool_results left pending tool call id(s) unanswered: {unanswered:?}"
1145 )));
1146 }
1147
1148 let Some(content) = OneOrMany::from_iter_optional(results) else {
1150 return Err(
1151 self.protocol_violation("internal: tool results vanished during validation")
1152 );
1153 };
1154
1155 self.new_messages.push(Message::User { content });
1156 self.state = RunState::PreparingRequest;
1157 Ok(())
1158 }
1159
1160 fn advance_resolution(&mut self) -> Result<ModelTurnOutcome, PromptError> {
1163 let mut resolving = match std::mem::replace(&mut self.state, RunState::Failed) {
1164 RunState::ResolvingToolCalls(resolving) => resolving,
1165 other => {
1166 self.state = other;
1167 return Err(self.protocol_violation(
1168 "internal: advance_resolution outside of tool-call resolution",
1169 ));
1170 }
1171 };
1172 while let Some(item) = resolving.items.get(resolving.next_index) {
1173 match item {
1174 AssistantContent::ToolCall(tool_call)
1175 if !resolving
1176 .allowed_tool_names
1177 .contains(&tool_call.function.name) =>
1178 {
1179 break;
1180 }
1181 _ => resolving.next_index += 1,
1182 }
1183 }
1184
1185 if resolving.next_index < resolving.items.len() {
1186 self.state = RunState::ResolvingToolCalls(resolving);
1187 return match self.pending_invalid_tool_call() {
1188 Some(context) => Ok(ModelTurnOutcome::NeedsResolution(context)),
1189 None => Err(self.protocol_violation(
1190 "internal: pending invalid tool call could not be derived",
1191 )),
1192 };
1193 }
1194
1195 let ResolvingState {
1196 message_id,
1197 items,
1198 mut skipped,
1199 recovered,
1200 any_skipped,
1201 has_tool_calls,
1202 ..
1203 } = *resolving;
1204
1205 if any_skipped {
1208 for item in &items {
1209 if let AssistantContent::ToolCall(tool_call) = item {
1210 skipped.entry(tool_call.id.clone()).or_insert_with(|| {
1211 tool_result_message(
1212 tool_call.id.clone(),
1213 tool_call.call_id.clone(),
1214 TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER.to_string(),
1215 )
1216 });
1217 }
1218 }
1219 }
1220
1221 self.finalize_turn(message_id, items, has_tool_calls, skipped, Vec::new());
1222 Ok(ModelTurnOutcome::Continue {
1223 response_hook_suppressed: recovered,
1224 })
1225 }
1226
1227 pub fn record_streamed_completion_call(
1241 &mut self,
1242 usage: Usage,
1243 ) -> Result<CompletionCall, PromptError> {
1244 let recordable = matches!(self.state, RunState::AwaitingModel)
1245 || (matches!(self.state, RunState::PreparingRequest) && self.rollback_pending);
1246 if !recordable {
1247 return Err(self.protocol_violation(
1248 "record_streamed_completion_call called without a pending or rolled-back CallModel step",
1249 ));
1250 }
1251 if self.streamed_completion_call_recorded {
1252 return Err(self.protocol_violation(
1253 "record_streamed_completion_call called twice for the same model turn",
1254 ));
1255 }
1256 self.streamed_completion_call_recorded = true;
1257
1258 Ok(self.record_completion_call(usage))
1259 }
1260
1261 pub fn streamed_invalid_tool_call_context(
1264 &self,
1265 partial: &PartialStreamedTurn,
1266 invalid: &StreamedInvalidToolCall,
1267 ) -> InvalidToolCallContext {
1268 InvalidToolCallContext {
1269 tool_name: invalid.tool_call.function.name.clone(),
1270 tool_call_id: Some(invalid.tool_call.id.clone()),
1271 internal_call_id: Some(invalid.internal_call_id.clone()),
1272 args: invalid.args.clone(),
1273 available_tools: invalid.executable_tool_names.iter().cloned().collect(),
1274 allowed_tools: invalid.allowed_tool_names.iter().cloned().collect(),
1275 tool_choice: self.tool_choice.clone(),
1276 chat_history: self
1277 .streamed_diagnostic_history(partial, Some(invalid.tool_call.clone())),
1278 is_streaming: true,
1279 }
1280 }
1281
1282 pub fn resolve_streamed_invalid_tool_call(
1290 &mut self,
1291 partial: &PartialStreamedTurn,
1292 invalid: &StreamedInvalidToolCall,
1293 action: InvalidToolCallAction,
1294 ) -> Result<StreamedResolution, PromptError> {
1295 if !matches!(self.state, RunState::AwaitingModel) {
1296 return Err(self.protocol_violation(
1297 "resolve_streamed_invalid_tool_call called without a pending CallModel step",
1298 ));
1299 }
1300
1301 let diagnostic_history =
1302 self.streamed_diagnostic_history(partial, Some(invalid.tool_call.clone()));
1303 let executable_tool_names: Vec<String> =
1304 invalid.executable_tool_names.iter().cloned().collect();
1305 let allowed_tool_names: Vec<String> = invalid.allowed_tool_names.iter().cloned().collect();
1306
1307 match action {
1308 InvalidToolCallAction::Fail => {
1309 self.state = RunState::Failed;
1310 Err(unknown_tool_call_error(
1311 invalid.tool_call.function.name.clone(),
1312 executable_tool_names,
1313 allowed_tool_names,
1314 diagnostic_history,
1315 ))
1316 }
1317 InvalidToolCallAction::Retry { feedback } => {
1318 if self.invalid_tool_call_retries >= self.max_invalid_tool_call_retries {
1319 self.state = RunState::Failed;
1320 return Err(unknown_tool_call_error(
1321 invalid.tool_call.function.name.clone(),
1322 executable_tool_names,
1323 allowed_tool_names,
1324 diagnostic_history,
1325 ));
1326 }
1327 self.invalid_tool_call_retries += 1;
1328
1329 let Some((assistant_message, user_message)) =
1330 partial.rollback_messages(invalid.tool_call.clone(), feedback)
1331 else {
1332 self.state = RunState::Failed;
1333 return Err(PromptError::prompt_cancelled(
1334 diagnostic_history,
1335 "invalid tool call retry produced no retry messages",
1336 ));
1337 };
1338 self.new_messages.push(assistant_message);
1339 self.new_messages.push(user_message);
1340 self.rollback_pending = true;
1341 self.state = RunState::PreparingRequest;
1342 Ok(StreamedResolution::TurnAbandoned {
1343 skipped_tool_result: None,
1344 })
1345 }
1346 InvalidToolCallAction::Repair { tool_name } => {
1347 if !invalid.allowed_tool_names.contains(&tool_name) {
1348 self.state = RunState::Failed;
1349 return Err(unknown_tool_call_error(
1350 tool_name,
1351 executable_tool_names,
1352 allowed_tool_names,
1353 diagnostic_history,
1354 ));
1355 }
1356 Ok(StreamedResolution::Repaired { tool_name })
1357 }
1358 InvalidToolCallAction::Stop { reason } => {
1359 self.state = RunState::Failed;
1360 Err(PromptError::prompt_cancelled(diagnostic_history, reason))
1361 }
1362 InvalidToolCallAction::Skip { reason } => {
1363 if matches!(self.tool_choice, Some(ToolChoice::None)) {
1364 self.state = RunState::Failed;
1365 return Err(unknown_tool_call_error(
1366 invalid.tool_call.function.name.clone(),
1367 executable_tool_names,
1368 allowed_tool_names,
1369 diagnostic_history,
1370 ));
1371 }
1372
1373 let skipped_tool_result = ToolResult {
1377 id: invalid.tool_call.id.clone(),
1378 call_id: invalid.tool_call.call_id.clone(),
1379 content: OneOrMany::one(ToolResultContent::text(reason.clone())),
1380 };
1381 let Some((assistant_message, user_message)) =
1382 partial.rollback_messages(invalid.tool_call.clone(), reason)
1383 else {
1384 self.state = RunState::Failed;
1385 return Err(PromptError::prompt_cancelled(
1386 diagnostic_history,
1387 "invalid tool call skip produced no recovery messages",
1388 ));
1389 };
1390 self.new_messages.push(assistant_message);
1391 self.new_messages.push(user_message);
1392 self.rollback_pending = true;
1393 self.state = RunState::PreparingRequest;
1394 Ok(StreamedResolution::TurnAbandoned {
1395 skipped_tool_result: Some(skipped_tool_result),
1396 })
1397 }
1398 }
1399 }
1400
1401 pub fn streamed_turn(&mut self, turn: StreamedTurn) -> Result<(), PromptError> {
1408 if !matches!(self.state, RunState::AwaitingModel) {
1409 return Err(
1410 self.protocol_violation("streamed_turn called without a pending CallModel step")
1411 );
1412 }
1413
1414 if !self.streamed_completion_call_recorded {
1418 self.record_completion_call(Usage::new());
1422 self.streamed_completion_call_recorded = true;
1423 }
1424
1425 let items: Vec<AssistantContent> = turn.choice.iter().cloned().collect();
1426 let has_tool_calls = items
1427 .iter()
1428 .any(|item| matches!(item, AssistantContent::ToolCall(_)));
1429
1430 for item in &items {
1431 let AssistantContent::ToolCall(tool_call) = item else {
1432 continue;
1433 };
1434 if !turn.allowed_tool_names.contains(&tool_call.function.name) {
1435 let mut diagnostic_messages = self.new_messages.clone();
1436 if !is_empty_assistant_turn(&turn.choice) {
1437 diagnostic_messages.push(Message::Assistant {
1438 id: turn.message_id.clone(),
1439 content: turn.choice.clone(),
1440 });
1441 }
1442 let diagnostic_history =
1443 build_full_history(self.chat_history.as_deref(), diagnostic_messages);
1444 self.state = RunState::Failed;
1445 return Err(unknown_tool_call_error(
1446 tool_call.function.name.clone(),
1447 turn.executable_tool_names.iter().cloned().collect(),
1448 turn.allowed_tool_names.iter().cloned().collect(),
1449 diagnostic_history,
1450 ));
1451 }
1452 }
1453
1454 self.finalize_turn(
1455 turn.message_id,
1456 items,
1457 has_tool_calls,
1458 BTreeMap::new(),
1459 turn.internal_call_ids,
1460 );
1461 Ok(())
1462 }
1463
1464 fn streamed_diagnostic_history(
1467 &self,
1468 partial: &PartialStreamedTurn,
1469 current_tool_call: Option<ToolCall>,
1470 ) -> Vec<Message> {
1471 let mut messages = self.new_messages.clone();
1472 if let Some(assistant) = partial.assistant_message(current_tool_call) {
1473 messages.push(assistant);
1474 }
1475 build_full_history(self.chat_history.as_deref(), messages)
1476 }
1477
1478 fn diagnostic_history(&self, resolving: &ResolvingState) -> Vec<Message> {
1481 let mut diagnostic_messages = self.new_messages.clone();
1482 diagnostic_messages.push(Message::Assistant {
1483 id: resolving.message_id.clone(),
1484 content: resolving.original_choice.clone(),
1485 });
1486 build_full_history(self.chat_history.as_deref(), diagnostic_messages)
1487 }
1488
1489 fn protocol_violation(&self, reason: &str) -> PromptError {
1490 PromptError::prompt_cancelled(
1491 self.full_history(),
1492 format!("agent run driver protocol violation: {reason}"),
1493 )
1494 }
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499 use super::*;
1500 use rig_core::message::{ToolFunction, ToolResultContent};
1501 use serde_json::json;
1502
1503 fn tool_names(names: &[&str]) -> BTreeSet<String> {
1504 names.iter().map(|name| (*name).to_string()).collect()
1505 }
1506
1507 fn usage(input_tokens: u64, output_tokens: u64) -> Usage {
1508 Usage {
1509 input_tokens,
1510 output_tokens,
1511 total_tokens: input_tokens + output_tokens,
1512 ..Usage::new()
1513 }
1514 }
1515
1516 fn text_turn(text: &str) -> ModelTurn {
1517 ModelTurn::new(
1518 None,
1519 OneOrMany::one(AssistantContent::text(text)),
1520 Usage::new(),
1521 tool_names(&["add"]),
1522 tool_names(&["add"]),
1523 )
1524 }
1525
1526 fn tool_call(id: &str, name: &str) -> AssistantContent {
1527 AssistantContent::ToolCall(ToolCall::new(
1528 id.to_string(),
1529 ToolFunction::new(name.to_string(), json!({"x": 1})),
1530 ))
1531 }
1532
1533 fn tool_call_turn(id: &str, name: &str) -> ModelTurn {
1534 ModelTurn::new(
1535 None,
1536 OneOrMany::one(tool_call(id, name)),
1537 Usage::new(),
1538 tool_names(&["add"]),
1539 tool_names(&["add"]),
1540 )
1541 }
1542
1543 fn tool_result(id: &str, output: &str) -> UserContent {
1544 UserContent::tool_result(
1545 id.to_string(),
1546 OneOrMany::one(ToolResultContent::text(output)),
1547 )
1548 }
1549
1550 fn expect_call_model(run: &mut AgentRun) -> (Message, Vec<Message>, usize) {
1551 match run.next_step().expect("next_step should succeed") {
1552 AgentRunStep::CallModel {
1553 prompt,
1554 history,
1555 turn,
1556 } => (prompt, history, turn),
1557 step => panic!("expected CallModel, got {step:?}"),
1558 }
1559 }
1560
1561 fn expect_call_tools(run: &mut AgentRun) -> Vec<PendingToolCall> {
1562 match run.next_step().expect("next_step should succeed") {
1563 AgentRunStep::CallTools { calls } => calls,
1564 step => panic!("expected CallTools, got {step:?}"),
1565 }
1566 }
1567
1568 fn expect_done(run: &mut AgentRun) -> PromptResponse {
1569 match run.next_step().expect("next_step should succeed") {
1570 AgentRunStep::Done(response) => response,
1571 step => panic!("expected Done, got {step:?}"),
1572 }
1573 }
1574
1575 fn expect_continue(outcome: ModelTurnOutcome) -> bool {
1576 match outcome {
1577 ModelTurnOutcome::Continue {
1578 response_hook_suppressed,
1579 } => response_hook_suppressed,
1580 outcome => panic!("expected Continue, got {outcome:?}"),
1581 }
1582 }
1583
1584 fn expect_needs_resolution(outcome: ModelTurnOutcome) -> InvalidToolCallContext {
1585 match outcome {
1586 ModelTurnOutcome::NeedsResolution(context) => context,
1587 outcome => panic!("expected NeedsResolution, got {outcome:?}"),
1588 }
1589 }
1590
1591 #[test]
1592 fn text_only_run_completes_in_one_turn() {
1593 let mut run = AgentRun::new("hello");
1594
1595 let (prompt, history, turn) = expect_call_model(&mut run);
1596 assert_eq!(prompt, Message::user("hello"));
1597 assert!(history.is_empty());
1598 assert_eq!(turn, 1);
1599
1600 let suppressed = expect_continue(
1601 run.model_response(text_turn("hi there"))
1602 .expect("model_response should succeed"),
1603 );
1604 assert!(!suppressed);
1605
1606 let response = expect_done(&mut run);
1607 assert_eq!(response.output, "hi there");
1608 let messages = response.messages.expect("messages should be recorded");
1609 assert_eq!(messages.len(), 2);
1610 assert!(run.is_done());
1611 }
1612
1613 #[test]
1614 fn input_history_prefixes_request_history() {
1615 let mut run = AgentRun::new("question")
1616 .with_history(vec![Message::user("earlier"), Message::assistant("reply")]);
1617
1618 let (_, history, _) = expect_call_model(&mut run);
1619 assert_eq!(
1620 history,
1621 vec![Message::user("earlier"), Message::assistant("reply")]
1622 );
1623
1624 expect_continue(
1625 run.model_response(text_turn("answer"))
1626 .expect("model_response should succeed"),
1627 );
1628 let response = expect_done(&mut run);
1629 assert_eq!(
1631 response
1632 .messages
1633 .expect("messages should be recorded")
1634 .len(),
1635 2
1636 );
1637 }
1638
1639 #[test]
1640 fn repeated_model_turn_reuses_prompt_without_recording_rejected_response() {
1641 let first_usage = usage(10, 3);
1642 let second_usage = usage(7, 2);
1643 let mut run = AgentRun::new("question").max_turns(2);
1644
1645 let (first_prompt, first_history, first_turn) = expect_call_model(&mut run);
1646 assert_eq!(first_prompt, Message::user("question"));
1647 assert!(first_history.is_empty());
1648 assert_eq!(first_turn, 1);
1649 expect_continue(
1650 run.model_response(text_turn("rejected").with_usage_for_test(first_usage))
1651 .expect("first response"),
1652 );
1653
1654 run.retry_model_turn(RetryRequest::Repeat)
1655 .expect("repeat should be accepted");
1656 let (second_prompt, second_history, second_turn) = expect_call_model(&mut run);
1657 assert_eq!(second_prompt, Message::user("question"));
1658 assert!(second_history.is_empty());
1659 assert_eq!(second_turn, 2);
1660 assert_eq!(run.messages(), &[Message::user("question")]);
1661
1662 expect_continue(
1663 run.model_response(text_turn("accepted").with_usage_for_test(second_usage))
1664 .expect("second response"),
1665 );
1666 let response = expect_done(&mut run);
1667 assert_eq!(response.output, "accepted");
1668 assert_eq!(response.usage, first_usage + second_usage);
1669 assert_eq!(response.completion_calls.len(), 2);
1670 let messages = response.messages.expect("response history");
1671 assert_eq!(messages.len(), 2);
1672 assert!(!format!("{messages:?}").contains("rejected"));
1673 }
1674
1675 #[test]
1676 fn feedback_retry_records_rejected_response_and_corrective_prompt() {
1677 let mut run = AgentRun::new("question").max_turns(2);
1678
1679 expect_call_model(&mut run);
1680 expect_continue(
1681 run.model_response(text_turn("rejected"))
1682 .expect("first response"),
1683 );
1684 run.retry_model_turn(RetryRequest::Feedback("try another approach".to_string()))
1685 .expect("feedback retry should be accepted");
1686
1687 let (prompt, history, turn) = expect_call_model(&mut run);
1688 assert_eq!(prompt, Message::user("try another approach"));
1689 assert_eq!(turn, 2);
1690 assert_eq!(
1691 history,
1692 vec![Message::user("question"), Message::assistant("rejected")]
1693 );
1694 }
1695
1696 #[test]
1697 fn repeated_model_turn_consumes_existing_max_turns_budget() {
1698 let mut run = AgentRun::new("question");
1699
1700 expect_call_model(&mut run);
1701 expect_continue(
1702 run.model_response(text_turn("rejected"))
1703 .expect("first response"),
1704 );
1705 run.retry_model_turn(RetryRequest::Repeat)
1706 .expect("state transition itself should succeed");
1707
1708 let err = run.next_step().expect_err("second call must exceed budget");
1709 assert!(matches!(
1710 err,
1711 PromptError::MaxTurnsError { max_turns: 1, .. }
1712 ));
1713 assert_eq!(run.completion_calls().len(), 1);
1714 }
1715
1716 #[test]
1717 fn model_turn_retry_rejects_tool_calls_without_advancing_to_execution() {
1718 let mut run = AgentRun::new("add things").max_turns(2);
1719
1720 expect_call_model(&mut run);
1721 expect_continue(
1722 run.model_response(tool_call_turn("call_1", "add"))
1723 .expect("tool response"),
1724 );
1725 let err = run
1726 .retry_model_turn(RetryRequest::Feedback("do not call tools".to_string()))
1727 .expect_err("tool-bearing retries must fail closed");
1728
1729 let PromptError::PromptCancelled {
1730 chat_history,
1731 reason,
1732 } = err
1733 else {
1734 panic!("tool-bearing retry should return PromptCancelled");
1735 };
1736 assert!(reason.contains("tool-bearing model turns"));
1737 assert!(reason.contains("tool-call hooks"));
1738 assert_eq!(chat_history, vec![Message::user("add things")]);
1739 assert!(run.next_step().is_err(), "failed run cannot execute tools");
1740 }
1741
1742 #[test]
1743 fn tool_roundtrip_threads_history_and_usage() {
1744 let mut run = AgentRun::new("add things").max_turns(2);
1745
1746 expect_call_model(&mut run);
1747 expect_continue(
1748 run.model_response(tool_call_turn("call_1", "add").with_usage_for_test(usage(10, 5)))
1749 .expect("model_response should succeed"),
1750 );
1751
1752 let calls = expect_call_tools(&mut run);
1753 assert_eq!(calls.len(), 1);
1754 assert_eq!(calls[0].tool_call.function.name, "add");
1755 assert!(calls[0].preresolved_result.is_none());
1756
1757 run.tool_results(vec![tool_result("call_1", "2")])
1758 .expect("tool_results should succeed");
1759
1760 let (prompt, history, turn) = expect_call_model(&mut run);
1761 assert_eq!(turn, 2);
1762 assert!(matches!(prompt, Message::User { .. }));
1765 assert_eq!(history.len(), 2);
1766
1767 expect_continue(
1768 run.model_response(text_turn("the answer is 2").with_usage_for_test(usage(20, 7)))
1769 .expect("model_response should succeed"),
1770 );
1771
1772 let response = expect_done(&mut run);
1773 assert_eq!(response.output, "the answer is 2");
1774 assert_eq!(response.usage, usage(30, 12));
1775 assert_eq!(response.completion_calls.len(), 2);
1776 assert_eq!(response.completion_calls[0].call_index, 0);
1777 assert_eq!(response.completion_calls[0].usage, usage(10, 5));
1778 assert_eq!(response.completion_calls[1].usage, usage(20, 7));
1779 assert_eq!(
1781 response
1782 .messages
1783 .expect("messages should be recorded")
1784 .len(),
1785 4
1786 );
1787 }
1788
1789 #[test]
1790 fn parallel_tool_calls_surface_in_emission_order() {
1791 let mut run = AgentRun::new("do both").max_turns(2);
1792
1793 expect_call_model(&mut run);
1794 let turn = ModelTurn::new(
1795 None,
1796 OneOrMany::many(vec![tool_call("call_1", "add"), tool_call("call_2", "add")])
1797 .expect("two items"),
1798 Usage::new(),
1799 tool_names(&["add"]),
1800 tool_names(&["add"]),
1801 );
1802 expect_continue(
1803 run.model_response(turn)
1804 .expect("model_response should succeed"),
1805 );
1806
1807 let calls = expect_call_tools(&mut run);
1808 assert_eq!(calls.len(), 2);
1809 assert_eq!(calls[0].tool_call.id, "call_1");
1810 assert_eq!(calls[1].tool_call.id, "call_2");
1811
1812 run.tool_results(vec![tool_result("call_2", "b"), tool_result("call_1", "a")])
1814 .expect("tool_results should succeed");
1815 let messages = run.messages();
1816 assert!(matches!(
1817 messages.last(),
1818 Some(Message::User { content }) if content.len() == 2
1819 ));
1820 }
1821
1822 #[test]
1823 fn max_turns_zero_rejects_initial_model_call() {
1824 let mut run = AgentRun::new("do not call").max_turns(0);
1825
1826 let err = run
1827 .next_step()
1828 .expect_err("zero budget should emit no call");
1829 assert!(matches!(
1830 err,
1831 PromptError::MaxTurnsError { max_turns: 0, .. }
1832 ));
1833 assert_eq!(run.turn(), 0);
1834 }
1835
1836 #[test]
1837 fn new_implicitly_allows_one_model_call_and_rejects_tool_continuation() {
1838 let mut run = AgentRun::new("add things");
1839
1840 let (_, _, turn) = expect_call_model(&mut run);
1841 assert_eq!(turn, 1);
1842 expect_continue(
1843 run.model_response(tool_call_turn("call_1", "add"))
1844 .expect("model_response should succeed"),
1845 );
1846 expect_call_tools(&mut run);
1847 run.tool_results(vec![tool_result("call_1", "2")])
1848 .expect("tool_results should succeed");
1849
1850 let err = run
1851 .next_step()
1852 .expect_err("second model call should exceed budget");
1853 assert!(matches!(
1854 err,
1855 PromptError::MaxTurnsError { max_turns: 1, .. }
1856 ));
1857 assert_eq!(run.turn(), 1);
1858 }
1859
1860 #[test]
1861 fn max_turns_n_allows_exactly_n_model_calls() {
1862 let mut run = AgentRun::new("loop").max_turns(3);
1863
1864 for (expected_turn, call_id) in [(1, "call_1"), (2, "call_2"), (3, "call_3")] {
1865 let (_, _, turn) = expect_call_model(&mut run);
1866 assert_eq!(turn, expected_turn);
1867 expect_continue(
1868 run.model_response(tool_call_turn(call_id, "add"))
1869 .expect("model_response should succeed"),
1870 );
1871 expect_call_tools(&mut run);
1872 run.tool_results(vec![tool_result(call_id, "0")])
1873 .expect("tool_results should succeed");
1874 }
1875
1876 let err = run
1877 .next_step()
1878 .expect_err("fourth model call should exceed budget");
1879 assert!(matches!(
1880 err,
1881 PromptError::MaxTurnsError { max_turns: 3, .. }
1882 ));
1883 assert_eq!(run.turn(), 3);
1884 }
1885
1886 #[test]
1887 fn invalid_tool_call_fail_returns_unknown_tool_call() {
1888 let mut run = AgentRun::new("call something");
1889
1890 expect_call_model(&mut run);
1891 let context = expect_needs_resolution(
1892 run.model_response(tool_call_turn("call_1", "unknown"))
1893 .expect("model_response should succeed"),
1894 );
1895 assert_eq!(context.tool_name, "unknown");
1896 assert_eq!(context.available_tools, vec!["add".to_string()]);
1897 assert!(!context.is_streaming);
1898 assert_eq!(context.chat_history.len(), 2);
1900
1901 let err = run
1902 .resolve_invalid_tool_call(InvalidToolCallAction::fail())
1903 .expect_err("fail action should error");
1904 assert!(matches!(
1905 err,
1906 PromptError::UnknownToolCall { tool_name, .. } if tool_name == "unknown"
1907 ));
1908 }
1909
1910 #[test]
1911 fn invalid_tool_call_stop_leaves_run_terminal() {
1912 let mut run = AgentRun::new("call something");
1913
1914 expect_call_model(&mut run);
1915 expect_needs_resolution(
1916 run.model_response(tool_call_turn("call_1", "unknown"))
1917 .expect("model_response should succeed"),
1918 );
1919 let err = run
1920 .resolve_invalid_tool_call(InvalidToolCallAction::stop("operator stop"))
1921 .expect_err("stop should cancel the run");
1922 assert!(matches!(
1923 err,
1924 PromptError::PromptCancelled { reason, .. } if reason == "operator stop"
1925 ));
1926
1927 let err = run
1928 .next_step()
1929 .expect_err("a stopped run must remain terminal");
1930 assert!(matches!(
1931 err,
1932 PromptError::PromptCancelled { reason, .. }
1933 if reason.contains("next_step called after the run already failed")
1934 ));
1935 }
1936
1937 #[test]
1938 fn invalid_tool_call_retry_rolls_back_with_feedback() {
1939 let mut run = AgentRun::new("call something")
1940 .max_turns(2)
1941 .max_invalid_tool_call_retries(1);
1942
1943 expect_call_model(&mut run);
1944 expect_needs_resolution(
1945 run.model_response(tool_call_turn("call_1", "unknown"))
1946 .expect("model_response should succeed"),
1947 );
1948 let outcome = run
1949 .resolve_invalid_tool_call(InvalidToolCallAction::retry("use add instead"))
1950 .expect("retry should be accepted");
1951 assert!(matches!(outcome, ModelTurnOutcome::TurnRetried));
1952
1953 assert_eq!(run.messages().len(), 3);
1955 let (prompt, _, turn) = expect_call_model(&mut run);
1956 assert_eq!(turn, 2);
1957 assert!(matches!(
1958 prompt,
1959 Message::User { ref content }
1960 if matches!(content.first(), UserContent::ToolResult(_))
1961 ));
1962
1963 expect_needs_resolution(
1965 run.model_response(tool_call_turn("call_2", "unknown"))
1966 .expect("model_response should succeed"),
1967 );
1968 let err = run
1969 .resolve_invalid_tool_call(InvalidToolCallAction::retry("again"))
1970 .expect_err("budget exhausted");
1971 assert!(matches!(err, PromptError::UnknownToolCall { .. }));
1972 }
1973
1974 #[test]
1975 fn invalid_tool_call_retry_cannot_emit_call_past_total_budget() {
1976 let mut run = AgentRun::new("call something")
1977 .max_turns(1)
1978 .max_invalid_tool_call_retries(1);
1979
1980 expect_call_model(&mut run);
1981 expect_needs_resolution(
1982 run.model_response(tool_call_turn("call_1", "unknown"))
1983 .expect("model_response should succeed"),
1984 );
1985 let outcome = run
1986 .resolve_invalid_tool_call(InvalidToolCallAction::retry("use add instead"))
1987 .expect("retry resolution should be accepted");
1988 assert!(matches!(outcome, ModelTurnOutcome::TurnRetried));
1989 assert_eq!(run.completion_calls().len(), 1);
1990
1991 let err = run
1992 .next_step()
1993 .expect_err("retry must not emit a second model call");
1994 assert!(matches!(
1995 err,
1996 PromptError::MaxTurnsError { max_turns: 1, .. }
1997 ));
1998 assert_eq!(run.turn(), 1);
1999 }
2000
2001 #[test]
2002 fn invalid_tool_call_repair_renames_and_suppresses_response_hook() {
2003 let mut run = AgentRun::new("call something").max_turns(2);
2004
2005 expect_call_model(&mut run);
2006 expect_needs_resolution(
2007 run.model_response(tool_call_turn("call_1", "default_api"))
2008 .expect("model_response should succeed"),
2009 );
2010 let suppressed = expect_continue(
2011 run.resolve_invalid_tool_call(InvalidToolCallAction::repair("add"))
2012 .expect("repair should be accepted"),
2013 );
2014 assert!(suppressed);
2015
2016 let calls = expect_call_tools(&mut run);
2017 assert_eq!(calls[0].tool_call.function.name, "add");
2018 assert!(calls[0].preresolved_result.is_none());
2019 }
2020
2021 #[test]
2022 fn invalid_tool_call_repair_to_disallowed_name_fails() {
2023 let mut run = AgentRun::new("call something");
2024
2025 expect_call_model(&mut run);
2026 expect_needs_resolution(
2027 run.model_response(tool_call_turn("call_1", "unknown"))
2028 .expect("model_response should succeed"),
2029 );
2030 let err = run
2031 .resolve_invalid_tool_call(InvalidToolCallAction::repair("also_unknown"))
2032 .expect_err("repair to disallowed name should fail");
2033 assert!(matches!(
2034 err,
2035 PromptError::UnknownToolCall { tool_name, .. } if tool_name == "also_unknown"
2036 ));
2037 }
2038
2039 #[test]
2040 fn invalid_tool_call_skip_suppresses_all_peer_executions() {
2041 let mut run = AgentRun::new("call things").max_turns(2);
2042
2043 expect_call_model(&mut run);
2044 let turn = ModelTurn::new(
2045 None,
2046 OneOrMany::many(vec![
2047 tool_call("call_1", "unknown"),
2048 tool_call("call_2", "add"),
2049 ])
2050 .expect("two items"),
2051 Usage::new(),
2052 tool_names(&["add"]),
2053 tool_names(&["add"]),
2054 );
2055 expect_needs_resolution(
2056 run.model_response(turn)
2057 .expect("model_response should succeed"),
2058 );
2059 let suppressed = expect_continue(
2060 run.resolve_invalid_tool_call(InvalidToolCallAction::skip("not available"))
2061 .expect("skip should be accepted"),
2062 );
2063 assert!(suppressed);
2064
2065 let calls = expect_call_tools(&mut run);
2066 assert_eq!(calls.len(), 2);
2067 assert!(calls.iter().all(|call| call.preresolved_result.is_some()));
2069 }
2070
2071 #[test]
2072 fn skip_under_tool_choice_none_fails() {
2073 let mut run = AgentRun::new("call something").with_tool_choice(ToolChoice::None);
2074
2075 expect_call_model(&mut run);
2076 expect_needs_resolution(
2077 run.model_response(ModelTurn::new(
2078 None,
2079 OneOrMany::one(tool_call("call_1", "add")),
2080 Usage::new(),
2081 tool_names(&["add"]),
2082 BTreeSet::new(),
2083 ))
2084 .expect("model_response should succeed"),
2085 );
2086 let err = run
2087 .resolve_invalid_tool_call(InvalidToolCallAction::skip("nope"))
2088 .expect_err("skip under ToolChoice::None should fail");
2089 assert!(matches!(err, PromptError::UnknownToolCall { .. }));
2090 }
2091
2092 #[test]
2093 fn empty_tool_results_cancel_the_run() {
2094 let mut run = AgentRun::new("call something").max_turns(2);
2095
2096 expect_call_model(&mut run);
2097 expect_continue(
2098 run.model_response(tool_call_turn("call_1", "add"))
2099 .expect("model_response should succeed"),
2100 );
2101 expect_call_tools(&mut run);
2102
2103 let err = run
2104 .tool_results(Vec::new())
2105 .expect_err("empty results should cancel");
2106 assert!(matches!(
2107 err,
2108 PromptError::PromptCancelled { reason, .. }
2109 if reason.contains("tool execution produced no tool results")
2110 ));
2111 }
2112
2113 #[test]
2114 fn out_of_protocol_calls_are_rejected_without_corrupting_state() {
2115 let mut run = AgentRun::new("hello");
2116
2117 let err = run
2118 .tool_results(vec![tool_result("call_1", "x")])
2119 .expect_err("no CallTools pending");
2120 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2121
2122 expect_call_model(&mut run);
2124 let err = run
2125 .next_step()
2126 .expect_err("model response is pending, next_step must be rejected");
2127 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2128 expect_continue(
2129 run.model_response(text_turn("hi"))
2130 .expect("model_response should still succeed"),
2131 );
2132 assert_eq!(expect_done(&mut run).output, "hi");
2133 }
2134
2135 #[test]
2136 fn model_response_rejected_after_streamed_completion_call_record() {
2137 let mut run = AgentRun::new("hello");
2138 expect_call_model(&mut run);
2139 run.record_streamed_completion_call(Usage::new())
2140 .expect("record should succeed");
2141
2142 let err = run
2143 .model_response(text_turn("hi"))
2144 .expect_err("mixed streamed/non-streamed ingestion must be rejected");
2145 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2146 assert_eq!(run.completion_calls().len(), 1);
2148 }
2149
2150 #[test]
2151 fn done_step_is_idempotent() {
2152 let mut run = AgentRun::new("hello");
2153 expect_call_model(&mut run);
2154 expect_continue(
2155 run.model_response(text_turn("hi"))
2156 .expect("model_response should succeed"),
2157 );
2158 assert_eq!(expect_done(&mut run).output, "hi");
2159 assert_eq!(expect_done(&mut run).output, "hi");
2160 }
2161
2162 #[test]
2163 fn serialized_run_alone_carries_pending_tool_calls() {
2164 let mut run = AgentRun::new("add things").max_turns(2);
2165 expect_call_model(&mut run);
2166 expect_continue(
2167 run.model_response(tool_call_turn("call_1", "add"))
2168 .expect("model_response should succeed"),
2169 );
2170 expect_call_tools(&mut run);
2171
2172 let serialized = serde_json::to_string(&run).expect("mid-run state should serialize");
2175 drop(run);
2176 let mut resumed: AgentRun =
2177 serde_json::from_str(&serialized).expect("mid-run state should deserialize");
2178
2179 let calls = expect_call_tools(&mut resumed);
2180 assert_eq!(calls.len(), 1);
2181 assert_eq!(calls[0].tool_call.function.name, "add");
2182 let calls_again = expect_call_tools(&mut resumed);
2184 assert_eq!(calls_again[0].tool_call.id, calls[0].tool_call.id);
2185
2186 let results = calls
2188 .iter()
2189 .map(|call| tool_result(&call.tool_call.id, "2"))
2190 .collect::<Vec<_>>();
2191 resumed
2192 .tool_results(results)
2193 .expect("tool_results should succeed");
2194 expect_call_model(&mut resumed);
2195 expect_continue(
2196 resumed
2197 .model_response(text_turn("done"))
2198 .expect("model_response should succeed"),
2199 );
2200 assert_eq!(expect_done(&mut resumed).output, "done");
2201 }
2202
2203 #[test]
2204 fn tool_results_validates_against_pending_calls() {
2205 let drive_to_pending_tools = || {
2206 let mut run = AgentRun::new("add things").max_turns(2);
2207 expect_call_model(&mut run);
2208 expect_continue(
2209 run.model_response(tool_call_turn("call_1", "add"))
2210 .expect("model_response should succeed"),
2211 );
2212 expect_call_tools(&mut run);
2213 run
2214 };
2215
2216 let mut run = drive_to_pending_tools();
2218 let err = run
2219 .tool_results(vec![tool_result("call_unknown", "2")])
2220 .expect_err("unknown tool call id must be rejected");
2221 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2222 run.tool_results(vec![tool_result("call_1", "2")])
2223 .expect("valid results should still be accepted after a rejection");
2224
2225 let mut run = drive_to_pending_tools();
2227 let err = run
2228 .tool_results(vec![tool_result("call_1", "2"), tool_result("call_1", "3")])
2229 .expect_err("answering one call twice must be rejected");
2230 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2231
2232 let mut run = drive_to_pending_tools();
2234 let err = run
2235 .tool_results(vec![UserContent::text("not a tool result")])
2236 .expect_err("non-tool-result content must be rejected");
2237 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2238 }
2239
2240 #[test]
2241 fn agent_run_deserializes_pre_monoid_suspended_state() {
2242 let fixture = r#"{"max_turns":2,"max_invalid_tool_call_retries":0,"tool_choice":null,"chat_history":null,"new_messages":[{"role":"user","content":[{"type":"text","text":"add things"}]},{"role":"assistant","id":null,"content":[{"id":"call_1","call_id":null,"function":{"name":"add","arguments":{"x":1}},"signature":null,"additional_params":null}]}],"current_turn":1,"usage":{"input_tokens":10,"output_tokens":5,"total_tokens":15,"cached_input_tokens":0,"cache_creation_input_tokens":0,"tool_use_prompt_tokens":0,"reasoning_tokens":0},"completion_calls":[{"call_index":0,"usage":null}],"completion_call_index":1,"invalid_tool_call_retries":0,"rollback_pending":false,"streamed_completion_call_recorded":false,"state":{"ExecutingTools":[{"tool_call":{"id":"call_1","call_id":null,"function":{"name":"add","arguments":{"x":1}},"signature":null,"additional_params":null},"preresolved_result":null,"internal_call_id":null}]}}"#;
2246
2247 let mut restored: AgentRun =
2248 serde_json::from_str(fixture).expect("old-format suspended run should deserialize");
2249 assert_eq!(restored.completion_calls()[0].usage, Usage::new());
2250
2251 let calls = expect_call_tools(&mut restored);
2252 assert_eq!(calls.len(), 1);
2253 restored
2254 .tool_results(vec![tool_result("call_1", "2")])
2255 .expect("tool_results should succeed");
2256 expect_call_model(&mut restored);
2257 }
2258
2259 #[test]
2260 fn serde_round_trip_at_exhausted_budget_preserves_boundary() {
2261 let mut run = AgentRun::new("add things").max_turns(1);
2262 expect_call_model(&mut run);
2263 expect_continue(
2264 run.model_response(tool_call_turn("call_1", "add"))
2265 .expect("model_response should succeed"),
2266 );
2267 expect_call_tools(&mut run);
2268 run.tool_results(vec![tool_result("call_1", "2")])
2269 .expect("tool_results should succeed");
2270
2271 let serialized = serde_json::to_string(&run).expect("exhausted run should serialize");
2272 let mut restored: AgentRun =
2273 serde_json::from_str(&serialized).expect("exhausted run should deserialize");
2274 assert_eq!(restored.completion_calls().len(), 1);
2275 let err = restored
2276 .next_step()
2277 .expect_err("restored run must not emit a second model call");
2278 assert!(matches!(
2279 err,
2280 PromptError::MaxTurnsError { max_turns: 1, .. }
2281 ));
2282 assert_eq!(restored.turn(), 1);
2283 }
2284
2285 #[test]
2286 fn serde_round_trip_mid_run_resumes_identically() {
2287 let drive_to_pending_tools = || {
2288 let mut run = AgentRun::new("add things").max_turns(2);
2289 expect_call_model(&mut run);
2290 expect_continue(
2291 run.model_response(
2292 tool_call_turn("call_1", "add").with_usage_for_test(usage(10, 5)),
2293 )
2294 .expect("model_response should succeed"),
2295 );
2296 expect_call_tools(&mut run);
2297 run
2298 };
2299
2300 let finish = |mut run: AgentRun| {
2301 run.tool_results(vec![tool_result("call_1", "2")])
2302 .expect("tool_results should succeed");
2303 expect_call_model(&mut run);
2304 expect_continue(
2305 run.model_response(text_turn("done").with_usage_for_test(usage(3, 4)))
2306 .expect("model_response should succeed"),
2307 );
2308 expect_done(&mut run)
2309 };
2310
2311 let uninterrupted = finish(drive_to_pending_tools());
2312
2313 let suspended = drive_to_pending_tools();
2314 let serialized = serde_json::to_string(&suspended).expect("mid-run state should serialize");
2315 let restored: AgentRun =
2316 serde_json::from_str(&serialized).expect("mid-run state should deserialize");
2317 let resumed = finish(restored);
2318
2319 assert_eq!(resumed.output, uninterrupted.output);
2320 assert_eq!(resumed.usage, uninterrupted.usage);
2321 assert_eq!(resumed.completion_calls, uninterrupted.completion_calls);
2322 assert_eq!(
2326 serde_json::to_value(&resumed.messages).expect("messages should serialize"),
2327 serde_json::to_value(&uninterrupted.messages).expect("messages should serialize"),
2328 );
2329 }
2330
2331 #[test]
2332 fn pending_invalid_tool_call_survives_serde_round_trip() {
2333 let mut run = AgentRun::new("call something");
2334 expect_call_model(&mut run);
2335 let context = expect_needs_resolution(
2336 run.model_response(tool_call_turn("call_1", "unknown"))
2337 .expect("model_response should succeed"),
2338 );
2339
2340 let serialized = serde_json::to_string(&run).expect("state should serialize");
2341 let restored: AgentRun =
2342 serde_json::from_str(&serialized).expect("state should deserialize");
2343 let restored_context = restored
2344 .pending_invalid_tool_call()
2345 .expect("pending resolution should survive serialization");
2346 assert_eq!(restored_context.tool_name, context.tool_name);
2347 assert_eq!(
2348 restored_context.chat_history.len(),
2349 context.chat_history.len()
2350 );
2351 }
2352
2353 fn output_tool_turn(id: &str, name: &str) -> ModelTurn {
2356 ModelTurn::new(
2357 None,
2358 OneOrMany::one(tool_call(id, name)),
2359 Usage::new(),
2360 tool_names(&["add"]),
2361 tool_names(&["add", name]),
2362 )
2363 }
2364
2365 fn output_tool_turn_with_args(id: &str, name: &str, arguments: serde_json::Value) -> ModelTurn {
2366 ModelTurn::new(
2367 None,
2368 OneOrMany::one(AssistantContent::ToolCall(ToolCall::new(
2369 id.to_string(),
2370 ToolFunction::new(name.to_string(), arguments),
2371 ))),
2372 Usage::new(),
2373 tool_names(&["add"]),
2374 tool_names(&["add", name]),
2375 )
2376 }
2377
2378 fn assert_no_orphan_tool_use(messages: &[Message]) {
2381 let mut answered = BTreeSet::new();
2382 for message in messages {
2383 if let Message::User { content } = message {
2384 for item in content.iter() {
2385 if let UserContent::ToolResult(result) = item {
2386 answered.insert(result.id.clone());
2387 }
2388 }
2389 }
2390 }
2391 for message in messages {
2392 if let Message::Assistant { content, .. } = message {
2393 for item in content.iter() {
2394 if let AssistantContent::ToolCall(call) = item {
2395 assert!(
2396 answered.contains(&call.id),
2397 "assistant tool_call {:?} has no matching tool_result in history",
2398 call.id
2399 );
2400 }
2401 }
2402 }
2403 }
2404 }
2405
2406 #[test]
2407 fn output_tool_call_finalizes_run_with_arguments() {
2408 let mut run = AgentRun::new("summarize").with_output_tool_name("final_result");
2409
2410 expect_call_model(&mut run);
2411 expect_continue(
2412 run.model_response(output_tool_turn("call_1", "final_result"))
2413 .expect("model_response should succeed"),
2414 );
2415
2416 let response = expect_done(&mut run);
2418 assert_eq!(response.output, r#"{"x":1}"#);
2419 assert!(run.is_done());
2420
2421 let messages = response.messages.expect("messages should be recorded");
2424 assert_no_orphan_tool_use(&messages);
2425 assert!(matches!(
2426 messages.last(),
2427 Some(Message::Assistant { content, .. })
2428 if assistant_text_from_choice(content) == r#"{"x":1}"#
2429 ));
2430 }
2431
2432 #[test]
2433 fn scalar_output_tool_call_is_serialized_as_reparseable_json() {
2434 let mut run = AgentRun::new("summarize").with_output_tool_name("final_result");
2435
2436 expect_call_model(&mut run);
2437 expect_continue(
2438 run.model_response(output_tool_turn_with_args(
2439 "call_1",
2440 "final_result",
2441 json!("complete"),
2442 ))
2443 .expect("model_response should succeed"),
2444 );
2445
2446 let response = expect_done(&mut run);
2447 assert_eq!(
2448 serde_json::from_str::<serde_json::Value>(&response.output)
2449 .expect("scalar output must remain valid JSON"),
2450 json!("complete")
2451 );
2452 assert_eq!(response.output, r#""complete""#);
2453
2454 let messages = response.messages.expect("messages should be recorded");
2455 assert_no_orphan_tool_use(&messages);
2456 assert!(matches!(
2457 messages.last(),
2458 Some(Message::Assistant { content, .. })
2459 if assistant_text_from_choice(content) == r#""complete""#
2460 ));
2461 }
2462
2463 #[test]
2464 fn output_tool_call_wins_over_sibling_real_tool_calls() {
2465 let mut run = AgentRun::new("do it")
2466 .max_turns(2)
2467 .with_output_tool_name("final_result");
2468
2469 expect_call_model(&mut run);
2470 let turn = ModelTurn::new(
2473 None,
2474 OneOrMany::many(vec![
2475 tool_call("call_1", "add"),
2476 tool_call("call_2", "final_result"),
2477 ])
2478 .expect("two items"),
2479 Usage::new(),
2480 tool_names(&["add"]),
2481 tool_names(&["add", "final_result"]),
2482 );
2483 expect_continue(
2484 run.model_response(turn)
2485 .expect("model_response should succeed"),
2486 );
2487
2488 let response = expect_done(&mut run);
2489 assert_eq!(response.output, r#"{"x":1}"#);
2490 assert!(run.is_done());
2491
2492 let messages = response.messages.expect("messages should be recorded");
2495 assert_no_orphan_tool_use(&messages);
2496 assert!(
2497 messages.iter().all(|message| match message {
2498 Message::Assistant { content, .. } => !content
2499 .iter()
2500 .any(|item| matches!(item, AssistantContent::ToolCall(_))),
2501 _ => true,
2502 }),
2503 "no assistant tool calls should survive in the finalized history"
2504 );
2505 }
2506
2507 #[test]
2508 fn real_tool_calls_still_execute_when_output_tool_unused() {
2509 let mut run = AgentRun::new("add things")
2512 .max_turns(2)
2513 .with_output_tool_name("final_result");
2514
2515 expect_call_model(&mut run);
2516 expect_continue(
2517 run.model_response(tool_call_turn("call_1", "add"))
2518 .expect("model_response should succeed"),
2519 );
2520
2521 let calls = expect_call_tools(&mut run);
2522 assert_eq!(calls.len(), 1);
2523 assert_eq!(calls[0].tool_call.function.name, "add");
2524 }
2525
2526 fn required_field_schema(field: &str) -> serde_json::Value {
2527 json!({
2528 "type": "object",
2529 "required": [field],
2530 "properties": { field: { "type": "string" } },
2531 })
2532 }
2533
2534 #[test]
2535 fn tool_mode_reprompts_when_output_tool_not_called() {
2536 let mut run = AgentRun::new("summarize")
2539 .max_turns(2)
2540 .with_output_tool_name("final_result")
2541 .with_output_validation(Some(required_field_schema("summary")), 1);
2542
2543 expect_call_model(&mut run);
2544 expect_continue(
2545 run.model_response(text_turn("here is the answer"))
2546 .expect("model_response should succeed"),
2547 );
2548
2549 let (prompt, _history, turn) = expect_call_model(&mut run);
2552 assert_eq!(turn, 2);
2553 let prompt_json = serde_json::to_string(&prompt).expect("prompt should serialize");
2554 assert!(
2555 prompt_json.contains("final_result"),
2556 "re-prompt feedback should name the output tool: {prompt_json}"
2557 );
2558 assert!(!run.is_done());
2559 }
2560
2561 #[test]
2562 fn tool_mode_reprompts_when_output_args_missing_required_fields() {
2563 let mut run = AgentRun::new("summarize")
2566 .max_turns(2)
2567 .with_output_tool_name("final_result")
2568 .with_output_validation(Some(required_field_schema("summary")), 1);
2570
2571 expect_call_model(&mut run);
2572 expect_continue(
2573 run.model_response(output_tool_turn("call_1", "final_result"))
2574 .expect("model_response should succeed"),
2575 );
2576
2577 let (_prompt, _history, turn) = expect_call_model(&mut run);
2578 assert_eq!(turn, 2);
2579 assert!(!run.is_done());
2580 }
2581
2582 #[test]
2583 fn tool_mode_accepts_valid_json_text_without_reprompting() {
2584 let mut run = AgentRun::new("summarize")
2587 .max_turns(3)
2588 .with_output_tool_name("final_result")
2589 .with_output_validation(Some(required_field_schema("summary")), 1);
2590
2591 expect_call_model(&mut run);
2592 expect_continue(
2593 run.model_response(text_turn(r#"{"summary":"all good"}"#))
2594 .expect("model_response should succeed"),
2595 );
2596
2597 let response = expect_done(&mut run);
2598 assert_eq!(response.output, r#"{"summary":"all good"}"#);
2599 assert!(run.is_done());
2600 }
2601
2602 #[test]
2603 fn tool_mode_finalizes_best_effort_when_model_call_budget_exhausted() {
2604 let mut run = AgentRun::new("summarize")
2605 .max_turns(1)
2606 .with_output_tool_name("final_result")
2607 .with_output_validation(Some(required_field_schema("summary")), 1);
2608
2609 expect_call_model(&mut run);
2610 expect_continue(
2611 run.model_response(text_turn("invalid output"))
2612 .expect("model_response should succeed"),
2613 );
2614
2615 let response = expect_done(&mut run);
2616 assert_eq!(response.output, "invalid output");
2617 assert_eq!(run.turn(), 1);
2618 }
2619
2620 #[test]
2621 fn tool_mode_finalizes_best_effort_when_output_retry_budget_exhausted() {
2622 let mut run = AgentRun::new("summarize")
2626 .max_turns(3)
2627 .with_output_tool_name("final_result")
2628 .with_output_validation(Some(required_field_schema("summary")), 0);
2629
2630 expect_call_model(&mut run);
2631 expect_continue(
2632 run.model_response(output_tool_turn("call_1", "final_result"))
2633 .expect("model_response should succeed"),
2634 );
2635
2636 let response = expect_done(&mut run);
2637 assert_eq!(response.output, r#"{"x":1}"#);
2638 let messages = response.messages.expect("messages should be recorded");
2639 assert_no_orphan_tool_use(&messages);
2640 }
2641
2642 #[test]
2643 fn set_output_tool_name_is_idempotent_and_only_fills_when_unset() {
2644 let mut run = AgentRun::new("x").with_output_tool_name("first");
2647 run.set_output_tool_name(Some("second".to_string()));
2648 run.set_output_tool_name(None);
2649 assert_eq!(run.output_tool_name.as_deref(), Some("first"));
2650
2651 let mut run = AgentRun::new("x");
2653 run.set_output_tool_name(None);
2654 assert_eq!(run.output_tool_name, None);
2655 run.set_output_tool_name(Some("filled".to_string()));
2656 assert_eq!(run.output_tool_name.as_deref(), Some("filled"));
2657 }
2658
2659 impl ModelTurn {
2660 fn with_usage_for_test(mut self, usage: Usage) -> Self {
2661 self.usage = usage;
2662 self
2663 }
2664 }
2665
2666 #[test]
2673 fn durable_human_in_the_loop_approval_survives_serialize_resume() {
2674 let mut run = AgentRun::new("pay two invoices").max_turns(3);
2675 let (_, _, turn) = expect_call_model(&mut run);
2676 assert_eq!(turn, 1);
2677
2678 let two_calls =
2680 OneOrMany::many([tool_call("c1", "add"), tool_call("c2", "add")]).expect("two calls");
2681 let outcome = run
2682 .model_response(ModelTurn::new(
2683 None,
2684 two_calls,
2685 Usage::new(),
2686 tool_names(&["add"]),
2687 tool_names(&["add"]),
2688 ))
2689 .expect("model_response");
2690 expect_continue(outcome);
2691
2692 let checkpoint = serde_json::to_string(&run).expect("serialize suspended run");
2695 let mut resumed: AgentRun = serde_json::from_str(&checkpoint).expect("deserialize run");
2696
2697 let calls = expect_call_tools(&mut resumed);
2699 assert_eq!(calls.len(), 2);
2700 assert_eq!(calls[0].tool_call.id, "c1");
2701 assert_eq!(calls[1].tool_call.id, "c2");
2702
2703 resumed
2706 .tool_results(vec![
2707 tool_result("c1", "approved-result"),
2708 tool_result("c2", "denied by reviewer: second payment not authorized"),
2709 ])
2710 .expect("tool_results on the resumed run");
2711
2712 let after = serde_json::to_string(&resumed).expect("serialize resumed run");
2714 assert!(
2715 after.contains("approved-result"),
2716 "the approved call's result must be in the resumed run state"
2717 );
2718 assert!(
2719 after.contains("denied by reviewer: second payment not authorized"),
2720 "the denied call's reason must be in the resumed run state"
2721 );
2722
2723 let (_, _, turn2) = expect_call_model(&mut resumed);
2725 assert_eq!(turn2, 2);
2726 expect_continue(
2727 resumed
2728 .model_response(text_turn("done"))
2729 .expect("model_response 2"),
2730 );
2731 let response = expect_done(&mut resumed);
2732 assert_eq!(response.output, "done");
2733 }
2734}