tirea_agent_loop/runtime/loop_runner/
outcome.rs1use super::*;
2use serde_json::{json, Value};
3
4#[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#[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#[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 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 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#[derive(Debug, thiserror::Error)]
66pub enum AgentLoopError {
67 #[error("LLM error: {0}")]
68 LlmError(String),
69 #[error("State error: {0}")]
70 StateError(String),
71 #[error("Agent stopped: {reason:?}")]
76 Stopped {
77 run_ctx: Box<crate::contracts::RunContext>,
78 reason: StopReason,
79 },
80 #[error("Pending interaction: {id} ({action})", id = interaction.id, action = interaction.action)]
85 PendingInteraction {
86 run_ctx: Box<crate::contracts::RunContext>,
87 interaction: Box<Interaction>,
88 },
89 #[error("Run cancelled")]
91 Cancelled {
92 run_ctx: Box<crate::contracts::RunContext>,
93 },
94}
95
96impl AgentLoopError {
97 pub fn termination_reason(&self) -> TerminationReason {
99 match self {
100 Self::Stopped { reason, .. } => TerminationReason::Stopped(reason.clone()),
101 Self::Cancelled { .. } => TerminationReason::Cancelled,
102 Self::PendingInteraction { .. } => TerminationReason::PendingInteraction,
103 Self::LlmError(_) | Self::StateError(_) => TerminationReason::Error,
104 }
105 }
106}
107
108impl From<crate::contracts::runtime::ToolExecutorError> for AgentLoopError {
109 fn from(value: crate::contracts::runtime::ToolExecutorError) -> Self {
110 match value {
111 crate::contracts::runtime::ToolExecutorError::Cancelled { thread_id } => {
112 Self::Cancelled {
113 run_ctx: Box::new(crate::contracts::RunContext::new(
114 thread_id,
115 serde_json::json!({}),
116 vec![],
117 crate::contracts::RunConfig::default(),
118 )),
119 }
120 }
121 crate::contracts::runtime::ToolExecutorError::Failed { message } => {
122 Self::StateError(message)
123 }
124 }
125 }
126}
127
128pub fn tool_map<I, T>(tools: I) -> HashMap<String, Arc<dyn Tool>>
130where
131 I: IntoIterator<Item = T>,
132 T: Tool + 'static,
133{
134 tools
135 .into_iter()
136 .map(|t| {
137 let name = t.descriptor().id.clone();
138 (name, Arc::new(t) as Arc<dyn Tool>)
139 })
140 .collect()
141}
142
143pub fn tool_map_from_arc<I>(tools: I) -> HashMap<String, Arc<dyn Tool>>
145where
146 I: IntoIterator<Item = Arc<dyn Tool>>,
147{
148 tools
149 .into_iter()
150 .map(|t| (t.descriptor().id.clone(), t))
151 .collect()
152}