Skip to main content

spec_ai/spec_ai_core/agent/
output.rs

1//! Shared agent output data types used by the core loop and CLI
2
3use crate::spec_ai_core::agent::model::TokenUsage;
4use crate::spec_ai_core::agent::safety::SafetyStats;
5use crate::spec_ai_core::tools::ToolResult;
6use crate::spec_ai_core::types::MessageRole;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Output from an agent execution step
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AgentOutput {
13    /// The response text
14    pub response: String,
15    /// Message identifier for the persisted assistant response
16    pub response_message_id: Option<i64>,
17    /// Token usage information
18    pub token_usage: Option<TokenUsage>,
19    /// Detailed tool invocations performed during this turn
20    pub tool_invocations: Vec<ToolInvocation>,
21    /// Finish reason
22    pub finish_reason: Option<String>,
23    /// Semantic memory recall statistics for this turn (if embeddings enabled)
24    pub recall_stats: Option<MemoryRecallStats>,
25    /// Unique identifier for correlating this run with logs/telemetry
26    pub run_id: String,
27    /// Optional recommendation produced by graph steering
28    pub next_action: Option<String>,
29    /// Model's internal reasoning/thinking process (extracted from <think> tags)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub reasoning: Option<String>,
32    /// Human-readable summary of the reasoning (if reasoning was present)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub reasoning_summary: Option<String>,
35    /// Snapshot of graph state for debugging purposes
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub graph_debug: Option<GraphDebugInfo>,
38    /// Recursion/cost safety counters for this run
39    #[serde(default)]
40    pub safety: SafetyStats,
41}
42
43/// Minimal snapshot of a recent graph node for debugging output
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct GraphDebugNode {
46    pub id: i64,
47    pub node_type: String,
48    pub label: String,
49}
50
51/// Debug information about the graph state captured for run stats
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct GraphDebugInfo {
54    pub enabled: bool,
55    pub graph_memory_enabled: bool,
56    pub auto_graph_enabled: bool,
57    pub graph_steering_enabled: bool,
58    pub node_count: usize,
59    pub edge_count: usize,
60    pub recent_nodes: Vec<GraphDebugNode>,
61}
62
63/// A single tool invocation, including arguments and outcome metadata
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ToolInvocation {
66    pub name: String,
67    pub arguments: Value,
68    pub success: bool,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub output: Option<String>,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub error: Option<String>,
73}
74
75/// Machine-readable events emitted during one-shot agent execution.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(tag = "type")]
78pub enum RunEvent {
79    #[serde(rename = "run.started")]
80    RunStarted { run_id: String, instruction: String },
81    #[serde(rename = "tool.call")]
82    ToolCall {
83        run_id: String,
84        tool_name: String,
85        arguments: Value,
86    },
87    #[serde(rename = "approval.requested")]
88    ApprovalRequested {
89        run_id: String,
90        tool_name: String,
91        arguments: Value,
92        mode: String,
93    },
94    #[serde(rename = "approval.decision")]
95    ApprovalDecision {
96        run_id: String,
97        tool_name: String,
98        approved: bool,
99        reason: String,
100        mode: String,
101    },
102    #[serde(rename = "tool.result")]
103    ToolResult {
104        run_id: String,
105        tool_name: String,
106        success: bool,
107        #[serde(skip_serializing_if = "Option::is_none")]
108        output: Option<String>,
109        #[serde(skip_serializing_if = "Option::is_none")]
110        error: Option<String>,
111    },
112    #[serde(rename = "message.final")]
113    MessageFinal {
114        run_id: String,
115        content: String,
116        #[serde(skip_serializing_if = "Option::is_none")]
117        finish_reason: Option<String>,
118    },
119    #[serde(rename = "run.completed")]
120    RunCompleted {
121        run_id: String,
122        success: bool,
123        #[serde(skip_serializing_if = "Option::is_none")]
124        finish_reason: Option<String>,
125    },
126    #[serde(rename = "error")]
127    Error {
128        #[serde(skip_serializing_if = "Option::is_none")]
129        run_id: Option<String>,
130        message: String,
131    },
132}
133
134impl ToolInvocation {
135    pub fn from_result(name: &str, arguments: Value, result: &ToolResult) -> Self {
136        let output = if result.output.trim().is_empty() {
137            None
138        } else {
139            Some(result.output.clone())
140        };
141
142        Self {
143            name: name.to_string(),
144            arguments,
145            success: result.success,
146            output,
147            error: result.error.clone(),
148        }
149    }
150}
151
152/// Telemetry about memory recall for a single turn
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct MemoryRecallStats {
155    pub strategy: MemoryRecallStrategy,
156    pub matches: Vec<MemoryRecallMatch>,
157}
158
159/// Strategy used for memory recall
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum MemoryRecallStrategy {
162    Semantic { requested: usize, returned: usize },
163    RecentContext { limit: usize },
164}
165
166/// Summary of an individual recalled memory
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct MemoryRecallMatch {
169    pub message_id: Option<i64>,
170    pub score: f32,
171    pub role: MessageRole,
172    pub preview: String,
173}