feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
use crate ::session ::{ ContentBlock , ConversationMessage , MessageRole , Session } ;
#[ derive(Debug, Clone, Copy, PartialEq, Eq) ]
pub struct CompactionConfig {
pub preserve_recent_messages : usize ,
pub max_estimated_tokens : usize ,
}
impl Default for CompactionConfig {
fn default ( ) -> Self {
Self {
preserve_recent_messages : 4 ,
max_estimated_tokens : 10_000 ,
}
}
}
#[ derive(Debug, Clone, PartialEq, Eq) ]
pub struct CompactionResult {
pub summary : String ,
2026-03-31 19:18:42 +00:00
pub formatted_summary : String ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
pub compacted_session : Session ,
pub removed_message_count : usize ,
}
#[ must_use ]
pub fn estimate_session_tokens ( session : & Session ) -> usize {
session . messages . iter ( ) . map ( estimate_message_tokens ) . sum ( )
}
#[ must_use ]
pub fn should_compact ( session : & Session , config : CompactionConfig ) -> bool {
session . messages . len ( ) > config . preserve_recent_messages
& & estimate_session_tokens ( session ) > = config . max_estimated_tokens
}
#[ must_use ]
pub fn format_compact_summary ( summary : & str ) -> String {
let without_analysis = strip_tag_block ( summary , " analysis " ) ;
let formatted = if let Some ( content ) = extract_tag_block ( & without_analysis , " summary " ) {
without_analysis . replace (
& format! ( " <summary> {content} </summary> " ) ,
& format! ( " Summary: \n {} " , content . trim ( ) ) ,
)
} else {
without_analysis
} ;
collapse_blank_lines ( & formatted ) . trim ( ) . to_string ( )
}
#[ must_use ]
pub fn get_compact_continuation_message (
summary : & str ,
suppress_follow_up_questions : bool ,
recent_messages_preserved : bool ,
) -> String {
let mut base = format! (
" This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation. \n \n {} " ,
format_compact_summary ( summary )
) ;
if recent_messages_preserved {
base . push_str ( " \n \n Recent messages are preserved verbatim. " ) ;
}
if suppress_follow_up_questions {
base . push_str ( " \n Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, and do not preface with continuation text. " ) ;
}
base
}
#[ must_use ]
pub fn compact_session ( session : & Session , config : CompactionConfig ) -> CompactionResult {
if ! should_compact ( session , config ) {
return CompactionResult {
summary : String ::new ( ) ,
2026-03-31 19:18:42 +00:00
formatted_summary : String ::new ( ) ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
compacted_session : session . clone ( ) ,
removed_message_count : 0 ,
} ;
}
let keep_from = session
. messages
. len ( )
. saturating_sub ( config . preserve_recent_messages ) ;
let removed = & session . messages [ .. keep_from ] ;
let preserved = session . messages [ keep_from .. ] . to_vec ( ) ;
let summary = summarize_messages ( removed ) ;
2026-03-31 19:18:42 +00:00
let formatted_summary = format_compact_summary ( & summary ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
let continuation = get_compact_continuation_message ( & summary , true , ! preserved . is_empty ( ) ) ;
let mut compacted_messages = vec! [ ConversationMessage {
role : MessageRole ::System ,
blocks : vec ! [ ContentBlock ::Text { text : continuation } ] ,
usage : None ,
} ] ;
compacted_messages . extend ( preserved ) ;
CompactionResult {
summary ,
2026-03-31 19:18:42 +00:00
formatted_summary ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
compacted_session : Session {
version : session . version ,
messages : compacted_messages ,
} ,
removed_message_count : removed . len ( ) ,
}
}
fn summarize_messages ( messages : & [ ConversationMessage ] ) -> String {
2026-03-31 19:18:42 +00:00
let user_messages = messages
. iter ( )
. filter ( | message | message . role = = MessageRole ::User )
. count ( ) ;
let assistant_messages = messages
. iter ( )
. filter ( | message | message . role = = MessageRole ::Assistant )
. count ( ) ;
let tool_messages = messages
. iter ( )
. filter ( | message | message . role = = MessageRole ::Tool )
. count ( ) ;
let mut tool_names = messages
. iter ( )
. flat_map ( | message | message . blocks . iter ( ) )
. filter_map ( | block | match block {
ContentBlock ::ToolUse { name , .. } = > Some ( name . as_str ( ) ) ,
ContentBlock ::ToolResult { tool_name , .. } = > Some ( tool_name . as_str ( ) ) ,
ContentBlock ::Text { .. } = > None ,
} )
. collect ::< Vec < _ > > ( ) ;
tool_names . sort_unstable ( ) ;
tool_names . dedup ( ) ;
let mut lines = vec! [
" <summary> " . to_string ( ) ,
" Conversation summary: " . to_string ( ) ,
format! (
" - Scope: {} earlier messages compacted (user={}, assistant={}, tool={}). " ,
messages . len ( ) ,
user_messages ,
assistant_messages ,
tool_messages
) ,
] ;
if ! tool_names . is_empty ( ) {
lines . push ( format! ( " - Tools mentioned: {} . " , tool_names . join ( " , " ) ) ) ;
}
lines . push ( " - Key timeline: " . to_string ( ) ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
for message in messages {
let role = match message . role {
MessageRole ::System = > " system " ,
MessageRole ::User = > " user " ,
MessageRole ::Assistant = > " assistant " ,
MessageRole ::Tool = > " tool " ,
} ;
let content = message
. blocks
. iter ( )
. map ( summarize_block )
. collect ::< Vec < _ > > ( )
. join ( " | " ) ;
2026-03-31 19:18:42 +00:00
lines . push ( format! ( " - {role} : {content} " ) ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
lines . push ( " </summary> " . to_string ( ) ) ;
lines . join ( " \n " )
}
fn summarize_block ( block : & ContentBlock ) -> String {
let raw = match block {
ContentBlock ::Text { text } = > text . clone ( ) ,
ContentBlock ::ToolUse { name , input , .. } = > format! ( " tool_use {name} ( {input} ) " ) ,
ContentBlock ::ToolResult {
tool_name ,
output ,
is_error ,
..
} = > format! (
" tool_result {tool_name}: {}{output} " ,
if * is_error { " error " } else { " " }
) ,
} ;
truncate_summary ( & raw , 160 )
}
fn truncate_summary ( content : & str , max_chars : usize ) -> String {
if content . chars ( ) . count ( ) < = max_chars {
return content . to_string ( ) ;
}
let mut truncated = content . chars ( ) . take ( max_chars ) . collect ::< String > ( ) ;
truncated . push ( '…' ) ;
truncated
}
fn estimate_message_tokens ( message : & ConversationMessage ) -> usize {
message
. blocks
. iter ( )
. map ( | block | match block {
ContentBlock ::Text { text } = > text . len ( ) / 4 + 1 ,
ContentBlock ::ToolUse { name , input , .. } = > ( name . len ( ) + input . len ( ) ) / 4 + 1 ,
ContentBlock ::ToolResult {
tool_name , output , ..
} = > ( tool_name . len ( ) + output . len ( ) ) / 4 + 1 ,
} )
. sum ( )
}
fn extract_tag_block ( content : & str , tag : & str ) -> Option < String > {
let start = format! ( " < {tag} > " ) ;
let end = format! ( " </ {tag} > " ) ;
let start_index = content . find ( & start ) ? + start . len ( ) ;
let end_index = content [ start_index .. ] . find ( & end ) ? + start_index ;
Some ( content [ start_index .. end_index ] . to_string ( ) )
}
fn strip_tag_block ( content : & str , tag : & str ) -> String {
let start = format! ( " < {tag} > " ) ;
let end = format! ( " </ {tag} > " ) ;
if let ( Some ( start_index ) , Some ( end_index_rel ) ) = ( content . find ( & start ) , content . find ( & end ) ) {
let end_index = end_index_rel + end . len ( ) ;
let mut stripped = String ::new ( ) ;
stripped . push_str ( & content [ .. start_index ] ) ;
stripped . push_str ( & content [ end_index .. ] ) ;
stripped
} else {
content . to_string ( )
}
}
fn collapse_blank_lines ( content : & str ) -> String {
let mut result = String ::new ( ) ;
let mut last_blank = false ;
for line in content . lines ( ) {
let is_blank = line . trim ( ) . is_empty ( ) ;
if is_blank & & last_blank {
continue ;
}
result . push_str ( line ) ;
result . push ( '\n' ) ;
last_blank = is_blank ;
}
result
}
#[ cfg(test) ]
mod tests {
use super ::{
compact_session , estimate_session_tokens , format_compact_summary , should_compact ,
CompactionConfig ,
} ;
use crate ::session ::{ ContentBlock , ConversationMessage , MessageRole , Session } ;
#[ test ]
fn formats_compact_summary_like_upstream ( ) {
let summary = " <analysis>scratch</analysis> \n <summary>Kept work</summary> " ;
assert_eq! ( format_compact_summary ( summary ) , " Summary: \n Kept work " ) ;
}
#[ test ]
fn leaves_small_sessions_unchanged ( ) {
let session = Session {
version : 1 ,
messages : vec ! [ ConversationMessage ::user_text ( " hello " ) ] ,
} ;
let result = compact_session ( & session , CompactionConfig ::default ( ) ) ;
assert_eq! ( result . removed_message_count , 0 ) ;
assert_eq! ( result . compacted_session , session ) ;
assert! ( result . summary . is_empty ( ) ) ;
2026-03-31 19:18:42 +00:00
assert! ( result . formatted_summary . is_empty ( ) ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
#[ test ]
fn compacts_older_messages_into_a_system_summary ( ) {
let session = Session {
version : 1 ,
messages : vec ! [
ConversationMessage ::user_text ( " one " . repeat ( 200 ) ) ,
ConversationMessage ::assistant ( vec! [ ContentBlock ::Text {
text : " two " . repeat ( 200 ) ,
} ] ) ,
ConversationMessage ::tool_result ( " 1 " , " bash " , " ok " . repeat ( 200 ) , false ) ,
ConversationMessage {
role : MessageRole ::Assistant ,
blocks : vec ! [ ContentBlock ::Text {
text : " recent " . to_string ( ) ,
} ] ,
usage : None ,
} ,
] ,
} ;
let result = compact_session (
& session ,
CompactionConfig {
preserve_recent_messages : 2 ,
max_estimated_tokens : 1 ,
} ,
) ;
assert_eq! ( result . removed_message_count , 2 ) ;
assert_eq! (
result . compacted_session . messages [ 0 ] . role ,
MessageRole ::System
) ;
assert! ( matches! (
& result . compacted_session . messages [ 0 ] . blocks [ 0 ] ,
ContentBlock ::Text { text } if text . contains ( " Summary: " )
) ) ;
2026-03-31 19:18:42 +00:00
assert! ( result . formatted_summary . contains ( " Scope: " ) ) ;
assert! ( result . formatted_summary . contains ( " Key timeline: " ) ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
assert! ( should_compact (
& session ,
CompactionConfig {
preserve_recent_messages : 2 ,
max_estimated_tokens : 1 ,
}
) ) ;
assert! (
estimate_session_tokens ( & result . compacted_session ) < estimate_session_tokens ( & session )
) ;
}
#[ test ]
fn truncates_long_blocks_in_summary ( ) {
let summary = super ::summarize_block ( & ContentBlock ::Text {
text : " x " . repeat ( 400 ) ,
} ) ;
assert! ( summary . ends_with ( '…' ) ) ;
assert! ( summary . chars ( ) . count ( ) < = 161 ) ;
}
}