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    #[allow(dead_code)]
38    pub(super) failure: Option<LoopFailure>,
39}
40
41impl LoopOutcome {
42    /// Build a `RunFinish.result` payload from the unified outcome.
43    pub fn run_finish_result(&self) -> Option<Value> {
44        if !matches!(self.termination, TerminationReason::NaturalEnd) {
45            return None;
46        }
47        self.response
48            .as_ref()
49            .filter(|s| !s.is_empty())
50            .map(|text| json!({ "response": text }))
51    }
52
53    /// Project unified outcome into stream `RunFinish` event.
54    pub fn to_run_finish_event(self, run_id: String) -> AgentEvent {
55        AgentEvent::RunFinish {
56            thread_id: self.run_ctx.thread_id().to_string(),
57            run_id,
58            result: self.run_finish_result(),
59            termination: self.termination,
60        }
61    }
62}
63
64/// Error type for agent loop operations.
65#[derive(Debug, thiserror::Error)]
66pub enum AgentLoopError {
67    #[error("LLM error: {0}")]
68    LlmError(String),
69    #[error("State error: {0}")]
70    StateError(String),
71    /// External cancellation signal requested run termination.
72    #[error("Run cancelled")]
73    Cancelled,
74}
75
76impl From<crate::contracts::runtime::ToolExecutorError> for AgentLoopError {
77    fn from(value: crate::contracts::runtime::ToolExecutorError) -> Self {
78        match value {
79            crate::contracts::runtime::ToolExecutorError::Cancelled { .. } => Self::Cancelled,
80            crate::contracts::runtime::ToolExecutorError::Failed { message } => {
81                Self::StateError(message)
82            }
83        }
84    }
85}
86
87/// Helper to create a tool map from an iterator of tools.
88pub fn tool_map<I, T>(tools: I) -> HashMap<String, Arc<dyn Tool>>
89where
90    I: IntoIterator<Item = T>,
91    T: Tool + 'static,
92{
93    tools
94        .into_iter()
95        .map(|t| {
96            let name = t.descriptor().id.clone();
97            (name, Arc::new(t) as Arc<dyn Tool>)
98        })
99        .collect()
100}
101
102/// Helper to create a tool map from Arc<dyn Tool>.
103pub fn tool_map_from_arc<I>(tools: I) -> HashMap<String, Arc<dyn Tool>>
104where
105    I: IntoIterator<Item = Arc<dyn Tool>>,
106{
107    tools
108        .into_iter()
109        .map(|t| (t.descriptor().id.clone(), t))
110        .collect()
111}