use std::marker::PhantomData;
use crate::reasoning::circuit_breaker::CircuitBreakerRegistry;
use crate::reasoning::context_manager::ContextManager;
use crate::reasoning::executor::ActionExecutor;
use crate::reasoning::inference::InferenceProvider;
use crate::reasoning::loop_types::*;
use crate::reasoning::policy_bridge::ReasoningPolicyGate;
pub struct Reasoning;
pub struct PolicyCheck;
pub struct ToolDispatching;
pub struct Observing;
pub trait AgentPhase {}
impl AgentPhase for Reasoning {}
impl AgentPhase for PolicyCheck {}
impl AgentPhase for ToolDispatching {}
impl AgentPhase for Observing {}
pub struct ReasoningOutput {
pub proposed_actions: Vec<ProposedAction>,
}
pub struct PolicyOutput {
pub approved_actions: Vec<ProposedAction>,
pub denied_reasons: Vec<(ProposedAction, String)>,
pub has_terminal_action: bool,
pub terminal_output: Option<String>,
}
pub struct DispatchOutput {
pub observations: Vec<Observation>,
pub should_terminate: bool,
pub terminal_output: Option<String>,
}
pub struct AgentLoop<Phase: AgentPhase> {
pub state: LoopState,
pub config: LoopConfig,
phase_data: Option<PhaseData>,
_phase: PhantomData<Phase>,
}
enum PhaseData {
Reasoning(ReasoningOutput),
Policy(PolicyOutput),
Dispatch(DispatchOutput),
}
impl AgentLoop<Reasoning> {
pub fn new(state: LoopState, config: LoopConfig) -> Self {
Self {
state,
config,
phase_data: None,
_phase: PhantomData,
}
}
pub async fn produce_output(
mut self,
provider: &dyn InferenceProvider,
context_manager: &dyn ContextManager,
) -> Result<AgentLoop<PolicyCheck>, LoopTermination> {
self.state.current_phase = "reasoning".into();
if self.state.iteration >= self.config.max_iterations {
return Err(LoopTermination {
reason: LoopTerminationReason::MaxIterations {
iterations: self.state.iteration,
},
state: self.state,
});
}
if self.state.total_usage.total_tokens >= self.config.max_total_tokens {
return Err(LoopTermination {
reason: LoopTerminationReason::MaxTokens {
tokens: self.state.total_usage.total_tokens,
},
state: self.state,
});
}
context_manager.manage_context(
&mut self.state.conversation,
self.config.context_token_budget,
);
self.state.pending_observations.clear();
let options = crate::reasoning::inference::InferenceOptions {
max_tokens: self
.config
.max_total_tokens
.saturating_sub(self.state.total_usage.total_tokens)
.min(16384),
temperature: self.config.temperature,
tool_definitions: self.config.tool_definitions.clone(),
..Default::default()
};
let response = match provider.complete(&self.state.conversation, &options).await {
Ok(r) => r,
Err(e) => {
return Err(LoopTermination {
reason: LoopTerminationReason::Error {
message: format!("Inference failed: {}", e),
},
state: self.state,
});
}
};
self.state.add_usage(&response.usage);
let proposed_actions = if response.has_tool_calls() {
let tool_calls: Vec<crate::reasoning::conversation::ToolCall> = response
.tool_calls
.iter()
.map(|tc| crate::reasoning::conversation::ToolCall {
id: tc.id.clone(),
name: tc.name.clone(),
arguments: tc.arguments.clone(),
})
.collect();
self.state.conversation.push(
crate::reasoning::conversation::ConversationMessage::assistant_tool_calls(
tool_calls,
),
);
response
.tool_calls
.into_iter()
.map(|tc| ProposedAction::ToolCall {
call_id: tc.id,
name: tc.name,
arguments: tc.arguments,
})
.collect()
} else {
self.state.conversation.push(
crate::reasoning::conversation::ConversationMessage::assistant(&response.content),
);
vec![ProposedAction::Respond {
content: response.content,
}]
};
self.state.iteration += 1;
Ok(AgentLoop {
state: self.state,
config: self.config,
phase_data: Some(PhaseData::Reasoning(ReasoningOutput { proposed_actions })),
_phase: PhantomData,
})
}
}
impl AgentLoop<PolicyCheck> {
pub fn proposed_actions(&self) -> Vec<ProposedAction> {
match &self.phase_data {
Some(PhaseData::Reasoning(output)) => output.proposed_actions.clone(),
_ => Vec::new(),
}
}
pub async fn check_policy(
mut self,
gate: &dyn ReasoningPolicyGate,
) -> Result<AgentLoop<ToolDispatching>, LoopTermination> {
self.state.current_phase = "policy_check".into();
let reasoning_output = match self.phase_data {
Some(PhaseData::Reasoning(output)) => output,
_ => {
return Err(LoopTermination {
reason: LoopTerminationReason::Error {
message: "Invalid phase data: expected ReasoningOutput".into(),
},
state: self.state,
});
}
};
let mut approved = Vec::new();
let mut denied = Vec::new();
let mut has_terminal = false;
let mut terminal_output = None;
for action in reasoning_output.proposed_actions {
let decision = gate
.evaluate_action(&self.state.agent_id, &action, &self.state)
.await;
match decision {
LoopDecision::Allow => {
if matches!(
action,
ProposedAction::Respond { .. } | ProposedAction::Terminate { .. }
) {
has_terminal = true;
if let ProposedAction::Respond { ref content } = action {
terminal_output = Some(content.clone());
}
if let ProposedAction::Terminate { ref output, .. } = action {
terminal_output = Some(output.clone());
}
}
approved.push(action);
}
LoopDecision::Deny { reason } => {
if let ProposedAction::ToolCall {
ref call_id,
ref name,
..
} = action
{
self.state.conversation.push(
crate::reasoning::conversation::ConversationMessage::tool_result(
call_id,
name,
format!("[Policy denied] {}", reason),
),
);
}
self.state
.pending_observations
.push(Observation::policy_denial(&reason));
denied.push((action, reason));
}
LoopDecision::Modify {
modified_action,
reason,
} => {
tracing::info!("Policy modified action: {}", reason);
if matches!(
*modified_action,
ProposedAction::Respond { .. } | ProposedAction::Terminate { .. }
) {
has_terminal = true;
if let ProposedAction::Respond { ref content } = *modified_action {
terminal_output = Some(content.clone());
}
}
approved.push(*modified_action);
}
}
}
Ok(AgentLoop {
state: self.state,
config: self.config,
phase_data: Some(PhaseData::Policy(PolicyOutput {
approved_actions: approved,
denied_reasons: denied,
has_terminal_action: has_terminal,
terminal_output,
})),
_phase: PhantomData,
})
}
}
impl AgentLoop<ToolDispatching> {
pub fn policy_summary(&self) -> (usize, usize) {
match &self.phase_data {
Some(PhaseData::Policy(output)) => (
output.approved_actions.len() + output.denied_reasons.len(),
output.denied_reasons.len(),
),
_ => (0, 0),
}
}
pub async fn dispatch_tools(
mut self,
executor: &dyn ActionExecutor,
circuit_breakers: &CircuitBreakerRegistry,
) -> Result<AgentLoop<Observing>, LoopTermination> {
self.state.current_phase = "tool_dispatching".into();
let policy_output = match self.phase_data {
Some(PhaseData::Policy(output)) => output,
_ => {
return Err(LoopTermination {
reason: LoopTerminationReason::Error {
message: "Invalid phase data: expected PolicyOutput".into(),
},
state: self.state,
});
}
};
if policy_output.has_terminal_action {
return Ok(AgentLoop {
state: self.state,
config: self.config,
phase_data: Some(PhaseData::Dispatch(DispatchOutput {
observations: Vec::new(),
should_terminate: true,
terminal_output: policy_output.terminal_output,
})),
_phase: PhantomData,
});
}
let observations = executor
.execute_actions(
&policy_output.approved_actions,
&self.config,
circuit_breakers,
)
.await;
for obs in &observations {
let tool_call_id = obs.call_id.as_deref().unwrap_or(&obs.source);
if !obs.is_error {
self.state.conversation.push(
crate::reasoning::conversation::ConversationMessage::tool_result(
tool_call_id,
&obs.source,
&obs.content,
),
);
} else {
self.state.conversation.push(
crate::reasoning::conversation::ConversationMessage::tool_result(
tool_call_id,
&obs.source,
format!("[Error] {}", obs.content),
),
);
}
}
Ok(AgentLoop {
state: self.state,
config: self.config,
phase_data: Some(PhaseData::Dispatch(DispatchOutput {
observations,
should_terminate: false,
terminal_output: None,
})),
_phase: PhantomData,
})
}
}
pub enum LoopContinuation {
Continue(Box<AgentLoop<Reasoning>>),
Complete(LoopResult),
}
impl AgentLoop<Observing> {
pub fn observation_count(&self) -> usize {
match &self.phase_data {
Some(PhaseData::Dispatch(output)) => output.observations.len(),
_ => 0,
}
}
pub fn observe_results(mut self) -> LoopContinuation {
self.state.current_phase = "observing".into();
let dispatch_output = match self.phase_data {
Some(PhaseData::Dispatch(output)) => output,
_ => {
return LoopContinuation::Complete(LoopResult {
output: String::new(),
iterations: self.state.iteration,
total_usage: self.state.total_usage.clone(),
termination_reason: TerminationReason::Error {
message: "Invalid phase data".into(),
},
duration: self.state.elapsed().to_std().unwrap_or_default(),
conversation: self.state.conversation,
});
}
};
if dispatch_output.should_terminate {
return LoopContinuation::Complete(LoopResult {
output: dispatch_output.terminal_output.unwrap_or_default(),
iterations: self.state.iteration,
total_usage: self.state.total_usage.clone(),
termination_reason: TerminationReason::Completed,
duration: self.state.elapsed().to_std().unwrap_or_default(),
conversation: self.state.conversation,
});
}
self.state
.pending_observations
.extend(dispatch_output.observations);
LoopContinuation::Continue(Box::new(AgentLoop {
state: self.state,
config: self.config,
phase_data: None,
_phase: PhantomData,
}))
}
}
#[derive(Debug)]
pub struct LoopTermination {
pub reason: LoopTerminationReason,
pub state: LoopState,
}
#[derive(Debug)]
pub enum LoopTerminationReason {
MaxIterations { iterations: u32 },
MaxTokens { tokens: u32 },
Timeout,
Error { message: String },
}
impl LoopTermination {
pub fn into_result(self) -> LoopResult {
let reason = match &self.reason {
LoopTerminationReason::MaxIterations { .. } => TerminationReason::MaxIterations,
LoopTerminationReason::MaxTokens { .. } => TerminationReason::MaxTokens,
LoopTerminationReason::Timeout => TerminationReason::Timeout,
LoopTerminationReason::Error { message } => TerminationReason::Error {
message: message.clone(),
},
};
LoopResult {
output: String::new(),
iterations: self.state.iteration,
total_usage: self.state.total_usage.clone(),
termination_reason: reason,
duration: self.state.elapsed().to_std().unwrap_or_default(),
conversation: self.state.conversation,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reasoning::conversation::Conversation;
use crate::types::AgentId;
#[test]
fn test_agent_loop_creation() {
let state = LoopState::new(AgentId::new(), Conversation::with_system("test"));
let config = LoopConfig::default();
let loop_instance = AgentLoop::<Reasoning>::new(state, config);
assert_eq!(loop_instance.state.iteration, 0);
}
#[test]
fn test_loop_termination_into_result() {
let state = LoopState::new(AgentId::new(), Conversation::new());
let termination = LoopTermination {
reason: LoopTerminationReason::MaxIterations { iterations: 25 },
state,
};
let result = termination.into_result();
assert!(matches!(
result.termination_reason,
TerminationReason::MaxIterations
));
}
fn _prove_reasoning_to_policy(_loop: AgentLoop<Reasoning>) {
}
fn _prove_policy_to_dispatch(_loop: AgentLoop<PolicyCheck>) {
}
fn _prove_dispatch_to_observing(_loop: AgentLoop<ToolDispatching>) {
}
fn _prove_observing_to_continuation(_loop: AgentLoop<Observing>) {
}
}