Skip to main content

tirea_agent_loop/runtime/loop_runner/
outcome.rs

1use super::*;
2use serde_json::{json, Value};
3
4/// Aggregated token usage for one loop run.
5#[derive(Debug, Clone, Default, PartialEq, Eq)]
6pub struct LoopUsage {
7    pub prompt_tokens: usize,
8    pub completion_tokens: usize,
9    pub total_tokens: usize,
10}
11
12/// Aggregated runtime metrics for one loop run.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct LoopStats {
15    pub duration_ms: u64,
16    pub steps: usize,
17    pub llm_calls: usize,
18    pub llm_retries: usize,
19    pub tool_calls: usize,
20    pub tool_errors: usize,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub(super) enum LoopFailure {
25    Llm(String),
26    State(String),
27}
28
29/// Unified terminal state for loop execution.
30#[derive(Debug)]
31pub struct LoopOutcome {
32    pub run_ctx: crate::contracts::RunContext,
33    pub termination: TerminationReason,
34    pub response: Option<String>,
35    pub usage: LoopUsage,
36    pub stats: LoopStats,
37    /// Error details when `termination` is `Error`. Read in tests.
38    #[allow(dead_code)]
39    pub(super) failure: Option<LoopFailure>,
40}
41
42impl LoopOutcome {
43    /// Build a `RunFinish.result` payload from the unified outcome.
44    pub fn run_finish_result(&self) -> Option<Value> {
45        if !matches!(self.termination, TerminationReason::NaturalEnd) {
46            return None;
47        }
48        self.response
49            .as_ref()
50            .filter(|s| !s.is_empty())
51            .map(|text| json!({ "response": text }))
52    }
53
54    /// Project unified outcome into stream `RunFinish` event.
55    pub fn to_run_finish_event(self, run_id: String) -> AgentEvent {
56        AgentEvent::RunFinish {
57            thread_id: self.run_ctx.thread_id().to_string(),
58            run_id,
59            result: self.run_finish_result(),
60            termination: self.termination,
61        }
62    }
63}
64
65/// Error type for agent loop operations.
66#[derive(Debug, thiserror::Error)]
67pub enum AgentLoopError {
68    #[error("LLM error: {0}")]
69    LlmError(String),
70    #[error("State error: {0}")]
71    StateError(String),
72    /// External cancellation signal requested run termination.
73    #[error("Run cancelled")]
74    Cancelled,
75}
76
77impl From<crate::contracts::runtime::ToolExecutorError> for AgentLoopError {
78    fn from(value: crate::contracts::runtime::ToolExecutorError) -> Self {
79        match value {
80            crate::contracts::runtime::ToolExecutorError::Cancelled { .. } => Self::Cancelled,
81            crate::contracts::runtime::ToolExecutorError::Failed { message } => {
82                Self::StateError(message)
83            }
84        }
85    }
86}
87
88/// Helper to create a tool map from an iterator of tools.
89pub fn tool_map<I, T>(tools: I) -> HashMap<String, Arc<dyn Tool>>
90where
91    I: IntoIterator<Item = T>,
92    T: Tool + 'static,
93{
94    tools
95        .into_iter()
96        .map(|t| {
97            let name = t.descriptor().id.clone();
98            (name, Arc::new(t) as Arc<dyn Tool>)
99        })
100        .collect()
101}
102
103/// Helper to create a tool map from Arc<dyn Tool>.
104pub fn tool_map_from_arc<I>(tools: I) -> HashMap<String, Arc<dyn Tool>>
105where
106    I: IntoIterator<Item = Arc<dyn Tool>>,
107{
108    tools
109        .into_iter()
110        .map(|t| (t.descriptor().id.clone(), t))
111        .collect()
112}