use std::future::Future;
use std::pin::Pin;
use std::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::LoopError;
pub use crate::config::LoopConfig;
pub use crate::tool::ToolDispatchResult;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LoopState {
Idle,
Processing {
turn: usize,
},
WaitingForTool {
tool: String,
started_at: SystemTime,
},
Compacting {
reason: crate::compact::types::CompactReason,
},
Reflecting {
error_count: usize,
},
Completed {
summary: String,
},
Failed {
error: String,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct TurnResult {
pub text: String,
pub tool_calls: Vec<ToolCall>,
pub tool_results: Vec<ToolDispatchResult>,
pub input_tokens: u64,
pub output_tokens: u64,
pub duration: Duration,
pub is_complete: bool,
pub stop_reason: StopReason,
}
impl TurnResult {
#[must_use]
pub fn completed(text: impl Into<String>) -> Self {
Self {
text: text.into(),
tool_calls: Vec::new(),
tool_results: Vec::new(),
input_tokens: 0,
output_tokens: 0,
duration: Duration::ZERO,
is_complete: true,
stop_reason: StopReason::EndTurn,
}
}
#[must_use]
pub fn continuing(text: impl Into<String>) -> Self {
Self {
text: text.into(),
tool_calls: Vec::new(),
tool_results: Vec::new(),
input_tokens: 0,
output_tokens: 0,
duration: Duration::ZERO,
is_complete: false,
stop_reason: StopReason::EndTurn,
}
}
#[must_use]
pub fn has_tool_calls(&self) -> bool {
!self.tool_calls.is_empty()
}
#[must_use]
pub fn total_tokens(&self) -> u64 {
self.input_tokens.saturating_add(self.output_tokens)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum StopReason {
EndTurn,
ToolCall,
MaxTokens,
StopSequence,
}
#[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: &crate::reflection::Correction,
_prior_result: &crate::tool::ToolDispatchResult,
) -> crate::reflection::CorrectionResult {
use crate::reflection::CorrectionType;
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();
crate::reflection::CorrectionResult::Applied
} else {
crate::reflection::CorrectionResult::Failed(
"InputFix correction modified_input must be a JSON object".to_string(),
)
}
} else {
crate::reflection::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);
crate::reflection::CorrectionResult::Applied
} else {
crate::reflection::CorrectionResult::Failed(
"ToolChange correction missing alternative_tool".to_string(),
)
}
}
CorrectionType::PrerequisiteFix | CorrectionType::ApproachChange => {
crate::reflection::CorrectionResult::Skipped
}
CorrectionType::Escalate => crate::reflection::CorrectionResult::Skipped,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SessionResult {
pub session_id: Uuid,
pub total_turns: usize,
pub input_tokens: u64,
pub output_tokens: u64,
pub total_duration: Duration,
pub tool_calls: usize,
pub success: bool,
pub final_output: Option<String>,
pub error: Option<String>,
}
impl Default for SessionResult {
fn default() -> Self {
Self {
session_id: Uuid::nil(),
total_turns: 0,
input_tokens: 0,
output_tokens: 0,
total_duration: Duration::ZERO,
tool_calls: 0,
success: false,
final_output: None,
error: None,
}
}
}
impl SessionResult {
#[must_use]
pub fn success(session_id: Uuid) -> Self {
Self {
session_id,
total_turns: 0,
input_tokens: 0,
output_tokens: 0,
total_duration: Duration::ZERO,
tool_calls: 0,
success: true,
final_output: None,
error: None,
}
}
#[must_use]
pub fn failed(session_id: Uuid, error: impl Into<String>) -> Self {
Self {
session_id,
total_turns: 0,
input_tokens: 0,
output_tokens: 0,
total_duration: Duration::ZERO,
tool_calls: 0,
success: false,
final_output: None,
error: Some(error.into()),
}
}
#[must_use]
pub fn total_tokens(&self) -> u64 {
self.input_tokens.saturating_add(self.output_tokens)
}
#[cfg(feature = "testing")]
#[must_use]
pub fn builder() -> SessionResultBuilder {
SessionResultBuilder {
session_id: Uuid::nil(),
total_turns: 0,
input_tokens: 0,
output_tokens: 0,
total_duration: Duration::ZERO,
tool_calls: 0,
success: false,
final_output: None,
error: None,
}
}
}
#[cfg(feature = "testing")]
#[derive(Debug, Clone)]
pub struct SessionResultBuilder {
session_id: Uuid,
total_turns: usize,
input_tokens: u64,
output_tokens: u64,
total_duration: Duration,
tool_calls: usize,
success: bool,
final_output: Option<String>,
error: Option<String>,
}
#[cfg(feature = "testing")]
impl SessionResultBuilder {
#[must_use]
pub fn session_id(mut self, id: Uuid) -> Self {
self.session_id = id;
self
}
#[must_use]
pub fn total_turns(mut self, turns: usize) -> Self {
self.total_turns = turns;
self
}
#[must_use]
pub fn input_tokens(mut self, tokens: u64) -> Self {
self.input_tokens = tokens;
self
}
#[must_use]
pub fn output_tokens(mut self, tokens: u64) -> Self {
self.output_tokens = tokens;
self
}
#[must_use]
pub fn total_duration(mut self, duration: Duration) -> Self {
self.total_duration = duration;
self
}
#[must_use]
pub fn tool_calls(mut self, calls: usize) -> Self {
self.tool_calls = calls;
self
}
#[must_use]
pub fn success(mut self, success: bool) -> Self {
self.success = success;
self
}
#[must_use]
pub fn final_output(mut self, output: impl Into<String>) -> Self {
self.final_output = Some(output.into());
self
}
#[must_use]
pub fn error(mut self, error: impl Into<String>) -> Self {
self.error = Some(error.into());
self
}
#[must_use]
pub fn build(self) -> SessionResult {
SessionResult {
session_id: self.session_id,
total_turns: self.total_turns,
input_tokens: self.input_tokens,
output_tokens: self.output_tokens,
total_duration: self.total_duration,
tool_calls: self.tool_calls,
success: self.success,
final_output: self.final_output,
error: self.error,
}
}
}
pub trait Loop: Send + Sync {
fn initialize<'a>(
&'a mut self,
config: &'a LoopConfig,
) -> Pin<Box<dyn Future<Output = Result<(), LoopError>> + Send + 'a>>;
fn process_turn<'a>(
&'a mut self,
input: &'a str,
) -> Pin<Box<dyn Future<Output = Result<TurnResult, LoopError>> + Send + 'a>>;
fn should_continue(&self) -> bool;
fn finalize<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<SessionResult, LoopError>> + Send + 'a>>;
fn state(&self) -> LoopState;
fn cancel(&self);
fn stop_reason(&self) -> Option<LoopError> {
None
}
fn run<'a>(
&'a mut self,
user_input: &'a str,
) -> Pin<Box<dyn Future<Output = Result<SessionResult, LoopError>> + Send + 'a>> {
Box::pin(async move {
let config = self.config().clone();
self.initialize(&config).await?;
let mut is_first_turn = true;
loop {
if !self.should_continue() {
break;
}
let input = if is_first_turn {
is_first_turn = false;
user_input
} else {
""
};
match self.process_turn(input).await {
Ok(turn_result) if turn_result.is_complete => {
return self.finalize().await;
}
Ok(_) => { }
Err(e) => {
self.finalize().await?;
return Err(e);
}
}
}
if let Some(err) = self.stop_reason() {
self.finalize().await?;
return Err(err);
}
self.finalize().await
})
}
fn config(&self) -> &LoopConfig;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reflection::{Correction, CorrectionResult, CorrectionType};
use crate::tool::ToolDispatchResult;
use serde_json::json;
use std::time::Duration;
fn make_call() -> ToolCall {
ToolCall {
id: "test".to_string(),
tool: "Read".to_string(),
input: json!({"path": "/tmp"}),
}
}
fn empty_prior_result() -> ToolDispatchResult {
ToolDispatchResult::ok("Read", String::new(), Duration::ZERO)
}
#[test]
fn input_fix_accepts_json_object() {
let mut call = make_call();
let correction = Correction {
correction_type: CorrectionType::InputFix,
description: "fix path".into(),
modified_input: Some(json!({"path": "/tmp/fixed"})),
alternative_tool: None,
guidance: None,
};
let result = call.apply_correction(&correction, &empty_prior_result());
assert!(matches!(result, CorrectionResult::Applied));
assert_eq!(call.input, json!({"path": "/tmp/fixed"}));
}
#[test]
fn input_fix_rejects_scalar() {
let mut call = make_call();
let correction = Correction {
correction_type: CorrectionType::InputFix,
description: "bad fix".into(),
modified_input: Some(json!("/tmp/scalar")),
alternative_tool: None,
guidance: None,
};
let result = call.apply_correction(&correction, &empty_prior_result());
match result {
CorrectionResult::Failed(msg) => {
assert!(msg.contains("JSON object"), "{msg}");
}
other => panic!("expected Failed, got {other:?}"),
}
assert_eq!(call.input, json!({"path": "/tmp"}));
}
#[test]
fn input_fix_rejects_array() {
let mut call = make_call();
let correction = Correction {
correction_type: CorrectionType::InputFix,
description: "bad fix".into(),
modified_input: Some(json!(["a", "b"])),
alternative_tool: None,
guidance: None,
};
let result = call.apply_correction(&correction, &empty_prior_result());
assert!(matches!(result, CorrectionResult::Failed(_)));
assert_eq!(call.input, json!({"path": "/tmp"}));
}
#[test]
fn input_fix_rejects_null() {
let mut call = make_call();
let correction = Correction {
correction_type: CorrectionType::InputFix,
description: "bad fix".into(),
modified_input: Some(serde_json::Value::Null),
alternative_tool: None,
guidance: None,
};
let result = call.apply_correction(&correction, &empty_prior_result());
assert!(matches!(result, CorrectionResult::Failed(_)));
assert_eq!(call.input, json!({"path": "/tmp"}));
}
#[test]
fn input_fix_rejects_missing() {
let mut call = make_call();
let correction = Correction {
correction_type: CorrectionType::InputFix,
description: "no input".into(),
modified_input: None,
alternative_tool: None,
guidance: None,
};
let result = call.apply_correction(&correction, &empty_prior_result());
assert!(matches!(result, CorrectionResult::Failed(_)));
assert_eq!(call.input, json!({"path": "/tmp"}));
}
}