Skip to main content

crabtalk_core/agent/
event.rs

1//! Agent event types for step-based execution and streaming.
2//!
3//! Two-level design:
4//! - [`AgentStep`]: data record of one LLM round (response + tool dispatch).
5//! - [`AgentEvent`]: fine-grained streaming enum for real-time UI updates.
6//! - [`AgentResponse`]: final result after a full agent run.
7//! - [`AgentStopReason`]: why the agent stopped.
8
9use crate::model::HistoryEntry;
10use crabllm_core::{FinishReason, Message, ToolCall, Usage};
11
12/// A fine-grained event emitted during agent execution.
13///
14/// Yielded by `Agent::run_stream()` or emitted via `Hook::on_event()`
15/// for real-time status reporting to clients.
16///
17/// Text and thinking deltas are bracketed by explicit
18/// `TextStart`/`TextEnd` and `ThinkingStart`/`ThinkingEnd` markers so
19/// clients can render coherent segments without inferring boundaries
20/// from neighboring events. Only one segment is open at a time —
21/// transitions emit the closing event of the previous segment before
22/// the opening of the next.
23#[derive(Debug, Clone)]
24pub enum AgentEvent {
25    /// A text segment is starting; subsequent `TextDelta`s belong to it.
26    TextStart,
27    /// Text content delta from the model.
28    TextDelta(String),
29    /// The current text segment has ended.
30    TextEnd,
31    /// A thinking segment is starting; subsequent `ThinkingDelta`s belong to it.
32    ThinkingStart,
33    /// Thinking/reasoning content delta from the model.
34    ThinkingDelta(String),
35    /// The current thinking segment has ended.
36    ThinkingEnd,
37    /// Early notification: model is generating tool calls (names only, args incomplete).
38    ToolCallsBegin(Vec<ToolCall>),
39    /// Model is calling tools (with the complete tool calls).
40    ToolCallsStart(Vec<ToolCall>),
41    /// A single tool completed execution.
42    ///
43    /// `output` is `Ok` for normal tool output and `Err` for a failure —
44    /// either a dispatch-level error (no sender, channel closed) or a
45    /// handler-reported failure. The inner string carries the text in
46    /// both cases so UIs can render it; the distinction lets clients
47    /// style errors differently and lets agents make retry decisions.
48    ToolResult {
49        /// The tool call ID this result belongs to.
50        call_id: String,
51        /// Success or error output from the tool.
52        output: Result<String, String>,
53        /// Wall-clock duration of the tool dispatch in milliseconds.
54        duration_ms: u64,
55    },
56    /// All tools completed, continuing to next iteration.
57    ToolCallsComplete,
58    /// User steering message injected at turn boundary.
59    UserSteered { content: String },
60    /// Context was compacted — carries the compaction summary.
61    Compact { summary: String },
62    /// Agent finished with final response.
63    Done(AgentResponse),
64}
65
66/// Data record of one LLM round (one model call + tool dispatch).
67///
68/// Carries only what downstream consumers actually read: the assistant
69/// message, token usage, the finish reason, and the tool calls / results.
70/// No synthesized wire response — the old `AgentStep.response: Response`
71/// field was a parallel type that only served to hold `usage` and the
72/// final text content. Those two fields are now on the step directly.
73#[derive(Debug, Clone)]
74pub struct AgentStep {
75    /// The assistant message produced by this step.
76    pub message: Message,
77    /// Token usage reported by the provider (zero if not reported).
78    pub usage: Usage,
79    /// Why the model stopped generating (if reported).
80    pub finish_reason: Option<FinishReason>,
81    /// Tool calls made in this step (if any).
82    pub tool_calls: Vec<ToolCall>,
83    /// Results from tool executions as history entries.
84    pub tool_results: Vec<HistoryEntry>,
85}
86
87/// Final response from a complete agent run.
88#[derive(Debug, Clone)]
89pub struct AgentResponse {
90    /// All steps taken during execution.
91    pub steps: Vec<AgentStep>,
92    /// Final text response (if any).
93    pub final_response: Option<String>,
94    /// Total number of iterations performed.
95    pub iterations: usize,
96    /// Why the agent stopped.
97    pub stop_reason: AgentStopReason,
98    /// The requested model name (from config, not the API-echoed value).
99    pub model: String,
100}
101
102impl AgentResponse {
103    /// Shorthand for a pre-run error (no steps, no model involved).
104    pub fn error(msg: impl Into<String>) -> Self {
105        Self {
106            steps: vec![],
107            final_response: None,
108            iterations: 0,
109            stop_reason: AgentStopReason::Error(msg.into()),
110            model: String::new(),
111        }
112    }
113}
114
115/// Why the agent stopped executing.
116#[derive(Debug, Clone, PartialEq)]
117pub enum AgentStopReason {
118    /// Model produced a text response with no tool calls.
119    TextResponse,
120    /// Maximum iterations reached.
121    MaxIterations,
122    /// No tool calls and no text response.
123    NoAction,
124    /// Error during execution.
125    Error(String),
126}
127
128impl std::fmt::Display for AgentStopReason {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        match self {
131            Self::TextResponse => write!(f, "text_response"),
132            Self::MaxIterations => write!(f, "max_iterations"),
133            Self::NoAction => write!(f, "no_action"),
134            Self::Error(msg) => write!(f, "error: {msg}"),
135        }
136    }
137}