use std::collections::HashSet;
use crate::engine::{ActInput, ActPlan};
use crate::events::{EventData, TokenUsage};
use everruns_contracts::tool_types::ToolCall;
use super::*;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct InterruptedToolCalls {
pub turn_id: TurnId,
pub tool_calls: Vec<ToolCall>,
}
struct OpenTurn {
turn_id: TurnId,
input_message_id: MessageId,
started_at: chrono::DateTime<Utc>,
reasons: u32,
tool_call_count: u32,
usage: Option<TokenUsage>,
final_message_id: Option<MessageId>,
calls: Vec<ToolCall>,
settled: HashSet<String>,
}
impl OpenTurn {
fn unfinished(self) -> Option<(Self, Vec<ToolCall>)> {
let calls: Vec<ToolCall> = self
.calls
.iter()
.filter(|call| !self.settled.contains(&call.id))
.cloned()
.collect();
(!calls.is_empty()).then_some((self, calls))
}
}
impl InProcessRuntime {
pub async fn interrupted_tool_calls(
&self,
session_id: SessionId,
) -> Result<Option<InterruptedToolCalls>> {
Ok(self
.open_turn(session_id)
.await?
.map(|(turn, tool_calls)| InterruptedToolCalls {
turn_id: turn.turn_id,
tool_calls,
}))
}
pub async fn resume_interrupted_turn(
&self,
session_id: SessionId,
steering: TurnSteering,
) -> Result<TurnResult> {
let (state, plan) = self
.interrupted_turn_plan(session_id)
.await?
.ok_or_else(|| no_interrupted_turn(session_id))?;
let snapshot = self.resolved_execution_snapshot(session_id).await?;
let drive = TurnDrive {
session_id,
org_id: state.org_id,
turn_id: state.turn_id.unwrap_or_default(),
input_message_id: state.input_message_id,
harness_id: snapshot.harness_id,
agent_id: snapshot.agent_id,
workspace_id: snapshot.workspace_id,
};
self.drive_turn_plan(
drive,
InProcessExecution::new(state),
TurnPlan::ScheduleAct(plan),
steering,
)
.await
}
#[doc(hidden)]
pub async fn interrupted_turn_plan(
&self,
session_id: SessionId,
) -> Result<Option<(TurnState, ActPlan)>> {
let Some((turn, tool_calls)) = self.open_turn(session_id).await? else {
return Ok(None);
};
let snapshot = self.resolved_execution_snapshot(session_id).await?;
let org_id = in_process_internal_org_id(&snapshot.organization_id);
let context = self.load_context(session_id).await?;
let state = TurnState {
org_id,
session_id,
harness_id: snapshot.harness_id,
agent_id: snapshot.agent_id,
input_message_id: turn.input_message_id,
turn_id: Some(turn.turn_id),
previous_response_id: None,
iteration: turn.reasons.max(1),
request_id: None,
started_at: Some(turn.started_at),
cumulative_usage: turn.usage.clone(),
tool_call_count: turn.tool_call_count,
llm_call_count: turn.reasons,
time_to_first_token_ms: None,
final_message_id: turn.final_message_id,
final_answer_preview: None,
};
let plan = ActPlan {
input: ActInput {
org_id: Some(org_id),
context: ExecutionContext::new(session_id, turn.turn_id, turn.input_message_id)
.with_workspace_id(snapshot.workspace_id)
.next_exec(),
harness_id: snapshot.harness_id,
agent_id: snapshot.agent_id,
tool_calls,
tool_definitions: context.runtime_agent.tools.clone(),
locale: context.resolved_locale.clone(),
blueprint_id: context.snapshot.blueprint_id.clone(),
network_access: context.runtime_agent.network_access.clone(),
parallel_tool_calls: context.runtime_agent.parallel_tool_calls,
},
previous_response_id: None,
iteration: state.iteration,
request_id: None,
resume_state: Box::new(state.clone()),
};
Ok(Some((state, plan)))
}
async fn open_turn(&self, session_id: SessionId) -> Result<Option<(OpenTurn, Vec<ToolCall>)>> {
if lock_parked(&self.parked_turns).contains_key(&session_id) {
return Ok(None);
}
let limit = EventReadLimit::default();
let mut request = EventReadRequest::new(session_id, limit);
let mut open: Option<OpenTurn> = None;
loop {
let page = self
.event_log
.read_page(request)
.await
.map_err(|error| AgentLoopError::store(error.to_string()))?;
for event in page.events {
observe(&mut open, event);
}
let Some(cursor) = page.next_cursor else {
break;
};
request = EventReadRequest::from_cursor(cursor, limit);
}
Ok(open.and_then(OpenTurn::unfinished))
}
}
fn no_interrupted_turn(session_id: SessionId) -> AgentLoopError {
AgentLoopError::store(format!(
"session {session_id} has no turn interrupted in its tool calls"
))
}
fn observe(open: &mut Option<OpenTurn>, event: Event) {
let ends = |turn_id: &TurnId| open.as_ref().is_some_and(|turn| turn.turn_id == *turn_id);
match &event.data {
EventData::TurnStarted(data) => {
*open = Some(OpenTurn {
turn_id: data.turn_id,
input_message_id: data.input_message_id,
started_at: event.ts,
reasons: 0,
tool_call_count: 0,
usage: None,
final_message_id: None,
calls: Vec::new(),
settled: HashSet::new(),
});
return;
}
EventData::TurnCompleted(data) if ends(&data.turn_id) => *open = None,
EventData::TurnFailed(data) if ends(&data.turn_id) => *open = None,
EventData::TurnCancelled(data) if ends(&data.turn_id) => *open = None,
EventData::TurnSealed(data) if ends(&data.turn_id) => *open = None,
_ => {}
}
let Some(turn) = open
.as_mut()
.filter(|turn| event.context.turn_id == Some(turn.turn_id))
else {
return;
};
match event.data {
EventData::ReasonCompleted(data) => {
turn.reasons = turn.reasons.saturating_add(1);
turn.tool_call_count = turn.tool_call_count.saturating_add(data.tool_call_count);
if let Some(usage) = &data.usage {
match &mut turn.usage {
Some(total) => total.add(usage),
None => turn.usage = Some(usage.clone()),
}
}
}
EventData::OutputMessageCompleted(data) => {
turn.final_message_id = Some(data.message.id);
turn.calls = data
.message
.tool_calls()
.into_iter()
.map(|call| ToolCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: call.arguments.clone(),
})
.collect();
turn.settled.clear();
}
EventData::ToolCompleted(data) => {
turn.settled.insert(data.tool_call_id);
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use crate::events::{OutputMessageCompletedData, TurnCancelledData, TurnStartedData};
use super::*;
fn call(id: &str) -> ToolCall {
ToolCall {
id: id.into(),
name: "share".into(),
arguments: serde_json::json!({ "to": id }),
}
}
fn turn_log(calls: &[&str], settled: &[&str], cancelled: bool) -> (TurnId, Vec<Event>) {
let session_id = SessionId::new();
let turn_id = TurnId::new();
let input_message_id = MessageId::new();
let context = EventContext::turn(turn_id, input_message_id);
let mut events = vec![
Event::new(
session_id,
context.clone(),
TurnStartedData {
turn_id,
input_message_id,
input_content: None,
agent_id: None,
agent_name: None,
agent_description: None,
},
),
Event::new(
session_id,
context.clone(),
OutputMessageCompletedData::new(RuntimeMessage::assistant_with_tools(
"",
calls.iter().map(|id| call(id)).collect(),
)),
),
];
for id in settled {
events.push(Event::new(
session_id,
context.clone(),
ToolCompletedData::success(id.to_string(), "share".into(), Vec::new(), None),
));
}
if cancelled {
events.push(Event::new(
session_id,
context,
TurnCancelledData {
turn_id,
reason: None,
usage: None,
},
));
}
(turn_id, events)
}
fn fold(events: Vec<Event>) -> Option<(TurnId, Vec<ToolCall>)> {
let mut open = None;
for event in events {
observe(&mut open, event);
}
open.and_then(OpenTurn::unfinished)
.map(|(turn, calls)| (turn.turn_id, calls))
}
#[test]
fn a_turn_without_an_end_reports_its_unfinished_calls() {
let (turn_id, events) = turn_log(&["a", "b"], &["a"], false);
assert_eq!(fold(events), Some((turn_id, vec![call("b")])));
}
#[test]
fn an_ended_turn_or_a_settled_act_is_not_interrupted() {
assert_eq!(fold(turn_log(&["a"], &[], true).1), None);
assert_eq!(fold(turn_log(&["a"], &["a"], false).1), None);
}
#[test]
fn only_the_last_turn_counts() {
let (_, mut events) = turn_log(&["a"], &[], false);
events.extend(turn_log(&["b"], &["b"], false).1);
assert_eq!(fold(events), None);
}
}