coro_core/output/
mod.rs

1//! Output abstraction layer for the Coro Code core
2//!
3//! This module provides an abstract interface for outputting agent execution information,
4//! allowing different implementations for CLI, API, logging, etc.
5
6use crate::tools::{ToolCall, ToolResult};
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11// Core only provides abstractions - implementations are in calling modules
12
13/// Null output handler that discards all events (useful for testing and backward compatibility)
14pub struct NullOutput;
15
16#[async_trait]
17impl AgentOutput for NullOutput {
18    async fn emit_event(
19        &self,
20        _event: AgentEvent,
21    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
22        Ok(())
23    }
24}
25
26/// Convenience module for backward compatibility
27pub mod events {
28    pub use super::NullOutput;
29}
30
31/// Status of tool execution
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33pub enum ToolExecutionStatus {
34    /// Tool is currently executing
35    Executing,
36    /// Tool completed successfully
37    Success,
38    /// Tool failed with an error
39    Error,
40}
41
42/// Confirmation kinds for interactive/safe operations
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44pub enum ConfirmationKind {
45    /// Confirm before executing a tool
46    ToolExecution,
47}
48
49/// A generic confirmation request that UI/API layers can handle
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ConfirmationRequest {
52    /// Request identifier (can reuse related entity id)
53    pub id: String,
54    /// The kind of confirmation
55    pub kind: ConfirmationKind,
56    /// Short title for display
57    pub title: String,
58    /// Detailed message for display
59    pub message: String,
60    /// Arbitrary metadata (e.g., tool_name, parameters)
61    pub metadata: HashMap<String, serde_json::Value>,
62}
63
64/// User/consumer decision for a confirmation request
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ConfirmationDecision {
67    /// Whether the action is approved
68    pub approved: bool,
69    /// Optional note/reason
70    pub note: Option<String>,
71}
72
73/// Rich tool execution information
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ToolExecutionInfo {
76    /// Unique identifier for this tool execution
77    pub execution_id: String,
78    /// Tool name (e.g., "bash", "str_replace_based_edit_tool")
79    pub tool_name: String,
80    /// Tool parameters/arguments
81    pub parameters: HashMap<String, serde_json::Value>,
82    /// Current execution status
83    pub status: ToolExecutionStatus,
84    /// Tool result (if completed)
85    pub result: Option<ToolResult>,
86    /// Timestamp of status change
87    pub timestamp: chrono::DateTime<chrono::Utc>,
88    /// Additional metadata for tool-specific information
89    pub metadata: HashMap<String, serde_json::Value>,
90}
91
92/// Agent execution step information
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct AgentStepInfo {
95    /// Step number in the execution sequence
96    pub step_number: usize,
97    /// Current task description
98    pub task: String,
99    /// LLM thinking/reasoning (if available)
100    pub thinking: Option<String>,
101    /// Tool executions in this step
102    pub tool_executions: Vec<ToolExecutionInfo>,
103    /// Step completion status
104    pub completed: bool,
105}
106
107/// Token usage statistics
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109pub struct TokenUsage {
110    /// Total input tokens consumed
111    pub input_tokens: u32,
112    /// Total output tokens generated
113    pub output_tokens: u32,
114    /// Total tokens (input + output)
115    pub total_tokens: u32,
116}
117
118/// Agent execution context information
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct AgentExecutionContext {
121    /// Agent configuration name or identifier
122    pub agent_id: String,
123    /// Original goal from the first user request (never overwritten)
124    pub original_goal: String,
125    /// Current task being executed (can be updated with new queries)
126    pub current_task: String,
127    /// Project path or working directory
128    pub project_path: String,
129    /// Maximum allowed steps
130    pub max_steps: usize,
131    /// Current step number
132    pub current_step: usize,
133    /// Total execution time so far
134    pub execution_time: std::time::Duration,
135    /// Token usage statistics
136    pub token_usage: TokenUsage,
137}
138
139/// Events that can be emitted during agent execution
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub enum AgentEvent {
142    /// Agent execution started
143    ExecutionStarted { context: AgentExecutionContext },
144    /// Agent execution completed
145    ExecutionCompleted {
146        context: AgentExecutionContext,
147        success: bool,
148        summary: String,
149    },
150    /// Agent execution interrupted (cancelled)
151    ExecutionInterrupted {
152        context: AgentExecutionContext,
153        reason: String,
154    },
155    /// New step started
156    StepStarted { step_info: AgentStepInfo },
157    /// Step completed
158    StepCompleted { step_info: AgentStepInfo },
159    /// Tool execution started
160    ToolExecutionStarted { tool_info: ToolExecutionInfo },
161    /// Tool execution status updated
162    ToolExecutionUpdated { tool_info: ToolExecutionInfo },
163    /// Tool execution completed
164    ToolExecutionCompleted { tool_info: ToolExecutionInfo },
165    /// Agent thinking/reasoning
166    AgentThinking {
167        step_number: usize,
168        thinking: String,
169    },
170    /// Token usage updated (emitted after each LLM call)
171    TokenUsageUpdated { token_usage: TokenUsage },
172    /// Agent status update (for interactive mode status reporting)
173    StatusUpdate {
174        status: String,
175        metadata: HashMap<String, serde_json::Value>,
176    },
177    /// General message or log
178    Message {
179        level: MessageLevel,
180        content: String,
181        metadata: HashMap<String, serde_json::Value>,
182    },
183    /// Conversation compression started
184    CompressionStarted {
185        level: String,
186        current_tokens: u32,
187        target_tokens: u32,
188        reason: String,
189    },
190    /// Conversation compression completed
191    CompressionCompleted {
192        summary: String,
193        tokens_saved: u32,
194        messages_before: u32,
195        messages_after: u32,
196    },
197    /// Conversation compression failed
198    CompressionFailed {
199        error: String,
200        fallback_action: String,
201    },
202}
203
204/// Message severity levels
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206pub enum MessageLevel {
207    Debug,
208    Info,
209    Normal,
210    Warning,
211    Error,
212}
213
214/// Abstract output interface for agent execution
215#[async_trait]
216pub trait AgentOutput: Send + Sync {
217    /// Emit an agent event
218    async fn emit_event(
219        &self,
220        event: AgentEvent,
221    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
222
223    /// Emit a message with specified level
224    async fn emit_message(
225        &self,
226        level: MessageLevel,
227        content: &str,
228    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
229        self.emit_event(AgentEvent::Message {
230            level,
231            content: content.to_string(),
232            metadata: HashMap::new(),
233        })
234        .await
235    }
236
237    /// Emit debug message
238    async fn debug(&self, content: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
239        self.emit_message(MessageLevel::Debug, content).await
240    }
241
242    /// Emit token usage update
243    async fn emit_token_update(
244        &self,
245        token_usage: TokenUsage,
246    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
247        self.emit_event(AgentEvent::TokenUsageUpdated { token_usage })
248            .await
249    }
250
251    /// Emit info message
252    async fn info(&self, content: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
253        self.emit_message(MessageLevel::Info, content).await
254    }
255
256    /// Emit warning message
257    async fn warning(&self, content: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
258        self.emit_message(MessageLevel::Warning, content).await
259    }
260
261    /// Emit error message
262    async fn error(&self, content: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
263        self.emit_message(MessageLevel::Error, content).await
264    }
265
266    /// Emit normal text message
267    async fn normal(&self, content: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
268        self.emit_message(MessageLevel::Normal, content).await
269    }
270
271    /// Emit status update (for interactive mode status reporting)
272    async fn emit_status_update(
273        &self,
274        status: &str,
275    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
276        self.emit_event(AgentEvent::StatusUpdate {
277            status: status.to_string(),
278            metadata: HashMap::new(),
279        })
280        .await
281    }
282
283    /// Request a confirmation decision from the output handler (UI/API)
284    /// Default: deny (safe). Concrete outputs (CLI/UI) should override to prompt.
285    async fn request_confirmation(
286        &self,
287        _request: &ConfirmationRequest,
288    ) -> Result<ConfirmationDecision, Box<dyn std::error::Error + Send + Sync>> {
289        Ok(ConfirmationDecision {
290            approved: false,
291            note: Some("No confirmation handler available; default deny".to_string()),
292        })
293    }
294
295    /// Check if this output handler supports real-time updates
296    fn supports_realtime_updates(&self) -> bool {
297        false
298    }
299
300    /// Flush any buffered output (for implementations that buffer)
301    async fn flush(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
302        Ok(())
303    }
304}
305
306/// Helper trait for creating tool execution info
307pub trait ToolExecutionInfoBuilder {
308    fn create_tool_execution_info(
309        tool_call: &ToolCall,
310        status: ToolExecutionStatus,
311        result: Option<&ToolResult>,
312    ) -> ToolExecutionInfo;
313}
314
315impl ToolExecutionInfoBuilder for ToolExecutionInfo {
316    fn create_tool_execution_info(
317        tool_call: &ToolCall,
318        status: ToolExecutionStatus,
319        result: Option<&ToolResult>,
320    ) -> ToolExecutionInfo {
321        let parameters = if let serde_json::Value::Object(map) = &tool_call.parameters {
322            map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
323        } else {
324            let mut map = HashMap::new();
325            map.insert("raw_parameters".to_string(), tool_call.parameters.clone());
326            map
327        };
328
329        ToolExecutionInfo {
330            execution_id: tool_call.id.clone(),
331            tool_name: tool_call.name.clone(),
332            parameters,
333            status,
334            result: result.cloned(),
335            timestamp: chrono::Utc::now(),
336            metadata: HashMap::new(),
337        }
338    }
339}