Skip to main content

oxi_agent/agent_loop/
helpers.rs

1/// Helper functions for agent loop
2use oxi_ai::{ContentBlock, TextContent, ToolCall, ToolResultMessage};
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5
6/// Extract tool calls from an assistant message.
7pub fn extract_tool_calls(message: &oxi_ai::AssistantMessage) -> Vec<ToolCall> {
8    let mut tool_calls = Vec::new();
9
10    for block in &message.content {
11        if let ContentBlock::ToolCall(tc) = block {
12            tool_calls.push(tc.clone());
13        }
14    }
15
16    tool_calls
17}
18
19/// Create a tool result message from a finalized tool call.
20pub fn create_tool_result_message(finalized: &FinalizedToolCall) -> ToolResultMessage {
21    let content_blocks = if let Some(ref blocks) = finalized.result.content_blocks {
22        blocks.clone()
23    } else {
24        vec![ContentBlock::Text(TextContent::new(
25            finalized.result.output.clone(),
26        ))]
27    };
28
29    ToolResultMessage::new(
30        finalized.tool_call.id.clone(),
31        &finalized.tool_call.name,
32        content_blocks,
33    )
34}
35
36/// Check if a batch of finalized tool calls should terminate the loop.
37/// pi-mono: ALL finalized results must have `terminate === true` for the
38/// batch to terminate. This is the unanimous consent pattern.
39pub fn should_terminate_batch(finalized_calls: &[FinalizedToolCall]) -> bool {
40    if finalized_calls.is_empty() {
41        return false;
42    }
43    finalized_calls.iter().all(|f| f.result.terminate)
44}
45
46/// Check if loop should stop after a turn.
47///
48/// pi-mono: shouldStopAfterTurn is an optional hook. If no hook is defined,
49/// the loop never stops here — it continues until no more tool calls AND
50/// no more steering/follow-up messages.
51///
52/// oxi: We check external_stop (Ctrl+C) and max_iterations.
53/// Stop/Length reasons are handled by the inner loop's has_more_tool_calls
54/// condition, NOT here.
55pub fn should_stop_after_turn(
56    _messages: &[oxi_ai::Message],
57    _assistant_message: &oxi_ai::AssistantMessage,
58    max_iterations: usize,
59    external_stop: &Arc<AtomicBool>,
60    turn_number: usize,
61) -> bool {
62    // External stop (Ctrl+C)
63    if external_stop.load(Ordering::SeqCst) {
64        return true;
65    }
66
67    // Max iterations guard — uses caller-provided turn_number
68    // instead of re-counting messages each time (O(n) → O(1)).
69    if turn_number >= max_iterations {
70        return true;
71    }
72
73    false
74}
75
76use crate::AgentToolResult;
77
78/// Finalized tool call with result.
79pub struct FinalizedToolCall {
80    /// pub.
81    pub tool_call: oxi_ai::ToolCall,
82    /// pub.
83    pub result: AgentToolResult,
84    /// pub.
85    pub is_error: bool,
86}