1pub mod output_mode;
68pub mod streamed;
69
70pub use output_mode::OutputMode;
71
72use std::collections::{BTreeMap, BTreeSet};
73
74use serde::{Deserialize, Serialize};
75
76use rig_core::completion::{CompletionError, FinishReason};
77use rig_core::message::{
78 AssistantContent, ToolCall, ToolChoice, ToolResult, ToolResultContent, UserContent,
79};
80
81use crate::{
82 agent::hook::{InvalidToolCallAction, InvalidToolCallContext, RetryRequest},
83 agent::prompt_request::{
84 CompletionCall, PromptResponse, ResponseIdentity, TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER,
85 assistant_text_from_choice, build_full_history, build_history_for_request,
86 invalid_tool_retry_user_message, is_empty_assistant_turn, tool_result_message,
87 turn_delivered_no_answer,
88 },
89 completion::{Message, PromptError, Usage},
90 json_utils,
91};
92
93pub use streamed::{
94 PartialStreamedTurn, StreamedInvalidToolCall, StreamedResolution, StreamedTurn,
95 StreamedTurnAssembler, StreamedTurnEvent,
96};
97
98fn unknown_tool_call_error(
104 tool_name: String,
105 available_tools: Vec<String>,
106 allowed_tools: Vec<String>,
107 chat_history: Vec<Message>,
108) -> PromptError {
109 PromptError::UnknownToolCall {
110 tool_name,
111 available_tools,
112 allowed_tools,
113 chat_history: Box::new(chat_history),
114 }
115}
116
117struct InvalidToolCallDiagnostic<'a> {
118 tool_call: &'a ToolCall,
119 executable_tool_names: &'a BTreeSet<String>,
120 allowed_tool_names: &'a BTreeSet<String>,
121 history: &'a [Message],
122}
123
124impl InvalidToolCallDiagnostic<'_> {
125 fn unknown(&self, tool_name: String) -> PromptError {
126 unknown_tool_call_error(
127 tool_name,
128 self.executable_tool_names.iter().cloned().collect(),
129 self.allowed_tool_names.iter().cloned().collect(),
130 self.history.to_vec(),
131 )
132 }
133
134 fn unknown_current(&self) -> PromptError {
135 self.unknown(self.tool_call.function.name.clone())
136 }
137
138 fn cancelled(&self, reason: String) -> PromptError {
139 PromptError::prompt_cancelled(self.history.to_vec(), reason)
140 }
141}
142
143enum ValidatedInvalidToolCallAction {
144 Retry { feedback: String },
145 Repair { tool_name: String },
146 Skip { reason: String },
147}
148
149pub(crate) const DEFAULT_OUTPUT_RETRIES: usize = 1;
153
154#[derive(Debug, Clone)]
159pub enum AgentRunStep {
160 CallModel {
163 prompt: Message,
165 history: Vec<Message>,
168 turn: usize,
170 },
171 CallTools {
174 calls: Vec<PendingToolCall>,
176 },
177 Done(PromptResponse),
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct PendingToolCall {
184 pub tool_call: ToolCall,
186 pub preresolved_result: Option<UserContent>,
190 #[serde(default)]
195 pub internal_call_id: Option<String>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct ModelTurn {
201 pub message_id: Option<String>,
203 pub response_id: Option<String>,
205 pub provider_request_id: Option<String>,
207 pub choice: Vec<AssistantContent>,
209 pub usage: Usage,
211 pub executable_tool_names: BTreeSet<String>,
213 pub allowed_tool_names: BTreeSet<String>,
215 #[serde(default)]
219 pub finish_reason: Option<FinishReason>,
220 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
228 pub raw: serde_json::Value,
229}
230
231impl ModelTurn {
232 pub fn new(
235 message_id: Option<String>,
236 choice: Vec<AssistantContent>,
237 usage: Usage,
238 executable_tool_names: BTreeSet<String>,
239 allowed_tool_names: BTreeSet<String>,
240 ) -> Self {
241 Self {
242 message_id,
243 response_id: None,
244 provider_request_id: None,
245 choice,
246 usage,
247 executable_tool_names,
248 allowed_tool_names,
249 finish_reason: None,
250 raw: serde_json::Value::Null,
251 }
252 }
253
254 pub fn with_identity(
256 mut self,
257 response_id: Option<String>,
258 provider_request_id: Option<String>,
259 ) -> Self {
260 self.response_id = response_id;
261 self.provider_request_id = provider_request_id;
262 self
263 }
264
265 pub fn with_finish_reason(mut self, finish_reason: Option<FinishReason>) -> Self {
267 self.finish_reason = finish_reason;
268 self
269 }
270
271 pub fn with_raw(mut self, raw: serde_json::Value) -> Self {
273 self.raw = raw;
274 self
275 }
276}
277
278#[derive(Debug)]
284pub enum ModelTurnOutcome {
285 Continue {
293 response_hook_suppressed: bool,
295 },
296 NeedsResolution(InvalidToolCallContext),
301 TurnRetried,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308struct ResolvingState {
309 message_id: Option<String>,
310 original_choice: Vec<AssistantContent>,
313 items: Vec<AssistantContent>,
315 next_index: usize,
317 executable_tool_names: BTreeSet<String>,
318 allowed_tool_names: BTreeSet<String>,
319 skipped: BTreeMap<usize, UserContent>,
325 recovered: bool,
326 any_skipped: bool,
327 has_tool_calls: bool,
328}
329
330fn pending_invalid_call(resolving: &ResolvingState) -> Option<&ToolCall> {
333 match resolving.items.get(resolving.next_index) {
334 Some(AssistantContent::ToolCall(tool_call))
335 if !resolving
336 .allowed_tool_names
337 .contains(&tool_call.function.name) =>
338 {
339 Some(tool_call)
340 }
341 _ => None,
342 }
343}
344
345fn has_tool_calls(items: &[AssistantContent]) -> bool {
346 items
347 .iter()
348 .any(|item| matches!(item, AssistantContent::ToolCall(_)))
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
352struct TurnState {
353 message_id: Option<String>,
354 items: Vec<AssistantContent>,
355 has_tool_calls: bool,
356 skipped: BTreeMap<usize, UserContent>,
358 #[serde(default)]
361 internal_call_ids: Vec<(String, String)>,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
365enum RunState {
366 PreparingRequest,
368 AwaitingModel,
370 ResolvingToolCalls(Box<ResolvingState>),
373 AwaitingAdvance(Box<TurnState>),
376 ExecutingTools(Vec<PendingToolCall>),
380 Done(Box<PromptResponse>),
382 Failed,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct AgentRun {
390 max_turns: usize,
391 max_invalid_tool_call_retries: usize,
392 tool_choice: Option<ToolChoice>,
393 #[serde(default)]
397 output_tool_name: Option<String>,
398 #[serde(default)]
401 output_schema: Option<serde_json::Value>,
402 #[serde(default)]
406 max_output_retries: usize,
407 #[serde(default)]
408 output_retries: usize,
409 chat_history: Option<Vec<Message>>,
410 new_messages: Vec<Message>,
411 current_turn: usize,
412 usage: Usage,
413 completion_calls: Vec<CompletionCall>,
414 completion_call_index: usize,
415 invalid_tool_call_retries: usize,
416 #[serde(default)]
419 rollback_pending: bool,
420 #[serde(default)]
424 streamed_completion_call_recorded: bool,
425 state: RunState,
426}
427
428impl AgentRun {
429 pub fn new(prompt: impl Into<Message>) -> Self {
432 Self {
433 max_turns: 1,
434 max_invalid_tool_call_retries: 0,
435 tool_choice: None,
436 output_tool_name: None,
437 output_schema: None,
438 max_output_retries: 0,
439 output_retries: 0,
440 chat_history: None,
441 new_messages: vec![prompt.into()],
442 current_turn: 0,
443 usage: Usage::new(),
444 completion_calls: Vec::new(),
445 completion_call_index: 0,
446 invalid_tool_call_retries: 0,
447 rollback_pending: false,
448 streamed_completion_call_recorded: false,
449 state: RunState::PreparingRequest,
450 }
451 }
452
453 pub fn with_history(mut self, history: Vec<Message>) -> Self {
455 self.chat_history = Some(history);
456 self
457 }
458
459 pub fn max_turns(mut self, max_turns: usize) -> Self {
464 self.max_turns = max_turns;
465 self
466 }
467
468 pub fn with_output_validation(
473 mut self,
474 output_schema: Option<serde_json::Value>,
475 max_output_retries: usize,
476 ) -> Self {
477 self.output_schema = output_schema;
478 self.max_output_retries = max_output_retries;
479 self
480 }
481
482 fn missing_required_output_fields(&self, args: &serde_json::Value) -> Vec<String> {
488 let Some(required) = self
489 .output_schema
490 .as_ref()
491 .and_then(|schema| schema.get("required"))
492 .and_then(|required| required.as_array())
493 else {
494 return Vec::new();
495 };
496 let object = args.as_object();
497 required
498 .iter()
499 .filter_map(|field| field.as_str())
500 .filter(|field| object.is_none_or(|object| !object.contains_key(*field)))
501 .map(str::to_owned)
502 .collect()
503 }
504
505 fn text_satisfies_output_schema(&self, text: &str) -> bool {
509 serde_json::from_str::<serde_json::Value>(text.trim())
510 .ok()
511 .is_some_and(|value| self.missing_required_output_fields(&value).is_empty())
512 }
513
514 fn can_reprompt_for_output(&self) -> bool {
518 self.output_retries < self.max_output_retries && self.current_turn < self.max_turns
519 }
520
521 fn reprompt_for_output(&mut self) -> Result<AgentRunStep, PromptError> {
526 self.output_retries += 1;
527 self.state = RunState::PreparingRequest;
528 self.next_step()
529 }
530
531 pub fn max_invalid_tool_call_retries(mut self, retries: usize) -> Self {
535 self.max_invalid_tool_call_retries = retries;
536 self
537 }
538
539 pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
543 self.tool_choice = Some(tool_choice);
544 self
545 }
546
547 pub fn with_output_tool_name(mut self, name: impl Into<String>) -> Self {
551 self.output_tool_name = Some(name.into());
552 self
553 }
554
555 pub(crate) fn set_output_tool_name(&mut self, name: Option<String>) {
559 if self.output_tool_name.is_none() {
563 self.output_tool_name = name;
564 }
565 }
566
567 pub(crate) fn output_tool_name(&self) -> Option<&str> {
571 self.output_tool_name.as_deref()
572 }
573
574 pub fn usage(&self) -> Usage {
576 self.usage
577 }
578
579 pub fn turn(&self) -> usize {
581 self.current_turn
582 }
583
584 pub fn completion_calls(&self) -> &[CompletionCall] {
586 &self.completion_calls
587 }
588
589 pub fn messages(&self) -> &[Message] {
592 &self.new_messages
593 }
594
595 pub(crate) fn accepted_turn_choice(&self) -> Option<Vec<AssistantContent>> {
597 let RunState::AwaitingAdvance(turn) = &self.state else {
598 return None;
599 };
600
601 if turn.items.is_empty() {
606 return None;
607 }
608 Some(turn.items.clone())
609 }
610
611 pub fn retry_model_turn(&mut self, request: RetryRequest) -> Result<(), PromptError> {
625 let turn = match std::mem::replace(&mut self.state, RunState::Failed) {
626 RunState::AwaitingAdvance(turn) => turn,
627 other => {
628 self.state = other;
629 return Err(self.protocol_violation(
630 "retry_model_turn called without an accepted turn awaiting advancement",
631 ));
632 }
633 };
634
635 if turn.has_tool_calls {
636 return Err(PromptError::prompt_cancelled(
637 self.full_history(),
638 "model-turn retry does not support tool-bearing model turns; use tool-call hooks instead",
639 ));
640 }
641
642 match request {
643 RetryRequest::Repeat => {}
644 RetryRequest::Feedback(feedback) => {
645 let content = turn.items;
653 if !is_empty_assistant_turn(&content) {
654 self.new_messages.push(Message::Assistant {
655 id: turn.message_id,
656 content,
657 });
658 }
659 self.new_messages.push(Message::user(feedback));
660 }
661 }
662
663 self.state = RunState::PreparingRequest;
664 Ok(())
665 }
666
667 pub fn full_history(&self) -> Vec<Message> {
669 build_full_history(self.chat_history.as_deref(), self.new_messages.clone())
670 }
671
672 pub fn is_done(&self) -> bool {
674 matches!(self.state, RunState::Done(_))
675 }
676
677 pub fn response(&self) -> Option<&PromptResponse> {
682 match &self.state {
683 RunState::Done(response) => Some(response),
684 _ => None,
685 }
686 }
687
688 pub fn cancel_error(&self, reason: impl Into<String>) -> PromptError {
691 PromptError::prompt_cancelled(self.full_history(), reason)
692 }
693
694 pub fn pending_invalid_tool_call(&self) -> Option<InvalidToolCallContext> {
698 let RunState::ResolvingToolCalls(resolving) = &self.state else {
699 return None;
700 };
701 let tool_call = pending_invalid_call(resolving)?;
702
703 Some(InvalidToolCallContext {
704 tool_name: tool_call.function.name.clone(),
705 tool_call_id: Some(tool_call.id.as_str().to_owned()),
706 internal_call_id: None,
707 args: Some(json_utils::serialize_json_value(
708 &tool_call.function.arguments,
709 )),
710 available_tools: resolving.executable_tool_names.iter().cloned().collect(),
711 allowed_tools: resolving.allowed_tool_names.iter().cloned().collect(),
712 tool_choice: self.tool_choice.clone(),
713 chat_history: self.diagnostic_history(resolving),
714 is_streaming: false,
715 })
716 }
717
718 pub fn next_step(&mut self) -> Result<AgentRunStep, PromptError> {
726 match std::mem::replace(&mut self.state, RunState::Failed) {
727 RunState::PreparingRequest => {
728 let Some((prompt_ref, history_for_turn)) = self.new_messages.split_last() else {
729 return Err(PromptError::prompt_cancelled(
730 self.full_history(),
731 "prompt loop lost its pending prompt",
732 ));
733 };
734 let prompt = prompt_ref.clone();
735
736 if self.current_turn >= self.max_turns {
737 return Err(PromptError::MaxTurnsError {
738 max_turns: self.max_turns,
739 chat_history: self.full_history().into(),
740 prompt: prompt.into(),
741 });
742 }
743
744 let history =
745 build_history_for_request(self.chat_history.as_deref(), history_for_turn);
746 self.current_turn += 1;
747 self.rollback_pending = false;
748 self.streamed_completion_call_recorded = false;
749 self.state = RunState::AwaitingModel;
750 Ok(AgentRunStep::CallModel {
751 prompt,
752 history,
753 turn: self.current_turn,
754 })
755 }
756 RunState::AwaitingAdvance(turn_state) => {
757 let TurnState {
758 message_id,
759 items,
760 has_tool_calls,
761 skipped,
762 mut internal_call_ids,
763 } = *turn_state;
764 if has_tool_calls
769 && let Some(output_tool_name) = self.output_tool_name.clone()
770 && let Some(tool_call) = items.iter().find_map(|item| match item {
771 AssistantContent::ToolCall(tc) if tc.function.name == output_tool_name => {
772 Some(tc)
773 }
774 _ => None,
775 })
776 {
777 let output_tool_calls = items
778 .iter()
779 .filter(|item| {
780 matches!(
781 item,
782 AssistantContent::ToolCall(tc)
783 if tc.function.name == output_tool_name
784 )
785 })
786 .count();
787 let args = tool_call.function.arguments.clone();
788 let tool_call_id = tool_call.id.clone();
789 let output = json_utils::serialize_json_value(&args);
790
791 let missing = self.missing_required_output_fields(&args);
795 if !missing.is_empty() && self.can_reprompt_for_output() {
796 self.new_messages.push(Message::Assistant {
797 id: message_id,
798 content: items.clone(),
799 });
800 let feedback = format!(
801 "The `{output_tool_name}` arguments were missing required field(s): \
802 {}. Call `{output_tool_name}` again with every required field.",
803 missing.join(", ")
804 );
805 if let Some(user_message) =
806 invalid_tool_retry_user_message(&items, &tool_call_id, feedback)
807 {
808 self.new_messages.push(user_message);
809 }
810 return self.reprompt_for_output();
811 }
812
813 let mut final_items: Vec<AssistantContent> = items
819 .iter()
820 .filter(|item| !matches!(item, AssistantContent::ToolCall(_)))
821 .cloned()
822 .collect();
823 final_items.push(AssistantContent::text(output.clone()));
824 self.new_messages.push(Message::Assistant {
825 id: message_id,
826 content: final_items.clone(),
827 });
828
829 return Ok(self.finish(output, final_items, output_tool_calls));
830 }
831
832 if !is_empty_assistant_turn(&items) {
839 self.new_messages.push(Message::Assistant {
840 id: message_id,
841 content: items.clone(),
842 });
843 }
844
845 if turn_delivered_no_answer(&items)
871 && let Some(reason) = self.truncating_finish_reason()
872 {
873 let remedy = match reason {
883 FinishReason::Length => {
884 "the turn ran out of output budget before producing one — \
885 raise max_tokens for this request"
886 }
887 FinishReason::ContentFilter => {
888 "the provider filtered the response — the content, not the \
889 budget, is what it objected to"
890 }
891 _ => "the turn ended before producing one",
894 };
895 return Err(CompletionError::ResponseError(format!(
896 "the model produced no answer and stopped with \
897 finish_reason={reason:?}; {remedy}"
898 ))
899 .into());
900 }
901
902 if has_tool_calls {
903 self.output_retries = 0;
908 let calls: Vec<PendingToolCall> = items
909 .iter()
910 .enumerate()
911 .filter_map(|(index, item)| match item {
912 AssistantContent::ToolCall(tool_call) => {
913 let internal_call_id = internal_call_ids
917 .iter()
918 .position(|(id, _)| tool_call.id == id.as_str())
919 .map(|pair| internal_call_ids.remove(pair).1);
920 Some(PendingToolCall {
921 tool_call: tool_call.clone(),
922 preresolved_result: skipped.get(&index).cloned(),
923 internal_call_id,
924 })
925 }
926 _ => None,
927 })
928 .collect();
929 self.state = RunState::ExecutingTools(calls.clone());
930 Ok(AgentRunStep::CallTools { calls })
931 } else {
932 if let Some(output_tool_name) = self.output_tool_name.clone()
943 && !is_empty_assistant_turn(&items)
944 && self.can_reprompt_for_output()
945 && !self.text_satisfies_output_schema(&assistant_text_from_choice(&items))
946 {
947 let feedback = format!(
948 "Provide your final answer by calling the `{output_tool_name}` tool \
949 with the structured result as its arguments, not as plain text."
950 );
951 self.new_messages.push(Message::user(feedback));
952 return self.reprompt_for_output();
953 }
954
955 Ok(self.finish(assistant_text_from_choice(&items), items, 0))
956 }
957 }
958 RunState::ExecutingTools(calls) => {
959 let step = AgentRunStep::CallTools {
962 calls: calls.clone(),
963 };
964 self.state = RunState::ExecutingTools(calls);
965 Ok(step)
966 }
967 RunState::Done(response) => {
968 let step = AgentRunStep::Done((*response).clone());
969 self.state = RunState::Done(response);
970 Ok(step)
971 }
972 state @ (RunState::AwaitingModel | RunState::ResolvingToolCalls(_)) => {
973 let reason = match &state {
974 RunState::AwaitingModel => {
975 "next_step called while a model response is pending; feed it via model_response first"
976 }
977 _ => {
978 "next_step called while an invalid tool-call resolution is pending; answer it via resolve_invalid_tool_call first"
979 }
980 };
981 self.state = state;
982 Err(self.protocol_violation(reason))
983 }
984 RunState::Failed => Err(self.protocol_violation(
985 "next_step called after the run already failed or was misdriven",
986 )),
987 }
988 }
989
990 pub fn model_response(&mut self, turn: ModelTurn) -> Result<ModelTurnOutcome, PromptError> {
996 if !matches!(self.state, RunState::AwaitingModel) {
997 return Err(
998 self.protocol_violation("model_response called without a pending CallModel step")
999 );
1000 }
1001 if self.streamed_completion_call_recorded {
1002 return Err(self.protocol_violation(
1003 "model_response called after record_streamed_completion_call for the same turn; feed streamed turns via streamed_turn",
1004 ));
1005 }
1006
1007 self.record_completion_call(
1008 turn.usage,
1009 ResponseIdentity {
1010 message_id: turn.message_id.clone(),
1011 response_id: turn.response_id.clone(),
1012 provider_request_id: turn.provider_request_id.clone(),
1013 },
1014 turn.finish_reason.clone(),
1015 turn.raw.clone(),
1016 );
1017
1018 let items: Vec<AssistantContent> = turn.choice.clone();
1019 let has_tool_calls = has_tool_calls(&items);
1020
1021 self.state = RunState::ResolvingToolCalls(Box::new(ResolvingState {
1022 message_id: turn.message_id,
1023 original_choice: turn.choice,
1024 items,
1025 next_index: 0,
1026 executable_tool_names: turn.executable_tool_names,
1027 allowed_tool_names: turn.allowed_tool_names,
1028 skipped: BTreeMap::new(),
1029 recovered: false,
1030 any_skipped: false,
1031 has_tool_calls,
1032 }));
1033
1034 self.advance_resolution()
1035 }
1036
1037 fn truncating_finish_reason(&self) -> Option<&FinishReason> {
1058 self.completion_calls
1059 .last()?
1060 .finish_reason
1061 .as_ref()
1062 .filter(|reason| reason.truncated_output())
1063 }
1064
1065 fn record_completion_call(
1066 &mut self,
1067 usage: Usage,
1068 identity: ResponseIdentity,
1069 finish_reason: Option<FinishReason>,
1070 raw: serde_json::Value,
1071 ) -> CompletionCall {
1072 let call = CompletionCall::new(self.completion_call_index, usage)
1073 .with_identity(identity)
1074 .with_finish_reason(finish_reason)
1075 .with_raw(raw);
1076 self.completion_call_index += 1;
1077 self.completion_calls.push(call.clone());
1078 self.usage += usage;
1079 call
1080 }
1081
1082 fn finish(
1086 &mut self,
1087 output: String,
1088 content: Vec<AssistantContent>,
1089 output_tool_calls: usize,
1090 ) -> AgentRunStep {
1091 let response = PromptResponse::new(output, self.usage)
1092 .with_messages(self.new_messages.clone())
1093 .with_completion_calls(self.completion_calls.clone())
1094 .with_output_tool_calls(output_tool_calls)
1095 .with_content(content);
1096 self.state = RunState::Done(Box::new(response.clone()));
1097 AgentRunStep::Done(response)
1098 }
1099
1100 fn finalize_turn(
1105 &mut self,
1106 message_id: Option<String>,
1107 items: Vec<AssistantContent>,
1108 has_tool_calls: bool,
1109 skipped: BTreeMap<usize, UserContent>,
1110 internal_call_ids: Vec<(String, String)>,
1111 ) {
1112 self.state = RunState::AwaitingAdvance(Box::new(TurnState {
1113 message_id,
1114 items,
1115 has_tool_calls,
1116 skipped,
1117 internal_call_ids,
1118 }));
1119 }
1120
1121 fn validate_invalid_tool_call_action(
1126 &mut self,
1127 action: InvalidToolCallAction,
1128 diagnostic: InvalidToolCallDiagnostic<'_>,
1129 ) -> Result<ValidatedInvalidToolCallAction, PromptError> {
1130 let result = match action {
1131 InvalidToolCallAction::Fail => Err(diagnostic.unknown_current()),
1132 InvalidToolCallAction::Retry { feedback } => {
1133 if self.invalid_tool_call_retries >= self.max_invalid_tool_call_retries {
1134 Err(diagnostic.unknown_current())
1135 } else {
1136 self.invalid_tool_call_retries += 1;
1137 Ok(ValidatedInvalidToolCallAction::Retry { feedback })
1138 }
1139 }
1140 InvalidToolCallAction::Repair { tool_name } => {
1141 if diagnostic.allowed_tool_names.contains(&tool_name) {
1142 Ok(ValidatedInvalidToolCallAction::Repair { tool_name })
1143 } else {
1144 Err(diagnostic.unknown(tool_name))
1145 }
1146 }
1147 InvalidToolCallAction::Stop { reason } => Err(diagnostic.cancelled(reason)),
1148 InvalidToolCallAction::Skip { reason } => {
1149 if matches!(self.tool_choice, Some(ToolChoice::None)) {
1150 Err(diagnostic.unknown_current())
1151 } else {
1152 Ok(ValidatedInvalidToolCallAction::Skip { reason })
1153 }
1154 }
1155 };
1156
1157 if result.is_err() {
1158 self.state = RunState::Failed;
1159 }
1160 result
1161 }
1162
1163 pub fn resolve_invalid_tool_call(
1179 &mut self,
1180 action: InvalidToolCallAction,
1181 ) -> Result<ModelTurnOutcome, PromptError> {
1182 let mut resolving = self.take_resolving(
1183 "resolve_invalid_tool_call called without a pending invalid tool call",
1184 )?;
1185 let Some(tool_call) = pending_invalid_call(&resolving).cloned() else {
1186 self.state = RunState::ResolvingToolCalls(resolving);
1187 return Err(self.protocol_violation(
1188 "resolve_invalid_tool_call called without a pending invalid tool call",
1189 ));
1190 };
1191
1192 let diagnostic_history = self.diagnostic_history(&resolving);
1193 let action = self.validate_invalid_tool_call_action(
1194 action,
1195 InvalidToolCallDiagnostic {
1196 tool_call: &tool_call,
1197 executable_tool_names: &resolving.executable_tool_names,
1198 allowed_tool_names: &resolving.allowed_tool_names,
1199 history: &diagnostic_history,
1200 },
1201 )?;
1202
1203 match action {
1204 ValidatedInvalidToolCallAction::Retry { feedback } => {
1205 self.new_messages.push(Message::Assistant {
1206 id: resolving.message_id.clone(),
1207 content: resolving.original_choice.clone(),
1208 });
1209 let Some(user_message) = invalid_tool_retry_user_message(
1210 &resolving.original_choice,
1211 &tool_call.id,
1212 feedback,
1213 ) else {
1214 return Err(PromptError::prompt_cancelled(
1215 diagnostic_history,
1216 "invalid tool call retry produced no retry messages",
1217 ));
1218 };
1219 self.new_messages.push(user_message);
1220 self.state = RunState::PreparingRequest;
1221 Ok(ModelTurnOutcome::TurnRetried)
1222 }
1223 ValidatedInvalidToolCallAction::Repair { tool_name } => {
1224 if let Some(AssistantContent::ToolCall(tool_call)) =
1225 resolving.items.get_mut(resolving.next_index)
1226 {
1227 tool_call.function.name = tool_name;
1228 }
1229 resolving.recovered = true;
1230 self.state = RunState::ResolvingToolCalls(resolving);
1231 self.advance_resolution()
1232 }
1233 ValidatedInvalidToolCallAction::Skip { reason } => {
1234 let user_content = UserContent::tool_result_for(
1235 tool_call.id.clone(),
1236 tool_call.provider.clone(),
1237 tool_call.function.name.clone(),
1238 vec![reason.into()],
1239 );
1240 resolving.skipped.insert(resolving.next_index, user_content);
1244 resolving.recovered = true;
1245 resolving.any_skipped = true;
1246 resolving.next_index += 1;
1247 self.state = RunState::ResolvingToolCalls(resolving);
1248 self.advance_resolution()
1249 }
1250 }
1251 }
1252
1253 pub(crate) fn ignore_invalid_tool_call(&mut self) -> Result<ModelTurnOutcome, PromptError> {
1262 let mut resolving = self.take_resolving(
1263 "ignore_invalid_tool_call called without a pending invalid tool call",
1264 )?;
1265
1266 if pending_invalid_call(&resolving).is_none() {
1267 self.state = RunState::ResolvingToolCalls(resolving);
1268 return Err(self.protocol_violation(
1269 "ignore_invalid_tool_call called without a pending invalid tool call",
1270 ));
1271 }
1272
1273 resolving.items.remove(resolving.next_index);
1274 resolving.has_tool_calls = has_tool_calls(&resolving.items);
1275 self.state = RunState::ResolvingToolCalls(resolving);
1280 self.advance_resolution()
1281 }
1282
1283 pub fn tool_results(&mut self, results: Vec<UserContent>) -> Result<(), PromptError> {
1291 let RunState::ExecutingTools(pending) = &self.state else {
1292 return Err(
1293 self.protocol_violation("tool_results called without a pending CallTools step")
1294 );
1295 };
1296 let mut unanswered: Vec<String> = pending
1299 .iter()
1300 .map(|call| call.tool_call.id.as_str().to_owned())
1301 .collect();
1302
1303 if results.is_empty() {
1304 self.state = RunState::Failed;
1305 return Err(PromptError::prompt_cancelled(
1306 self.full_history(),
1307 "tool execution produced no tool results",
1308 ));
1309 }
1310 for result in &results {
1311 let UserContent::ToolResult(tool_result) = result else {
1312 return Err(self.protocol_violation(
1313 "tool_results received content that is not a tool result",
1314 ));
1315 };
1316 let Some(index) = unanswered
1317 .iter()
1318 .position(|id| tool_result.call == id.as_str())
1319 else {
1320 return Err(self.protocol_violation(&format!(
1321 "tool_results received a result for unknown or already-answered tool call id `{}`",
1322 tool_result.call
1323 )));
1324 };
1325 unanswered.swap_remove(index);
1326 }
1327 if !unanswered.is_empty() {
1328 return Err(self.protocol_violation(&format!(
1329 "tool_results left pending tool call id(s) unanswered: {unanswered:?}"
1330 )));
1331 }
1332
1333 self.new_messages.push(Message::User { content: results });
1334 self.state = RunState::PreparingRequest;
1335 Ok(())
1336 }
1337
1338 fn take_resolving(&mut self, violation: &str) -> Result<Box<ResolvingState>, PromptError> {
1342 match std::mem::replace(&mut self.state, RunState::Failed) {
1343 RunState::ResolvingToolCalls(resolving) => Ok(resolving),
1344 other => {
1345 self.state = other;
1346 Err(self.protocol_violation(violation))
1347 }
1348 }
1349 }
1350
1351 fn advance_resolution(&mut self) -> Result<ModelTurnOutcome, PromptError> {
1354 let mut resolving =
1355 self.take_resolving("internal: advance_resolution outside of tool-call resolution")?;
1356 while let Some(item) = resolving.items.get(resolving.next_index) {
1357 match item {
1358 AssistantContent::ToolCall(tool_call)
1359 if !resolving
1360 .allowed_tool_names
1361 .contains(&tool_call.function.name) =>
1362 {
1363 break;
1364 }
1365 _ => resolving.next_index += 1,
1366 }
1367 }
1368
1369 if resolving.next_index < resolving.items.len() {
1370 self.state = RunState::ResolvingToolCalls(resolving);
1371 return match self.pending_invalid_tool_call() {
1372 Some(context) => Ok(ModelTurnOutcome::NeedsResolution(context)),
1373 None => Err(self.protocol_violation(
1374 "internal: pending invalid tool call could not be derived",
1375 )),
1376 };
1377 }
1378
1379 let ResolvingState {
1380 message_id,
1381 items,
1382 mut skipped,
1383 recovered,
1384 any_skipped,
1385 has_tool_calls,
1386 ..
1387 } = *resolving;
1388
1389 if any_skipped {
1392 for (index, item) in items.iter().enumerate() {
1393 if let AssistantContent::ToolCall(tool_call) = item {
1394 skipped.entry(index).or_insert_with(|| {
1395 tool_result_message(
1396 tool_call.id.clone(),
1397 tool_call.provider.clone(),
1398 tool_call.function.name.clone(),
1399 TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER.to_string(),
1400 )
1401 });
1402 }
1403 }
1404 }
1405
1406 self.finalize_turn(message_id, items, has_tool_calls, skipped, Vec::new());
1407 Ok(ModelTurnOutcome::Continue {
1408 response_hook_suppressed: recovered,
1409 })
1410 }
1411
1412 pub fn record_streamed_completion_call(
1431 &mut self,
1432 usage: Usage,
1433 identity: ResponseIdentity,
1434 finish_reason: Option<FinishReason>,
1435 raw: serde_json::Value,
1436 ) -> Result<CompletionCall, PromptError> {
1437 let recordable = matches!(self.state, RunState::AwaitingModel)
1438 || (matches!(self.state, RunState::PreparingRequest) && self.rollback_pending);
1439 if !recordable {
1440 return Err(self.protocol_violation(
1441 "record_streamed_completion_call called without a pending or rolled-back CallModel step",
1442 ));
1443 }
1444 if self.streamed_completion_call_recorded {
1445 return Err(self.protocol_violation(
1446 "record_streamed_completion_call called twice for the same model turn",
1447 ));
1448 }
1449 self.streamed_completion_call_recorded = true;
1450
1451 Ok(self.record_completion_call(usage, identity, finish_reason, raw))
1452 }
1453
1454 pub fn streamed_invalid_tool_call_context(
1457 &self,
1458 partial: &PartialStreamedTurn,
1459 invalid: &StreamedInvalidToolCall,
1460 ) -> InvalidToolCallContext {
1461 InvalidToolCallContext {
1462 tool_name: invalid.tool_call.function.name.clone(),
1463 tool_call_id: Some(invalid.tool_call.id.as_str().to_owned()),
1464 internal_call_id: Some(invalid.internal_call_id.clone()),
1465 args: invalid.args.clone(),
1466 available_tools: invalid.executable_tool_names.iter().cloned().collect(),
1467 allowed_tools: invalid.allowed_tool_names.iter().cloned().collect(),
1468 tool_choice: self.tool_choice.clone(),
1469 chat_history: self
1470 .streamed_diagnostic_history(partial, Some(invalid.tool_call.clone())),
1471 is_streaming: true,
1472 }
1473 }
1474
1475 pub fn resolve_streamed_invalid_tool_call(
1483 &mut self,
1484 partial: &PartialStreamedTurn,
1485 invalid: &StreamedInvalidToolCall,
1486 action: InvalidToolCallAction,
1487 ) -> Result<StreamedResolution, PromptError> {
1488 if !matches!(self.state, RunState::AwaitingModel) {
1489 return Err(self.protocol_violation(
1490 "resolve_streamed_invalid_tool_call called without a pending CallModel step",
1491 ));
1492 }
1493
1494 let diagnostic_history =
1495 self.streamed_diagnostic_history(partial, Some(invalid.tool_call.clone()));
1496 let action = self.validate_invalid_tool_call_action(
1497 action,
1498 InvalidToolCallDiagnostic {
1499 tool_call: &invalid.tool_call,
1500 executable_tool_names: &invalid.executable_tool_names,
1501 allowed_tool_names: &invalid.allowed_tool_names,
1502 history: &diagnostic_history,
1503 },
1504 )?;
1505
1506 match action {
1507 ValidatedInvalidToolCallAction::Retry { feedback } => self.abandon_streamed_turn(
1508 partial,
1509 invalid,
1510 feedback,
1511 diagnostic_history,
1512 "invalid tool call retry produced no retry messages",
1513 None,
1514 ),
1515 ValidatedInvalidToolCallAction::Repair { tool_name } => {
1516 Ok(StreamedResolution::Repaired { tool_name })
1517 }
1518 ValidatedInvalidToolCallAction::Skip { reason } => {
1519 let skipped_tool_result = ToolResult {
1523 call: invalid.tool_call.id.clone(),
1524 provider: invalid.tool_call.provider.clone(),
1525 name: invalid.tool_call.function.name.clone(),
1526 content: vec![ToolResultContent::text(reason.clone())],
1527 };
1528 self.abandon_streamed_turn(
1529 partial,
1530 invalid,
1531 reason,
1532 diagnostic_history,
1533 "invalid tool call skip produced no recovery messages",
1534 Some(Box::new(skipped_tool_result)),
1535 )
1536 }
1537 }
1538 }
1539
1540 fn abandon_streamed_turn(
1544 &mut self,
1545 partial: &PartialStreamedTurn,
1546 invalid: &StreamedInvalidToolCall,
1547 feedback: String,
1548 diagnostic_history: Vec<Message>,
1549 no_messages_reason: &str,
1550 skipped_tool_result: Option<Box<ToolResult>>,
1551 ) -> Result<StreamedResolution, PromptError> {
1552 let Some((assistant_message, user_message)) =
1553 partial.rollback_messages(invalid.tool_call.clone(), feedback)
1554 else {
1555 self.state = RunState::Failed;
1556 return Err(PromptError::prompt_cancelled(
1557 diagnostic_history,
1558 no_messages_reason,
1559 ));
1560 };
1561 self.new_messages.push(assistant_message);
1562 self.new_messages.push(user_message);
1563 self.rollback_pending = true;
1564 self.state = RunState::PreparingRequest;
1565 Ok(StreamedResolution::TurnAbandoned {
1566 skipped_tool_result,
1567 })
1568 }
1569
1570 pub fn streamed_turn(&mut self, turn: StreamedTurn) -> Result<(), PromptError> {
1577 if !matches!(self.state, RunState::AwaitingModel) {
1578 return Err(
1579 self.protocol_violation("streamed_turn called without a pending CallModel step")
1580 );
1581 }
1582
1583 if !self.streamed_completion_call_recorded {
1587 self.record_completion_call(
1594 Usage::new(),
1595 ResponseIdentity {
1596 message_id: turn.message_id.clone(),
1597 ..ResponseIdentity::default()
1598 },
1599 turn.finish_reason.clone(),
1600 serde_json::Value::Null,
1604 );
1605 self.streamed_completion_call_recorded = true;
1606 }
1607
1608 let has_tool_calls = has_tool_calls(&turn.choice);
1609
1610 for item in &turn.choice {
1611 let AssistantContent::ToolCall(tool_call) = item else {
1612 continue;
1613 };
1614 if !turn.allowed_tool_names.contains(&tool_call.function.name) {
1615 let mut diagnostic_messages = self.new_messages.clone();
1616 if !is_empty_assistant_turn(&turn.choice) {
1617 diagnostic_messages.push(Message::Assistant {
1618 id: turn.message_id.clone(),
1619 content: turn.choice.clone(),
1620 });
1621 }
1622 let diagnostic_history =
1623 build_full_history(self.chat_history.as_deref(), diagnostic_messages);
1624 self.state = RunState::Failed;
1625 return Err(unknown_tool_call_error(
1626 tool_call.function.name.clone(),
1627 turn.executable_tool_names.iter().cloned().collect(),
1628 turn.allowed_tool_names.iter().cloned().collect(),
1629 diagnostic_history,
1630 ));
1631 }
1632 }
1633
1634 self.finalize_turn(
1635 turn.message_id,
1636 turn.choice,
1637 has_tool_calls,
1638 BTreeMap::new(),
1639 turn.internal_call_ids,
1640 );
1641 Ok(())
1642 }
1643
1644 fn streamed_diagnostic_history(
1647 &self,
1648 partial: &PartialStreamedTurn,
1649 current_tool_call: Option<ToolCall>,
1650 ) -> Vec<Message> {
1651 let mut messages = self.new_messages.clone();
1652 if let Some(assistant) = partial.assistant_message(current_tool_call) {
1653 messages.push(assistant);
1654 }
1655 build_full_history(self.chat_history.as_deref(), messages)
1656 }
1657
1658 fn diagnostic_history(&self, resolving: &ResolvingState) -> Vec<Message> {
1661 let mut diagnostic_messages = self.new_messages.clone();
1662 diagnostic_messages.push(Message::Assistant {
1663 id: resolving.message_id.clone(),
1664 content: resolving.original_choice.clone(),
1665 });
1666 build_full_history(self.chat_history.as_deref(), diagnostic_messages)
1667 }
1668
1669 fn protocol_violation(&self, reason: &str) -> PromptError {
1670 PromptError::prompt_cancelled(
1671 self.full_history(),
1672 format!("agent run driver protocol violation: {reason}"),
1673 )
1674 }
1675}
1676
1677#[cfg(test)]
1678mod tests {
1679 use super::*;
1680 use rig_core::message::{ToolFunction, ToolResultContent};
1681 use serde_json::json;
1682
1683 fn tool_names(names: &[&str]) -> BTreeSet<String> {
1684 names.iter().map(|name| (*name).to_string()).collect()
1685 }
1686
1687 fn usage(input_tokens: u64, output_tokens: u64) -> Usage {
1688 Usage {
1689 input_tokens,
1690 output_tokens,
1691 total_tokens: input_tokens + output_tokens,
1692 ..Usage::new()
1693 }
1694 }
1695
1696 fn text_turn(text: &str) -> ModelTurn {
1697 ModelTurn::new(
1698 None,
1699 vec![AssistantContent::text(text)],
1700 Usage::new(),
1701 tool_names(&["add"]),
1702 tool_names(&["add"]),
1703 )
1704 }
1705
1706 fn tool_call(id: &str, name: &str) -> AssistantContent {
1707 AssistantContent::ToolCall(ToolCall::from_wire(
1711 id,
1712 ToolFunction::new(name.to_string(), json!({"x": 1})),
1713 ))
1714 }
1715
1716 fn tool_call_turn(id: &str, name: &str) -> ModelTurn {
1717 ModelTurn::new(
1718 None,
1719 vec![tool_call(id, name)],
1720 Usage::new(),
1721 tool_names(&["add"]),
1722 tool_names(&["add"]),
1723 )
1724 }
1725
1726 fn tool_result(id: &str, output: &str) -> UserContent {
1727 UserContent::tool_result(id, "add", vec![ToolResultContent::text(output)])
1730 }
1731
1732 fn expect_call_model(run: &mut AgentRun) -> (Message, Vec<Message>, usize) {
1733 match run.next_step().expect("next_step should succeed") {
1734 AgentRunStep::CallModel {
1735 prompt,
1736 history,
1737 turn,
1738 } => (prompt, history, turn),
1739 step => panic!("expected CallModel, got {step:?}"),
1740 }
1741 }
1742
1743 fn expect_call_tools(run: &mut AgentRun) -> Vec<PendingToolCall> {
1744 match run.next_step().expect("next_step should succeed") {
1745 AgentRunStep::CallTools { calls } => calls,
1746 step => panic!("expected CallTools, got {step:?}"),
1747 }
1748 }
1749
1750 fn expect_done(run: &mut AgentRun) -> PromptResponse {
1751 match run.next_step().expect("next_step should succeed") {
1752 AgentRunStep::Done(response) => response,
1753 step => panic!("expected Done, got {step:?}"),
1754 }
1755 }
1756
1757 fn expect_continue(outcome: ModelTurnOutcome) -> bool {
1758 match outcome {
1759 ModelTurnOutcome::Continue {
1760 response_hook_suppressed,
1761 } => response_hook_suppressed,
1762 outcome => panic!("expected Continue, got {outcome:?}"),
1763 }
1764 }
1765
1766 fn expect_needs_resolution(outcome: ModelTurnOutcome) -> InvalidToolCallContext {
1767 match outcome {
1768 ModelTurnOutcome::NeedsResolution(context) => context,
1769 outcome => panic!("expected NeedsResolution, got {outcome:?}"),
1770 }
1771 }
1772
1773 #[test]
1774 fn text_only_run_completes_in_one_turn() {
1775 let mut run = AgentRun::new("hello");
1776
1777 let (prompt, history, turn) = expect_call_model(&mut run);
1778 assert_eq!(prompt, Message::user("hello"));
1779 assert!(history.is_empty());
1780 assert_eq!(turn, 1);
1781
1782 let suppressed = expect_continue(
1783 run.model_response(text_turn("hi there"))
1784 .expect("model_response should succeed"),
1785 );
1786 assert!(!suppressed);
1787
1788 let response = expect_done(&mut run);
1789 assert_eq!(response.output, "hi there");
1790 let messages = response.messages.expect("messages should be recorded");
1791 assert_eq!(messages.len(), 2);
1792 assert!(run.is_done());
1793 }
1794
1795 #[test]
1796 fn input_history_prefixes_request_history() {
1797 let mut run = AgentRun::new("question")
1798 .with_history(vec![Message::user("earlier"), Message::assistant("reply")]);
1799
1800 let (_, history, _) = expect_call_model(&mut run);
1801 assert_eq!(
1802 history,
1803 vec![Message::user("earlier"), Message::assistant("reply")]
1804 );
1805
1806 expect_continue(
1807 run.model_response(text_turn("answer"))
1808 .expect("model_response should succeed"),
1809 );
1810 let response = expect_done(&mut run);
1811 assert_eq!(
1813 response
1814 .messages
1815 .expect("messages should be recorded")
1816 .len(),
1817 2
1818 );
1819 }
1820
1821 #[test]
1822 fn repeated_model_turn_reuses_prompt_without_recording_rejected_response() {
1823 let first_usage = usage(10, 3);
1824 let second_usage = usage(7, 2);
1825 let mut run = AgentRun::new("question").max_turns(2);
1826
1827 let (first_prompt, first_history, first_turn) = expect_call_model(&mut run);
1828 assert_eq!(first_prompt, Message::user("question"));
1829 assert!(first_history.is_empty());
1830 assert_eq!(first_turn, 1);
1831 expect_continue(
1832 run.model_response(text_turn("rejected").with_usage_for_test(first_usage))
1833 .expect("first response"),
1834 );
1835
1836 run.retry_model_turn(RetryRequest::Repeat)
1837 .expect("repeat should be accepted");
1838 let (second_prompt, second_history, second_turn) = expect_call_model(&mut run);
1839 assert_eq!(second_prompt, Message::user("question"));
1840 assert!(second_history.is_empty());
1841 assert_eq!(second_turn, 2);
1842 assert_eq!(run.messages(), &[Message::user("question")]);
1843
1844 expect_continue(
1845 run.model_response(text_turn("accepted").with_usage_for_test(second_usage))
1846 .expect("second response"),
1847 );
1848 let response = expect_done(&mut run);
1849 assert_eq!(response.output, "accepted");
1850 assert_eq!(response.usage, first_usage + second_usage);
1851 assert_eq!(response.completion_calls.len(), 2);
1852 let messages = response.messages.expect("response history");
1853 assert_eq!(messages.len(), 2);
1854 assert!(!format!("{messages:?}").contains("rejected"));
1855 }
1856
1857 #[test]
1858 fn feedback_retry_records_rejected_response_and_corrective_prompt() {
1859 let mut run = AgentRun::new("question").max_turns(2);
1860
1861 expect_call_model(&mut run);
1862 expect_continue(
1863 run.model_response(text_turn("rejected"))
1864 .expect("first response"),
1865 );
1866 run.retry_model_turn(RetryRequest::Feedback("try another approach".to_string()))
1867 .expect("feedback retry should be accepted");
1868
1869 let (prompt, history, turn) = expect_call_model(&mut run);
1870 assert_eq!(prompt, Message::user("try another approach"));
1871 assert_eq!(turn, 2);
1872 assert_eq!(
1873 history,
1874 vec![Message::user("question"), Message::assistant("rejected")]
1875 );
1876 }
1877
1878 #[test]
1879 fn repeated_model_turn_consumes_existing_max_turns_budget() {
1880 let mut run = AgentRun::new("question");
1881
1882 expect_call_model(&mut run);
1883 expect_continue(
1884 run.model_response(text_turn("rejected"))
1885 .expect("first response"),
1886 );
1887 run.retry_model_turn(RetryRequest::Repeat)
1888 .expect("state transition itself should succeed");
1889
1890 let err = run.next_step().expect_err("second call must exceed budget");
1891 assert!(matches!(
1892 err,
1893 PromptError::MaxTurnsError { max_turns: 1, .. }
1894 ));
1895 assert_eq!(run.completion_calls().len(), 1);
1896 }
1897
1898 #[test]
1899 fn model_turn_retry_rejects_tool_calls_without_advancing_to_execution() {
1900 let mut run = AgentRun::new("add things").max_turns(2);
1901
1902 expect_call_model(&mut run);
1903 expect_continue(
1904 run.model_response(tool_call_turn("call_1", "add"))
1905 .expect("tool response"),
1906 );
1907 let err = run
1908 .retry_model_turn(RetryRequest::Feedback("do not call tools".to_string()))
1909 .expect_err("tool-bearing retries must fail closed");
1910
1911 let PromptError::PromptCancelled {
1912 chat_history,
1913 reason,
1914 } = err
1915 else {
1916 panic!("tool-bearing retry should return PromptCancelled");
1917 };
1918 assert!(reason.contains("tool-bearing model turns"));
1919 assert!(reason.contains("tool-call hooks"));
1920 assert_eq!(chat_history, vec![Message::user("add things")]);
1921 assert!(run.next_step().is_err(), "failed run cannot execute tools");
1922 }
1923
1924 #[test]
1925 fn tool_roundtrip_threads_history_and_usage() {
1926 let mut run = AgentRun::new("add things").max_turns(2);
1927
1928 expect_call_model(&mut run);
1929 expect_continue(
1930 run.model_response(tool_call_turn("call_1", "add").with_usage_for_test(usage(10, 5)))
1931 .expect("model_response should succeed"),
1932 );
1933
1934 let calls = expect_call_tools(&mut run);
1935 assert_eq!(calls.len(), 1);
1936 assert_eq!(calls[0].tool_call.function.name, "add");
1937 assert!(calls[0].preresolved_result.is_none());
1938
1939 run.tool_results(vec![tool_result("call_1", "2")])
1940 .expect("tool_results should succeed");
1941
1942 let (prompt, history, turn) = expect_call_model(&mut run);
1943 assert_eq!(turn, 2);
1944 assert!(matches!(prompt, Message::User { .. }));
1947 assert_eq!(history.len(), 2);
1948
1949 expect_continue(
1950 run.model_response(text_turn("the answer is 2").with_usage_for_test(usage(20, 7)))
1951 .expect("model_response should succeed"),
1952 );
1953
1954 let response = expect_done(&mut run);
1955 assert_eq!(response.output, "the answer is 2");
1956 assert_eq!(response.usage, usage(30, 12));
1957 assert_eq!(response.completion_calls.len(), 2);
1958 assert_eq!(response.completion_calls[0].call_index, 0);
1959 assert_eq!(response.completion_calls[0].usage, usage(10, 5));
1960 assert_eq!(response.completion_calls[1].usage, usage(20, 7));
1961 assert_eq!(
1963 response
1964 .messages
1965 .expect("messages should be recorded")
1966 .len(),
1967 4
1968 );
1969 }
1970
1971 #[test]
1972 fn parallel_tool_calls_surface_in_emission_order() {
1973 let mut run = AgentRun::new("do both").max_turns(2);
1974
1975 expect_call_model(&mut run);
1976 let turn = ModelTurn::new(
1977 None,
1978 vec![tool_call("call_1", "add"), tool_call("call_2", "add")],
1979 Usage::new(),
1980 tool_names(&["add"]),
1981 tool_names(&["add"]),
1982 );
1983 expect_continue(
1984 run.model_response(turn)
1985 .expect("model_response should succeed"),
1986 );
1987
1988 let calls = expect_call_tools(&mut run);
1989 assert_eq!(calls.len(), 2);
1990 assert_eq!(calls[0].tool_call.id, "call_1");
1991 assert_eq!(calls[1].tool_call.id, "call_2");
1992
1993 run.tool_results(vec![tool_result("call_2", "b"), tool_result("call_1", "a")])
1995 .expect("tool_results should succeed");
1996 let messages = run.messages();
1997 assert!(matches!(
1998 messages.last(),
1999 Some(Message::User { content }) if content.len() == 2
2000 ));
2001 }
2002
2003 #[test]
2004 fn max_turns_zero_rejects_initial_model_call() {
2005 let mut run = AgentRun::new("do not call").max_turns(0);
2006
2007 let err = run
2008 .next_step()
2009 .expect_err("zero budget should emit no call");
2010 assert!(matches!(
2011 err,
2012 PromptError::MaxTurnsError { max_turns: 0, .. }
2013 ));
2014 assert_eq!(run.turn(), 0);
2015 }
2016
2017 #[test]
2018 fn new_implicitly_allows_one_model_call_and_rejects_tool_continuation() {
2019 let mut run = AgentRun::new("add things");
2020
2021 let (_, _, turn) = expect_call_model(&mut run);
2022 assert_eq!(turn, 1);
2023 expect_continue(
2024 run.model_response(tool_call_turn("call_1", "add"))
2025 .expect("model_response should succeed"),
2026 );
2027 expect_call_tools(&mut run);
2028 run.tool_results(vec![tool_result("call_1", "2")])
2029 .expect("tool_results should succeed");
2030
2031 let err = run
2032 .next_step()
2033 .expect_err("second model call should exceed budget");
2034 assert!(matches!(
2035 err,
2036 PromptError::MaxTurnsError { max_turns: 1, .. }
2037 ));
2038 assert_eq!(run.turn(), 1);
2039 }
2040
2041 #[test]
2042 fn max_turns_n_allows_exactly_n_model_calls() {
2043 let mut run = AgentRun::new("loop").max_turns(3);
2044
2045 for (expected_turn, call_id) in [(1, "call_1"), (2, "call_2"), (3, "call_3")] {
2046 let (_, _, turn) = expect_call_model(&mut run);
2047 assert_eq!(turn, expected_turn);
2048 expect_continue(
2049 run.model_response(tool_call_turn(call_id, "add"))
2050 .expect("model_response should succeed"),
2051 );
2052 expect_call_tools(&mut run);
2053 run.tool_results(vec![tool_result(call_id, "0")])
2054 .expect("tool_results should succeed");
2055 }
2056
2057 let err = run
2058 .next_step()
2059 .expect_err("fourth model call should exceed budget");
2060 assert!(matches!(
2061 err,
2062 PromptError::MaxTurnsError { max_turns: 3, .. }
2063 ));
2064 assert_eq!(run.turn(), 3);
2065 }
2066
2067 #[test]
2068 fn invalid_tool_call_fail_returns_unknown_tool_call() {
2069 let mut run = AgentRun::new("call something");
2070
2071 expect_call_model(&mut run);
2072 let context = expect_needs_resolution(
2073 run.model_response(tool_call_turn("call_1", "unknown"))
2074 .expect("model_response should succeed"),
2075 );
2076 assert_eq!(context.tool_name, "unknown");
2077 assert_eq!(context.available_tools, vec!["add".to_string()]);
2078 assert!(!context.is_streaming);
2079 assert_eq!(context.chat_history.len(), 2);
2081
2082 let err = run
2083 .resolve_invalid_tool_call(InvalidToolCallAction::fail())
2084 .expect_err("fail action should error");
2085 assert!(matches!(
2086 err,
2087 PromptError::UnknownToolCall { tool_name, .. } if tool_name == "unknown"
2088 ));
2089 }
2090
2091 #[test]
2092 fn invalid_tool_call_stop_leaves_run_terminal() {
2093 let mut run = AgentRun::new("call something");
2094
2095 expect_call_model(&mut run);
2096 expect_needs_resolution(
2097 run.model_response(tool_call_turn("call_1", "unknown"))
2098 .expect("model_response should succeed"),
2099 );
2100 let err = run
2101 .resolve_invalid_tool_call(InvalidToolCallAction::stop("operator stop"))
2102 .expect_err("stop should cancel the run");
2103 assert!(matches!(
2104 err,
2105 PromptError::PromptCancelled { reason, .. } if reason == "operator stop"
2106 ));
2107
2108 let err = run
2109 .next_step()
2110 .expect_err("a stopped run must remain terminal");
2111 assert!(matches!(
2112 err,
2113 PromptError::PromptCancelled { reason, .. }
2114 if reason.contains("next_step called after the run already failed")
2115 ));
2116 }
2117
2118 #[test]
2119 fn invalid_tool_call_retry_rolls_back_with_feedback() {
2120 let mut run = AgentRun::new("call something")
2121 .max_turns(2)
2122 .max_invalid_tool_call_retries(1);
2123
2124 expect_call_model(&mut run);
2125 expect_needs_resolution(
2126 run.model_response(tool_call_turn("call_1", "unknown"))
2127 .expect("model_response should succeed"),
2128 );
2129 let outcome = run
2130 .resolve_invalid_tool_call(InvalidToolCallAction::retry("use add instead"))
2131 .expect("retry should be accepted");
2132 assert!(matches!(outcome, ModelTurnOutcome::TurnRetried));
2133
2134 assert_eq!(run.messages().len(), 3);
2136 let (prompt, _, turn) = expect_call_model(&mut run);
2137 assert_eq!(turn, 2);
2138 assert!(matches!(
2139 prompt,
2140 Message::User { ref content }
2141 if matches!(content.first(), Some(UserContent::ToolResult(_)))
2142 ));
2143
2144 expect_needs_resolution(
2146 run.model_response(tool_call_turn("call_2", "unknown"))
2147 .expect("model_response should succeed"),
2148 );
2149 let err = run
2150 .resolve_invalid_tool_call(InvalidToolCallAction::retry("again"))
2151 .expect_err("budget exhausted");
2152 assert!(matches!(err, PromptError::UnknownToolCall { .. }));
2153 }
2154
2155 #[test]
2156 fn invalid_tool_call_retry_cannot_emit_call_past_total_budget() {
2157 let mut run = AgentRun::new("call something")
2158 .max_turns(1)
2159 .max_invalid_tool_call_retries(1);
2160
2161 expect_call_model(&mut run);
2162 expect_needs_resolution(
2163 run.model_response(tool_call_turn("call_1", "unknown"))
2164 .expect("model_response should succeed"),
2165 );
2166 let outcome = run
2167 .resolve_invalid_tool_call(InvalidToolCallAction::retry("use add instead"))
2168 .expect("retry resolution should be accepted");
2169 assert!(matches!(outcome, ModelTurnOutcome::TurnRetried));
2170 assert_eq!(run.completion_calls().len(), 1);
2171
2172 let err = run
2173 .next_step()
2174 .expect_err("retry must not emit a second model call");
2175 assert!(matches!(
2176 err,
2177 PromptError::MaxTurnsError { max_turns: 1, .. }
2178 ));
2179 assert_eq!(run.turn(), 1);
2180 }
2181
2182 #[test]
2183 fn invalid_tool_call_repair_renames_and_suppresses_response_hook() {
2184 let mut run = AgentRun::new("call something").max_turns(2);
2185
2186 expect_call_model(&mut run);
2187 expect_needs_resolution(
2188 run.model_response(tool_call_turn("call_1", "default_api"))
2189 .expect("model_response should succeed"),
2190 );
2191 let suppressed = expect_continue(
2192 run.resolve_invalid_tool_call(InvalidToolCallAction::repair("add"))
2193 .expect("repair should be accepted"),
2194 );
2195 assert!(suppressed);
2196
2197 let calls = expect_call_tools(&mut run);
2198 assert_eq!(calls[0].tool_call.function.name, "add");
2199 assert!(calls[0].preresolved_result.is_none());
2200 }
2201
2202 #[test]
2203 fn invalid_tool_call_repair_to_disallowed_name_fails() {
2204 let mut run = AgentRun::new("call something");
2205
2206 expect_call_model(&mut run);
2207 expect_needs_resolution(
2208 run.model_response(tool_call_turn("call_1", "unknown"))
2209 .expect("model_response should succeed"),
2210 );
2211 let err = run
2212 .resolve_invalid_tool_call(InvalidToolCallAction::repair("also_unknown"))
2213 .expect_err("repair to disallowed name should fail");
2214 assert!(matches!(
2215 err,
2216 PromptError::UnknownToolCall { tool_name, .. } if tool_name == "also_unknown"
2217 ));
2218 }
2219
2220 #[test]
2221 fn invalid_tool_call_skip_suppresses_all_peer_executions() {
2222 let mut run = AgentRun::new("call things").max_turns(2);
2223
2224 expect_call_model(&mut run);
2225 let turn = ModelTurn::new(
2226 None,
2227 vec![tool_call("call_1", "unknown"), tool_call("call_2", "add")],
2228 Usage::new(),
2229 tool_names(&["add"]),
2230 tool_names(&["add"]),
2231 );
2232 expect_needs_resolution(
2233 run.model_response(turn)
2234 .expect("model_response should succeed"),
2235 );
2236 let suppressed = expect_continue(
2237 run.resolve_invalid_tool_call(InvalidToolCallAction::skip("not available"))
2238 .expect("skip should be accepted"),
2239 );
2240 assert!(suppressed);
2241
2242 let calls = expect_call_tools(&mut run);
2243 assert_eq!(calls.len(), 2);
2244 assert!(calls.iter().all(|call| call.preresolved_result.is_some()));
2246 }
2247
2248 #[test]
2254 fn id_less_calls_keep_distinct_skip_results() {
2255 let mut run = AgentRun::new("call things").max_turns(2);
2256
2257 expect_call_model(&mut run);
2258 let turn = ModelTurn::new(
2259 None,
2260 vec![tool_call("", "unknown"), tool_call("", "add")],
2261 Usage::new(),
2262 tool_names(&["add"]),
2263 tool_names(&["add"]),
2264 );
2265 expect_needs_resolution(
2266 run.model_response(turn)
2267 .expect("model_response should succeed"),
2268 );
2269 let suppressed = expect_continue(
2270 run.resolve_invalid_tool_call(InvalidToolCallAction::skip("not available"))
2271 .expect("skip should be accepted"),
2272 );
2273 assert!(suppressed);
2274
2275 let calls = expect_call_tools(&mut run);
2276 assert_eq!(calls.len(), 2);
2277 assert_ne!(
2278 calls[0].tool_call.id, calls[1].tool_call.id,
2279 "id-less calls mint distinct correlation handles, never a shared sentinel"
2280 );
2281 assert!(
2282 calls
2283 .iter()
2284 .all(|call| !call.tool_call.id.is_empty() && call.tool_call.provider.is_none()),
2285 "minted handles are non-empty and record the provider's absence"
2286 );
2287 let results: Vec<String> = calls
2288 .iter()
2289 .map(|call| match call.preresolved_result.as_ref() {
2290 Some(rig_core::message::UserContent::ToolResult(result)) => result
2291 .content
2292 .iter()
2293 .filter_map(|content| match content {
2294 rig_core::message::ToolResultContent::Text(text) => Some(text.text.clone()),
2295 _ => None,
2296 })
2297 .collect(),
2298 _ => panic!("both calls carry preresolved results"),
2299 })
2300 .collect();
2301 assert_eq!(
2302 results[0], "not available",
2303 "the skipped call reads its own feedback"
2304 );
2305 assert_eq!(
2306 results[1], TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER,
2307 "the peer reads its own synthetic result, not the skipped one\'s"
2308 );
2309 }
2310
2311 #[test]
2312 fn skip_under_tool_choice_none_fails() {
2313 let mut run = AgentRun::new("call something").with_tool_choice(ToolChoice::None);
2314
2315 expect_call_model(&mut run);
2316 expect_needs_resolution(
2317 run.model_response(ModelTurn::new(
2318 None,
2319 vec![tool_call("call_1", "add")],
2320 Usage::new(),
2321 tool_names(&["add"]),
2322 BTreeSet::new(),
2323 ))
2324 .expect("model_response should succeed"),
2325 );
2326 let err = run
2327 .resolve_invalid_tool_call(InvalidToolCallAction::skip("nope"))
2328 .expect_err("skip under ToolChoice::None should fail");
2329 assert!(matches!(err, PromptError::UnknownToolCall { .. }));
2330 }
2331
2332 #[test]
2333 fn empty_tool_results_cancel_the_run() {
2334 let mut run = AgentRun::new("call something").max_turns(2);
2335
2336 expect_call_model(&mut run);
2337 expect_continue(
2338 run.model_response(tool_call_turn("call_1", "add"))
2339 .expect("model_response should succeed"),
2340 );
2341 expect_call_tools(&mut run);
2342
2343 let err = run
2344 .tool_results(Vec::new())
2345 .expect_err("empty results should cancel");
2346 assert!(matches!(
2347 err,
2348 PromptError::PromptCancelled { reason, .. }
2349 if reason.contains("tool execution produced no tool results")
2350 ));
2351 }
2352
2353 #[test]
2354 fn out_of_protocol_calls_are_rejected_without_corrupting_state() {
2355 let mut run = AgentRun::new("hello");
2356
2357 let err = run
2358 .tool_results(vec![tool_result("call_1", "x")])
2359 .expect_err("no CallTools pending");
2360 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2361
2362 expect_call_model(&mut run);
2364 let err = run
2365 .next_step()
2366 .expect_err("model response is pending, next_step must be rejected");
2367 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2368 expect_continue(
2369 run.model_response(text_turn("hi"))
2370 .expect("model_response should still succeed"),
2371 );
2372 assert_eq!(expect_done(&mut run).output, "hi");
2373 }
2374
2375 #[test]
2376 fn model_response_rejected_after_streamed_completion_call_record() {
2377 let mut run = AgentRun::new("hello");
2378 expect_call_model(&mut run);
2379 run.record_streamed_completion_call(
2380 Usage::new(),
2381 ResponseIdentity::default(),
2382 None,
2383 serde_json::Value::Null,
2384 )
2385 .expect("record should succeed");
2386
2387 let err = run
2388 .model_response(text_turn("hi"))
2389 .expect_err("mixed streamed/non-streamed ingestion must be rejected");
2390 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2391 assert_eq!(run.completion_calls().len(), 1);
2393 }
2394
2395 #[test]
2396 fn done_step_is_idempotent() {
2397 let mut run = AgentRun::new("hello");
2398 expect_call_model(&mut run);
2399 expect_continue(
2400 run.model_response(text_turn("hi"))
2401 .expect("model_response should succeed"),
2402 );
2403 assert_eq!(expect_done(&mut run).output, "hi");
2404 assert_eq!(expect_done(&mut run).output, "hi");
2405 }
2406
2407 #[test]
2408 fn serialized_run_alone_carries_pending_tool_calls() {
2409 let mut run = AgentRun::new("add things").max_turns(2);
2410 expect_call_model(&mut run);
2411 expect_continue(
2412 run.model_response(tool_call_turn("call_1", "add"))
2413 .expect("model_response should succeed"),
2414 );
2415 expect_call_tools(&mut run);
2416
2417 let serialized = serde_json::to_string(&run).expect("mid-run state should serialize");
2420 drop(run);
2421 let mut resumed: AgentRun =
2422 serde_json::from_str(&serialized).expect("mid-run state should deserialize");
2423
2424 let calls = expect_call_tools(&mut resumed);
2425 assert_eq!(calls.len(), 1);
2426 assert_eq!(calls[0].tool_call.function.name, "add");
2427 let calls_again = expect_call_tools(&mut resumed);
2429 assert_eq!(calls_again[0].tool_call.id, calls[0].tool_call.id);
2430
2431 let results = calls
2433 .iter()
2434 .map(|call| tool_result(&call.tool_call.id, "2"))
2435 .collect::<Vec<_>>();
2436 resumed
2437 .tool_results(results)
2438 .expect("tool_results should succeed");
2439 expect_call_model(&mut resumed);
2440 expect_continue(
2441 resumed
2442 .model_response(text_turn("done"))
2443 .expect("model_response should succeed"),
2444 );
2445 assert_eq!(expect_done(&mut resumed).output, "done");
2446 }
2447
2448 #[test]
2449 fn tool_results_validates_against_pending_calls() {
2450 let drive_to_pending_tools = || {
2451 let mut run = AgentRun::new("add things").max_turns(2);
2452 expect_call_model(&mut run);
2453 expect_continue(
2454 run.model_response(tool_call_turn("call_1", "add"))
2455 .expect("model_response should succeed"),
2456 );
2457 expect_call_tools(&mut run);
2458 run
2459 };
2460
2461 let mut run = drive_to_pending_tools();
2463 let err = run
2464 .tool_results(vec![tool_result("call_unknown", "2")])
2465 .expect_err("unknown tool call id must be rejected");
2466 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2467 run.tool_results(vec![tool_result("call_1", "2")])
2468 .expect("valid results should still be accepted after a rejection");
2469
2470 let mut run = drive_to_pending_tools();
2472 let err = run
2473 .tool_results(vec![tool_result("call_1", "2"), tool_result("call_1", "3")])
2474 .expect_err("answering one call twice must be rejected");
2475 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2476
2477 let mut run = drive_to_pending_tools();
2479 let err = run
2480 .tool_results(vec![UserContent::text("not a tool result")])
2481 .expect_err("non-tool-result content must be rejected");
2482 assert!(matches!(err, PromptError::PromptCancelled { .. }));
2483 }
2484
2485 #[test]
2486 fn agent_run_deserializes_pre_monoid_suspended_state() {
2487 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":[{"type":"toolcall","id":"call_1","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","function":{"name":"add","arguments":{"x":1}},"signature":null,"additional_params":null},"preresolved_result":null,"internal_call_id":null}]}}"#;
2494
2495 let mut restored: AgentRun =
2496 serde_json::from_str(fixture).expect("old-format suspended run should deserialize");
2497 assert_eq!(restored.completion_calls()[0].usage, Usage::new());
2498
2499 let calls = expect_call_tools(&mut restored);
2500 assert_eq!(calls.len(), 1);
2501 restored
2502 .tool_results(vec![tool_result("call_1", "2")])
2503 .expect("tool_results should succeed");
2504 expect_call_model(&mut restored);
2505 }
2506
2507 #[test]
2508 fn serde_round_trip_at_exhausted_budget_preserves_boundary() {
2509 let mut run = AgentRun::new("add things").max_turns(1);
2510 expect_call_model(&mut run);
2511 expect_continue(
2512 run.model_response(tool_call_turn("call_1", "add"))
2513 .expect("model_response should succeed"),
2514 );
2515 expect_call_tools(&mut run);
2516 run.tool_results(vec![tool_result("call_1", "2")])
2517 .expect("tool_results should succeed");
2518
2519 let serialized = serde_json::to_string(&run).expect("exhausted run should serialize");
2520 let mut restored: AgentRun =
2521 serde_json::from_str(&serialized).expect("exhausted run should deserialize");
2522 assert_eq!(restored.completion_calls().len(), 1);
2523 let err = restored
2524 .next_step()
2525 .expect_err("restored run must not emit a second model call");
2526 assert!(matches!(
2527 err,
2528 PromptError::MaxTurnsError { max_turns: 1, .. }
2529 ));
2530 assert_eq!(restored.turn(), 1);
2531 }
2532
2533 #[test]
2534 fn serde_round_trip_mid_run_resumes_identically() {
2535 let drive_to_pending_tools = || {
2536 let mut run = AgentRun::new("add things").max_turns(2);
2537 expect_call_model(&mut run);
2538 expect_continue(
2539 run.model_response(
2540 tool_call_turn("call_1", "add").with_usage_for_test(usage(10, 5)),
2541 )
2542 .expect("model_response should succeed"),
2543 );
2544 expect_call_tools(&mut run);
2545 run
2546 };
2547
2548 let finish = |mut run: AgentRun| {
2549 run.tool_results(vec![tool_result("call_1", "2")])
2550 .expect("tool_results should succeed");
2551 expect_call_model(&mut run);
2552 expect_continue(
2553 run.model_response(text_turn("done").with_usage_for_test(usage(3, 4)))
2554 .expect("model_response should succeed"),
2555 );
2556 expect_done(&mut run)
2557 };
2558
2559 let uninterrupted = finish(drive_to_pending_tools());
2560
2561 let suspended = drive_to_pending_tools();
2562 let serialized = serde_json::to_string(&suspended).expect("mid-run state should serialize");
2563 let restored: AgentRun =
2564 serde_json::from_str(&serialized).expect("mid-run state should deserialize");
2565 let resumed = finish(restored);
2566
2567 assert_eq!(resumed.output, uninterrupted.output);
2568 assert_eq!(resumed.usage, uninterrupted.usage);
2569 assert_eq!(resumed.completion_calls, uninterrupted.completion_calls);
2570 assert_eq!(resumed.messages, uninterrupted.messages);
2574 }
2575
2576 #[test]
2577 fn pending_invalid_tool_call_survives_serde_round_trip() {
2578 let mut run = AgentRun::new("call something");
2579 expect_call_model(&mut run);
2580 let context = expect_needs_resolution(
2581 run.model_response(tool_call_turn("call_1", "unknown"))
2582 .expect("model_response should succeed"),
2583 );
2584
2585 let serialized = serde_json::to_string(&run).expect("state should serialize");
2586 let restored: AgentRun =
2587 serde_json::from_str(&serialized).expect("state should deserialize");
2588 let restored_context = restored
2589 .pending_invalid_tool_call()
2590 .expect("pending resolution should survive serialization");
2591 assert_eq!(restored_context.tool_name, context.tool_name);
2592 assert_eq!(
2593 restored_context.chat_history.len(),
2594 context.chat_history.len()
2595 );
2596 }
2597
2598 fn output_tool_turn(id: &str, name: &str) -> ModelTurn {
2601 ModelTurn::new(
2602 None,
2603 vec![tool_call(id, name)],
2604 Usage::new(),
2605 tool_names(&["add"]),
2606 tool_names(&["add", name]),
2607 )
2608 }
2609
2610 fn output_tool_turn_with_args(id: &str, name: &str, arguments: serde_json::Value) -> ModelTurn {
2611 ModelTurn::new(
2612 None,
2613 vec![AssistantContent::ToolCall(ToolCall::from_wire(
2614 id,
2615 ToolFunction::new(name.to_string(), arguments),
2616 ))],
2617 Usage::new(),
2618 tool_names(&["add"]),
2619 tool_names(&["add", name]),
2620 )
2621 }
2622
2623 fn assert_no_orphan_tool_use(messages: &[Message]) {
2626 let mut answered = BTreeSet::new();
2627 for message in messages {
2628 if let Message::User { content } = message {
2629 for item in content.iter() {
2630 if let UserContent::ToolResult(result) = item {
2631 answered.insert(result.call.to_string());
2632 }
2633 }
2634 }
2635 }
2636 for message in messages {
2637 if let Message::Assistant { content, .. } = message {
2638 for item in content.iter() {
2639 if let AssistantContent::ToolCall(call) = item {
2640 assert!(
2641 answered.contains(call.id.as_str()),
2642 "assistant tool_call {:?} has no matching tool_result in history",
2643 call.id
2644 );
2645 }
2646 }
2647 }
2648 }
2649 }
2650
2651 #[test]
2652 fn output_tool_call_finalizes_run_with_arguments() {
2653 let mut run = AgentRun::new("summarize").with_output_tool_name("final_result");
2654
2655 expect_call_model(&mut run);
2656 expect_continue(
2657 run.model_response(output_tool_turn("call_1", "final_result"))
2658 .expect("model_response should succeed"),
2659 );
2660
2661 let response = expect_done(&mut run);
2663 assert_eq!(response.output, r#"{"x":1}"#);
2664 assert!(run.is_done());
2665
2666 let messages = response.messages.expect("messages should be recorded");
2669 assert_no_orphan_tool_use(&messages);
2670 assert!(matches!(
2671 messages.last(),
2672 Some(Message::Assistant { content, .. })
2673 if assistant_text_from_choice(content) == r#"{"x":1}"#
2674 ));
2675 }
2676
2677 #[test]
2678 fn scalar_output_tool_call_is_serialized_as_reparseable_json() {
2679 let mut run = AgentRun::new("summarize").with_output_tool_name("final_result");
2680
2681 expect_call_model(&mut run);
2682 expect_continue(
2683 run.model_response(output_tool_turn_with_args(
2684 "call_1",
2685 "final_result",
2686 json!("complete"),
2687 ))
2688 .expect("model_response should succeed"),
2689 );
2690
2691 let response = expect_done(&mut run);
2692 assert_eq!(
2693 serde_json::from_str::<serde_json::Value>(&response.output)
2694 .expect("scalar output must remain valid JSON"),
2695 json!("complete")
2696 );
2697 assert_eq!(response.output, r#""complete""#);
2698
2699 let messages = response.messages.expect("messages should be recorded");
2700 assert_no_orphan_tool_use(&messages);
2701 assert!(matches!(
2702 messages.last(),
2703 Some(Message::Assistant { content, .. })
2704 if assistant_text_from_choice(content) == r#""complete""#
2705 ));
2706 }
2707
2708 #[test]
2709 fn output_tool_call_wins_over_sibling_real_tool_calls() {
2710 let mut run = AgentRun::new("do it")
2711 .max_turns(2)
2712 .with_output_tool_name("final_result");
2713
2714 expect_call_model(&mut run);
2715 let turn = ModelTurn::new(
2718 None,
2719 vec![
2720 tool_call("call_1", "add"),
2721 tool_call("call_2", "final_result"),
2722 ],
2723 Usage::new(),
2724 tool_names(&["add"]),
2725 tool_names(&["add", "final_result"]),
2726 );
2727 expect_continue(
2728 run.model_response(turn)
2729 .expect("model_response should succeed"),
2730 );
2731
2732 let response = expect_done(&mut run);
2733 assert_eq!(response.output, r#"{"x":1}"#);
2734 assert!(run.is_done());
2735
2736 let messages = response.messages.expect("messages should be recorded");
2739 assert_no_orphan_tool_use(&messages);
2740 assert!(
2741 messages.iter().all(|message| match message {
2742 Message::Assistant { content, .. } => !content
2743 .iter()
2744 .any(|item| matches!(item, AssistantContent::ToolCall(_))),
2745 _ => true,
2746 }),
2747 "no assistant tool calls should survive in the finalized history"
2748 );
2749 }
2750
2751 #[test]
2752 fn real_tool_calls_still_execute_when_output_tool_unused() {
2753 let mut run = AgentRun::new("add things")
2756 .max_turns(2)
2757 .with_output_tool_name("final_result");
2758
2759 expect_call_model(&mut run);
2760 expect_continue(
2761 run.model_response(tool_call_turn("call_1", "add"))
2762 .expect("model_response should succeed"),
2763 );
2764
2765 let calls = expect_call_tools(&mut run);
2766 assert_eq!(calls.len(), 1);
2767 assert_eq!(calls[0].tool_call.function.name, "add");
2768 }
2769
2770 fn required_field_schema(field: &str) -> serde_json::Value {
2771 json!({
2772 "type": "object",
2773 "required": [field],
2774 "properties": { field: { "type": "string" } },
2775 })
2776 }
2777
2778 #[test]
2779 fn tool_mode_reprompts_when_output_tool_not_called() {
2780 let mut run = AgentRun::new("summarize")
2783 .max_turns(2)
2784 .with_output_tool_name("final_result")
2785 .with_output_validation(Some(required_field_schema("summary")), 1);
2786
2787 expect_call_model(&mut run);
2788 expect_continue(
2789 run.model_response(text_turn("here is the answer"))
2790 .expect("model_response should succeed"),
2791 );
2792
2793 let (prompt, _history, turn) = expect_call_model(&mut run);
2796 assert_eq!(turn, 2);
2797 let prompt_json = serde_json::to_string(&prompt).expect("prompt should serialize");
2798 assert!(
2799 prompt_json.contains("final_result"),
2800 "re-prompt feedback should name the output tool: {prompt_json}"
2801 );
2802 assert!(!run.is_done());
2803 }
2804
2805 #[test]
2806 fn tool_mode_reprompts_when_output_args_missing_required_fields() {
2807 let mut run = AgentRun::new("summarize")
2810 .max_turns(2)
2811 .with_output_tool_name("final_result")
2812 .with_output_validation(Some(required_field_schema("summary")), 1);
2814
2815 expect_call_model(&mut run);
2816 expect_continue(
2817 run.model_response(output_tool_turn("call_1", "final_result"))
2818 .expect("model_response should succeed"),
2819 );
2820
2821 let (_prompt, _history, turn) = expect_call_model(&mut run);
2822 assert_eq!(turn, 2);
2823 assert!(!run.is_done());
2824 }
2825
2826 #[test]
2827 fn tool_mode_accepts_valid_json_text_without_reprompting() {
2828 let mut run = AgentRun::new("summarize")
2831 .max_turns(3)
2832 .with_output_tool_name("final_result")
2833 .with_output_validation(Some(required_field_schema("summary")), 1);
2834
2835 expect_call_model(&mut run);
2836 expect_continue(
2837 run.model_response(text_turn(r#"{"summary":"all good"}"#))
2838 .expect("model_response should succeed"),
2839 );
2840
2841 let response = expect_done(&mut run);
2842 assert_eq!(response.output, r#"{"summary":"all good"}"#);
2843 assert!(run.is_done());
2844 }
2845
2846 #[test]
2847 fn tool_mode_finalizes_best_effort_when_model_call_budget_exhausted() {
2848 let mut run = AgentRun::new("summarize")
2849 .max_turns(1)
2850 .with_output_tool_name("final_result")
2851 .with_output_validation(Some(required_field_schema("summary")), 1);
2852
2853 expect_call_model(&mut run);
2854 expect_continue(
2855 run.model_response(text_turn("invalid output"))
2856 .expect("model_response should succeed"),
2857 );
2858
2859 let response = expect_done(&mut run);
2860 assert_eq!(response.output, "invalid output");
2861 assert_eq!(run.turn(), 1);
2862 }
2863
2864 #[test]
2865 fn tool_mode_finalizes_best_effort_when_output_retry_budget_exhausted() {
2866 let mut run = AgentRun::new("summarize")
2870 .max_turns(3)
2871 .with_output_tool_name("final_result")
2872 .with_output_validation(Some(required_field_schema("summary")), 0);
2873
2874 expect_call_model(&mut run);
2875 expect_continue(
2876 run.model_response(output_tool_turn("call_1", "final_result"))
2877 .expect("model_response should succeed"),
2878 );
2879
2880 let response = expect_done(&mut run);
2881 assert_eq!(response.output, r#"{"x":1}"#);
2882 let messages = response.messages.expect("messages should be recorded");
2883 assert_no_orphan_tool_use(&messages);
2884 }
2885
2886 #[test]
2887 fn set_output_tool_name_is_idempotent_and_only_fills_when_unset() {
2888 let mut run = AgentRun::new("x").with_output_tool_name("first");
2891 run.set_output_tool_name(Some("second".to_string()));
2892 run.set_output_tool_name(None);
2893 assert_eq!(run.output_tool_name.as_deref(), Some("first"));
2894
2895 let mut run = AgentRun::new("x");
2897 run.set_output_tool_name(None);
2898 assert_eq!(run.output_tool_name, None);
2899 run.set_output_tool_name(Some("filled".to_string()));
2900 assert_eq!(run.output_tool_name.as_deref(), Some("filled"));
2901 }
2902
2903 impl ModelTurn {
2904 fn with_usage_for_test(mut self, usage: Usage) -> Self {
2905 self.usage = usage;
2906 self
2907 }
2908 }
2909
2910 #[test]
2917 fn durable_human_in_the_loop_approval_survives_serialize_resume() {
2918 let mut run = AgentRun::new("pay two invoices").max_turns(3);
2919 let (_, _, turn) = expect_call_model(&mut run);
2920 assert_eq!(turn, 1);
2921
2922 let two_calls = vec![tool_call("c1", "add"), tool_call("c2", "add")];
2924 let outcome = run
2925 .model_response(ModelTurn::new(
2926 None,
2927 two_calls,
2928 Usage::new(),
2929 tool_names(&["add"]),
2930 tool_names(&["add"]),
2931 ))
2932 .expect("model_response");
2933 expect_continue(outcome);
2934
2935 let checkpoint = serde_json::to_string(&run).expect("serialize suspended run");
2938 let mut resumed: AgentRun = serde_json::from_str(&checkpoint).expect("deserialize run");
2939
2940 let calls = expect_call_tools(&mut resumed);
2942 assert_eq!(calls.len(), 2);
2943 assert_eq!(calls[0].tool_call.id, "c1");
2944 assert_eq!(calls[1].tool_call.id, "c2");
2945
2946 resumed
2949 .tool_results(vec![
2950 tool_result("c1", "approved-result"),
2951 tool_result("c2", "denied by reviewer: second payment not authorized"),
2952 ])
2953 .expect("tool_results on the resumed run");
2954
2955 let after = serde_json::to_string(&resumed).expect("serialize resumed run");
2957 assert!(
2958 after.contains("approved-result"),
2959 "the approved call's result must be in the resumed run state"
2960 );
2961 assert!(
2962 after.contains("denied by reviewer: second payment not authorized"),
2963 "the denied call's reason must be in the resumed run state"
2964 );
2965
2966 let (_, _, turn2) = expect_call_model(&mut resumed);
2968 assert_eq!(turn2, 2);
2969 expect_continue(
2970 resumed
2971 .model_response(text_turn("done"))
2972 .expect("model_response 2"),
2973 );
2974 let response = expect_done(&mut resumed);
2975 assert_eq!(response.output, "done");
2976 }
2977
2978 fn raw_payload(attempt: &str) -> serde_json::Value {
2990 json!({
2991 "id": format!("resp-{attempt}"),
2992 "provider_only": attempt,
2993 })
2994 }
2995
2996 #[test]
2997 fn model_turn_raw_is_recorded_on_the_completion_call() {
2998 let first = raw_payload("turn-1");
2999 let second = raw_payload("turn-2");
3000 let mut run = AgentRun::new("add things").max_turns(2);
3001
3002 expect_call_model(&mut run);
3003 expect_continue(
3004 run.model_response(tool_call_turn("call_1", "add").with_raw(first.clone()))
3005 .expect("model_response should succeed"),
3006 );
3007 expect_call_tools(&mut run);
3008 run.tool_results(vec![tool_result("call_1", "2")])
3009 .expect("tool_results should succeed");
3010 expect_call_model(&mut run);
3011 expect_continue(
3012 run.model_response(text_turn("done").with_raw(second.clone()))
3013 .expect("model_response should succeed"),
3014 );
3015
3016 let response = expect_done(&mut run);
3017 let raws: Vec<_> = response
3018 .completion_calls
3019 .iter()
3020 .map(|call| call.raw.clone())
3021 .collect();
3022 assert_eq!(
3023 raws,
3024 [first, second],
3025 "each call carries its own turn's payload"
3026 );
3027 }
3028
3029 #[test]
3033 fn model_turn_without_raw_records_null() {
3034 let mut run = AgentRun::new("hello");
3035 expect_call_model(&mut run);
3036 expect_continue(
3037 run.model_response(text_turn("hi"))
3038 .expect("model_response should succeed"),
3039 );
3040 assert_eq!(run.completion_calls()[0].raw, serde_json::Value::Null);
3041 }
3042
3043 #[test]
3044 fn streamed_completion_call_record_carries_raw() {
3045 let raw = raw_payload("streamed");
3046 let mut run = AgentRun::new("hello");
3047 expect_call_model(&mut run);
3048 let call = run
3049 .record_streamed_completion_call(
3050 usage(3, 4),
3051 ResponseIdentity::default(),
3052 None,
3053 raw.clone(),
3054 )
3055 .expect("record should succeed");
3056 assert_eq!(call.raw, raw);
3057 assert_eq!(run.completion_calls()[0].raw, raw);
3058
3059 let mut run = AgentRun::new("hello");
3060 expect_call_model(&mut run);
3061 let call = run
3062 .record_streamed_completion_call(
3063 usage(3, 4),
3064 ResponseIdentity::default(),
3065 None,
3066 serde_json::Value::Null,
3067 )
3068 .expect("record should succeed");
3069 assert_eq!(
3070 call.raw,
3071 serde_json::Value::Null,
3072 "a terminal with no payload behind it records Value::Null"
3073 );
3074 }
3075
3076 #[test]
3080 fn recorded_raw_survives_serde_round_trip() {
3081 let raw = raw_payload("suspended");
3082 let mut run = AgentRun::new("add things").max_turns(2);
3083 expect_call_model(&mut run);
3084 expect_continue(
3085 run.model_response(tool_call_turn("call_1", "add").with_raw(raw.clone()))
3086 .expect("model_response should succeed"),
3087 );
3088 expect_call_tools(&mut run);
3089
3090 let serialized = serde_json::to_string(&run).expect("mid-run state should serialize");
3091 let restored: AgentRun =
3092 serde_json::from_str(&serialized).expect("mid-run state should deserialize");
3093 assert_eq!(restored.completion_calls().len(), 1);
3094 assert_eq!(restored.completion_calls()[0].raw, raw);
3095 assert_eq!(restored.completion_calls(), run.completion_calls());
3096 }
3097
3098 #[test]
3102 fn model_turn_raw_round_trips_and_missing_key_loads_as_null() {
3103 let raw = raw_payload("turn");
3104 let turn = text_turn("hi").with_raw(raw.clone());
3105
3106 let value = serde_json::to_value(&turn).expect("turn should serialize");
3107 assert_eq!(value["raw"], raw);
3108 let restored: ModelTurn =
3109 serde_json::from_value(value.clone()).expect("turn should deserialize");
3110 assert_eq!(restored.raw, raw);
3111
3112 let mut without_raw = value;
3113 without_raw
3114 .as_object_mut()
3115 .expect("turn serializes as an object")
3116 .remove("raw")
3117 .expect("the raw key was present");
3118 let legacy: ModelTurn =
3119 serde_json::from_value(without_raw).expect("a turn without a raw key still loads");
3120 assert_eq!(legacy.raw, serde_json::Value::Null);
3121 assert_eq!(legacy.choice, turn.choice);
3122 }
3123
3124 #[test]
3128 fn completion_call_raw_round_trips_and_missing_key_loads_as_null() {
3129 let raw = raw_payload("call");
3130 let call = CompletionCall::new(0, usage(1, 2)).with_raw(raw.clone());
3131
3132 let value = serde_json::to_value(&call).expect("call should serialize");
3133 assert_eq!(value["raw"], raw);
3134 let restored: CompletionCall =
3135 serde_json::from_value(value).expect("call should deserialize");
3136 assert_eq!(restored, call);
3137
3138 let unset = serde_json::to_value(CompletionCall::new(0, usage(1, 2)))
3139 .expect("call should serialize");
3140 assert!(
3141 unset.get("raw").is_none(),
3142 "a Value::Null raw is not written, so pre-field state is unchanged"
3143 );
3144 let legacy: CompletionCall =
3145 serde_json::from_value(unset).expect("a call without a raw key still loads");
3146 assert_eq!(legacy.raw, serde_json::Value::Null);
3147 }
3148}