use crate::core::message::{ConversationStats, TokenUsage};
use crate::core::{Error, Message, Result, StreamFormat};
use futures::{Stream, StreamExt};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::sync::mpsc;
use tracing::{debug, error};
pub struct MessageStream {
receiver: mpsc::Receiver<Result<Message>>,
}
impl MessageStream {
pub fn new(receiver: mpsc::Receiver<Result<Message>>, _format: StreamFormat) -> Self {
Self { receiver }
}
pub async fn from_line_stream(
mut line_receiver: mpsc::Receiver<Result<String>>,
format: StreamFormat,
) -> Self {
let config = crate::runtime::stream_config::get_stream_config();
let (tx, rx) = mpsc::channel(config.channel_buffer_size);
tokio::spawn(async move {
let config = crate::runtime::stream_config::get_stream_config();
let parser = MessageParser::new(format);
let mut accumulated_content = String::with_capacity(config.string_capacity);
while let Some(line_result) = line_receiver.recv().await {
match line_result {
Ok(line) => {
debug!("Received line: {}", line);
match format {
StreamFormat::Text => {
accumulated_content.push_str(&line);
accumulated_content.push('\n');
let message = Message::Assistant {
content: line,
meta: crate::core::MessageMeta {
session_id: "stream-session".to_string(),
timestamp: Some(std::time::SystemTime::now()),
cost_usd: None,
duration_ms: None,
tokens_used: None,
},
};
if tx.send(Ok(message)).await.is_err() {
debug!("Message receiver dropped");
break;
}
}
StreamFormat::Json => {
accumulated_content.push_str(&line);
accumulated_content.push('\n');
}
StreamFormat::StreamJson => {
if let Ok(Some(message)) = parser.parse_line(&line) {
if tx.send(Ok(message)).await.is_err() {
debug!("Message receiver dropped");
break;
}
} else if !line.trim().is_empty() {
debug!("Failed to parse line as message: {}", line);
}
}
}
}
Err(e) => {
if tx.send(Err(e)).await.is_err() {
debug!("Error receiver dropped");
}
break;
}
}
}
match format {
StreamFormat::Json => {
if !accumulated_content.trim().is_empty() {
if let Ok(Some(message)) =
parser.parse_accumulated_json(&accumulated_content)
{
let _ = tx.send(Ok(message)).await;
}
}
}
StreamFormat::Text => {
let final_message = Message::Result {
meta: crate::core::MessageMeta {
session_id: "stream-session".to_string(),
timestamp: Some(std::time::SystemTime::now()),
cost_usd: None,
duration_ms: None,
tokens_used: None,
},
stats: ConversationStats {
total_messages: 1,
total_cost_usd: 0.0,
total_duration_ms: 0,
total_tokens: TokenUsage {
input: 0,
output: 0,
total: 0,
},
},
};
let _ = tx.send(Ok(final_message)).await;
}
StreamFormat::StreamJson => {
}
}
});
Self { receiver: rx }
}
pub async fn collect_full_response(mut self) -> Result<String> {
let config = crate::runtime::stream_config::get_stream_config();
let mut response = String::with_capacity(config.string_capacity);
while let Some(result) = self.next().await {
match result? {
Message::Assistant { content, .. } => {
response.push_str(&content);
}
Message::Result { .. } => {
break;
}
_ => {}
}
}
Ok(response)
}
}
impl Stream for MessageStream {
type Item = Result<Message>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.receiver.poll_recv(cx)
}
}
pub struct MessageParser {
format: StreamFormat,
}
impl MessageParser {
pub fn new(format: StreamFormat) -> Self {
Self { format }
}
pub fn parse_line(&self, line: &str) -> Result<Option<Message>> {
match self.format {
StreamFormat::Text => {
Ok(None)
}
StreamFormat::Json | StreamFormat::StreamJson => {
if line.trim().is_empty() {
return Ok(None);
}
match serde_json::from_str::<Message>(line) {
Ok(message) => Ok(Some(message)),
Err(e) => {
error!("Failed to parse message: {}, line: {}", e, line);
Err(Error::SerializationError(e))
}
}
}
}
}
pub fn parse_accumulated_json(&self, content: &str) -> Result<Option<Message>> {
if content.trim().is_empty() {
return Ok(None);
}
if let Ok(message) = serde_json::from_str::<Message>(content) {
return Ok(Some(message));
}
if let Ok(cli_response) = serde_json::from_str::<crate::core::ClaudeCliResponse>(content) {
let message = Message::Assistant {
content: cli_response.result,
meta: crate::core::MessageMeta {
session_id: "json-response".to_string(),
timestamp: Some(std::time::SystemTime::now()),
cost_usd: None,
duration_ms: None,
tokens_used: None,
},
};
return Ok(Some(message));
}
let message = self.parse_text_response(content);
Ok(Some(message))
}
pub fn parse_text_response(&self, text: &str) -> Message {
Message::Assistant {
content: text.to_string(),
meta: crate::core::MessageMeta {
session_id: "text-response".to_string(),
timestamp: Some(std::time::SystemTime::now()),
cost_usd: None,
duration_ms: None,
tokens_used: None,
},
}
}
}