#[cfg(test)]
use super::AgentOutputSink;
#[cfg(test)]
use super::AssistantTextSink;
use crate::sessions::{Session, SessionEventKind, try_record_session_event_batch};
use serde_json::json;
use std::path::Path;
#[cfg(test)]
pub(super) struct AssistantOnlySink<'a> {
pub(super) on_text_delta: &'a mut AssistantTextSink<'a>,
}
#[cfg(test)]
impl AgentOutputSink for AssistantOnlySink<'_> {
fn assistant_delta(&mut self, text: &str) -> anyhow::Result<()> {
(self.on_text_delta)(text)
}
fn tool_block(&mut self, _block: &str) -> anyhow::Result<()> {
Ok(())
}
}
pub(super) fn normalize_agent_identifier(value: String, fallback: &str) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
fallback.to_string()
} else {
trimmed.to_string()
}
}
pub(super) fn safe_error_message(error: &anyhow::Error) -> String {
error
.chain()
.next()
.map(ToString::to_string)
.unwrap_or_else(|| "unknown error".to_string())
}
const ASSISTANT_CHUNK_BATCH_BYTES: usize = 4 * 1024;
pub(super) struct AssistantChunkBatch<'a> {
session: Option<&'a Session>,
cwd: &'a Path,
chunks: Vec<String>,
bytes: usize,
}
impl<'a> AssistantChunkBatch<'a> {
pub(super) fn new(session: Option<&'a Session>, cwd: &'a Path) -> Self {
Self {
session,
cwd,
chunks: Vec::new(),
bytes: 0,
}
}
pub(super) fn push(&mut self, delta: &str) {
self.bytes = self.bytes.saturating_add(delta.len());
self.chunks.push(delta.to_string());
}
pub(super) fn flush_if_full(&mut self) -> anyhow::Result<()> {
if self.bytes >= ASSISTANT_CHUNK_BATCH_BYTES {
self.flush()?;
}
Ok(())
}
pub(super) fn flush(&mut self) -> anyhow::Result<()> {
if self.chunks.iter().all(|chunk| chunk.trim().is_empty()) {
self.chunks.clear();
self.bytes = 0;
return Ok(());
}
let chunks = std::mem::take(&mut self.chunks);
self.bytes = 0;
let text = chunks.concat();
try_record_session_event_batch(
self.session,
self.cwd,
SessionEventKind::AssistantChunk,
std::iter::once(json!({"text": text})),
)
.map(|_| ())
}
}