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}