spec-ai 0.8.4

A framework for building AI agents with structured outputs, policy enforcement, and execution tracking
Documentation
//! Shared agent output data types used by the core loop and CLI

use crate::spec_ai_core::agent::model::TokenUsage;
use crate::spec_ai_core::agent::safety::SafetyStats;
use crate::spec_ai_core::tools::ToolResult;
use crate::spec_ai_core::types::MessageRole;
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Output from an agent execution step
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentOutput {
    /// The response text
    pub response: String,
    /// Message identifier for the persisted assistant response
    pub response_message_id: Option<i64>,
    /// Token usage information
    pub token_usage: Option<TokenUsage>,
    /// Detailed tool invocations performed during this turn
    pub tool_invocations: Vec<ToolInvocation>,
    /// Finish reason
    pub finish_reason: Option<String>,
    /// Semantic memory recall statistics for this turn (if embeddings enabled)
    pub recall_stats: Option<MemoryRecallStats>,
    /// Unique identifier for correlating this run with logs/telemetry
    pub run_id: String,
    /// Optional recommendation produced by graph steering
    pub next_action: Option<String>,
    /// Model's internal reasoning/thinking process (extracted from <think> tags)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reasoning: Option<String>,
    /// Human-readable summary of the reasoning (if reasoning was present)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reasoning_summary: Option<String>,
    /// Snapshot of graph state for debugging purposes
    #[serde(skip_serializing_if = "Option::is_none")]
    pub graph_debug: Option<GraphDebugInfo>,
    /// Recursion/cost safety counters for this run
    #[serde(default)]
    pub safety: SafetyStats,
}

/// Minimal snapshot of a recent graph node for debugging output
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphDebugNode {
    pub id: i64,
    pub node_type: String,
    pub label: String,
}

/// Debug information about the graph state captured for run stats
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphDebugInfo {
    pub enabled: bool,
    pub graph_memory_enabled: bool,
    pub auto_graph_enabled: bool,
    pub graph_steering_enabled: bool,
    pub node_count: usize,
    pub edge_count: usize,
    pub recent_nodes: Vec<GraphDebugNode>,
}

/// A single tool invocation, including arguments and outcome metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInvocation {
    pub name: String,
    pub arguments: Value,
    pub success: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// Machine-readable events emitted during one-shot agent execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RunEvent {
    #[serde(rename = "run.started")]
    RunStarted { run_id: String, instruction: String },
    #[serde(rename = "tool.call")]
    ToolCall {
        run_id: String,
        tool_name: String,
        arguments: Value,
    },
    #[serde(rename = "approval.requested")]
    ApprovalRequested {
        run_id: String,
        tool_name: String,
        arguments: Value,
        mode: String,
    },
    #[serde(rename = "approval.decision")]
    ApprovalDecision {
        run_id: String,
        tool_name: String,
        approved: bool,
        reason: String,
        mode: String,
    },
    #[serde(rename = "tool.result")]
    ToolResult {
        run_id: String,
        tool_name: String,
        success: bool,
        #[serde(skip_serializing_if = "Option::is_none")]
        output: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        error: Option<String>,
    },
    #[serde(rename = "message.final")]
    MessageFinal {
        run_id: String,
        content: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        finish_reason: Option<String>,
    },
    #[serde(rename = "run.completed")]
    RunCompleted {
        run_id: String,
        success: bool,
        #[serde(skip_serializing_if = "Option::is_none")]
        finish_reason: Option<String>,
    },
    #[serde(rename = "error")]
    Error {
        #[serde(skip_serializing_if = "Option::is_none")]
        run_id: Option<String>,
        message: String,
    },
}

impl ToolInvocation {
    pub fn from_result(name: &str, arguments: Value, result: &ToolResult) -> Self {
        let output = if result.output.trim().is_empty() {
            None
        } else {
            Some(result.output.clone())
        };

        Self {
            name: name.to_string(),
            arguments,
            success: result.success,
            output,
            error: result.error.clone(),
        }
    }
}

/// Telemetry about memory recall for a single turn
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRecallStats {
    pub strategy: MemoryRecallStrategy,
    pub matches: Vec<MemoryRecallMatch>,
}

/// Strategy used for memory recall
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MemoryRecallStrategy {
    Semantic { requested: usize, returned: usize },
    RecentContext { limit: usize },
}

/// Summary of an individual recalled memory
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRecallMatch {
    pub message_id: Option<i64>,
    pub score: f32,
    pub role: MessageRole,
    pub preview: String,
}