feat(runtime): add streaming support to LlmDriver trait
- Add StreamChunk and StreamEvent types for Tauri event emission - Add stream() method to LlmDriver trait with async-stream - Implement Anthropic streaming with SSE parsing - Implement OpenAI streaming with SSE parsing - Add placeholder stream() for Gemini and Local drivers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,58 @@
|
||||
//! Streaming utilities
|
||||
//! Streaming response types
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc;
|
||||
use zclaw_types::Result;
|
||||
|
||||
/// Stream event for LLM responses
|
||||
/// Stream chunk emitted during streaming
|
||||
/// This is the serializable type sent via Tauri events
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum StreamChunk {
|
||||
/// Text delta
|
||||
TextDelta { delta: String },
|
||||
/// Thinking delta (for extended thinking models)
|
||||
ThinkingDelta { delta: String },
|
||||
/// Tool use started
|
||||
ToolUseStart { id: String, name: String },
|
||||
/// Tool use input delta
|
||||
ToolUseDelta { id: String, delta: String },
|
||||
/// Tool use completed
|
||||
ToolUseEnd { id: String, input: serde_json::Value },
|
||||
/// Stream completed
|
||||
Complete {
|
||||
input_tokens: u32,
|
||||
output_tokens: u32,
|
||||
stop_reason: String,
|
||||
},
|
||||
/// Error occurred
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
/// Streaming event for Tauri emission
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StreamEvent {
|
||||
/// Session ID for routing
|
||||
pub session_id: String,
|
||||
/// Agent ID for routing
|
||||
pub agent_id: String,
|
||||
/// The chunk content
|
||||
pub chunk: StreamChunk,
|
||||
}
|
||||
|
||||
impl StreamEvent {
|
||||
pub fn new(session_id: impl Into<String>, agent_id: impl Into<String>, chunk: StreamChunk) -> Self {
|
||||
Self {
|
||||
session_id: session_id.into(),
|
||||
agent_id: agent_id.into(),
|
||||
chunk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy stream event for internal use with mpsc channels
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StreamEvent {
|
||||
pub enum InternalStreamEvent {
|
||||
/// Text delta received
|
||||
TextDelta(String),
|
||||
/// Thinking delta received
|
||||
@@ -24,31 +71,31 @@ pub enum StreamEvent {
|
||||
|
||||
/// Stream sender wrapper
|
||||
pub struct StreamSender {
|
||||
tx: mpsc::Sender<StreamEvent>,
|
||||
tx: mpsc::Sender<InternalStreamEvent>,
|
||||
}
|
||||
|
||||
impl StreamSender {
|
||||
pub fn new(tx: mpsc::Sender<StreamEvent>) -> Self {
|
||||
pub fn new(tx: mpsc::Sender<InternalStreamEvent>) -> Self {
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
pub async fn send_text(&self, delta: impl Into<String>) -> Result<()> {
|
||||
self.tx.send(StreamEvent::TextDelta(delta.into())).await.ok();
|
||||
self.tx.send(InternalStreamEvent::TextDelta(delta.into())).await.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_thinking(&self, delta: impl Into<String>) -> Result<()> {
|
||||
self.tx.send(StreamEvent::ThinkingDelta(delta.into())).await.ok();
|
||||
self.tx.send(InternalStreamEvent::ThinkingDelta(delta.into())).await.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_complete(&self, input_tokens: u32, output_tokens: u32) -> Result<()> {
|
||||
self.tx.send(StreamEvent::Complete { input_tokens, output_tokens }).await.ok();
|
||||
self.tx.send(InternalStreamEvent::Complete { input_tokens, output_tokens }).await.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_error(&self, error: impl Into<String>) -> Result<()> {
|
||||
self.tx.send(StreamEvent::Error(error.into())).await.ok();
|
||||
self.tx.send(InternalStreamEvent::Error(error.into())).await.ok();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user