Skip to main content

mermaid_cli/models/
stream.rs

1//! Typed streaming events emitted by model adapters.
2//!
3//! Replaces the legacy `StreamCallback = Arc<dyn Fn(&str)>` text-only
4//! callback. The typed event surface lets future adapters (Anthropic,
5//! OpenAI, etc.) emit reasoning chunks, tool calls, and completion
6//! signals as first-class events instead of stuffing them into the text
7//! channel. Roo Code (`src/api/transform/stream.rs`) and OpenCode
8//! (`provider/processor.ts`) both validated this pattern as the way out
9//! of per-provider stream-shape sniffing.
10//!
11//! For Step 1 the only adapter is Ollama; Wave 3 wires it to emit these
12//! events. Earlier waves carry the legacy text callback through a default
13//! impl that synthesizes `Text` + `Done` events from the existing
14//! callback shape — see `Model::chat_typed` in `traits.rs`.
15
16use std::sync::Arc;
17
18use super::reasoning::ReasoningChunk;
19use super::tool_call::ToolCall;
20
21/// A single event emitted during a streaming model call.
22///
23/// Adapters MUST emit `Done` exactly once at the end of a successful
24/// stream. `Text` and `Reasoning` may interleave in any order. `ToolCall`
25/// events typically arrive at the end of generation but the contract is
26/// "before `Done`".
27#[derive(Debug, Clone)]
28pub enum StreamEvent {
29    /// Plain assistant content. Append to the response buffer.
30    Text(String),
31    /// Reasoning / thinking content. Render separately from regular text;
32    /// renderer decides whether to display or hide based on user prefs.
33    Reasoning(ReasoningChunk),
34    /// A tool/function call extracted from the model response.
35    ToolCall(ToolCall),
36    /// Stream complete. Carries the total token usage if reported by the
37    /// provider; `0` if unavailable (still meaningful — the caller knows
38    /// generation finished).
39    Done { tokens: usize },
40}
41
42/// Callback invoked once per `StreamEvent` during a chat request.
43///
44/// `Send + Sync` so the callback can be shared across spawned tasks.
45pub type StreamCallback = Arc<dyn Fn(StreamEvent) + Send + Sync>;
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn stream_event_clone() {
53        let ev = StreamEvent::Text("hello".to_string());
54        let cloned = ev.clone();
55        match (ev, cloned) {
56            (StreamEvent::Text(a), StreamEvent::Text(b)) => assert_eq!(a, b),
57            _ => panic!("clone should produce same variant"),
58        }
59    }
60
61    #[test]
62    fn stream_event_done_carries_tokens() {
63        let ev = StreamEvent::Done { tokens: 42 };
64        match ev {
65            StreamEvent::Done { tokens } => assert_eq!(tokens, 42),
66            _ => panic!("expected Done"),
67        }
68    }
69
70    #[test]
71    fn stream_event_reasoning_with_chunk() {
72        let chunk = ReasoningChunk {
73            text: "weighing options".to_string(),
74            signature: None,
75        };
76        let ev = StreamEvent::Reasoning(chunk.clone());
77        match ev {
78            StreamEvent::Reasoning(c) => {
79                assert_eq!(c.text, chunk.text);
80                assert_eq!(c.signature, chunk.signature);
81            },
82            _ => panic!("expected Reasoning"),
83        }
84    }
85
86    #[test]
87    fn callback_is_send_sync() {
88        // Compile-time: callback must satisfy Send + Sync to be carried
89        // through tokio::spawn boundaries in the agent loop.
90        fn assert_send_sync<T: Send + Sync>() {}
91        assert_send_sync::<StreamCallback>();
92    }
93}