1pub mod output_mode;
57pub mod streamed;
58
59pub use output_mode::OutputMode;
60
61use std::collections::{BTreeMap, BTreeSet};
62
63use serde::{Deserialize, Serialize};
64
65use crate::{
66 OneOrMany,
67 agent::hook::{InvalidToolCallContext, InvalidToolCallHookAction},
68 agent::prompt_request::{
69 CompletionCall, PromptResponse, TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER,
70 assistant_text_from_choice, build_full_history, build_history_for_request,
71 invalid_tool_retry_user_message, is_empty_assistant_turn, tool_result_message,
72 },
73 completion::{Message, PromptError, Usage},
74 json_utils,
75 message::{AssistantContent, ToolCall, ToolChoice, ToolResult, ToolResultContent, UserContent},
76};
77
78pub use streamed::{
79 PartialStreamedTurn, StreamedInvalidToolCall, StreamedResolution, StreamedTurn,
80 StreamedTurnAssembler, StreamedTurnEvent,
81};
82
83fn unknown_tool_call_error(
89 tool_name: String,
90 available_tools: Vec<String>,
91 allowed_tools: Vec<String>,
92 chat_history: Vec<Message>,
93) -> PromptError {
94 PromptError::UnknownToolCall {
95 tool_name,
96 available_tools,
97 allowed_tools,
98 chat_history: Box::new(chat_history),
99 }
100}
101
102pub(crate) const DEFAULT_OUTPUT_RETRIES: usize = 1;
106
107#[derive(Debug, Clone)]
112pub enum AgentRunStep {
113 CallModel {
116 prompt: Message,
118 history: Vec<Message>,
121 turn: usize,
123 },
124 CallTools {
127 calls: Vec<PendingToolCall>,
129 },
130 Done(PromptResponse),
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136#[non_exhaustive]
137pub struct PendingToolCall {
138 pub tool_call: ToolCall,
140 pub preresolved_result: Option<UserContent>,
144 #[serde(default)]
149 pub internal_call_id: Option<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154#[non_exhaustive]
155pub struct ModelTurn {
156 pub message_id: Option<String>,
158 pub choice: OneOrMany<AssistantContent>,
160 pub usage: Usage,
162 pub executable_tool_names: BTreeSet<String>,
164 pub allowed_tool_names: BTreeSet<String>,
166}
167
168impl ModelTurn {
169 pub fn new(
172 message_id: Option<String>,
173 choice: OneOrMany<AssistantContent>,
174 usage: Usage,
175 executable_tool_names: BTreeSet<String>,
176 allowed_tool_names: BTreeSet<String>,
177 ) -> Self {
178 Self {
179 message_id,
180 choice,
181 usage,
182 executable_tool_names,
183 allowed_tool_names,
184 }
185 }
186}
187
188#[derive(Debug)]
194pub enum ModelTurnOutcome {
195 Continue {
203 response_hook_suppressed: bool,
205 },
206 NeedsResolution(InvalidToolCallContext),
211 TurnRetried,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218struct ResolvingState {
219 message_id: Option<String>,
220 original_choice: OneOrMany<AssistantContent>,
223 items: Vec<AssistantContent>,
225 next_index: usize,
227 executable_tool_names: BTreeSet<String>,
228 allowed_tool_names: BTreeSet<String>,
229 skipped: BTreeMap<String, UserContent>,
231 recovered: bool,
232 any_skipped: bool,
233 has_tool_calls: bool,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237struct TurnState {
238 message_id: Option<String>,
239 items: Vec<AssistantContent>,
240 has_tool_calls: bool,
241 skipped: BTreeMap<String, UserContent>,
242 #[serde(default)]
245 internal_call_ids: Vec<(String, String)>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249enum RunState {
250 PreparingRequest,
252 AwaitingModel,
254 ResolvingToolCalls(Box<ResolvingState>),
257 AwaitingAdvance(Box<TurnState>),
260 ExecutingTools(Vec<PendingToolCall>),
264 Done(Box<PromptResponse>),
266 Failed,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct AgentRun {
274 max_turns: usize,
275 max_invalid_tool_call_retries: usize,
276 tool_choice: Option<ToolChoice>,
277 #[serde(default)]
281 output_tool_name: Option<String>,
282 #[serde(default)]
285 output_schema: Option<serde_json::Value>,
286 #[serde(default)]
290 max_output_retries: usize,
291 #[serde(default)]
292 output_retries: usize,
293 chat_history: Option<Vec<Message>>,
294 new_messages: Vec<Message>,
295 current_turn: usize,
296 usage: Usage,
297 completion_calls: Vec<CompletionCall>,
298 completion_call_index: usize,
299 invalid_tool_call_retries: usize,
300 #[serde(default)]
303 rollback_pending: bool,
304 #[serde(default)]
308 streamed_completion_call_recorded: bool,
309 state: RunState,
310}
311
312impl AgentRun {
313 pub fn new(prompt: impl Into<Message>) -> Self {
316 Self {
317 max_turns: 1,
318 max_invalid_tool_call_retries: 0,
319 tool_choice: None,
320 output_tool_name: None,
321 output_schema: None,
322 max_output_retries: 0,
323 output_retries: 0,
324 chat_history: None,
325 new_messages: vec![prompt.into()],
326 current_turn: 0,
327 usage: Usage::new(),
328 completion_calls: Vec::new(),
329 completion_call_index: 0,
330 invalid_tool_call_retries: 0,
331 rollback_pending: false,
332 streamed_completion_call_recorded: false,
333 state: RunState::PreparingRequest,
334 }
335 }
336
337 pub fn with_history(mut self, history: Vec<Message>) -> Self {
339 self.chat_history = Some(history);
340 self
341 }
342
343 pub fn max_turns(mut self, max_turns: usize) -> Self {
348 self.max_turns = max_turns;
349 self
350 }
351
352 pub fn with_output_validation(
357 mut self,
358 output_schema: Option<serde_json::Value>,
359 max_output_retries: usize,
360 ) -> Self {
361 self.output_schema = output_schema;
362 self.max_output_retries = max_output_retries;
363 self
364 }
365
366 fn missing_required_output_fields(&self, args: &serde_json::Value) -> Vec<String> {
372 let Some(required) = self
373 .output_schema
374 .as_ref()
375 .and_then(|schema| schema.get("required"))
376 .and_then(|required| required.as_array())
377 else {
378 return Vec::new();
379 };
380 let object = args.as_object();
381 required
382 .iter()
383 .filter_map(|field| field.as_str())
384 .filter(|field| object.is_none_or(|object| !object.contains_key(*field)))
385 .map(str::to_owned)
386 .collect()
387 }
388
389 fn text_satisfies_output_schema(&self, text: &str) -> bool {
393 serde_json::from_str::<serde_json::Value>(text.trim())
394 .ok()
395 .is_some_and(|value| self.missing_required_output_fields(&value).is_empty())
396 }
397
398 fn can_reprompt_for_output(&self) -> bool {
402 self.output_retries < self.max_output_retries && self.current_turn < self.max_turns
403 }
404
405 fn reprompt_for_output(&mut self) -> Result<AgentRunStep, PromptError> {
410 self.output_retries += 1;
411 self.state = RunState::PreparingRequest;
412 self.next_step()
413 }
414
415 pub fn max_invalid_tool_call_retries(mut self, retries: usize) -> Self {
419 self.max_invalid_tool_call_retries = retries;
420 self
421 }
422
423 pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
427 self.tool_choice = Some(tool_choice);
428 self
429 }
430
431 pub fn with_output_tool_name(mut self, name: impl Into<String>) -> Self {
435 self.output_tool_name = Some(name.into());
436 self
437 }
438
439 pub(crate) fn set_output_tool_name(&mut self, name: Option<String>) {
443 if self.output_tool_name.is_none() {
447 self.output_tool_name = name;
448 }
449 }
450
451 pub(crate) fn output_tool_name(&self) -> Option<&str> {
455 self.output_tool_name.as_deref()
456 }
457
458 pub fn usage(&self) -> Usage {
460 self.usage
461 }
462
463 pub fn turn(&self) -> usize {
465 self.current_turn
466 }
467
468 pub fn completion_calls(&self) -> &[CompletionCall] {
470 &self.completion_calls
471 }
472
473 pub fn messages(&self) -> &[Message] {
476 &self.new_messages
477 }
478
479 pub fn full_history(&self) -> Vec<Message> {
481 build_full_history(self.chat_history.as_deref(), self.new_messages.clone())
482 }
483
484 pub fn is_done(&self) -> bool {
486 matches!(self.state, RunState::Done(_))
487 }
488
489 pub fn response(&self) -> Option<&PromptResponse> {
494 match &self.state {
495 RunState::Done(response) => Some(response),
496 _ => None,
497 }
498 }
499
500 pub fn cancel_error(&self, reason: impl Into<String>) -> PromptError {
503 PromptError::prompt_cancelled(self.full_history(), reason)
504 }
505
506 pub fn pending_invalid_tool_call(&self) -> Option<InvalidToolCallContext> {
510 let RunState::ResolvingToolCalls(resolving) = &self.state else {
511 return None;
512 };
513 let AssistantContent::ToolCall(tool_call) = resolving.items.get(resolving.next_index)?
514 else {
515 return None;
516 };
517 if resolving
518 .allowed_tool_names
519 .contains(&tool_call.function.name)
520 {
521 return None;
522 }
523
524 Some(InvalidToolCallContext {
525 tool_name: tool_call.function.name.clone(),
526 tool_call_id: Some(tool_call.id.clone()),
527 internal_call_id: None,
528 args: Some(json_utils::value_to_json_string(
529 &tool_call.function.arguments,
530 )),
531 available_tools: resolving.executable_tool_names.iter().cloned().collect(),
532 allowed_tools: resolving.allowed_tool_names.iter().cloned().collect(),
533 tool_choice: self.tool_choice.clone(),
534 chat_history: self.diagnostic_history(resolving),
535 is_streaming: false,
536 })
537 }
538
539 pub fn next_step(&mut self) -> Result<AgentRunStep, PromptError> {
547 match std::mem::replace(&mut self.state, RunState::Failed) {
548 RunState::PreparingRequest => {
549 let Some((prompt_ref, history_for_turn)) = self.new_messages.split_last() else {
550 return Err(PromptError::prompt_cancelled(
551 self.full_history(),
552 "prompt loop lost its pending prompt",
553 ));
554 };
555 let prompt = prompt_ref.clone();
556
557 if self.current_turn >= self.max_turns {
558 return Err(PromptError::MaxTurnsError {
559 max_turns: self.max_turns,
560 chat_history: self.full_history().into(),
561 prompt: prompt.into(),
562 });
563 }
564
565 let history =
566 build_history_for_request(self.chat_history.as_deref(), history_for_turn);
567 self.current_turn += 1;
568 self.rollback_pending = false;
569 self.streamed_completion_call_recorded = false;
570 self.state = RunState::AwaitingModel;
571 Ok(AgentRunStep::CallModel {
572 prompt,
573 history,
574 turn: self.current_turn,
575 })
576 }
577 RunState::AwaitingAdvance(turn_state) => {
578 let TurnState {
579 message_id,
580 items,
581 has_tool_calls,
582 skipped,
583 mut internal_call_ids,
584 } = *turn_state;
585 let Some(choice) = OneOrMany::from_iter_optional(items.clone()) else {
586 return Err(PromptError::prompt_cancelled(
587 self.full_history(),
588 "model turn lost its assistant content",
589 ));
590 };
591
592 if has_tool_calls
597 && let Some(output_tool_name) = self.output_tool_name.clone()
598 && let Some(tool_call) = items.iter().find_map(|item| match item {
599 AssistantContent::ToolCall(tc) if tc.function.name == output_tool_name => {
600 Some(tc)
601 }
602 _ => None,
603 })
604 {
605 let args = tool_call.function.arguments.clone();
606 let tool_call_id = tool_call.id.clone();
607 let output = json_utils::value_to_json_string(&args);
608
609 let missing = self.missing_required_output_fields(&args);
613 if !missing.is_empty() && self.can_reprompt_for_output() {
614 self.new_messages.push(Message::Assistant {
615 id: message_id,
616 content: choice.clone(),
617 });
618 let feedback = format!(
619 "The `{output_tool_name}` arguments were missing required field(s): \
620 {}. Call `{output_tool_name}` again with every required field.",
621 missing.join(", ")
622 );
623 if let Some(user_message) =
624 invalid_tool_retry_user_message(&choice, &tool_call_id, feedback)
625 {
626 self.new_messages.push(user_message);
627 }
628 return self.reprompt_for_output();
629 }
630
631 let mut final_items: Vec<AssistantContent> = items
637 .iter()
638 .filter(|item| !matches!(item, AssistantContent::ToolCall(_)))
639 .cloned()
640 .collect();
641 final_items.push(AssistantContent::text(output.clone()));
642 let final_content = OneOrMany::from_iter_optional(final_items);
643 if let Some(content) = final_content.clone() {
644 self.new_messages.push(Message::Assistant {
645 id: message_id,
646 content,
647 });
648 }
649
650 let mut response = PromptResponse::new(output, self.usage)
651 .with_messages(self.new_messages.clone())
652 .with_completion_calls(self.completion_calls.clone());
653 if let Some(content) = final_content {
654 response = response.with_content(content);
655 }
656 self.state = RunState::Done(Box::new(response.clone()));
657 return Ok(AgentRunStep::Done(response));
658 }
659
660 if !is_empty_assistant_turn(&choice) {
661 self.new_messages.push(Message::Assistant {
662 id: message_id,
663 content: choice.clone(),
664 });
665 }
666
667 if has_tool_calls {
668 self.output_retries = 0;
673 let calls: Vec<PendingToolCall> = items
674 .iter()
675 .filter_map(|item| match item {
676 AssistantContent::ToolCall(tool_call) => {
677 let internal_call_id = internal_call_ids
681 .iter()
682 .position(|(id, _)| *id == tool_call.id)
683 .map(|index| internal_call_ids.remove(index).1);
684 Some(PendingToolCall {
685 tool_call: tool_call.clone(),
686 preresolved_result: skipped.get(&tool_call.id).cloned(),
687 internal_call_id,
688 })
689 }
690 _ => None,
691 })
692 .collect();
693 self.state = RunState::ExecutingTools(calls.clone());
694 Ok(AgentRunStep::CallTools { calls })
695 } else {
696 if let Some(output_tool_name) = self.output_tool_name.clone()
707 && !is_empty_assistant_turn(&choice)
708 && self.can_reprompt_for_output()
709 && !self.text_satisfies_output_schema(&assistant_text_from_choice(&choice))
710 {
711 let feedback = format!(
712 "Provide your final answer by calling the `{output_tool_name}` tool \
713 with the structured result as its arguments, not as plain text."
714 );
715 self.new_messages.push(Message::user(feedback));
716 return self.reprompt_for_output();
717 }
718
719 let response =
720 PromptResponse::new(assistant_text_from_choice(&choice), self.usage)
721 .with_messages(self.new_messages.clone())
722 .with_completion_calls(self.completion_calls.clone())
723 .with_content(choice.clone());
724 self.state = RunState::Done(Box::new(response.clone()));
725 Ok(AgentRunStep::Done(response))
726 }
727 }
728 RunState::ExecutingTools(calls) => {
729 let step = AgentRunStep::CallTools {
732 calls: calls.clone(),
733 };
734 self.state = RunState::ExecutingTools(calls);
735 Ok(step)
736 }
737 RunState::Done(response) => {
738 let step = AgentRunStep::Done((*response).clone());
739 self.state = RunState::Done(response);
740 Ok(step)
741 }
742 state @ (RunState::AwaitingModel | RunState::ResolvingToolCalls(_)) => {
743 let reason = match &state {
744 RunState::AwaitingModel => {
745 "next_step called while a model response is pending; feed it via model_response first"
746 }
747 _ => {
748 "next_step called while an invalid tool-call resolution is pending; answer it via resolve_invalid_tool_call first"
749 }
750 };
751 self.state = state;
752 Err(self.protocol_violation(reason))
753 }
754 RunState::Failed => Err(self.protocol_violation(
755 "next_step called after the run already failed or was misdriven",
756 )),
757 }
758 }
759
760 pub fn model_response(&mut self, turn: ModelTurn) -> Result<ModelTurnOutcome, PromptError> {
766 if !matches!(self.state, RunState::AwaitingModel) {
767 return Err(
768 self.protocol_violation("model_response called without a pending CallModel step")
769 );
770 }
771 if self.streamed_completion_call_recorded {
772 return Err(self.protocol_violation(
773 "model_response called after record_streamed_completion_call for the same turn; feed streamed turns via streamed_turn",
774 ));
775 }
776
777 self.record_completion_call(turn.usage);
778
779 let items: Vec<AssistantContent> = turn.choice.iter().cloned().collect();
780 let has_tool_calls = items
781 .iter()
782 .any(|item| matches!(item, AssistantContent::ToolCall(_)));
783
784 self.state = RunState::ResolvingToolCalls(Box::new(ResolvingState {
785 message_id: turn.message_id,
786 original_choice: turn.choice,
787 items,
788 next_index: 0,
789 executable_tool_names: turn.executable_tool_names,
790 allowed_tool_names: turn.allowed_tool_names,
791 skipped: BTreeMap::new(),
792 recovered: false,
793 any_skipped: false,
794 has_tool_calls,
795 }));
796
797 self.advance_resolution()
798 }
799
800 fn record_completion_call(&mut self, usage: Usage) -> CompletionCall {
807 let call = CompletionCall::new(self.completion_call_index, usage);
808 self.completion_call_index += 1;
809 self.completion_calls.push(call);
810 self.usage += usage;
811 call
812 }
813
814 fn finalize_turn(
819 &mut self,
820 message_id: Option<String>,
821 items: Vec<AssistantContent>,
822 has_tool_calls: bool,
823 skipped: BTreeMap<String, UserContent>,
824 internal_call_ids: Vec<(String, String)>,
825 ) {
826 self.state = RunState::AwaitingAdvance(Box::new(TurnState {
827 message_id,
828 items,
829 has_tool_calls,
830 skipped,
831 internal_call_ids,
832 }));
833 }
834
835 pub fn resolve_invalid_tool_call(
849 &mut self,
850 action: InvalidToolCallHookAction,
851 ) -> Result<ModelTurnOutcome, PromptError> {
852 let mut resolving = match std::mem::replace(&mut self.state, RunState::Failed) {
855 RunState::ResolvingToolCalls(resolving) => resolving,
856 other => {
857 self.state = other;
858 return Err(self.protocol_violation(
859 "resolve_invalid_tool_call called without a pending invalid tool call",
860 ));
861 }
862 };
863 let tool_call = match resolving.items.get(resolving.next_index) {
864 Some(AssistantContent::ToolCall(tool_call))
865 if !resolving
866 .allowed_tool_names
867 .contains(&tool_call.function.name) =>
868 {
869 tool_call.clone()
870 }
871 _ => {
872 self.state = RunState::ResolvingToolCalls(resolving);
873 return Err(self.protocol_violation(
874 "resolve_invalid_tool_call called without a pending invalid tool call",
875 ));
876 }
877 };
878
879 let diagnostic_history = self.diagnostic_history(&resolving);
880 let executable_tool_names: Vec<String> =
881 resolving.executable_tool_names.iter().cloned().collect();
882 let allowed_tool_names: Vec<String> =
883 resolving.allowed_tool_names.iter().cloned().collect();
884
885 match action {
886 InvalidToolCallHookAction::Fail => Err(unknown_tool_call_error(
887 tool_call.function.name,
888 executable_tool_names,
889 allowed_tool_names,
890 diagnostic_history,
891 )),
892 InvalidToolCallHookAction::Retry { feedback } => {
893 if self.invalid_tool_call_retries >= self.max_invalid_tool_call_retries {
894 return Err(unknown_tool_call_error(
895 tool_call.function.name,
896 executable_tool_names,
897 allowed_tool_names,
898 diagnostic_history,
899 ));
900 }
901 self.invalid_tool_call_retries += 1;
902
903 self.new_messages.push(Message::Assistant {
904 id: resolving.message_id.clone(),
905 content: resolving.original_choice.clone(),
906 });
907 let Some(user_message) = invalid_tool_retry_user_message(
908 &resolving.original_choice,
909 &tool_call.id,
910 feedback,
911 ) else {
912 return Err(PromptError::prompt_cancelled(
913 diagnostic_history,
914 "invalid tool call retry produced no retry messages",
915 ));
916 };
917 self.new_messages.push(user_message);
918 self.state = RunState::PreparingRequest;
919 Ok(ModelTurnOutcome::TurnRetried)
920 }
921 InvalidToolCallHookAction::Repair { tool_name } => {
922 if !allowed_tool_names.contains(&tool_name) {
923 return Err(unknown_tool_call_error(
924 tool_name,
925 executable_tool_names,
926 allowed_tool_names,
927 diagnostic_history,
928 ));
929 }
930 if let Some(AssistantContent::ToolCall(tool_call)) =
931 resolving.items.get_mut(resolving.next_index)
932 {
933 tool_call.function.name = tool_name;
934 }
935 resolving.recovered = true;
936 self.state = RunState::ResolvingToolCalls(resolving);
937 self.advance_resolution()
938 }
939 InvalidToolCallHookAction::Skip { reason } => {
940 if matches!(self.tool_choice, Some(ToolChoice::None)) {
941 return Err(unknown_tool_call_error(
942 tool_call.function.name,
943 executable_tool_names,
944 allowed_tool_names,
945 diagnostic_history,
946 ));
947 }
948 let user_content = if let Some(call_id) = tool_call.call_id.clone() {
949 UserContent::tool_result_with_call_id(
950 tool_call.id.clone(),
951 call_id,
952 OneOrMany::one(reason.into()),
953 )
954 } else {
955 UserContent::tool_result(tool_call.id.clone(), OneOrMany::one(reason.into()))
956 };
957 resolving.skipped.insert(tool_call.id.clone(), user_content);
958 resolving.recovered = true;
959 resolving.any_skipped = true;
960 resolving.next_index += 1;
961 self.state = RunState::ResolvingToolCalls(resolving);
962 self.advance_resolution()
963 }
964 }
965 }
966
967 pub fn tool_results(&mut self, results: Vec<UserContent>) -> Result<(), PromptError> {
975 let RunState::ExecutingTools(pending) = &self.state else {
976 return Err(
977 self.protocol_violation("tool_results called without a pending CallTools step")
978 );
979 };
980 let mut unanswered: Vec<String> = pending
983 .iter()
984 .map(|call| call.tool_call.id.clone())
985 .collect();
986
987 if results.is_empty() {
988 self.state = RunState::Failed;
989 return Err(PromptError::prompt_cancelled(
990 self.full_history(),
991 "tool execution produced no tool results",
992 ));
993 }
994 for result in &results {
995 let UserContent::ToolResult(tool_result) = result else {
996 return Err(self.protocol_violation(
997 "tool_results received content that is not a tool result",
998 ));
999 };
1000 let Some(index) = unanswered.iter().position(|id| *id == tool_result.id) else {
1001 return Err(self.protocol_violation(&format!(
1002 "tool_results received a result for unknown or already-answered tool call id `{}`",
1003 tool_result.id
1004 )));
1005 };
1006 unanswered.swap_remove(index);
1007 }
1008 if !unanswered.is_empty() {
1009 return Err(self.protocol_violation(&format!(
1010 "tool_results left pending tool call id(s) unanswered: {unanswered:?}"
1011 )));
1012 }
1013
1014 let Some(content) = OneOrMany::from_iter_optional(results) else {
1016 return Err(
1017 self.protocol_violation("internal: tool results vanished during validation")
1018 );
1019 };
1020
1021 self.new_messages.push(Message::User { content });
1022 self.state = RunState::PreparingRequest;
1023 Ok(())
1024 }
1025
1026 fn advance_resolution(&mut self) -> Result<ModelTurnOutcome, PromptError> {
1029 let mut resolving = match std::mem::replace(&mut self.state, RunState::Failed) {
1030 RunState::ResolvingToolCalls(resolving) => resolving,
1031 other => {
1032 self.state = other;
1033 return Err(self.protocol_violation(
1034 "internal: advance_resolution outside of tool-call resolution",
1035 ));
1036 }
1037 };
1038 while let Some(item) = resolving.items.get(resolving.next_index) {
1039 match item {
1040 AssistantContent::ToolCall(tool_call)
1041 if !resolving
1042 .allowed_tool_names
1043 .contains(&tool_call.function.name) =>
1044 {
1045 break;
1046 }
1047 _ => resolving.next_index += 1,
1048 }
1049 }
1050
1051 if resolving.next_index < resolving.items.len() {
1052 self.state = RunState::ResolvingToolCalls(resolving);
1053 return match self.pending_invalid_tool_call() {
1054 Some(context) => Ok(ModelTurnOutcome::NeedsResolution(context)),
1055 None => Err(self.protocol_violation(
1056 "internal: pending invalid tool call could not be derived",
1057 )),
1058 };
1059 }
1060
1061 let ResolvingState {
1062 message_id,
1063 items,
1064 mut skipped,
1065 recovered,
1066 any_skipped,
1067 has_tool_calls,
1068 ..
1069 } = *resolving;
1070
1071 if any_skipped {
1074 for item in &items {
1075 if let AssistantContent::ToolCall(tool_call) = item {
1076 skipped.entry(tool_call.id.clone()).or_insert_with(|| {
1077 tool_result_message(
1078 tool_call.id.clone(),
1079 tool_call.call_id.clone(),
1080 TOOL_NOT_EXECUTED_DUE_TO_INVALID_PEER.to_string(),
1081 )
1082 });
1083 }
1084 }
1085 }
1086
1087 self.finalize_turn(message_id, items, has_tool_calls, skipped, Vec::new());
1088 Ok(ModelTurnOutcome::Continue {
1089 response_hook_suppressed: recovered,
1090 })
1091 }
1092
1093 pub fn record_streamed_completion_call(
1107 &mut self,
1108 usage: Usage,
1109 ) -> Result<CompletionCall, PromptError> {
1110 let recordable = matches!(self.state, RunState::AwaitingModel)
1111 || (matches!(self.state, RunState::PreparingRequest) && self.rollback_pending);
1112 if !recordable {
1113 return Err(self.protocol_violation(
1114 "record_streamed_completion_call called without a pending or rolled-back CallModel step",
1115 ));
1116 }
1117 if self.streamed_completion_call_recorded {
1118 return Err(self.protocol_violation(
1119 "record_streamed_completion_call called twice for the same model turn",
1120 ));
1121 }
1122 self.streamed_completion_call_recorded = true;
1123
1124 Ok(self.record_completion_call(usage))
1125 }
1126
1127 pub fn streamed_invalid_tool_call_context(
1130 &self,
1131 partial: &PartialStreamedTurn,
1132 invalid: &StreamedInvalidToolCall,
1133 ) -> InvalidToolCallContext {
1134 InvalidToolCallContext {
1135 tool_name: invalid.tool_call.function.name.clone(),
1136 tool_call_id: Some(invalid.tool_call.id.clone()),
1137 internal_call_id: Some(invalid.internal_call_id.clone()),
1138 args: invalid.args.clone(),
1139 available_tools: invalid.executable_tool_names.iter().cloned().collect(),
1140 allowed_tools: invalid.allowed_tool_names.iter().cloned().collect(),
1141 tool_choice: self.tool_choice.clone(),
1142 chat_history: self
1143 .streamed_diagnostic_history(partial, Some(invalid.tool_call.clone())),
1144 is_streaming: true,
1145 }
1146 }
1147
1148 pub fn resolve_streamed_invalid_tool_call(
1156 &mut self,
1157 partial: &PartialStreamedTurn,
1158 invalid: &StreamedInvalidToolCall,
1159 action: InvalidToolCallHookAction,
1160 ) -> Result<StreamedResolution, PromptError> {
1161 if !matches!(self.state, RunState::AwaitingModel) {
1162 return Err(self.protocol_violation(
1163 "resolve_streamed_invalid_tool_call called without a pending CallModel step",
1164 ));
1165 }
1166
1167 let diagnostic_history =
1168 self.streamed_diagnostic_history(partial, Some(invalid.tool_call.clone()));
1169 let executable_tool_names: Vec<String> =
1170 invalid.executable_tool_names.iter().cloned().collect();
1171 let allowed_tool_names: Vec<String> = invalid.allowed_tool_names.iter().cloned().collect();
1172
1173 match action {
1174 InvalidToolCallHookAction::Fail => {
1175 self.state = RunState::Failed;
1176 Err(unknown_tool_call_error(
1177 invalid.tool_call.function.name.clone(),
1178 executable_tool_names,
1179 allowed_tool_names,
1180 diagnostic_history,
1181 ))
1182 }
1183 InvalidToolCallHookAction::Retry { feedback } => {
1184 if self.invalid_tool_call_retries >= self.max_invalid_tool_call_retries {
1185 self.state = RunState::Failed;
1186 return Err(unknown_tool_call_error(
1187 invalid.tool_call.function.name.clone(),
1188 executable_tool_names,
1189 allowed_tool_names,
1190 diagnostic_history,
1191 ));
1192 }
1193 self.invalid_tool_call_retries += 1;
1194
1195 let Some((assistant_message, user_message)) =
1196 partial.rollback_messages(invalid.tool_call.clone(), feedback)
1197 else {
1198 self.state = RunState::Failed;
1199 return Err(PromptError::prompt_cancelled(
1200 diagnostic_history,
1201 "invalid tool call retry produced no retry messages",
1202 ));
1203 };
1204 self.new_messages.push(assistant_message);
1205 self.new_messages.push(user_message);
1206 self.rollback_pending = true;
1207 self.state = RunState::PreparingRequest;
1208 Ok(StreamedResolution::TurnAbandoned {
1209 skipped_tool_result: None,
1210 })
1211 }
1212 InvalidToolCallHookAction::Repair { tool_name } => {
1213 if !invalid.allowed_tool_names.contains(&tool_name) {
1214 self.state = RunState::Failed;
1215 return Err(unknown_tool_call_error(
1216 tool_name,
1217 executable_tool_names,
1218 allowed_tool_names,
1219 diagnostic_history,
1220 ));
1221 }
1222 Ok(StreamedResolution::Repaired { tool_name })
1223 }
1224 InvalidToolCallHookAction::Skip { reason } => {
1225 if matches!(self.tool_choice, Some(ToolChoice::None)) {
1226 self.state = RunState::Failed;
1227 return Err(unknown_tool_call_error(
1228 invalid.tool_call.function.name.clone(),
1229 executable_tool_names,
1230 allowed_tool_names,
1231 diagnostic_history,
1232 ));
1233 }
1234
1235 let skipped_tool_result = ToolResult {
1239 id: invalid.tool_call.id.clone(),
1240 call_id: invalid.tool_call.call_id.clone(),
1241 content: OneOrMany::one(ToolResultContent::text(reason.clone())),
1242 };
1243 let Some((assistant_message, user_message)) =
1244 partial.rollback_messages(invalid.tool_call.clone(), reason)
1245 else {
1246 self.state = RunState::Failed;
1247 return Err(PromptError::prompt_cancelled(
1248 diagnostic_history,
1249 "invalid tool call skip produced no recovery messages",
1250 ));
1251 };
1252 self.new_messages.push(assistant_message);
1253 self.new_messages.push(user_message);
1254 self.rollback_pending = true;
1255 self.state = RunState::PreparingRequest;
1256 Ok(StreamedResolution::TurnAbandoned {
1257 skipped_tool_result: Some(skipped_tool_result),
1258 })
1259 }
1260 }
1261 }
1262
1263 pub fn streamed_turn(&mut self, turn: StreamedTurn) -> Result<(), PromptError> {
1270 if !matches!(self.state, RunState::AwaitingModel) {
1271 return Err(
1272 self.protocol_violation("streamed_turn called without a pending CallModel step")
1273 );
1274 }
1275
1276 if !self.streamed_completion_call_recorded {
1280 self.record_completion_call(Usage::new());
1284 self.streamed_completion_call_recorded = true;
1285 }
1286
1287 let items: Vec<AssistantContent> = turn.choice.iter().cloned().collect();
1288 let has_tool_calls = items
1289 .iter()
1290 .any(|item| matches!(item, AssistantContent::ToolCall(_)));
1291
1292 for item in &items {
1293 let AssistantContent::ToolCall(tool_call) = item else {
1294 continue;
1295 };
1296 if !turn.allowed_tool_names.contains(&tool_call.function.name) {
1297 let mut diagnostic_messages = self.new_messages.clone();
1298 if !is_empty_assistant_turn(&turn.choice) {
1299 diagnostic_messages.push(Message::Assistant {
1300 id: turn.message_id.clone(),
1301 content: turn.choice.clone(),
1302 });
1303 }
1304 let diagnostic_history =
1305 build_full_history(self.chat_history.as_deref(), diagnostic_messages);
1306 self.state = RunState::Failed;
1307 return Err(unknown_tool_call_error(
1308 tool_call.function.name.clone(),
1309 turn.executable_tool_names.iter().cloned().collect(),
1310 turn.allowed_tool_names.iter().cloned().collect(),
1311 diagnostic_history,
1312 ));
1313 }
1314 }
1315
1316 self.finalize_turn(
1317 turn.message_id,
1318 items,
1319 has_tool_calls,
1320 BTreeMap::new(),
1321 turn.internal_call_ids,
1322 );
1323 Ok(())
1324 }
1325
1326 fn streamed_diagnostic_history(
1329 &self,
1330 partial: &PartialStreamedTurn,
1331 current_tool_call: Option<ToolCall>,
1332 ) -> Vec<Message> {
1333 let mut messages = self.new_messages.clone();
1334 if let Some(assistant) = partial.assistant_message(current_tool_call) {
1335 messages.push(assistant);
1336 }
1337 build_full_history(self.chat_history.as_deref(), messages)
1338 }
1339
1340 fn diagnostic_history(&self, resolving: &ResolvingState) -> Vec<Message> {
1343 let mut diagnostic_messages = self.new_messages.clone();
1344 diagnostic_messages.push(Message::Assistant {
1345 id: resolving.message_id.clone(),
1346 content: resolving.original_choice.clone(),
1347 });
1348 build_full_history(self.chat_history.as_deref(), diagnostic_messages)
1349 }
1350
1351 fn protocol_violation(&self, reason: &str) -> PromptError {
1352 PromptError::prompt_cancelled(
1353 self.full_history(),
1354 format!("agent run driver protocol violation: {reason}"),
1355 )
1356 }
1357}
1358
1359#[cfg(test)]
1360mod tests {
1361 use super::*;
1362 use crate::message::{ToolFunction, ToolResultContent};
1363 use serde_json::json;
1364
1365 fn tool_names(names: &[&str]) -> BTreeSet<String> {
1366 names.iter().map(|name| (*name).to_string()).collect()
1367 }
1368
1369 fn usage(input_tokens: u64, output_tokens: u64) -> Usage {
1370 Usage {
1371 input_tokens,
1372 output_tokens,
1373 total_tokens: input_tokens + output_tokens,
1374 ..Usage::new()
1375 }
1376 }
1377
1378 fn text_turn(text: &str) -> ModelTurn {
1379 ModelTurn::new(
1380 None,
1381 OneOrMany::one(AssistantContent::text(text)),
1382 Usage::new(),
1383 tool_names(&["add"]),
1384 tool_names(&["add"]),
1385 )
1386 }
1387
1388 fn tool_call(id: &str, name: &str) -> AssistantContent {
1389 AssistantContent::ToolCall(ToolCall::new(
1390 id.to_string(),
1391 ToolFunction::new(name.to_string(), json!({"x": 1})),
1392 ))
1393 }
1394
1395 fn tool_call_turn(id: &str, name: &str) -> ModelTurn {
1396 ModelTurn::new(
1397 None,
1398 OneOrMany::one(tool_call(id, name)),
1399 Usage::new(),
1400 tool_names(&["add"]),
1401 tool_names(&["add"]),
1402 )
1403 }
1404
1405 fn tool_result(id: &str, output: &str) -> UserContent {
1406 UserContent::tool_result(
1407 id.to_string(),
1408 ToolResultContent::from_tool_output(output.to_string()),
1409 )
1410 }
1411
1412 fn expect_call_model(run: &mut AgentRun) -> (Message, Vec<Message>, usize) {
1413 match run.next_step().expect("next_step should succeed") {
1414 AgentRunStep::CallModel {
1415 prompt,
1416 history,
1417 turn,
1418 } => (prompt, history, turn),
1419 step => panic!("expected CallModel, got {step:?}"),
1420 }
1421 }
1422
1423 fn expect_call_tools(run: &mut AgentRun) -> Vec<PendingToolCall> {
1424 match run.next_step().expect("next_step should succeed") {
1425 AgentRunStep::CallTools { calls } => calls,
1426 step => panic!("expected CallTools, got {step:?}"),
1427 }
1428 }
1429
1430 fn expect_done(run: &mut AgentRun) -> PromptResponse {
1431 match run.next_step().expect("next_step should succeed") {
1432 AgentRunStep::Done(response) => response,
1433 step => panic!("expected Done, got {step:?}"),
1434 }
1435 }
1436
1437 fn expect_continue(outcome: ModelTurnOutcome) -> bool {
1438 match outcome {
1439 ModelTurnOutcome::Continue {
1440 response_hook_suppressed,
1441 } => response_hook_suppressed,
1442 outcome => panic!("expected Continue, got {outcome:?}"),
1443 }
1444 }
1445
1446 fn expect_needs_resolution(outcome: ModelTurnOutcome) -> InvalidToolCallContext {
1447 match outcome {
1448 ModelTurnOutcome::NeedsResolution(context) => context,
1449 outcome => panic!("expected NeedsResolution, got {outcome:?}"),
1450 }
1451 }
1452
1453 #[test]
1454 fn text_only_run_completes_in_one_turn() {
1455 let mut run = AgentRun::new("hello");
1456
1457 let (prompt, history, turn) = expect_call_model(&mut run);
1458 assert_eq!(prompt, Message::user("hello"));
1459 assert!(history.is_empty());
1460 assert_eq!(turn, 1);
1461
1462 let suppressed = expect_continue(
1463 run.model_response(text_turn("hi there"))
1464 .expect("model_response should succeed"),
1465 );
1466 assert!(!suppressed);
1467
1468 let response = expect_done(&mut run);
1469 assert_eq!(response.output, "hi there");
1470 let messages = response.messages.expect("messages should be recorded");
1471 assert_eq!(messages.len(), 2);
1472 assert!(run.is_done());
1473 }
1474
1475 #[test]
1476 fn input_history_prefixes_request_history() {
1477 let mut run = AgentRun::new("question")
1478 .with_history(vec![Message::user("earlier"), Message::assistant("reply")]);
1479
1480 let (_, history, _) = expect_call_model(&mut run);
1481 assert_eq!(
1482 history,
1483 vec![Message::user("earlier"), Message::assistant("reply")]
1484 );
1485
1486 expect_continue(
1487 run.model_response(text_turn("answer"))
1488 .expect("model_response should succeed"),
1489 );
1490 let response = expect_done(&mut run);
1491 assert_eq!(
1493 response
1494 .messages
1495 .expect("messages should be recorded")
1496 .len(),
1497 2
1498 );
1499 }
1500
1501 #[test]
1502 fn tool_roundtrip_threads_history_and_usage() {
1503 let mut run = AgentRun::new("add things").max_turns(2);
1504
1505 expect_call_model(&mut run);
1506 expect_continue(
1507 run.model_response(tool_call_turn("call_1", "add").with_usage_for_test(usage(10, 5)))
1508 .expect("model_response should succeed"),
1509 );
1510
1511 let calls = expect_call_tools(&mut run);
1512 assert_eq!(calls.len(), 1);
1513 assert_eq!(calls[0].tool_call.function.name, "add");
1514 assert!(calls[0].preresolved_result.is_none());
1515
1516 run.tool_results(vec![tool_result("call_1", "2")])
1517 .expect("tool_results should succeed");
1518
1519 let (prompt, history, turn) = expect_call_model(&mut run);
1520 assert_eq!(turn, 2);
1521 assert!(matches!(prompt, Message::User { .. }));
1524 assert_eq!(history.len(), 2);
1525
1526 expect_continue(
1527 run.model_response(text_turn("the answer is 2").with_usage_for_test(usage(20, 7)))
1528 .expect("model_response should succeed"),
1529 );
1530
1531 let response = expect_done(&mut run);
1532 assert_eq!(response.output, "the answer is 2");
1533 assert_eq!(response.usage, usage(30, 12));
1534 assert_eq!(response.completion_calls.len(), 2);
1535 assert_eq!(response.completion_calls[0].call_index, 0);
1536 assert_eq!(response.completion_calls[0].usage, usage(10, 5));
1537 assert_eq!(response.completion_calls[1].usage, usage(20, 7));
1538 assert_eq!(
1540 response
1541 .messages
1542 .expect("messages should be recorded")
1543 .len(),
1544 4
1545 );
1546 }
1547
1548 #[test]
1549 fn parallel_tool_calls_surface_in_emission_order() {
1550 let mut run = AgentRun::new("do both").max_turns(2);
1551
1552 expect_call_model(&mut run);
1553 let turn = ModelTurn::new(
1554 None,
1555 OneOrMany::many(vec![tool_call("call_1", "add"), tool_call("call_2", "add")])
1556 .expect("two items"),
1557 Usage::new(),
1558 tool_names(&["add"]),
1559 tool_names(&["add"]),
1560 );
1561 expect_continue(
1562 run.model_response(turn)
1563 .expect("model_response should succeed"),
1564 );
1565
1566 let calls = expect_call_tools(&mut run);
1567 assert_eq!(calls.len(), 2);
1568 assert_eq!(calls[0].tool_call.id, "call_1");
1569 assert_eq!(calls[1].tool_call.id, "call_2");
1570
1571 run.tool_results(vec![tool_result("call_2", "b"), tool_result("call_1", "a")])
1573 .expect("tool_results should succeed");
1574 let messages = run.messages();
1575 assert!(matches!(
1576 messages.last(),
1577 Some(Message::User { content }) if content.len() == 2
1578 ));
1579 }
1580
1581 #[test]
1582 fn max_turns_zero_rejects_initial_model_call() {
1583 let mut run = AgentRun::new("do not call").max_turns(0);
1584
1585 let err = run
1586 .next_step()
1587 .expect_err("zero budget should emit no call");
1588 assert!(matches!(
1589 err,
1590 PromptError::MaxTurnsError { max_turns: 0, .. }
1591 ));
1592 assert_eq!(run.turn(), 0);
1593 }
1594
1595 #[test]
1596 fn new_implicitly_allows_one_model_call_and_rejects_tool_continuation() {
1597 let mut run = AgentRun::new("add things");
1598
1599 let (_, _, turn) = expect_call_model(&mut run);
1600 assert_eq!(turn, 1);
1601 expect_continue(
1602 run.model_response(tool_call_turn("call_1", "add"))
1603 .expect("model_response should succeed"),
1604 );
1605 expect_call_tools(&mut run);
1606 run.tool_results(vec![tool_result("call_1", "2")])
1607 .expect("tool_results should succeed");
1608
1609 let err = run
1610 .next_step()
1611 .expect_err("second model call should exceed budget");
1612 assert!(matches!(
1613 err,
1614 PromptError::MaxTurnsError { max_turns: 1, .. }
1615 ));
1616 assert_eq!(run.turn(), 1);
1617 }
1618
1619 #[test]
1620 fn max_turns_n_allows_exactly_n_model_calls() {
1621 let mut run = AgentRun::new("loop").max_turns(3);
1622
1623 for (expected_turn, call_id) in [(1, "call_1"), (2, "call_2"), (3, "call_3")] {
1624 let (_, _, turn) = expect_call_model(&mut run);
1625 assert_eq!(turn, expected_turn);
1626 expect_continue(
1627 run.model_response(tool_call_turn(call_id, "add"))
1628 .expect("model_response should succeed"),
1629 );
1630 expect_call_tools(&mut run);
1631 run.tool_results(vec![tool_result(call_id, "0")])
1632 .expect("tool_results should succeed");
1633 }
1634
1635 let err = run
1636 .next_step()
1637 .expect_err("fourth model call should exceed budget");
1638 assert!(matches!(
1639 err,
1640 PromptError::MaxTurnsError { max_turns: 3, .. }
1641 ));
1642 assert_eq!(run.turn(), 3);
1643 }
1644
1645 #[test]
1646 fn invalid_tool_call_fail_returns_unknown_tool_call() {
1647 let mut run = AgentRun::new("call something");
1648
1649 expect_call_model(&mut run);
1650 let context = expect_needs_resolution(
1651 run.model_response(tool_call_turn("call_1", "unknown"))
1652 .expect("model_response should succeed"),
1653 );
1654 assert_eq!(context.tool_name, "unknown");
1655 assert_eq!(context.available_tools, vec!["add".to_string()]);
1656 assert!(!context.is_streaming);
1657 assert_eq!(context.chat_history.len(), 2);
1659
1660 let err = run
1661 .resolve_invalid_tool_call(InvalidToolCallHookAction::fail())
1662 .expect_err("fail action should error");
1663 assert!(matches!(
1664 err,
1665 PromptError::UnknownToolCall { tool_name, .. } if tool_name == "unknown"
1666 ));
1667 }
1668
1669 #[test]
1670 fn invalid_tool_call_retry_rolls_back_with_feedback() {
1671 let mut run = AgentRun::new("call something")
1672 .max_turns(2)
1673 .max_invalid_tool_call_retries(1);
1674
1675 expect_call_model(&mut run);
1676 expect_needs_resolution(
1677 run.model_response(tool_call_turn("call_1", "unknown"))
1678 .expect("model_response should succeed"),
1679 );
1680 let outcome = run
1681 .resolve_invalid_tool_call(InvalidToolCallHookAction::retry("use add instead"))
1682 .expect("retry should be accepted");
1683 assert!(matches!(outcome, ModelTurnOutcome::TurnRetried));
1684
1685 assert_eq!(run.messages().len(), 3);
1687 let (prompt, _, turn) = expect_call_model(&mut run);
1688 assert_eq!(turn, 2);
1689 assert!(matches!(
1690 prompt,
1691 Message::User { ref content }
1692 if matches!(content.first(), UserContent::ToolResult(_))
1693 ));
1694
1695 expect_needs_resolution(
1697 run.model_response(tool_call_turn("call_2", "unknown"))
1698 .expect("model_response should succeed"),
1699 );
1700 let err = run
1701 .resolve_invalid_tool_call(InvalidToolCallHookAction::retry("again"))
1702 .expect_err("budget exhausted");
1703 assert!(matches!(err, PromptError::UnknownToolCall { .. }));
1704 }
1705
1706 #[test]
1707 fn invalid_tool_call_retry_cannot_emit_call_past_total_budget() {
1708 let mut run = AgentRun::new("call something")
1709 .max_turns(1)
1710 .max_invalid_tool_call_retries(1);
1711
1712 expect_call_model(&mut run);
1713 expect_needs_resolution(
1714 run.model_response(tool_call_turn("call_1", "unknown"))
1715 .expect("model_response should succeed"),
1716 );
1717 let outcome = run
1718 .resolve_invalid_tool_call(InvalidToolCallHookAction::retry("use add instead"))
1719 .expect("retry resolution should be accepted");
1720 assert!(matches!(outcome, ModelTurnOutcome::TurnRetried));
1721 assert_eq!(run.completion_calls().len(), 1);
1722
1723 let err = run
1724 .next_step()
1725 .expect_err("retry must not emit a second model call");
1726 assert!(matches!(
1727 err,
1728 PromptError::MaxTurnsError { max_turns: 1, .. }
1729 ));
1730 assert_eq!(run.turn(), 1);
1731 }
1732
1733 #[test]
1734 fn invalid_tool_call_repair_renames_and_suppresses_response_hook() {
1735 let mut run = AgentRun::new("call something").max_turns(2);
1736
1737 expect_call_model(&mut run);
1738 expect_needs_resolution(
1739 run.model_response(tool_call_turn("call_1", "default_api"))
1740 .expect("model_response should succeed"),
1741 );
1742 let suppressed = expect_continue(
1743 run.resolve_invalid_tool_call(InvalidToolCallHookAction::repair("add"))
1744 .expect("repair should be accepted"),
1745 );
1746 assert!(suppressed);
1747
1748 let calls = expect_call_tools(&mut run);
1749 assert_eq!(calls[0].tool_call.function.name, "add");
1750 assert!(calls[0].preresolved_result.is_none());
1751 }
1752
1753 #[test]
1754 fn invalid_tool_call_repair_to_disallowed_name_fails() {
1755 let mut run = AgentRun::new("call something");
1756
1757 expect_call_model(&mut run);
1758 expect_needs_resolution(
1759 run.model_response(tool_call_turn("call_1", "unknown"))
1760 .expect("model_response should succeed"),
1761 );
1762 let err = run
1763 .resolve_invalid_tool_call(InvalidToolCallHookAction::repair("also_unknown"))
1764 .expect_err("repair to disallowed name should fail");
1765 assert!(matches!(
1766 err,
1767 PromptError::UnknownToolCall { tool_name, .. } if tool_name == "also_unknown"
1768 ));
1769 }
1770
1771 #[test]
1772 fn invalid_tool_call_skip_suppresses_all_peer_executions() {
1773 let mut run = AgentRun::new("call things").max_turns(2);
1774
1775 expect_call_model(&mut run);
1776 let turn = ModelTurn::new(
1777 None,
1778 OneOrMany::many(vec![
1779 tool_call("call_1", "unknown"),
1780 tool_call("call_2", "add"),
1781 ])
1782 .expect("two items"),
1783 Usage::new(),
1784 tool_names(&["add"]),
1785 tool_names(&["add"]),
1786 );
1787 expect_needs_resolution(
1788 run.model_response(turn)
1789 .expect("model_response should succeed"),
1790 );
1791 let suppressed = expect_continue(
1792 run.resolve_invalid_tool_call(InvalidToolCallHookAction::skip("not available"))
1793 .expect("skip should be accepted"),
1794 );
1795 assert!(suppressed);
1796
1797 let calls = expect_call_tools(&mut run);
1798 assert_eq!(calls.len(), 2);
1799 assert!(calls.iter().all(|call| call.preresolved_result.is_some()));
1801 }
1802
1803 #[test]
1804 fn skip_under_tool_choice_none_fails() {
1805 let mut run = AgentRun::new("call something").with_tool_choice(ToolChoice::None);
1806
1807 expect_call_model(&mut run);
1808 expect_needs_resolution(
1809 run.model_response(ModelTurn::new(
1810 None,
1811 OneOrMany::one(tool_call("call_1", "add")),
1812 Usage::new(),
1813 tool_names(&["add"]),
1814 BTreeSet::new(),
1815 ))
1816 .expect("model_response should succeed"),
1817 );
1818 let err = run
1819 .resolve_invalid_tool_call(InvalidToolCallHookAction::skip("nope"))
1820 .expect_err("skip under ToolChoice::None should fail");
1821 assert!(matches!(err, PromptError::UnknownToolCall { .. }));
1822 }
1823
1824 #[test]
1825 fn empty_tool_results_cancel_the_run() {
1826 let mut run = AgentRun::new("call something").max_turns(2);
1827
1828 expect_call_model(&mut run);
1829 expect_continue(
1830 run.model_response(tool_call_turn("call_1", "add"))
1831 .expect("model_response should succeed"),
1832 );
1833 expect_call_tools(&mut run);
1834
1835 let err = run
1836 .tool_results(Vec::new())
1837 .expect_err("empty results should cancel");
1838 assert!(matches!(
1839 err,
1840 PromptError::PromptCancelled { reason, .. }
1841 if reason.contains("tool execution produced no tool results")
1842 ));
1843 }
1844
1845 #[test]
1846 fn out_of_protocol_calls_are_rejected_without_corrupting_state() {
1847 let mut run = AgentRun::new("hello");
1848
1849 let err = run
1850 .tool_results(vec![tool_result("call_1", "x")])
1851 .expect_err("no CallTools pending");
1852 assert!(matches!(err, PromptError::PromptCancelled { .. }));
1853
1854 expect_call_model(&mut run);
1856 let err = run
1857 .next_step()
1858 .expect_err("model response is pending, next_step must be rejected");
1859 assert!(matches!(err, PromptError::PromptCancelled { .. }));
1860 expect_continue(
1861 run.model_response(text_turn("hi"))
1862 .expect("model_response should still succeed"),
1863 );
1864 assert_eq!(expect_done(&mut run).output, "hi");
1865 }
1866
1867 #[test]
1868 fn model_response_rejected_after_streamed_completion_call_record() {
1869 let mut run = AgentRun::new("hello");
1870 expect_call_model(&mut run);
1871 run.record_streamed_completion_call(Usage::new())
1872 .expect("record should succeed");
1873
1874 let err = run
1875 .model_response(text_turn("hi"))
1876 .expect_err("mixed streamed/non-streamed ingestion must be rejected");
1877 assert!(matches!(err, PromptError::PromptCancelled { .. }));
1878 assert_eq!(run.completion_calls().len(), 1);
1880 }
1881
1882 #[test]
1883 fn done_step_is_idempotent() {
1884 let mut run = AgentRun::new("hello");
1885 expect_call_model(&mut run);
1886 expect_continue(
1887 run.model_response(text_turn("hi"))
1888 .expect("model_response should succeed"),
1889 );
1890 assert_eq!(expect_done(&mut run).output, "hi");
1891 assert_eq!(expect_done(&mut run).output, "hi");
1892 }
1893
1894 #[test]
1895 fn serialized_run_alone_carries_pending_tool_calls() {
1896 let mut run = AgentRun::new("add things").max_turns(2);
1897 expect_call_model(&mut run);
1898 expect_continue(
1899 run.model_response(tool_call_turn("call_1", "add"))
1900 .expect("model_response should succeed"),
1901 );
1902 expect_call_tools(&mut run);
1903
1904 let serialized = serde_json::to_string(&run).expect("mid-run state should serialize");
1907 drop(run);
1908 let mut resumed: AgentRun =
1909 serde_json::from_str(&serialized).expect("mid-run state should deserialize");
1910
1911 let calls = expect_call_tools(&mut resumed);
1912 assert_eq!(calls.len(), 1);
1913 assert_eq!(calls[0].tool_call.function.name, "add");
1914 let calls_again = expect_call_tools(&mut resumed);
1916 assert_eq!(calls_again[0].tool_call.id, calls[0].tool_call.id);
1917
1918 let results = calls
1920 .iter()
1921 .map(|call| tool_result(&call.tool_call.id, "2"))
1922 .collect::<Vec<_>>();
1923 resumed
1924 .tool_results(results)
1925 .expect("tool_results should succeed");
1926 expect_call_model(&mut resumed);
1927 expect_continue(
1928 resumed
1929 .model_response(text_turn("done"))
1930 .expect("model_response should succeed"),
1931 );
1932 assert_eq!(expect_done(&mut resumed).output, "done");
1933 }
1934
1935 #[test]
1936 fn tool_results_validates_against_pending_calls() {
1937 let drive_to_pending_tools = || {
1938 let mut run = AgentRun::new("add things").max_turns(2);
1939 expect_call_model(&mut run);
1940 expect_continue(
1941 run.model_response(tool_call_turn("call_1", "add"))
1942 .expect("model_response should succeed"),
1943 );
1944 expect_call_tools(&mut run);
1945 run
1946 };
1947
1948 let mut run = drive_to_pending_tools();
1950 let err = run
1951 .tool_results(vec![tool_result("call_unknown", "2")])
1952 .expect_err("unknown tool call id must be rejected");
1953 assert!(matches!(err, PromptError::PromptCancelled { .. }));
1954 run.tool_results(vec![tool_result("call_1", "2")])
1955 .expect("valid results should still be accepted after a rejection");
1956
1957 let mut run = drive_to_pending_tools();
1959 let err = run
1960 .tool_results(vec![tool_result("call_1", "2"), tool_result("call_1", "3")])
1961 .expect_err("answering one call twice must be rejected");
1962 assert!(matches!(err, PromptError::PromptCancelled { .. }));
1963
1964 let mut run = drive_to_pending_tools();
1966 let err = run
1967 .tool_results(vec![UserContent::text("not a tool result")])
1968 .expect_err("non-tool-result content must be rejected");
1969 assert!(matches!(err, PromptError::PromptCancelled { .. }));
1970 }
1971
1972 #[test]
1973 fn agent_run_deserializes_pre_monoid_suspended_state() {
1974 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}]}}"#;
1978
1979 let mut restored: AgentRun =
1980 serde_json::from_str(fixture).expect("old-format suspended run should deserialize");
1981 assert_eq!(restored.completion_calls()[0].usage, Usage::new());
1982
1983 let calls = expect_call_tools(&mut restored);
1984 assert_eq!(calls.len(), 1);
1985 restored
1986 .tool_results(vec![tool_result("call_1", "2")])
1987 .expect("tool_results should succeed");
1988 expect_call_model(&mut restored);
1989 }
1990
1991 #[test]
1992 fn serde_round_trip_at_exhausted_budget_preserves_boundary() {
1993 let mut run = AgentRun::new("add things").max_turns(1);
1994 expect_call_model(&mut run);
1995 expect_continue(
1996 run.model_response(tool_call_turn("call_1", "add"))
1997 .expect("model_response should succeed"),
1998 );
1999 expect_call_tools(&mut run);
2000 run.tool_results(vec![tool_result("call_1", "2")])
2001 .expect("tool_results should succeed");
2002
2003 let serialized = serde_json::to_string(&run).expect("exhausted run should serialize");
2004 let mut restored: AgentRun =
2005 serde_json::from_str(&serialized).expect("exhausted run should deserialize");
2006 assert_eq!(restored.completion_calls().len(), 1);
2007 let err = restored
2008 .next_step()
2009 .expect_err("restored run must not emit a second model call");
2010 assert!(matches!(
2011 err,
2012 PromptError::MaxTurnsError { max_turns: 1, .. }
2013 ));
2014 assert_eq!(restored.turn(), 1);
2015 }
2016
2017 #[test]
2018 fn serde_round_trip_mid_run_resumes_identically() {
2019 let drive_to_pending_tools = || {
2020 let mut run = AgentRun::new("add things").max_turns(2);
2021 expect_call_model(&mut run);
2022 expect_continue(
2023 run.model_response(
2024 tool_call_turn("call_1", "add").with_usage_for_test(usage(10, 5)),
2025 )
2026 .expect("model_response should succeed"),
2027 );
2028 expect_call_tools(&mut run);
2029 run
2030 };
2031
2032 let finish = |mut run: AgentRun| {
2033 run.tool_results(vec![tool_result("call_1", "2")])
2034 .expect("tool_results should succeed");
2035 expect_call_model(&mut run);
2036 expect_continue(
2037 run.model_response(text_turn("done").with_usage_for_test(usage(3, 4)))
2038 .expect("model_response should succeed"),
2039 );
2040 expect_done(&mut run)
2041 };
2042
2043 let uninterrupted = finish(drive_to_pending_tools());
2044
2045 let suspended = drive_to_pending_tools();
2046 let serialized = serde_json::to_string(&suspended).expect("mid-run state should serialize");
2047 let restored: AgentRun =
2048 serde_json::from_str(&serialized).expect("mid-run state should deserialize");
2049 let resumed = finish(restored);
2050
2051 assert_eq!(resumed.output, uninterrupted.output);
2052 assert_eq!(resumed.usage, uninterrupted.usage);
2053 assert_eq!(resumed.completion_calls, uninterrupted.completion_calls);
2054 assert_eq!(
2058 serde_json::to_value(&resumed.messages).expect("messages should serialize"),
2059 serde_json::to_value(&uninterrupted.messages).expect("messages should serialize"),
2060 );
2061 }
2062
2063 #[test]
2064 fn pending_invalid_tool_call_survives_serde_round_trip() {
2065 let mut run = AgentRun::new("call something");
2066 expect_call_model(&mut run);
2067 let context = expect_needs_resolution(
2068 run.model_response(tool_call_turn("call_1", "unknown"))
2069 .expect("model_response should succeed"),
2070 );
2071
2072 let serialized = serde_json::to_string(&run).expect("state should serialize");
2073 let restored: AgentRun =
2074 serde_json::from_str(&serialized).expect("state should deserialize");
2075 let restored_context = restored
2076 .pending_invalid_tool_call()
2077 .expect("pending resolution should survive serialization");
2078 assert_eq!(restored_context.tool_name, context.tool_name);
2079 assert_eq!(
2080 restored_context.chat_history.len(),
2081 context.chat_history.len()
2082 );
2083 }
2084
2085 fn output_tool_turn(id: &str, name: &str) -> ModelTurn {
2088 ModelTurn::new(
2089 None,
2090 OneOrMany::one(tool_call(id, name)),
2091 Usage::new(),
2092 tool_names(&["add"]),
2093 tool_names(&["add", name]),
2094 )
2095 }
2096
2097 fn assert_no_orphan_tool_use(messages: &[Message]) {
2100 let mut answered = BTreeSet::new();
2101 for message in messages {
2102 if let Message::User { content } = message {
2103 for item in content.iter() {
2104 if let UserContent::ToolResult(result) = item {
2105 answered.insert(result.id.clone());
2106 }
2107 }
2108 }
2109 }
2110 for message in messages {
2111 if let Message::Assistant { content, .. } = message {
2112 for item in content.iter() {
2113 if let AssistantContent::ToolCall(call) = item {
2114 assert!(
2115 answered.contains(&call.id),
2116 "assistant tool_call {:?} has no matching tool_result in history",
2117 call.id
2118 );
2119 }
2120 }
2121 }
2122 }
2123 }
2124
2125 #[test]
2126 fn output_tool_call_finalizes_run_with_arguments() {
2127 let mut run = AgentRun::new("summarize").with_output_tool_name("final_result");
2128
2129 expect_call_model(&mut run);
2130 expect_continue(
2131 run.model_response(output_tool_turn("call_1", "final_result"))
2132 .expect("model_response should succeed"),
2133 );
2134
2135 let response = expect_done(&mut run);
2137 assert_eq!(response.output, r#"{"x":1}"#);
2138 assert!(run.is_done());
2139
2140 let messages = response.messages.expect("messages should be recorded");
2143 assert_no_orphan_tool_use(&messages);
2144 assert!(matches!(
2145 messages.last(),
2146 Some(Message::Assistant { content, .. })
2147 if assistant_text_from_choice(content) == r#"{"x":1}"#
2148 ));
2149 }
2150
2151 #[test]
2152 fn output_tool_call_wins_over_sibling_real_tool_calls() {
2153 let mut run = AgentRun::new("do it")
2154 .max_turns(2)
2155 .with_output_tool_name("final_result");
2156
2157 expect_call_model(&mut run);
2158 let turn = ModelTurn::new(
2161 None,
2162 OneOrMany::many(vec![
2163 tool_call("call_1", "add"),
2164 tool_call("call_2", "final_result"),
2165 ])
2166 .expect("two items"),
2167 Usage::new(),
2168 tool_names(&["add"]),
2169 tool_names(&["add", "final_result"]),
2170 );
2171 expect_continue(
2172 run.model_response(turn)
2173 .expect("model_response should succeed"),
2174 );
2175
2176 let response = expect_done(&mut run);
2177 assert_eq!(response.output, r#"{"x":1}"#);
2178 assert!(run.is_done());
2179
2180 let messages = response.messages.expect("messages should be recorded");
2183 assert_no_orphan_tool_use(&messages);
2184 assert!(
2185 messages.iter().all(|message| match message {
2186 Message::Assistant { content, .. } => !content
2187 .iter()
2188 .any(|item| matches!(item, AssistantContent::ToolCall(_))),
2189 _ => true,
2190 }),
2191 "no assistant tool calls should survive in the finalized history"
2192 );
2193 }
2194
2195 #[test]
2196 fn real_tool_calls_still_execute_when_output_tool_unused() {
2197 let mut run = AgentRun::new("add things")
2200 .max_turns(2)
2201 .with_output_tool_name("final_result");
2202
2203 expect_call_model(&mut run);
2204 expect_continue(
2205 run.model_response(tool_call_turn("call_1", "add"))
2206 .expect("model_response should succeed"),
2207 );
2208
2209 let calls = expect_call_tools(&mut run);
2210 assert_eq!(calls.len(), 1);
2211 assert_eq!(calls[0].tool_call.function.name, "add");
2212 }
2213
2214 fn required_field_schema(field: &str) -> serde_json::Value {
2215 json!({
2216 "type": "object",
2217 "required": [field],
2218 "properties": { field: { "type": "string" } },
2219 })
2220 }
2221
2222 #[test]
2223 fn tool_mode_reprompts_when_output_tool_not_called() {
2224 let mut run = AgentRun::new("summarize")
2227 .max_turns(2)
2228 .with_output_tool_name("final_result")
2229 .with_output_validation(Some(required_field_schema("summary")), 1);
2230
2231 expect_call_model(&mut run);
2232 expect_continue(
2233 run.model_response(text_turn("here is the answer"))
2234 .expect("model_response should succeed"),
2235 );
2236
2237 let (prompt, _history, turn) = expect_call_model(&mut run);
2240 assert_eq!(turn, 2);
2241 let prompt_json = serde_json::to_string(&prompt).expect("prompt should serialize");
2242 assert!(
2243 prompt_json.contains("final_result"),
2244 "re-prompt feedback should name the output tool: {prompt_json}"
2245 );
2246 assert!(!run.is_done());
2247 }
2248
2249 #[test]
2250 fn tool_mode_reprompts_when_output_args_missing_required_fields() {
2251 let mut run = AgentRun::new("summarize")
2254 .max_turns(2)
2255 .with_output_tool_name("final_result")
2256 .with_output_validation(Some(required_field_schema("summary")), 1);
2258
2259 expect_call_model(&mut run);
2260 expect_continue(
2261 run.model_response(output_tool_turn("call_1", "final_result"))
2262 .expect("model_response should succeed"),
2263 );
2264
2265 let (_prompt, _history, turn) = expect_call_model(&mut run);
2266 assert_eq!(turn, 2);
2267 assert!(!run.is_done());
2268 }
2269
2270 #[test]
2271 fn tool_mode_accepts_valid_json_text_without_reprompting() {
2272 let mut run = AgentRun::new("summarize")
2275 .max_turns(3)
2276 .with_output_tool_name("final_result")
2277 .with_output_validation(Some(required_field_schema("summary")), 1);
2278
2279 expect_call_model(&mut run);
2280 expect_continue(
2281 run.model_response(text_turn(r#"{"summary":"all good"}"#))
2282 .expect("model_response should succeed"),
2283 );
2284
2285 let response = expect_done(&mut run);
2286 assert_eq!(response.output, r#"{"summary":"all good"}"#);
2287 assert!(run.is_done());
2288 }
2289
2290 #[test]
2291 fn tool_mode_finalizes_best_effort_when_model_call_budget_exhausted() {
2292 let mut run = AgentRun::new("summarize")
2293 .max_turns(1)
2294 .with_output_tool_name("final_result")
2295 .with_output_validation(Some(required_field_schema("summary")), 1);
2296
2297 expect_call_model(&mut run);
2298 expect_continue(
2299 run.model_response(text_turn("invalid output"))
2300 .expect("model_response should succeed"),
2301 );
2302
2303 let response = expect_done(&mut run);
2304 assert_eq!(response.output, "invalid output");
2305 assert_eq!(run.turn(), 1);
2306 }
2307
2308 #[test]
2309 fn tool_mode_finalizes_best_effort_when_output_retry_budget_exhausted() {
2310 let mut run = AgentRun::new("summarize")
2314 .max_turns(3)
2315 .with_output_tool_name("final_result")
2316 .with_output_validation(Some(required_field_schema("summary")), 0);
2317
2318 expect_call_model(&mut run);
2319 expect_continue(
2320 run.model_response(output_tool_turn("call_1", "final_result"))
2321 .expect("model_response should succeed"),
2322 );
2323
2324 let response = expect_done(&mut run);
2325 assert_eq!(response.output, r#"{"x":1}"#);
2326 let messages = response.messages.expect("messages should be recorded");
2327 assert_no_orphan_tool_use(&messages);
2328 }
2329
2330 #[test]
2331 fn set_output_tool_name_is_idempotent_and_only_fills_when_unset() {
2332 let mut run = AgentRun::new("x").with_output_tool_name("first");
2335 run.set_output_tool_name(Some("second".to_string()));
2336 run.set_output_tool_name(None);
2337 assert_eq!(run.output_tool_name.as_deref(), Some("first"));
2338
2339 let mut run = AgentRun::new("x");
2341 run.set_output_tool_name(None);
2342 assert_eq!(run.output_tool_name, None);
2343 run.set_output_tool_name(Some("filled".to_string()));
2344 assert_eq!(run.output_tool_name.as_deref(), Some("filled"));
2345 }
2346
2347 impl ModelTurn {
2348 fn with_usage_for_test(mut self, usage: Usage) -> Self {
2349 self.usage = usage;
2350 self
2351 }
2352 }
2353
2354 #[test]
2361 fn durable_human_in_the_loop_approval_survives_serialize_resume() {
2362 let mut run = AgentRun::new("pay two invoices").max_turns(3);
2363 let (_, _, turn) = expect_call_model(&mut run);
2364 assert_eq!(turn, 1);
2365
2366 let two_calls =
2368 OneOrMany::many([tool_call("c1", "add"), tool_call("c2", "add")]).expect("two calls");
2369 let outcome = run
2370 .model_response(ModelTurn::new(
2371 None,
2372 two_calls,
2373 Usage::new(),
2374 tool_names(&["add"]),
2375 tool_names(&["add"]),
2376 ))
2377 .expect("model_response");
2378 expect_continue(outcome);
2379
2380 let checkpoint = serde_json::to_string(&run).expect("serialize suspended run");
2383 let mut resumed: AgentRun = serde_json::from_str(&checkpoint).expect("deserialize run");
2384
2385 let calls = expect_call_tools(&mut resumed);
2387 assert_eq!(calls.len(), 2);
2388 assert_eq!(calls[0].tool_call.id, "c1");
2389 assert_eq!(calls[1].tool_call.id, "c2");
2390
2391 resumed
2394 .tool_results(vec![
2395 tool_result("c1", "approved-result"),
2396 tool_result("c2", "denied by reviewer: second payment not authorized"),
2397 ])
2398 .expect("tool_results on the resumed run");
2399
2400 let after = serde_json::to_string(&resumed).expect("serialize resumed run");
2402 assert!(
2403 after.contains("approved-result"),
2404 "the approved call's result must be in the resumed run state"
2405 );
2406 assert!(
2407 after.contains("denied by reviewer: second payment not authorized"),
2408 "the denied call's reason must be in the resumed run state"
2409 );
2410
2411 let (_, _, turn2) = expect_call_model(&mut resumed);
2413 assert_eq!(turn2, 2);
2414 expect_continue(
2415 resumed
2416 .model_response(text_turn("done"))
2417 .expect("model_response 2"),
2418 );
2419 let response = expect_done(&mut resumed);
2420 assert_eq!(response.output, "done");
2421 }
2422}