Skip to main content

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