use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
pub enum BlockType {
Thinking,
Text,
ToolCall,
ToolOutput,
}
#[derive(Debug, Clone)]
pub struct Block {
pub id: Option<String>,
pub block_type: BlockType,
pub content: String,
pub label: Option<String>,
}
impl Block {
pub fn new(block_type: BlockType, content: String) -> Self {
Self {
id: None,
block_type,
content,
label: None,
}
}
pub fn with_id(block_type: BlockType, content: String, id: String) -> Self {
Self {
id: Some(id),
block_type,
content,
label: None,
}
}
pub fn with_label(block_type: BlockType, label: String, id: Option<String>) -> Self {
Self {
id,
block_type,
content: String::new(),
label: Some(label),
}
}
pub fn render(&self) -> String {
match &self.block_type {
BlockType::Thinking => {
if self.content.trim().is_empty() {
return String::new();
}
self.content
.lines()
.map(|l| format!("> {}", l))
.collect::<Vec<_>>()
.join("\n")
}
BlockType::Text => self.content.clone(),
BlockType::ToolCall => self.label.as_deref().unwrap_or("🛠️ **Tool:**").to_string(),
BlockType::ToolOutput => {
if self.content.trim().is_empty() {
return String::new();
}
let char_count = self.content.chars().count();
let display_content = if char_count > 500 {
if let Some((byte_pos, _)) = self.content.char_indices().nth(500) {
format!("{}... (truncated)", &self.content[..byte_pos])
} else {
self.content.clone()
}
} else {
self.content.clone()
};
format!("```\n{}\n```", display_content)
}
}
.trim_end()
.to_string()
}
}
pub struct EmbedComposer {
pub blocks: VecDeque<Block>,
max_len: usize,
pub has_truncated: bool,
}
impl EmbedComposer {
pub fn new(max_len: usize) -> Self {
Self {
blocks: VecDeque::new(),
max_len,
has_truncated: false,
}
}
fn prune(&mut self) {
while self.blocks.len() > 10 {
self.blocks.pop_front();
self.has_truncated = true;
}
}
pub fn update_block_by_id(&mut self, id: &str, block_type: BlockType, content: String) {
for block in self.blocks.iter_mut() {
if block.id.as_deref() == Some(id) && block.block_type == block_type {
if content.len() >= block.content.len() {
block.content = content;
}
return;
}
}
if block_type == BlockType::ToolCall || block_type == BlockType::ToolOutput {
return;
}
self.blocks
.push_back(Block::with_id(block_type, content, id.to_string()));
self.prune();
}
pub fn push_delta(&mut self, id: Option<String>, block_type: BlockType, delta: &str) {
if delta.is_empty() {
return;
}
if let Some(ref id_str) = id {
for block in self.blocks.iter_mut() {
if block.id.as_deref() == Some(id_str) && block.block_type == block_type {
block.content.push_str(delta);
return;
}
}
if block_type == BlockType::ToolCall || block_type == BlockType::ToolOutput {
return;
}
if let Some(last) = self.blocks.back_mut() {
if last.block_type == block_type && last.id.is_none() {
last.id = Some(id_str.clone());
last.content.push_str(delta);
return;
}
}
self.blocks.push_back(Block::with_id(
block_type,
delta.to_string(),
id_str.clone(),
));
} else {
if let Some(last) = self.blocks.back_mut() {
if last.block_type == block_type && last.id.is_none() {
last.content.push_str(delta);
return;
}
}
self.blocks
.push_back(Block::new(block_type, delta.to_string()));
}
self.prune();
}
pub fn set_tool_call(&mut self, id: String, label: String) {
for block in self.blocks.iter_mut() {
if block.id.as_deref() == Some(&id) && block.block_type == BlockType::ToolCall {
block.label = Some(label);
return;
}
}
self.blocks
.push_back(Block::with_label(BlockType::ToolCall, label, Some(id)));
self.prune();
}
pub fn sync_content(&mut self, items: Vec<Block>) {
if items.is_empty() {
return;
}
let mut new_list = VecDeque::new();
for item in items {
let mut merged = item.clone();
if let Some(local) = self.blocks.iter().find(|b| match (&b.id, &item.id) {
(Some(id1), Some(id2)) => id1 == id2,
_ => b.block_type == item.block_type && b.id.is_none() && item.id.is_none(),
}) {
if local.content.len() > merged.content.len() {
merged.content = local.content.clone();
}
if merged.id.is_none() {
merged.id = local.id.clone();
}
}
new_list.push_back(merged);
}
for local in &self.blocks {
if local.id.is_some() && !new_list.iter().any(|b| b.id == local.id) {
new_list.push_back(local.clone());
}
}
self.blocks = new_list;
self.prune();
}
pub fn render(&self) -> String {
if self.blocks.is_empty() {
return String::new();
}
let renderings: Vec<String> = self
.blocks
.iter()
.map(|b| b.render())
.filter(|r| !r.is_empty())
.collect();
let mut res = renderings.join("\n\n");
let char_count = res.chars().count();
let fold_msg = "*...[部分歷史內容已截斷]*\n\n";
if self.has_truncated || char_count > self.max_len {
let target_len = self.max_len - fold_msg.len();
if char_count > target_len {
if let Some((byte_pos, _)) = res.char_indices().nth(char_count - target_len) {
res = format!("{}{}", fold_msg, &res[byte_pos..]);
}
} else if self.has_truncated {
res = format!("{}{}", fold_msg, res);
}
}
let backtick_count = res.matches("```").count();
if !backtick_count.is_multiple_of(2) {
res.push_str("\n```");
}
res.trim().to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_output_truncation() {
let long_content = "A".repeat(1000);
let block = Block::new(BlockType::ToolOutput, long_content);
let rendered = block.render();
assert!(rendered.contains("... (truncated)"));
assert!(rendered.len() < 600); }
#[test]
fn test_markdown_guard() {
let mut composer = EmbedComposer::new(100);
composer
.blocks
.push_back(Block::new(BlockType::Text, "```rust\n unfinished".into()));
let rendered = composer.render();
assert!(
rendered.ends_with("```"),
"Should automatically close code block"
);
assert_eq!(rendered.matches("```").count() % 2, 0);
}
#[test]
fn test_thinking_block_rendering() {
let block = Block::new(BlockType::Thinking, "Line 1\nLine 2".into());
let rendered = block.render();
assert_eq!(rendered, "> Line 1\n> Line 2");
}
#[test]
fn test_composer_prune() {
let mut composer = EmbedComposer::new(1000);
for i in 0..15 {
composer.push_delta(Some(i.to_string()), BlockType::Text, "data");
}
assert_eq!(composer.blocks.len(), 10);
assert!(composer.has_truncated);
}
#[test]
fn test_composer_sync_content() {
let mut composer = EmbedComposer::new(1000);
composer.push_delta(Some("id1".into()), BlockType::Text, "longer_old_data");
let new_items = vec![
Block::with_id(BlockType::Text, "shorter".into(), "id1".into()),
Block::with_id(BlockType::Text, "fresh".into(), "id2".into()),
];
composer.sync_content(new_items);
assert_eq!(composer.blocks.len(), 2);
assert_eq!(composer.blocks[0].content, "longer_old_data");
}
}