From cef196848af9a6acd86be2cbcbc136f5870c829c Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 14:01:13 +0530 Subject: [PATCH 01/23] feat(compact): add lossless compaction transformer with token threshold --- crates/forge_app/src/app.rs | 6 +- crates/forge_app/src/compact_transform.rs | 123 ++++++++++++++++++ crates/forge_app/src/hooks/compaction.rs | 1 + crates/forge_app/src/lib.rs | 1 + crates/forge_app/src/orch.rs | 11 +- ...ansform__tests__multi_round_evolution.snap | 59 +++++++++ ...t_transform__tests__single_compaction.snap | 22 ++++ 7 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 crates/forge_app/src/compact_transform.rs create mode 100644 crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap create mode 100644 crates/forge_app/src/snapshots/forge_app__compact_transform__tests__single_compaction.snap diff --git a/crates/forge_app/src/app.rs b/crates/forge_app/src/app.rs index 5308d67dd2..0957c16581 100644 --- a/crates/forge_app/src/app.rs +++ b/crates/forge_app/src/app.rs @@ -10,7 +10,7 @@ use crate::apply_tunable_parameters::ApplyTunableParameters; use crate::authenticator::Authenticator; use crate::changed_files::ChangedFiles; use crate::dto::ToolsOverview; -use crate::hooks::{CompactionHandler, TitleGenerationHandler, TracingHandler}; +use crate::hooks::{TitleGenerationHandler, TracingHandler}; use crate::init_conversation_metrics::InitConversationMetrics; use crate::orch::Orchestrator; use crate::services::{ @@ -148,9 +148,7 @@ impl ForgeApp { .on_start(tracing_handler.clone().and(title_handler.clone())) .on_request(tracing_handler.clone()) .on_response( - tracing_handler - .clone() - .and(CompactionHandler::new(agent.clone(), environment.clone())), + tracing_handler.clone(), // .and(CompactionHandler::new(agent.clone(), environment.clone())), ) .on_toolcall_start(tracing_handler.clone()) .on_toolcall_end(tracing_handler.clone()) diff --git a/crates/forge_app/src/compact_transform.rs b/crates/forge_app/src/compact_transform.rs new file mode 100644 index 0000000000..c2f13cc83b --- /dev/null +++ b/crates/forge_app/src/compact_transform.rs @@ -0,0 +1,123 @@ +use forge_domain::{Agent, Context, Environment, Transformer}; +use tracing::info; + +use crate::compact::Compactor; + +pub struct Compaction { + agent: Agent, + environment: Environment, +} + +impl Compaction { + /// Creates a new compaction handler + /// + /// # Arguments + /// * `agent` - The agent configuration containing compaction settings + /// * `environment` - The environment configuration + pub fn new(agent: Agent, environment: Environment) -> Self { + Self { agent, environment } + } +} + +impl Transformer for Compaction { + type Value = Context; + fn transform(&mut self, context: Self::Value) -> Self::Value { + info!(agent_id = %self.agent.id, "Compaction triggered"); + let msg_len = context.messages.len(); + let mut running_context = context.clone().messages(Vec::default()); + let compactor = Compactor::new(self.agent.compact.clone(), self.environment.clone()); + for idx in 0..=msg_len { + if let Some(entry) = context.messages.get(idx).cloned() { + running_context.messages.push(entry); + running_context = compactor.compact(running_context, false).unwrap(); + } + } + running_context + } +} + +#[cfg(test)] +mod tests { + use forge_domain::{Compact, Context, ContextMessage, MessagePattern, Role}; + + use super::*; + + fn compactor(retention: usize) -> Compactor { + use fake::{Fake, Faker}; + let env: Environment = Faker.fake(); + Compactor::new( + Compact::new().retention_window(retention), + env.cwd(std::path::PathBuf::from("/test/working/dir")), + ) + } + + /// One-line label per message: `[System] Message 1` or `[Compacted]`. + fn describe(ctx: &Context) -> Vec { + ctx.messages + .iter() + .map(|e| { + let text = e.content().unwrap_or(""); + if text.contains("summary frames") { + return "[Compacted]".to_string(); + } + let role = match () { + _ if e.has_role(Role::System) => "System", + _ if e.has_role(Role::Assistant) => "Assistant", + _ if e.has_tool_result() => "ToolResult", + _ => "User", + }; + format!("[{role}] {}", &text[..text.len().min(30)]) + }) + .collect() + } + + /// Compacts different-sized conversations in a single shot. + #[test] + fn test_single_compaction() { + let c = compactor(2); + let cases: Vec<(&str, Vec)> = [ + ("uaua", "4 messages"), + ("uauauaua", "8 messages"), + ("uauauauauaua", "12 messages"), + ("suauauauauau", "with system"), + ] + .into_iter() + .map(|(pat, label)| { + let ctx = MessagePattern::new(pat).build(); + let compacted = c.compact(ctx, false).unwrap(); + (label, describe(&compacted)) + }) + .collect(); + + insta::assert_yaml_snapshot!(cases); + } + + /// Multi-round conversation: start with a seed, compact, then add + /// a user+assistant pair each round and compact again. + #[test] + fn test_multi_round_evolution() { + let c = compactor(2); + let seed = MessagePattern::new("suauauau").build(); + + let mut rounds: Vec<(String, Vec)> = Vec::new(); + rounds.push(("before compaction".into(), describe(&seed))); + + let mut current = c.compact(seed, false).unwrap(); + rounds.push(("request 1".into(), describe(¤t))); + + for r in 2..=5 { + current = current + .add_message(ContextMessage::user(format!("Question {r}"), None)) + .add_message(ContextMessage::assistant( + format!("Answer {r}"), + None, + None, + None, + )); + current = c.compact(current, false).unwrap(); + rounds.push((format!("request {r}"), describe(¤t))); + } + + insta::assert_yaml_snapshot!(rounds); + } +} diff --git a/crates/forge_app/src/hooks/compaction.rs b/crates/forge_app/src/hooks/compaction.rs index 76e58df83d..1c25dc77fa 100644 --- a/crates/forge_app/src/hooks/compaction.rs +++ b/crates/forge_app/src/hooks/compaction.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use async_trait::async_trait; use forge_domain::{Agent, Conversation, Environment, EventData, EventHandle, ResponsePayload}; use tracing::{debug, info}; diff --git a/crates/forge_app/src/lib.rs b/crates/forge_app/src/lib.rs index 2c0883a87d..1b4a309e4f 100644 --- a/crates/forge_app/src/lib.rs +++ b/crates/forge_app/src/lib.rs @@ -38,6 +38,7 @@ pub mod user_prompt; pub mod utils; mod walker; mod workspace_status; +mod compact_transform; pub use agent::*; pub use agent_provider_resolver::*; diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 06b55de441..500a107081 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -10,6 +10,7 @@ use tracing::warn; use crate::TemplateEngine; use crate::agent::AgentService; +use crate::compact_transform::Compaction; #[derive(Clone, Setters)] #[setters(into)] @@ -152,12 +153,18 @@ impl Orchestrator { .pipe(TransformToolCalls::new().when(|_| !tool_supported)) .pipe(ImageHandling::new()) .pipe(DropReasoningDetails.when(|_| !reasoning_supported)) - .pipe(ReasoningNormalizer.when(|_| reasoning_supported)); + .pipe(ReasoningNormalizer.when(|_| reasoning_supported)) + .pipe( + Compaction::new(self.agent.clone(), self.environment.clone()).when(|_ctx| { + let token_count = _ctx.token_count(); + self.agent.compact.should_compact(&_ctx, *token_count) + }), + ); let response = self .services .chat_agent( model_id, - transformers.transform(context), + transformers.transform(context.clone()), Some(self.agent.provider.clone()), ) .await?; diff --git a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap new file mode 100644 index 0000000000..47928b1c5e --- /dev/null +++ b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap @@ -0,0 +1,59 @@ +--- +source: crates/forge_app/src/compact_transform.rs +expression: rounds +--- +- - before compaction + - - "[System] Message 1" + - "[User] Message 2" + - "[Assistant] Message 3" + - "[User] Message 4" + - "[Assistant] Message 5" + - "[User] Message 6" + - "[Assistant] Message 7" + - "[User] Message 8" +- - request 1 + - - "[System] Message 1" + - "[User] Message 2" + - "[Compacted]" + - "[Assistant] Message 7" + - "[User] Message 8" +- - request 2 + - - "[System] Message 1" + - "[User] Message 2" + - "[Compacted]" + - "[Compacted]" + - "[User] Question 2" + - "[Assistant] Answer 2" +- - request 3 + - - "[System] Message 1" + - "[User] Message 2" + - "[Compacted]" + - "[Compacted]" + - "[User] Question 2" + - "[Compacted]" + - "[User] Question 3" + - "[Assistant] Answer 3" +- - request 4 + - - "[System] Message 1" + - "[User] Message 2" + - "[Compacted]" + - "[Compacted]" + - "[User] Question 2" + - "[Compacted]" + - "[User] Question 3" + - "[Compacted]" + - "[User] Question 4" + - "[Assistant] Answer 4" +- - request 5 + - - "[System] Message 1" + - "[User] Message 2" + - "[Compacted]" + - "[Compacted]" + - "[User] Question 2" + - "[Compacted]" + - "[User] Question 3" + - "[Compacted]" + - "[User] Question 4" + - "[Compacted]" + - "[User] Question 5" + - "[Assistant] Answer 5" diff --git a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__single_compaction.snap b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__single_compaction.snap new file mode 100644 index 0000000000..08f85d5905 --- /dev/null +++ b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__single_compaction.snap @@ -0,0 +1,22 @@ +--- +source: crates/forge_app/src/compact_transform.rs +expression: cases +--- +- - 4 messages + - - "[User] Message 1" + - "[Compacted]" +- - 8 messages + - - "[User] Message 1" + - "[Compacted]" + - "[Assistant] Message 8" +- - 12 messages + - - "[User] Message 1" + - "[Compacted]" + - "[User] Message 11" + - "[Assistant] Message 12" +- - with system + - - "[System] Message 1" + - "[User] Message 2" + - "[Compacted]" + - "[Assistant] Message 11" + - "[User] Message 12" From aa01548fa5067811c2f74a1df04c290d5bad34d4 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 14:02:26 +0530 Subject: [PATCH 02/23] fix(orchestrator): remove unnecessary clone in chat agent transformer call --- crates/forge_app/src/orch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 500a107081..19963a9739 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -164,7 +164,7 @@ impl Orchestrator { .services .chat_agent( model_id, - transformers.transform(context.clone()), + transformers.transform(context), Some(self.agent.provider.clone()), ) .await?; From 0b5b1d2a8bfda1a53f834f0ac02caf2b863bbb59 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 14:03:35 +0530 Subject: [PATCH 03/23] refactor(hooks): remove compaction handler and related module --- crates/forge_app/src/hooks/compaction.rs | 52 ------------------------ crates/forge_app/src/hooks/mod.rs | 2 - 2 files changed, 54 deletions(-) delete mode 100644 crates/forge_app/src/hooks/compaction.rs diff --git a/crates/forge_app/src/hooks/compaction.rs b/crates/forge_app/src/hooks/compaction.rs deleted file mode 100644 index 1c25dc77fa..0000000000 --- a/crates/forge_app/src/hooks/compaction.rs +++ /dev/null @@ -1,52 +0,0 @@ -#![allow(dead_code)] -use async_trait::async_trait; -use forge_domain::{Agent, Conversation, Environment, EventData, EventHandle, ResponsePayload}; -use tracing::{debug, info}; - -use crate::compact::Compactor; - -/// Hook handler that performs context compaction when needed -/// -/// This handler checks if the conversation context has grown too large -/// and compacts it according to the agent's compaction configuration. -/// The handler mutates the conversation's context in-place if compaction -/// is triggered. -#[derive(Clone)] -pub struct CompactionHandler { - agent: Agent, - environment: Environment, -} - -impl CompactionHandler { - /// Creates a new compaction handler - /// - /// # Arguments - /// * `agent` - The agent configuration containing compaction settings - /// * `environment` - The environment configuration - pub fn new(agent: Agent, environment: Environment) -> Self { - Self { agent, environment } - } -} - -#[async_trait] -impl EventHandle> for CompactionHandler { - async fn handle( - &self, - _event: &EventData, - conversation: &mut Conversation, - ) -> anyhow::Result<()> { - if let Some(context) = &conversation.context { - let token_count = context.token_count(); - if self.agent.compact.should_compact(context, *token_count) { - info!(agent_id = %self.agent.id, "Compaction triggered by hook"); - let compacted = - Compactor::new(self.agent.compact.clone(), self.environment.clone()) - .compact(context.clone(), false)?; - conversation.context = Some(compacted); - } else { - debug!(agent_id = %self.agent.id, "Compaction not needed"); - } - } - Ok(()) - } -} diff --git a/crates/forge_app/src/hooks/mod.rs b/crates/forge_app/src/hooks/mod.rs index f64a62f689..1ddb2807cf 100644 --- a/crates/forge_app/src/hooks/mod.rs +++ b/crates/forge_app/src/hooks/mod.rs @@ -1,7 +1,5 @@ -mod compaction; mod title_generation; mod tracing; -pub use compaction::CompactionHandler; pub use title_generation::TitleGenerationHandler; pub use tracing::TracingHandler; From 4f392f65219f2723e2eb5e569b3765f0ffab2b45 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 14:04:15 +0530 Subject: [PATCH 04/23] refactor(app): remove unused compaction handler from response hooks --- crates/forge_app/src/app.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/forge_app/src/app.rs b/crates/forge_app/src/app.rs index 0957c16581..76ecea40c2 100644 --- a/crates/forge_app/src/app.rs +++ b/crates/forge_app/src/app.rs @@ -147,9 +147,7 @@ impl ForgeApp { let hook = Hook::default() .on_start(tracing_handler.clone().and(title_handler.clone())) .on_request(tracing_handler.clone()) - .on_response( - tracing_handler.clone(), // .and(CompactionHandler::new(agent.clone(), environment.clone())), - ) + .on_response(tracing_handler.clone()) .on_toolcall_start(tracing_handler.clone()) .on_toolcall_end(tracing_handler.clone()) .on_end(tracing_handler.and(title_handler)); From c45cb4fb928c339d5cf53cef3bf553cd86b0719d Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 14:05:03 +0530 Subject: [PATCH 05/23] refactor(orchestrator): simplify context parameter in compaction logic --- crates/forge_app/src/orch.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 19963a9739..6f7790a53a 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -155,9 +155,9 @@ impl Orchestrator { .pipe(DropReasoningDetails.when(|_| !reasoning_supported)) .pipe(ReasoningNormalizer.when(|_| reasoning_supported)) .pipe( - Compaction::new(self.agent.clone(), self.environment.clone()).when(|_ctx| { - let token_count = _ctx.token_count(); - self.agent.compact.should_compact(&_ctx, *token_count) + Compaction::new(self.agent.clone(), self.environment.clone()).when(|ctx| { + let token_count = ctx.token_count(); + self.agent.compact.should_compact(&ctx, *token_count) }), ); let response = self From 1027a8e5d2e4d71e48287c5a68f60a41269ab1ad Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 14:05:21 +0530 Subject: [PATCH 06/23] refactor(orchestrator): streamline compaction condition in execute_chat_turn --- crates/forge_app/src/orch.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 6f7790a53a..15fed5461d 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -155,10 +155,8 @@ impl Orchestrator { .pipe(DropReasoningDetails.when(|_| !reasoning_supported)) .pipe(ReasoningNormalizer.when(|_| reasoning_supported)) .pipe( - Compaction::new(self.agent.clone(), self.environment.clone()).when(|ctx| { - let token_count = ctx.token_count(); - self.agent.compact.should_compact(&ctx, *token_count) - }), + Compaction::new(self.agent.clone(), self.environment.clone()) + .when(|ctx| self.agent.compact.should_compact(&ctx, *ctx.token_count())), ); let response = self .services From cf8635399f66527b0d44762ad981bc84297dd1fa Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 08:38:23 +0000 Subject: [PATCH 07/23] [autofix.ci] apply automated fixes --- crates/forge_app/src/lib.rs | 2 +- crates/forge_app/src/orch.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/forge_app/src/lib.rs b/crates/forge_app/src/lib.rs index 1b4a309e4f..3c6ff49d48 100644 --- a/crates/forge_app/src/lib.rs +++ b/crates/forge_app/src/lib.rs @@ -7,6 +7,7 @@ mod authenticator; mod changed_files; mod command_generator; mod compact; +mod compact_transform; mod data_gen; pub mod dto; mod error; @@ -38,7 +39,6 @@ pub mod user_prompt; pub mod utils; mod walker; mod workspace_status; -mod compact_transform; pub use agent::*; pub use agent_provider_resolver::*; diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 15fed5461d..97565e10c8 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -156,7 +156,7 @@ impl Orchestrator { .pipe(ReasoningNormalizer.when(|_| reasoning_supported)) .pipe( Compaction::new(self.agent.clone(), self.environment.clone()) - .when(|ctx| self.agent.compact.should_compact(&ctx, *ctx.token_count())), + .when(|ctx| self.agent.compact.should_compact(ctx, *ctx.token_count())), ); let response = self .services From 437be6ef04aba94e397066e17f80d21518c7b7e2 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:26:18 +0530 Subject: [PATCH 08/23] refactor(compaction): enhance compaction logic and update test snapshots for multi-round evolution --- crates/forge_app/src/compact_transform.rs | 76 ++++++++----------- ...ansform__tests__multi_round_evolution.snap | 75 +++++++++--------- 2 files changed, 65 insertions(+), 86 deletions(-) diff --git a/crates/forge_app/src/compact_transform.rs b/crates/forge_app/src/compact_transform.rs index c2f13cc83b..9c4123f907 100644 --- a/crates/forge_app/src/compact_transform.rs +++ b/crates/forge_app/src/compact_transform.rs @@ -29,7 +29,13 @@ impl Transformer for Compaction { for idx in 0..=msg_len { if let Some(entry) = context.messages.get(idx).cloned() { running_context.messages.push(entry); - running_context = compactor.compact(running_context, false).unwrap(); + if self + .agent + .compact + .should_compact(&running_context, *running_context.token_count()) + { + running_context = compactor.compact(running_context, false).unwrap(); + } } } running_context @@ -38,17 +44,23 @@ impl Transformer for Compaction { #[cfg(test)] mod tests { - use forge_domain::{Compact, Context, ContextMessage, MessagePattern, Role}; + use forge_domain::{AgentId, Compact, Context, MessagePattern, ModelId, ProviderId, Role}; use super::*; - fn compactor(retention: usize) -> Compactor { + fn compaction(retention: usize, message_thresh: usize) -> impl Transformer { use fake::{Fake, Faker}; let env: Environment = Faker.fake(); - Compactor::new( - Compact::new().retention_window(retention), - env.cwd(std::path::PathBuf::from("/test/working/dir")), + let env = env.cwd(std::path::PathBuf::from("/test/working/dir")); + let compact = Compact::new().message_threshold(message_thresh).retention_window(retention); + let agent = Agent::new( + AgentId::new("test"), + ProviderId::ANTHROPIC, + ModelId::new("test-model"), ) + .compact(compact.clone()); + Compaction::new(agent, env) + .when(move |ctx: &Context| compact.should_compact(ctx, *ctx.token_count())) } /// One-line label per message: `[System] Message 1` or `[Compacted]`. @@ -71,51 +83,25 @@ mod tests { .collect() } - /// Compacts different-sized conversations in a single shot. - #[test] - fn test_single_compaction() { - let c = compactor(2); - let cases: Vec<(&str, Vec)> = [ - ("uaua", "4 messages"), - ("uauauaua", "8 messages"), - ("uauauauauaua", "12 messages"), - ("suauauauauau", "with system"), - ] - .into_iter() - .map(|(pat, label)| { - let ctx = MessagePattern::new(pat).build(); - let compacted = c.compact(ctx, false).unwrap(); - (label, describe(&compacted)) - }) - .collect(); - - insta::assert_yaml_snapshot!(cases); - } - - /// Multi-round conversation: start with a seed, compact, then add - /// a user+assistant pair each round and compact again. + /// Multi-round conversation: each round builds the full lossless context + /// from the pattern and passes it through the Compaction transformer. + /// The transformer is pure -- same input always produces same output. #[test] fn test_multi_round_evolution() { - let c = compactor(2); - let seed = MessagePattern::new("suauauau").build(); + let mut c = compaction(2, 10); + let mut base = String::from("sauau"); + let seed = MessagePattern::new(base.clone()).build(); + // Request 1 let mut rounds: Vec<(String, Vec)> = Vec::new(); - rounds.push(("before compaction".into(), describe(&seed))); - - let mut current = c.compact(seed, false).unwrap(); + let current = c.transform(seed); rounds.push(("request 1".into(), describe(¤t))); - for r in 2..=5 { - current = current - .add_message(ContextMessage::user(format!("Question {r}"), None)) - .add_message(ContextMessage::assistant( - format!("Answer {r}"), - None, - None, - None, - )); - current = c.compact(current, false).unwrap(); - rounds.push((format!("request {r}"), describe(¤t))); + for i in 1..=5 { + base.push_str("au"); + let seed = MessagePattern::new(base.clone()).build(); + let current = c.transform(seed); + rounds.push((format!("request {}", i), describe(¤t))); } insta::assert_yaml_snapshot!(rounds); diff --git a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap index 47928b1c5e..5d1101f9f2 100644 --- a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap +++ b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap @@ -2,58 +2,51 @@ source: crates/forge_app/src/compact_transform.rs expression: rounds --- -- - before compaction +- - request 1 - - "[System] Message 1" - - "[User] Message 2" - - "[Assistant] Message 3" - - "[User] Message 4" - - "[Assistant] Message 5" - - "[User] Message 6" - - "[Assistant] Message 7" - - "[User] Message 8" + - "[Assistant] Message 2" + - "[User] Message 3" + - "[Assistant] Message 4" + - "[User] Message 5" - - request 1 - - "[System] Message 1" - - "[User] Message 2" - - "[Compacted]" - - "[Assistant] Message 7" - - "[User] Message 8" + - "[Assistant] Message 2" + - "[User] Message 3" + - "[Assistant] Message 4" + - "[User] Message 5" + - "[Assistant] Message 6" + - "[User] Message 7" - - request 2 - - "[System] Message 1" - - "[User] Message 2" - - "[Compacted]" - - "[Compacted]" - - "[User] Question 2" - - "[Assistant] Answer 2" + - "[Assistant] Message 2" + - "[User] Message 3" + - "[Assistant] Message 4" + - "[User] Message 5" + - "[Assistant] Message 6" + - "[User] Message 7" + - "[Assistant] Message 8" + - "[User] Message 9" - - request 3 - - "[System] Message 1" - - "[User] Message 2" - "[Compacted]" - - "[Compacted]" - - "[User] Question 2" - - "[Compacted]" - - "[User] Question 3" - - "[Assistant] Answer 3" + - "[User] Message 9" + - "[Assistant] Message 10" + - "[User] Message 11" - - request 4 - - "[System] Message 1" - - "[User] Message 2" - - "[Compacted]" - "[Compacted]" - - "[User] Question 2" - - "[Compacted]" - - "[User] Question 3" - - "[Compacted]" - - "[User] Question 4" - - "[Assistant] Answer 4" + - "[User] Message 9" + - "[Assistant] Message 10" + - "[User] Message 11" + - "[Assistant] Message 12" + - "[User] Message 13" - - request 5 - - "[System] Message 1" - - "[User] Message 2" - - "[Compacted]" - - "[Compacted]" - - "[User] Question 2" - - "[Compacted]" - - "[User] Question 3" - - "[Compacted]" - - "[User] Question 4" - "[Compacted]" - - "[User] Question 5" - - "[Assistant] Answer 5" + - "[User] Message 9" + - "[Assistant] Message 10" + - "[User] Message 11" + - "[Assistant] Message 12" + - "[User] Message 13" + - "[Assistant] Message 14" + - "[User] Message 15" From 97475d2930f36b69f3ade2d53ce99f582ab9950c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:58:02 +0000 Subject: [PATCH 09/23] [autofix.ci] apply automated fixes --- crates/forge_app/src/compact_transform.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/forge_app/src/compact_transform.rs b/crates/forge_app/src/compact_transform.rs index 9c4123f907..0d27caa147 100644 --- a/crates/forge_app/src/compact_transform.rs +++ b/crates/forge_app/src/compact_transform.rs @@ -52,7 +52,9 @@ mod tests { use fake::{Fake, Faker}; let env: Environment = Faker.fake(); let env = env.cwd(std::path::PathBuf::from("/test/working/dir")); - let compact = Compact::new().message_threshold(message_thresh).retention_window(retention); + let compact = Compact::new() + .message_threshold(message_thresh) + .retention_window(retention); let agent = Agent::new( AgentId::new("test"), ProviderId::ANTHROPIC, From 90f008f91ecc23cb116f6c62f3787c8462a1d8f3 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:28:46 +0530 Subject: [PATCH 10/23] test(compaction): extend multi-round evolution tests to cover additional rounds --- crates/forge_app/src/compact_transform.rs | 2 +- ...p__compact_transform__tests__multi_round_evolution.snap | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/forge_app/src/compact_transform.rs b/crates/forge_app/src/compact_transform.rs index 0d27caa147..4bb2683060 100644 --- a/crates/forge_app/src/compact_transform.rs +++ b/crates/forge_app/src/compact_transform.rs @@ -99,7 +99,7 @@ mod tests { let current = c.transform(seed); rounds.push(("request 1".into(), describe(¤t))); - for i in 1..=5 { + for i in 1..=6 { base.push_str("au"); let seed = MessagePattern::new(base.clone()).build(); let current = c.transform(seed); diff --git a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap index 5d1101f9f2..f01d4a9b28 100644 --- a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap +++ b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap @@ -50,3 +50,10 @@ expression: rounds - "[User] Message 13" - "[Assistant] Message 14" - "[User] Message 15" +- - request 6 + - - "[System] Message 1" + - "[Compacted]" + - "[User] Message 9" + - "[Compacted]" + - "[Assistant] Message 16" + - "[User] Message 17" From a77f118839abcbe4979f6a8c0062d542f0cfffed Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:35:15 +0530 Subject: [PATCH 11/23] delete(tests): remove obsolete single compaction snapshot test file --- ...t_transform__tests__single_compaction.snap | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 crates/forge_app/src/snapshots/forge_app__compact_transform__tests__single_compaction.snap diff --git a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__single_compaction.snap b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__single_compaction.snap deleted file mode 100644 index 08f85d5905..0000000000 --- a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__single_compaction.snap +++ /dev/null @@ -1,22 +0,0 @@ ---- -source: crates/forge_app/src/compact_transform.rs -expression: cases ---- -- - 4 messages - - - "[User] Message 1" - - "[Compacted]" -- - 8 messages - - - "[User] Message 1" - - "[Compacted]" - - "[Assistant] Message 8" -- - 12 messages - - - "[User] Message 1" - - "[Compacted]" - - "[User] Message 11" - - "[Assistant] Message 12" -- - with system - - - "[System] Message 1" - - "[User] Message 2" - - "[Compacted]" - - "[Assistant] Message 11" - - "[User] Message 12" From 8f093a39b42b63092e73f17dabfa349b68be70e1 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:36:29 +0530 Subject: [PATCH 12/23] test(compaction): update multi-round evolution test to adjust base string and iteration count --- crates/forge_app/src/compact_transform.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/forge_app/src/compact_transform.rs b/crates/forge_app/src/compact_transform.rs index 4bb2683060..a203f69b29 100644 --- a/crates/forge_app/src/compact_transform.rs +++ b/crates/forge_app/src/compact_transform.rs @@ -91,15 +91,9 @@ mod tests { #[test] fn test_multi_round_evolution() { let mut c = compaction(2, 10); - let mut base = String::from("sauau"); - let seed = MessagePattern::new(base.clone()).build(); - - // Request 1 + let mut base = String::from("s"); let mut rounds: Vec<(String, Vec)> = Vec::new(); - let current = c.transform(seed); - rounds.push(("request 1".into(), describe(¤t))); - - for i in 1..=6 { + for i in 1..=7 { base.push_str("au"); let seed = MessagePattern::new(base.clone()).build(); let current = c.transform(seed); From aec9e7df9e8551a05050a94e27b1938f32fd0541 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:36:55 +0530 Subject: [PATCH 13/23] refactor(compaction): simplify compaction initialization in orchestrator --- crates/forge_app/src/compact_transform.rs | 1 - crates/forge_app/src/orch.rs | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/forge_app/src/compact_transform.rs b/crates/forge_app/src/compact_transform.rs index a203f69b29..15f3783027 100644 --- a/crates/forge_app/src/compact_transform.rs +++ b/crates/forge_app/src/compact_transform.rs @@ -62,7 +62,6 @@ mod tests { ) .compact(compact.clone()); Compaction::new(agent, env) - .when(move |ctx: &Context| compact.should_compact(ctx, *ctx.token_count())) } /// One-line label per message: `[System] Message 1` or `[Compacted]`. diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 97565e10c8..cafe5cabee 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -154,10 +154,10 @@ impl Orchestrator { .pipe(ImageHandling::new()) .pipe(DropReasoningDetails.when(|_| !reasoning_supported)) .pipe(ReasoningNormalizer.when(|_| reasoning_supported)) - .pipe( - Compaction::new(self.agent.clone(), self.environment.clone()) - .when(|ctx| self.agent.compact.should_compact(ctx, *ctx.token_count())), - ); + .pipe(Compaction::new( + self.agent.clone(), + self.environment.clone(), + )); let response = self .services .chat_agent( From 12002c927b6d612f252b5d0ead43dae268cede5d Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:41:12 +0530 Subject: [PATCH 14/23] refactor(compact): move Compaction transformer from separate module --- crates/forge_app/src/compact.rs | 97 +++++++++++++++- crates/forge_app/src/compact_transform.rs | 104 ------------------ crates/forge_app/src/lib.rs | 1 - crates/forge_app/src/orch.rs | 2 +- ...compact__tests__multi_round_evolution.snap | 56 ++++++++++ ...ansform__tests__multi_round_evolution.snap | 21 ++-- 6 files changed, 161 insertions(+), 120 deletions(-) delete mode 100644 crates/forge_app/src/compact_transform.rs create mode 100644 crates/forge_app/src/snapshots/forge_app__compact__tests__multi_round_evolution.snap diff --git a/crates/forge_app/src/compact.rs b/crates/forge_app/src/compact.rs index 397e92aa95..ba66fbdf81 100644 --- a/crates/forge_app/src/compact.rs +++ b/crates/forge_app/src/compact.rs @@ -1,6 +1,5 @@ use forge_domain::{ - Compact, CompactionStrategy, Context, ContextMessage, ContextSummary, Environment, - MessageEntry, Transformer, + Agent, Compact, CompactionStrategy, Context, ContextMessage, ContextSummary, Environment, MessageEntry, Transformer }; use tracing::info; @@ -156,9 +155,49 @@ impl Compactor { } } +pub struct Compaction { + agent: Agent, + environment: Environment, +} + +impl Compaction { + /// Creates a new compaction handler + /// + /// # Arguments + /// * `agent` - The agent configuration containing compaction settings + /// * `environment` - The environment configuration + pub fn new(agent: Agent, environment: Environment) -> Self { + Self { agent, environment } + } +} + +impl Transformer for Compaction { + type Value = Context; + fn transform(&mut self, context: Self::Value) -> Self::Value { + info!(agent_id = %self.agent.id, "Compaction triggered"); + let msg_len = context.messages.len(); + let mut running_context = context.clone().messages(Vec::default()); + let compactor = Compactor::new(self.agent.compact.clone(), self.environment.clone()); + for idx in 0..=msg_len { + if let Some(entry) = context.messages.get(idx).cloned() { + running_context.messages.push(entry); + if self + .agent + .compact + .should_compact(&running_context, *running_context.token_count()) + { + running_context = compactor.compact(running_context, false).unwrap(); + } + } + } + running_context + } +} + #[cfg(test)] mod tests { use std::path::PathBuf; + use forge_domain::{AgentId, Compact, Context, MessagePattern, ModelId, ProviderId, Role}; use forge_domain::MessageEntry; use pretty_assertions::assert_eq; @@ -647,4 +686,58 @@ mod tests { "accumulate_usage() must include usage from both compacted and surviving messages" ); } + + fn compaction(retention: usize, message_thresh: usize) -> impl Transformer { + use fake::{Fake, Faker}; + let env: Environment = Faker.fake(); + let env = env.cwd(std::path::PathBuf::from("/test/working/dir")); + let compact = Compact::new() + .message_threshold(message_thresh) + .retention_window(retention); + let agent = Agent::new( + AgentId::new("test"), + ProviderId::ANTHROPIC, + ModelId::new("test-model"), + ) + .compact(compact.clone()); + Compaction::new(agent, env) + } + + /// One-line label per message: `[System] Message 1` or `[Compacted]`. + fn describe(ctx: &Context) -> Vec { + ctx.messages + .iter() + .map(|e| { + let text = e.content().unwrap_or(""); + if text.contains("summary frames") { + return "[Compacted]".to_string(); + } + let role = match () { + _ if e.has_role(Role::System) => "System", + _ if e.has_role(Role::Assistant) => "Assistant", + _ if e.has_tool_result() => "ToolResult", + _ => "User", + }; + format!("[{role}] {}", &text[..text.len().min(30)]) + }) + .collect() + } + + /// Multi-round conversation: each round builds the full lossless context + /// from the pattern and passes it through the Compaction transformer. + /// The transformer is pure -- same input always produces same output. + #[test] + fn test_multi_round_evolution() { + let mut c = compaction(2, 10); + let mut base = String::from("s"); + let mut rounds: Vec<(String, Vec)> = Vec::new(); + for i in 1..=7 { + base.push_str("au"); + let seed = MessagePattern::new(base.clone()).build(); + let current = c.transform(seed); + rounds.push((format!("request {}", i), describe(¤t))); + } + + insta::assert_yaml_snapshot!(rounds); + } } diff --git a/crates/forge_app/src/compact_transform.rs b/crates/forge_app/src/compact_transform.rs deleted file mode 100644 index 15f3783027..0000000000 --- a/crates/forge_app/src/compact_transform.rs +++ /dev/null @@ -1,104 +0,0 @@ -use forge_domain::{Agent, Context, Environment, Transformer}; -use tracing::info; - -use crate::compact::Compactor; - -pub struct Compaction { - agent: Agent, - environment: Environment, -} - -impl Compaction { - /// Creates a new compaction handler - /// - /// # Arguments - /// * `agent` - The agent configuration containing compaction settings - /// * `environment` - The environment configuration - pub fn new(agent: Agent, environment: Environment) -> Self { - Self { agent, environment } - } -} - -impl Transformer for Compaction { - type Value = Context; - fn transform(&mut self, context: Self::Value) -> Self::Value { - info!(agent_id = %self.agent.id, "Compaction triggered"); - let msg_len = context.messages.len(); - let mut running_context = context.clone().messages(Vec::default()); - let compactor = Compactor::new(self.agent.compact.clone(), self.environment.clone()); - for idx in 0..=msg_len { - if let Some(entry) = context.messages.get(idx).cloned() { - running_context.messages.push(entry); - if self - .agent - .compact - .should_compact(&running_context, *running_context.token_count()) - { - running_context = compactor.compact(running_context, false).unwrap(); - } - } - } - running_context - } -} - -#[cfg(test)] -mod tests { - use forge_domain::{AgentId, Compact, Context, MessagePattern, ModelId, ProviderId, Role}; - - use super::*; - - fn compaction(retention: usize, message_thresh: usize) -> impl Transformer { - use fake::{Fake, Faker}; - let env: Environment = Faker.fake(); - let env = env.cwd(std::path::PathBuf::from("/test/working/dir")); - let compact = Compact::new() - .message_threshold(message_thresh) - .retention_window(retention); - let agent = Agent::new( - AgentId::new("test"), - ProviderId::ANTHROPIC, - ModelId::new("test-model"), - ) - .compact(compact.clone()); - Compaction::new(agent, env) - } - - /// One-line label per message: `[System] Message 1` or `[Compacted]`. - fn describe(ctx: &Context) -> Vec { - ctx.messages - .iter() - .map(|e| { - let text = e.content().unwrap_or(""); - if text.contains("summary frames") { - return "[Compacted]".to_string(); - } - let role = match () { - _ if e.has_role(Role::System) => "System", - _ if e.has_role(Role::Assistant) => "Assistant", - _ if e.has_tool_result() => "ToolResult", - _ => "User", - }; - format!("[{role}] {}", &text[..text.len().min(30)]) - }) - .collect() - } - - /// Multi-round conversation: each round builds the full lossless context - /// from the pattern and passes it through the Compaction transformer. - /// The transformer is pure -- same input always produces same output. - #[test] - fn test_multi_round_evolution() { - let mut c = compaction(2, 10); - let mut base = String::from("s"); - let mut rounds: Vec<(String, Vec)> = Vec::new(); - for i in 1..=7 { - base.push_str("au"); - let seed = MessagePattern::new(base.clone()).build(); - let current = c.transform(seed); - rounds.push((format!("request {}", i), describe(¤t))); - } - - insta::assert_yaml_snapshot!(rounds); - } -} diff --git a/crates/forge_app/src/lib.rs b/crates/forge_app/src/lib.rs index 3c6ff49d48..2c0883a87d 100644 --- a/crates/forge_app/src/lib.rs +++ b/crates/forge_app/src/lib.rs @@ -7,7 +7,6 @@ mod authenticator; mod changed_files; mod command_generator; mod compact; -mod compact_transform; mod data_gen; pub mod dto; mod error; diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index cafe5cabee..5790799b78 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -10,7 +10,7 @@ use tracing::warn; use crate::TemplateEngine; use crate::agent::AgentService; -use crate::compact_transform::Compaction; +use crate::compact::Compaction; #[derive(Clone, Setters)] #[setters(into)] diff --git a/crates/forge_app/src/snapshots/forge_app__compact__tests__multi_round_evolution.snap b/crates/forge_app/src/snapshots/forge_app__compact__tests__multi_round_evolution.snap new file mode 100644 index 0000000000..8aadc03757 --- /dev/null +++ b/crates/forge_app/src/snapshots/forge_app__compact__tests__multi_round_evolution.snap @@ -0,0 +1,56 @@ +--- +source: crates/forge_app/src/compact.rs +expression: rounds +--- +- - request 1 + - - "[System] Message 1" + - "[Assistant] Message 2" + - "[User] Message 3" +- - request 2 + - - "[System] Message 1" + - "[Assistant] Message 2" + - "[User] Message 3" + - "[Assistant] Message 4" + - "[User] Message 5" +- - request 3 + - - "[System] Message 1" + - "[Assistant] Message 2" + - "[User] Message 3" + - "[Assistant] Message 4" + - "[User] Message 5" + - "[Assistant] Message 6" + - "[User] Message 7" +- - request 4 + - - "[System] Message 1" + - "[Assistant] Message 2" + - "[User] Message 3" + - "[Assistant] Message 4" + - "[User] Message 5" + - "[Assistant] Message 6" + - "[User] Message 7" + - "[Assistant] Message 8" + - "[User] Message 9" +- - request 5 + - - "[System] Message 1" + - "[Compacted]" + - "[User] Message 9" + - "[Assistant] Message 10" + - "[User] Message 11" +- - request 6 + - - "[System] Message 1" + - "[Compacted]" + - "[User] Message 9" + - "[Assistant] Message 10" + - "[User] Message 11" + - "[Assistant] Message 12" + - "[User] Message 13" +- - request 7 + - - "[System] Message 1" + - "[Compacted]" + - "[User] Message 9" + - "[Assistant] Message 10" + - "[User] Message 11" + - "[Assistant] Message 12" + - "[User] Message 13" + - "[Assistant] Message 14" + - "[User] Message 15" diff --git a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap index f01d4a9b28..729fcf4eb4 100644 --- a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap +++ b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap @@ -3,12 +3,16 @@ source: crates/forge_app/src/compact_transform.rs expression: rounds --- - - request 1 + - - "[System] Message 1" + - "[Assistant] Message 2" + - "[User] Message 3" +- - request 2 - - "[System] Message 1" - "[Assistant] Message 2" - "[User] Message 3" - "[Assistant] Message 4" - "[User] Message 5" -- - request 1 +- - request 3 - - "[System] Message 1" - "[Assistant] Message 2" - "[User] Message 3" @@ -16,7 +20,7 @@ expression: rounds - "[User] Message 5" - "[Assistant] Message 6" - "[User] Message 7" -- - request 2 +- - request 4 - - "[System] Message 1" - "[Assistant] Message 2" - "[User] Message 3" @@ -26,13 +30,13 @@ expression: rounds - "[User] Message 7" - "[Assistant] Message 8" - "[User] Message 9" -- - request 3 +- - request 5 - - "[System] Message 1" - "[Compacted]" - "[User] Message 9" - "[Assistant] Message 10" - "[User] Message 11" -- - request 4 +- - request 6 - - "[System] Message 1" - "[Compacted]" - "[User] Message 9" @@ -40,7 +44,7 @@ expression: rounds - "[User] Message 11" - "[Assistant] Message 12" - "[User] Message 13" -- - request 5 +- - request 7 - - "[System] Message 1" - "[Compacted]" - "[User] Message 9" @@ -50,10 +54,3 @@ expression: rounds - "[User] Message 13" - "[Assistant] Message 14" - "[User] Message 15" -- - request 6 - - - "[System] Message 1" - - "[Compacted]" - - "[User] Message 9" - - "[Compacted]" - - "[Assistant] Message 16" - - "[User] Message 17" From 426dec17b483ff9363baf2ec7523fa1153d71515 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:13:01 +0000 Subject: [PATCH 15/23] [autofix.ci] apply automated fixes --- crates/forge_app/src/compact.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/forge_app/src/compact.rs b/crates/forge_app/src/compact.rs index ba66fbdf81..6b4d3a9cb5 100644 --- a/crates/forge_app/src/compact.rs +++ b/crates/forge_app/src/compact.rs @@ -1,5 +1,6 @@ use forge_domain::{ - Agent, Compact, CompactionStrategy, Context, ContextMessage, ContextSummary, Environment, MessageEntry, Transformer + Agent, Compact, CompactionStrategy, Context, ContextMessage, ContextSummary, Environment, + MessageEntry, Transformer, }; use tracing::info; @@ -197,9 +198,10 @@ impl Transformer for Compaction { #[cfg(test)] mod tests { use std::path::PathBuf; - use forge_domain::{AgentId, Compact, Context, MessagePattern, ModelId, ProviderId, Role}; - use forge_domain::MessageEntry; + use forge_domain::{ + AgentId, Compact, Context, MessageEntry, MessagePattern, ModelId, ProviderId, Role, + }; use pretty_assertions::assert_eq; use super::*; From 23ca19384d6a23de84c7c097a5db7575689dfd19 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:43:20 +0530 Subject: [PATCH 16/23] delete(tests): remove obsolete multi-round evolution snapshot test file --- ...ansform__tests__multi_round_evolution.snap | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap diff --git a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap b/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap deleted file mode 100644 index 729fcf4eb4..0000000000 --- a/crates/forge_app/src/snapshots/forge_app__compact_transform__tests__multi_round_evolution.snap +++ /dev/null @@ -1,56 +0,0 @@ ---- -source: crates/forge_app/src/compact_transform.rs -expression: rounds ---- -- - request 1 - - - "[System] Message 1" - - "[Assistant] Message 2" - - "[User] Message 3" -- - request 2 - - - "[System] Message 1" - - "[Assistant] Message 2" - - "[User] Message 3" - - "[Assistant] Message 4" - - "[User] Message 5" -- - request 3 - - - "[System] Message 1" - - "[Assistant] Message 2" - - "[User] Message 3" - - "[Assistant] Message 4" - - "[User] Message 5" - - "[Assistant] Message 6" - - "[User] Message 7" -- - request 4 - - - "[System] Message 1" - - "[Assistant] Message 2" - - "[User] Message 3" - - "[Assistant] Message 4" - - "[User] Message 5" - - "[Assistant] Message 6" - - "[User] Message 7" - - "[Assistant] Message 8" - - "[User] Message 9" -- - request 5 - - - "[System] Message 1" - - "[Compacted]" - - "[User] Message 9" - - "[Assistant] Message 10" - - "[User] Message 11" -- - request 6 - - - "[System] Message 1" - - "[Compacted]" - - "[User] Message 9" - - "[Assistant] Message 10" - - "[User] Message 11" - - "[Assistant] Message 12" - - "[User] Message 13" -- - request 7 - - - "[System] Message 1" - - "[Compacted]" - - "[User] Message 9" - - "[Assistant] Message 10" - - "[User] Message 11" - - "[Assistant] Message 12" - - "[User] Message 13" - - "[Assistant] Message 14" - - "[User] Message 15" From b83f62c839ec33bcf981e90edcfded80da92b76f Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:45:39 +0530 Subject: [PATCH 17/23] refactor(compaction): enhance logging during context compaction process --- crates/forge_app/src/compact.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/forge_app/src/compact.rs b/crates/forge_app/src/compact.rs index 6b4d3a9cb5..5177d33256 100644 --- a/crates/forge_app/src/compact.rs +++ b/crates/forge_app/src/compact.rs @@ -175,7 +175,6 @@ impl Compaction { impl Transformer for Compaction { type Value = Context; fn transform(&mut self, context: Self::Value) -> Self::Value { - info!(agent_id = %self.agent.id, "Compaction triggered"); let msg_len = context.messages.len(); let mut running_context = context.clone().messages(Vec::default()); let compactor = Compactor::new(self.agent.compact.clone(), self.environment.clone()); @@ -187,6 +186,12 @@ impl Transformer for Compaction { .compact .should_compact(&running_context, *running_context.token_count()) { + info!( + agent_id = %self.agent.id, + message_count = running_context.messages.len(), + token_count = *running_context.token_count(), + "Compaction threshold reached, compacting context" + ); running_context = compactor.compact(running_context, false).unwrap(); } } From 03eede3b434e5805ca6435e1a494f9a602a36a09 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:52:38 +0530 Subject: [PATCH 18/23] refactor(commands): remove '/compact' command from SlashCommand and UI handling --- crates/forge_main/src/model.rs | 6 ------ crates/forge_main/src/ui.rs | 4 ---- 2 files changed, 10 deletions(-) diff --git a/crates/forge_main/src/model.rs b/crates/forge_main/src/model.rs index b1415a187e..401860ddd1 100644 --- a/crates/forge_main/src/model.rs +++ b/crates/forge_main/src/model.rs @@ -332,7 +332,6 @@ impl ForgeCommandManager { // TODO: Can leverage Clap to parse commands and provide correct error messages match command { - "/compact" => Ok(SlashCommand::Compact), "/new" => Ok(SlashCommand::New), "/info" => Ok(SlashCommand::Info), "/env" => Ok(SlashCommand::Env), @@ -410,10 +409,6 @@ impl ForgeCommandManager { /// - File content #[derive(Debug, Clone, PartialEq, Eq, EnumProperty, EnumIter)] pub enum SlashCommand { - /// Compact the conversation context. This can be triggered with the - /// '/compact' command. - #[strum(props(usage = "Compact the conversation context"))] - Compact, /// Start a new conversation while preserving history. /// This can be triggered with the '/new' command. #[strum(props(usage = "Start a new conversation"))] @@ -523,7 +518,6 @@ pub enum SlashCommand { impl SlashCommand { pub fn name(&self) -> &str { match self { - SlashCommand::Compact => "compact", SlashCommand::New => "new", SlashCommand::Message(_) => "message", SlashCommand::Update => "update", diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 217cb8a82c..addfab116d 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -1810,10 +1810,6 @@ impl A + Send + Sync> UI { SlashCommand::Conversations => { self.list_conversations().await?; } - SlashCommand::Compact => { - self.spinner.start(Some("Compacting"))?; - self.on_compaction().await?; - } SlashCommand::Delete => { self.handle_delete_conversation().await?; } From 4c0c4b30ab7491c716a267500b6d3ed18def5afd Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 15:54:29 +0530 Subject: [PATCH 19/23] refactor(commands): remove 'compact' command from built-in commands --- crates/forge_main/src/built_in_commands.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/forge_main/src/built_in_commands.json b/crates/forge_main/src/built_in_commands.json index 1bf786a2d1..5b88c5dbb2 100644 --- a/crates/forge_main/src/built_in_commands.json +++ b/crates/forge_main/src/built_in_commands.json @@ -31,10 +31,6 @@ "command": "retry", "description": "Retry the last command [alias: r]" }, - { - "command": "compact", - "description": "Compact the conversation context" - }, { "command": "edit", "description": "Use an external editor to write a prompt" From 5ab312a9ef495efa6153f8a7e10d24880001a6a3 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 16:01:22 +0530 Subject: [PATCH 20/23] refactor(compaction): optimize message transformation logic in Compaction transformer --- crates/forge_app/src/compact.rs | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/forge_app/src/compact.rs b/crates/forge_app/src/compact.rs index 5177d33256..78eda9beeb 100644 --- a/crates/forge_app/src/compact.rs +++ b/crates/forge_app/src/compact.rs @@ -175,25 +175,25 @@ impl Compaction { impl Transformer for Compaction { type Value = Context; fn transform(&mut self, context: Self::Value) -> Self::Value { - let msg_len = context.messages.len(); let mut running_context = context.clone().messages(Vec::default()); let compactor = Compactor::new(self.agent.compact.clone(), self.environment.clone()); - for idx in 0..=msg_len { - if let Some(entry) = context.messages.get(idx).cloned() { - running_context.messages.push(entry); - if self - .agent - .compact - .should_compact(&running_context, *running_context.token_count()) - { - info!( - agent_id = %self.agent.id, - message_count = running_context.messages.len(), - token_count = *running_context.token_count(), - "Compaction threshold reached, compacting context" - ); - running_context = compactor.compact(running_context, false).unwrap(); - } + for entry in context.messages { + running_context.messages.push(entry); + let token_count = *running_context.token_count(); + if self + .agent + .compact + .should_compact(&running_context, token_count) + { + info!( + agent_id = %self.agent.id, + message_count = running_context.messages.len(), + token_count, + "Compaction threshold reached, compacting context" + ); + running_context = compactor + .compact(running_context, false) + .expect("Compaction failed during context transformation"); } } running_context From 0f437475a11393f2ad60869a3d72d84da114a240 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 16:06:56 +0530 Subject: [PATCH 21/23] refactor(compaction): streamline context transformation logic in Compaction transformer --- crates/forge_app/src/compact.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/forge_app/src/compact.rs b/crates/forge_app/src/compact.rs index 78eda9beeb..9d23ec0d49 100644 --- a/crates/forge_app/src/compact.rs +++ b/crates/forge_app/src/compact.rs @@ -174,29 +174,25 @@ impl Compaction { impl Transformer for Compaction { type Value = Context; - fn transform(&mut self, context: Self::Value) -> Self::Value { - let mut running_context = context.clone().messages(Vec::default()); + fn transform(&mut self, mut context: Self::Value) -> Self::Value { + let messages = std::mem::take(&mut context.messages); let compactor = Compactor::new(self.agent.compact.clone(), self.environment.clone()); - for entry in context.messages { - running_context.messages.push(entry); - let token_count = *running_context.token_count(); - if self - .agent - .compact - .should_compact(&running_context, token_count) - { + for entry in messages { + context.messages.push(entry); + let token_count = *context.token_count(); + if self.agent.compact.should_compact(&context, token_count) { info!( agent_id = %self.agent.id, - message_count = running_context.messages.len(), + message_count = context.messages.len(), token_count, "Compaction threshold reached, compacting context" ); - running_context = compactor - .compact(running_context, false) + context = compactor + .compact(context, false) .expect("Compaction failed during context transformation"); } } - running_context + context } } From 305944f65251f1b50ea32ab7afeee614e23845e7 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 16:26:19 +0530 Subject: [PATCH 22/23] test(compaction): extend multi-round evolution tests to include additional request scenarios --- crates/forge_app/src/compact.rs | 2 +- .../forge_app__compact__tests__multi_round_evolution.snap | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/forge_app/src/compact.rs b/crates/forge_app/src/compact.rs index 9d23ec0d49..887392b89c 100644 --- a/crates/forge_app/src/compact.rs +++ b/crates/forge_app/src/compact.rs @@ -734,7 +734,7 @@ mod tests { let mut c = compaction(2, 10); let mut base = String::from("s"); let mut rounds: Vec<(String, Vec)> = Vec::new(); - for i in 1..=7 { + for i in 1..=8 { base.push_str("au"); let seed = MessagePattern::new(base.clone()).build(); let current = c.transform(seed); diff --git a/crates/forge_app/src/snapshots/forge_app__compact__tests__multi_round_evolution.snap b/crates/forge_app/src/snapshots/forge_app__compact__tests__multi_round_evolution.snap index 8aadc03757..40a3181da9 100644 --- a/crates/forge_app/src/snapshots/forge_app__compact__tests__multi_round_evolution.snap +++ b/crates/forge_app/src/snapshots/forge_app__compact__tests__multi_round_evolution.snap @@ -54,3 +54,10 @@ expression: rounds - "[User] Message 13" - "[Assistant] Message 14" - "[User] Message 15" +- - request 8 + - - "[System] Message 1" + - "[Compacted]" + - "[User] Message 9" + - "[Compacted]" + - "[Assistant] Message 16" + - "[User] Message 17" From 7a74a86148790001d88418563efb76d623332531 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Tue, 10 Mar 2026 16:33:46 +0530 Subject: [PATCH 23/23] refactor(compaction): handle compaction failures gracefully --- crates/forge_app/src/compact.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/forge_app/src/compact.rs b/crates/forge_app/src/compact.rs index 887392b89c..136a08fa39 100644 --- a/crates/forge_app/src/compact.rs +++ b/crates/forge_app/src/compact.rs @@ -2,7 +2,7 @@ use forge_domain::{ Agent, Compact, CompactionStrategy, Context, ContextMessage, ContextSummary, Environment, MessageEntry, Transformer, }; -use tracing::info; +use tracing::{info, warn}; use crate::TemplateEngine; use crate::transformers::SummaryTransformer; @@ -187,9 +187,16 @@ impl Transformer for Compaction { token_count, "Compaction threshold reached, compacting context" ); - context = compactor - .compact(context, false) - .expect("Compaction failed during context transformation"); + match compactor.compact(context.clone(), false) { + Ok(compacted) => context = compacted, + Err(err) => { + warn!( + agent_id = %self.agent.id, + error = %err, + "Compaction failed, continuing with original context" + ); + } + } } } context