Skip to main content

tirea_contract/runtime/inference/
response.rs

1pub use crate::runtime::tool_call::{ToolResult, ToolStatus};
2use crate::thread::ToolCall;
3use serde::{Deserialize, Serialize};
4
5/// Why the LLM stopped generating output.
6///
7/// Mapped from provider-specific stop reasons (Anthropic `stop_reason`,
8/// OpenAI `finish_reason`). Used by plugins to detect truncation and
9/// trigger recovery.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum StopReason {
12    /// Model finished naturally (Anthropic `end_turn`, OpenAI `stop`).
13    EndTurn,
14    /// Output hit the `max_tokens` limit — response may be truncated.
15    MaxTokens,
16    /// Model emitted one or more tool-use calls.
17    ToolUse,
18    /// A stop sequence was matched.
19    StopSequence,
20}
21
22/// Provider-neutral token usage.
23#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
24pub struct TokenUsage {
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub prompt_tokens: Option<i32>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub completion_tokens: Option<i32>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub total_tokens: Option<i32>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub cache_read_tokens: Option<i32>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub cache_creation_tokens: Option<i32>,
35}
36
37/// Result of stream collection used by runtime and plugin phase contracts.
38#[derive(Debug, Clone)]
39pub struct StreamResult {
40    /// Accumulated text content.
41    pub text: String,
42    /// Collected tool calls.
43    pub tool_calls: Vec<ToolCall>,
44    /// Token usage from the LLM response.
45    pub usage: Option<TokenUsage>,
46    /// Why the model stopped generating. `None` when the backend cannot
47    /// determine or map the provider stop reason.
48    pub stop_reason: Option<StopReason>,
49}
50
51impl StreamResult {
52    /// Check if tool execution is needed.
53    pub fn needs_tools(&self) -> bool {
54        !self.tool_calls.is_empty()
55    }
56}
57
58/// Inference error emitted by the loop and consumed by telemetry plugins.
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60pub struct InferenceError {
61    /// Stable error class used for metrics/telemetry dimensions.
62    #[serde(rename = "type")]
63    pub error_type: String,
64    /// Human-readable error message.
65    pub message: String,
66    /// Classified error category (e.g. `rate_limit`, `timeout`, `connection`).
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub error_class: Option<String>,
69}
70
71/// LLM response extension: set after inference completes (success or error).
72#[derive(Debug, Clone)]
73pub struct LLMResponse {
74    /// Inference outcome: success with a [`StreamResult`] or failure with an [`InferenceError`].
75    pub outcome: Result<StreamResult, InferenceError>,
76}
77
78impl LLMResponse {
79    pub fn success(result: StreamResult) -> Self {
80        Self {
81            outcome: Ok(result),
82        }
83    }
84
85    pub fn error(error: InferenceError) -> Self {
86        Self {
87            outcome: Err(error),
88        }
89    }
90}