use std::collections::BTreeSet;
use crate::{ContentBlock, Message, Role};
pub(crate) const INTERRUPTED_TOOL_CALL_RESULT: &str = "The process stopped while this tool call \
was running, so its effect is unknown: it may have completed, partly run, or not run at all. \
It was not retried. Check the current state before repeating it.";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct RecoveryOutcome {
pub interrupted: bool,
pub interrupted_run_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum TurnState {
Finished,
Interrupted { unresolved: Vec<String> },
}
impl TurnState {
pub(crate) fn of(history: &[Message]) -> Self {
let Some(index) = history
.iter()
.rposition(|message| message.role == Role::Assistant)
else {
return Self::Interrupted {
unresolved: Vec::new(),
};
};
let answered: BTreeSet<&str> = history[index + 1..]
.iter()
.flat_map(|message| &message.content)
.filter_map(|block| match block {
ContentBlock::ToolResult { tool_use_id, .. } => Some(tool_use_id.as_str()),
_ => None,
})
.collect();
let unresolved: Vec<String> = history[index]
.content
.iter()
.filter_map(|block| match block {
ContentBlock::ToolUse { id, .. } if !answered.contains(id.as_str()) => {
Some(id.clone())
}
_ => None,
})
.collect();
if unresolved.is_empty() && index + 1 == history.len() {
Self::Finished
} else {
Self::Interrupted { unresolved }
}
}
}
pub(crate) fn closing_message(unresolved: &[String]) -> Option<Message> {
if unresolved.is_empty() {
return None;
}
Some(Message {
role: Role::User,
content: unresolved
.iter()
.map(|id| ContentBlock::ToolResult {
tool_use_id: id.clone(),
content: INTERRUPTED_TOOL_CALL_RESULT.to_string().into(),
is_error: true,
})
.collect(),
})
}