use std::future::Future;
use std::pin::Pin;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::config::ParallelDispatchConfig;
use crate::engine::core::machine::MachineState;
use crate::error::LoopError;
use crate::reflection::{Correction, CorrectionResult, CorrectionType};
fn instant_now() -> Instant {
Instant::now()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RunConfig {
pub max_turns: usize,
pub parallel_tool_dispatch: ParallelDispatchConfig,
pub reset_managers: bool,
pub memory_top_k: usize,
}
impl Default for RunConfig {
fn default() -> Self {
Self {
max_turns: 200,
parallel_tool_dispatch: ParallelDispatchConfig::default(),
reset_managers: false,
memory_top_k: 3,
}
}
}
impl RunConfig {
#[must_use]
pub fn with_parallel_dispatch(mut self, config: crate::config::ParallelDispatchConfig) -> Self {
self.parallel_tool_dispatch = config;
self
}
#[must_use]
pub fn with_max_turns(mut self, max_turns: usize) -> Self {
self.max_turns = max_turns;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum StopReason {
EndTurn,
ToolCall,
MaxTokens,
StopSequence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TurnMode {
NonStreaming,
#[cfg(feature = "streaming")]
Streaming,
}
pub(crate) fn default_turn_mode() -> TurnMode {
#[cfg(feature = "streaming")]
{
TurnMode::Streaming
}
#[cfg(not(feature = "streaming"))]
{
TurnMode::NonStreaming
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub tool: String,
pub input: serde_json::Value,
}
impl ToolCall {
pub fn apply_correction(&mut self, correction: &Correction) -> CorrectionResult {
match correction.correction_type {
CorrectionType::InputFix => {
if let Some(ref modified) = correction.modified_input {
if modified.is_object() {
tracing::debug!(
tool = %self.tool,
"applying InputFix correction from reflector"
);
self.input = modified.clone();
CorrectionResult::Applied
} else {
CorrectionResult::Failed(
"InputFix correction modified_input must be a JSON object".to_string(),
)
}
} else {
CorrectionResult::Failed(
"InputFix correction missing modified_input".to_string(),
)
}
}
CorrectionType::ToolChange => {
if let Some(ref alt) = correction.alternative_tool {
tracing::debug!(
old_tool = %self.tool,
new_tool = %alt,
"applying ToolChange correction from reflector"
);
self.tool.clone_from(alt);
CorrectionResult::Applied
} else {
CorrectionResult::Failed(
"ToolChange correction missing alternative_tool".to_string(),
)
}
}
CorrectionType::PrerequisiteFix | CorrectionType::ApproachChange => {
CorrectionResult::Skipped
}
CorrectionType::Escalate => CorrectionResult::Skipped,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Turn {
pub turn: usize,
pub input: String,
pub output: String,
pub tool_calls: Vec<ToolCall>,
pub input_tokens: u64,
pub output_tokens: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Run {
pub id: Uuid,
#[serde(skip, default = "instant_now")]
pub start: Instant,
#[serde(skip)]
pub end: Option<Instant>,
pub turns: Vec<Turn>,
pub input: String,
pub output: Option<String>,
pub config: RunConfig,
#[serde(skip)]
pub stop_reason: Option<LoopError>,
}
impl Run {
#[must_use]
pub fn new(input: impl Into<String>, config: &RunConfig) -> Self {
Self {
id: Uuid::new_v4(),
start: Instant::now(),
end: None,
turns: Vec::new(),
input: input.into(),
output: None,
config: config.clone(),
stop_reason: None,
}
}
#[must_use]
pub fn turn_count(&self) -> usize {
self.turns.len()
}
#[must_use]
pub fn input_tokens(&self) -> u64 {
self.turns.iter().map(|t| t.input_tokens).sum()
}
#[must_use]
pub fn output_tokens(&self) -> u64 {
self.turns.iter().map(|t| t.output_tokens).sum()
}
#[must_use]
pub fn tool_call_count(&self) -> usize {
self.turns.iter().map(|t| t.tool_calls.len()).sum()
}
#[must_use]
pub fn duration(&self) -> Duration {
match self.end {
Some(end) => end.saturating_duration_since(self.start),
None => self.start.elapsed(),
}
}
#[must_use]
pub fn total_tokens(&self) -> u64 {
self.input_tokens().saturating_add(self.output_tokens())
}
}
impl Default for Run {
fn default() -> Self {
Self::new(String::new(), &RunConfig::default())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: Uuid,
pub config: crate::config::SessionConfig,
#[serde(skip)]
pub session_start: Option<Instant>,
pub runs: Vec<Run>,
}
impl Session {
#[must_use]
pub fn new(config: crate::config::SessionConfig) -> Self {
Self {
id: Uuid::new_v4(),
config,
session_start: None,
runs: Vec::new(),
}
}
#[must_use]
pub fn current_run(&self) -> Option<&Run> {
self.runs.last()
}
#[must_use]
pub fn current_run_mut(&mut self) -> Option<&mut Run> {
self.runs.last_mut()
}
#[must_use]
pub fn total_turns(&self) -> usize {
self.runs.iter().map(Run::turn_count).sum()
}
#[must_use]
pub fn total_duration(&self) -> Duration {
self.runs.iter().map(Run::duration).sum()
}
#[must_use]
pub fn total_input_tokens(&self) -> u64 {
self.runs.iter().map(Run::input_tokens).sum()
}
#[must_use]
pub fn total_output_tokens(&self) -> u64 {
self.runs.iter().map(Run::output_tokens).sum()
}
}
pub type RunResult = Result<Run, LoopError>;
pub trait Loop: Send + Sync {
fn run<'a>(
&'a mut self,
input: &'a str,
run_config: &'a RunConfig,
) -> Pin<Box<dyn Future<Output = RunResult> + Send + 'a>>;
fn should_continue(&self) -> bool;
fn finalize<'a>(
&'a mut self,
error: Option<&'a LoopError>,
) -> Pin<Box<dyn Future<Output = RunResult> + Send + 'a>>;
fn state(&self) -> MachineState;
fn cancel(&self);
fn stop_reason(&self) -> Option<LoopError> {
None
}
}