1use std::marker::PhantomData;
16
17use crate::reasoning::circuit_breaker::CircuitBreakerRegistry;
18use crate::reasoning::context_manager::ContextManager;
19use crate::reasoning::executor::ActionExecutor;
20use crate::reasoning::inference::InferenceProvider;
21use crate::reasoning::loop_types::*;
22use crate::reasoning::policy_bridge::ReasoningPolicyGate;
23
24pub struct Reasoning;
28pub struct PolicyCheck;
30pub struct ToolDispatching;
32pub struct Observing;
34
35pub trait AgentPhase {}
37impl AgentPhase for Reasoning {}
38impl AgentPhase for PolicyCheck {}
39impl AgentPhase for ToolDispatching {}
40impl AgentPhase for Observing {}
41
42pub struct ReasoningOutput {
46 pub proposed_actions: Vec<ProposedAction>,
48}
49
50pub struct PolicyOutput {
52 pub approved_actions: Vec<ProposedAction>,
54 pub denied_reasons: Vec<(ProposedAction, String)>,
56 pub has_terminal_action: bool,
58 pub terminal_output: Option<String>,
60}
61
62pub struct DispatchOutput {
64 pub observations: Vec<Observation>,
66 pub should_terminate: bool,
68 pub terminal_output: Option<String>,
70}
71
72pub struct AgentLoop<Phase: AgentPhase> {
79 pub state: LoopState,
81 pub config: LoopConfig,
83 phase_data: Option<PhaseData>,
85 _phase: PhantomData<Phase>,
87}
88
89enum PhaseData {
91 Reasoning(ReasoningOutput),
92 Policy(PolicyOutput),
93 Dispatch(DispatchOutput),
94}
95
96impl AgentLoop<Reasoning> {
97 pub fn new(state: LoopState, config: LoopConfig) -> Self {
99 Self {
100 state,
101 config,
102 phase_data: None,
103 _phase: PhantomData,
104 }
105 }
106
107 pub async fn produce_output(
111 mut self,
112 provider: &dyn InferenceProvider,
113 context_manager: &dyn ContextManager,
114 ) -> Result<AgentLoop<PolicyCheck>, LoopTermination> {
115 self.state.current_phase = "reasoning".into();
116
117 if self.state.iteration >= self.config.max_iterations {
119 return Err(LoopTermination {
120 reason: LoopTerminationReason::MaxIterations {
121 iterations: self.state.iteration,
122 },
123 state: self.state,
124 });
125 }
126 if self.state.total_usage.total_tokens >= self.config.max_total_tokens {
127 return Err(LoopTermination {
128 reason: LoopTerminationReason::MaxTokens {
129 tokens: self.state.total_usage.total_tokens,
130 },
131 state: self.state,
132 });
133 }
134
135 context_manager.manage_context(
137 &mut self.state.conversation,
138 self.config.context_token_budget,
139 );
140
141 self.state.pending_observations.clear();
146
147 let options = crate::reasoning::inference::InferenceOptions {
149 max_tokens: self
150 .config
151 .max_total_tokens
152 .saturating_sub(self.state.total_usage.total_tokens)
153 .min(16384),
154 temperature: self.config.temperature,
155 tool_definitions: self.config.tool_definitions.clone(),
156 ..Default::default()
157 };
158
159 let response = match provider.complete(&self.state.conversation, &options).await {
161 Ok(r) => r,
162 Err(e) => {
163 return Err(LoopTermination {
164 reason: LoopTerminationReason::Error {
165 message: format!("Inference failed: {}", e),
166 },
167 state: self.state,
168 });
169 }
170 };
171
172 self.state.add_usage(&response.usage);
174
175 let proposed_actions = if response.has_tool_calls() {
177 let tool_calls: Vec<crate::reasoning::conversation::ToolCall> = response
179 .tool_calls
180 .iter()
181 .map(|tc| crate::reasoning::conversation::ToolCall {
182 id: tc.id.clone(),
183 name: tc.name.clone(),
184 arguments: tc.arguments.clone(),
185 })
186 .collect();
187 self.state.conversation.push(
188 crate::reasoning::conversation::ConversationMessage::assistant_tool_calls(
189 tool_calls,
190 ),
191 );
192
193 response
194 .tool_calls
195 .into_iter()
196 .map(|tc| ProposedAction::ToolCall {
197 call_id: tc.id,
198 name: tc.name,
199 arguments: tc.arguments,
200 })
201 .collect()
202 } else {
203 self.state.conversation.push(
205 crate::reasoning::conversation::ConversationMessage::assistant(&response.content),
206 );
207
208 vec![ProposedAction::Respond {
209 content: response.content,
210 }]
211 };
212
213 self.state.iteration += 1;
214
215 Ok(AgentLoop {
216 state: self.state,
217 config: self.config,
218 phase_data: Some(PhaseData::Reasoning(ReasoningOutput { proposed_actions })),
219 _phase: PhantomData,
220 })
221 }
222}
223
224impl AgentLoop<PolicyCheck> {
225 pub fn proposed_actions(&self) -> Vec<ProposedAction> {
229 match &self.phase_data {
230 Some(PhaseData::Reasoning(output)) => output.proposed_actions.clone(),
231 _ => Vec::new(),
232 }
233 }
234
235 pub async fn check_policy(
239 mut self,
240 gate: &dyn ReasoningPolicyGate,
241 ) -> Result<AgentLoop<ToolDispatching>, LoopTermination> {
242 self.state.current_phase = "policy_check".into();
243
244 let reasoning_output = match self.phase_data {
245 Some(PhaseData::Reasoning(output)) => output,
246 _ => {
247 return Err(LoopTermination {
248 reason: LoopTerminationReason::Error {
249 message: "Invalid phase data: expected ReasoningOutput".into(),
250 },
251 state: self.state,
252 });
253 }
254 };
255
256 let mut approved = Vec::new();
257 let mut denied = Vec::new();
258 let mut has_terminal = false;
259 let mut terminal_output = None;
260
261 for action in reasoning_output.proposed_actions {
262 let decision = gate
263 .evaluate_action(&self.state.agent_id, &action, &self.state)
264 .await;
265
266 match decision {
267 LoopDecision::Allow => {
268 if matches!(
269 action,
270 ProposedAction::Respond { .. } | ProposedAction::Terminate { .. }
271 ) {
272 has_terminal = true;
273 if let ProposedAction::Respond { ref content } = action {
274 terminal_output = Some(content.clone());
275 }
276 if let ProposedAction::Terminate { ref output, .. } = action {
277 terminal_output = Some(output.clone());
278 }
279 }
280 approved.push(action);
281 }
282 LoopDecision::Deny { reason } => {
283 if let ProposedAction::ToolCall {
288 ref call_id,
289 ref name,
290 ..
291 } = action
292 {
293 self.state.conversation.push(
294 crate::reasoning::conversation::ConversationMessage::tool_result(
295 call_id,
296 name,
297 format!("[Policy denied] {}", reason),
298 ),
299 );
300 }
301 self.state
303 .pending_observations
304 .push(Observation::policy_denial(&reason));
305 denied.push((action, reason));
306 }
307 LoopDecision::Modify {
308 modified_action,
309 reason,
310 } => {
311 tracing::info!("Policy modified action: {}", reason);
312 if matches!(
313 *modified_action,
314 ProposedAction::Respond { .. } | ProposedAction::Terminate { .. }
315 ) {
316 has_terminal = true;
317 if let ProposedAction::Respond { ref content } = *modified_action {
318 terminal_output = Some(content.clone());
319 }
320 }
321 approved.push(*modified_action);
322 }
323 }
324 }
325
326 Ok(AgentLoop {
327 state: self.state,
328 config: self.config,
329 phase_data: Some(PhaseData::Policy(PolicyOutput {
330 approved_actions: approved,
331 denied_reasons: denied,
332 has_terminal_action: has_terminal,
333 terminal_output,
334 })),
335 _phase: PhantomData,
336 })
337 }
338}
339
340impl AgentLoop<ToolDispatching> {
341 pub fn policy_summary(&self) -> (usize, usize) {
343 match &self.phase_data {
344 Some(PhaseData::Policy(output)) => (
345 output.approved_actions.len() + output.denied_reasons.len(),
346 output.denied_reasons.len(),
347 ),
348 _ => (0, 0),
349 }
350 }
351
352 pub async fn dispatch_tools(
356 mut self,
357 executor: &dyn ActionExecutor,
358 circuit_breakers: &CircuitBreakerRegistry,
359 ) -> Result<AgentLoop<Observing>, LoopTermination> {
360 self.state.current_phase = "tool_dispatching".into();
361
362 let policy_output = match self.phase_data {
363 Some(PhaseData::Policy(output)) => output,
364 _ => {
365 return Err(LoopTermination {
366 reason: LoopTerminationReason::Error {
367 message: "Invalid phase data: expected PolicyOutput".into(),
368 },
369 state: self.state,
370 });
371 }
372 };
373
374 if policy_output.has_terminal_action {
376 return Ok(AgentLoop {
377 state: self.state,
378 config: self.config,
379 phase_data: Some(PhaseData::Dispatch(DispatchOutput {
380 observations: Vec::new(),
381 should_terminate: true,
382 terminal_output: policy_output.terminal_output,
383 })),
384 _phase: PhantomData,
385 });
386 }
387
388 let observations = executor
390 .execute_actions(
391 &policy_output.approved_actions,
392 &self.config,
393 circuit_breakers,
394 )
395 .await;
396
397 for obs in &observations {
399 let tool_call_id = obs.call_id.as_deref().unwrap_or(&obs.source);
400 if !obs.is_error {
401 self.state.conversation.push(
402 crate::reasoning::conversation::ConversationMessage::tool_result(
403 tool_call_id,
404 &obs.source,
405 &obs.content,
406 ),
407 );
408 } else {
409 self.state.conversation.push(
410 crate::reasoning::conversation::ConversationMessage::tool_result(
411 tool_call_id,
412 &obs.source,
413 format!("[Error] {}", obs.content),
414 ),
415 );
416 }
417 }
418
419 Ok(AgentLoop {
420 state: self.state,
421 config: self.config,
422 phase_data: Some(PhaseData::Dispatch(DispatchOutput {
423 observations,
424 should_terminate: false,
425 terminal_output: None,
426 })),
427 _phase: PhantomData,
428 })
429 }
430}
431
432pub enum LoopContinuation {
434 Continue(Box<AgentLoop<Reasoning>>),
436 Complete(LoopResult),
438}
439
440impl AgentLoop<Observing> {
441 pub fn observation_count(&self) -> usize {
444 match &self.phase_data {
445 Some(PhaseData::Dispatch(output)) => output.observations.len(),
446 _ => 0,
447 }
448 }
449
450 pub fn observe_results(mut self) -> LoopContinuation {
454 self.state.current_phase = "observing".into();
455
456 let dispatch_output = match self.phase_data {
457 Some(PhaseData::Dispatch(output)) => output,
458 _ => {
459 return LoopContinuation::Complete(LoopResult {
460 output: String::new(),
461 iterations: self.state.iteration,
462 total_usage: self.state.total_usage.clone(),
463 termination_reason: TerminationReason::Error {
464 message: "Invalid phase data".into(),
465 },
466 duration: self.state.elapsed().to_std().unwrap_or_default(),
467 conversation: self.state.conversation,
468 });
469 }
470 };
471
472 if dispatch_output.should_terminate {
473 return LoopContinuation::Complete(LoopResult {
474 output: dispatch_output.terminal_output.unwrap_or_default(),
475 iterations: self.state.iteration,
476 total_usage: self.state.total_usage.clone(),
477 termination_reason: TerminationReason::Completed,
478 duration: self.state.elapsed().to_std().unwrap_or_default(),
479 conversation: self.state.conversation,
480 });
481 }
482
483 self.state
485 .pending_observations
486 .extend(dispatch_output.observations);
487
488 LoopContinuation::Continue(Box::new(AgentLoop {
489 state: self.state,
490 config: self.config,
491 phase_data: None,
492 _phase: PhantomData,
493 }))
494 }
495}
496
497#[derive(Debug)]
500pub struct LoopTermination {
501 pub reason: LoopTerminationReason,
502 pub state: LoopState,
503}
504
505#[derive(Debug)]
507pub enum LoopTerminationReason {
508 MaxIterations { iterations: u32 },
509 MaxTokens { tokens: u32 },
510 Timeout,
511 Error { message: String },
512}
513
514impl LoopTermination {
515 pub fn into_result(self) -> LoopResult {
517 let reason = match &self.reason {
518 LoopTerminationReason::MaxIterations { .. } => TerminationReason::MaxIterations,
519 LoopTerminationReason::MaxTokens { .. } => TerminationReason::MaxTokens,
520 LoopTerminationReason::Timeout => TerminationReason::Timeout,
521 LoopTerminationReason::Error { message } => TerminationReason::Error {
522 message: message.clone(),
523 },
524 };
525 LoopResult {
526 output: String::new(),
527 iterations: self.state.iteration,
528 total_usage: self.state.total_usage.clone(),
529 termination_reason: reason,
530 duration: self.state.elapsed().to_std().unwrap_or_default(),
531 conversation: self.state.conversation,
532 }
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539 use crate::reasoning::conversation::Conversation;
540 use crate::types::AgentId;
541
542 #[test]
543 fn test_agent_loop_creation() {
544 let state = LoopState::new(AgentId::new(), Conversation::with_system("test"));
545 let config = LoopConfig::default();
546 let loop_instance = AgentLoop::<Reasoning>::new(state, config);
547 assert_eq!(loop_instance.state.iteration, 0);
548 }
549
550 #[test]
551 fn test_loop_termination_into_result() {
552 let state = LoopState::new(AgentId::new(), Conversation::new());
553 let termination = LoopTermination {
554 reason: LoopTerminationReason::MaxIterations { iterations: 25 },
555 state,
556 };
557 let result = termination.into_result();
558 assert!(matches!(
559 result.termination_reason,
560 TerminationReason::MaxIterations
561 ));
562 }
563
564 fn _prove_reasoning_to_policy(_loop: AgentLoop<Reasoning>) {
570 }
575
576 fn _prove_policy_to_dispatch(_loop: AgentLoop<PolicyCheck>) {
577 }
581
582 fn _prove_dispatch_to_observing(_loop: AgentLoop<ToolDispatching>) {
583 }
587
588 fn _prove_observing_to_continuation(_loop: AgentLoop<Observing>) {
589 }
593}