Skip to content

Commit 6a2139a

Browse files
quangdang46claude
andcommitted
feat: initial DCP integration (J1-J10)
Phase A: Foundation - J1: Cargo.toml git dependency for dynamic_context_pruning - J2: mod dcp_bridge registered in lib.rs - J3: DcpPlugin struct wrapping ContextPruner Phase B: Core Integration - J4+J5: Agent field + DCP Phase 1 before CompactionManager Phase 2 Phase C: Tools & Commands - J6: System prompt injection via transform_system() Phase D: Persistence & Config - J10: dcp_enabled config option Build: cargo check --features dcp PASSED (3m57s) Note: Full release build skipped (30+ min due to LLM crates) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9440c61 commit 6a2139a

6 files changed

Lines changed: 242 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ aws-sdk-bedrock = "1.141.0"
227227
aws-sdk-sts = "1.103.0"
228228

229229
# DCP integration (dynamic_context_pruning)
230-
dynamic_context_pruning = { path = "/data/projects/dynamic_context_pruning/crates/dynamic_context_pruning", optional = true }
230+
dynamic_context_pruning = { git = "https://github.com/quangdang46/dynamic_context_pruning", branch = "main", package = "dynamic_context_pruning", optional = true }
231231

232232
[features]
233233
# Include local ONNX/tokenizer embeddings in default builds so memory recall,

src/agent.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ pub struct Agent {
227227
stdin_request_tx: Option<tokio::sync::mpsc::UnboundedSender<crate::tool::StdinInputRequest>>,
228228
/// Canonical reducer-backed view of runtime provider/model selection.
229229
provider_runtime_state: ProviderRuntimeState,
230+
/// DCP plugin for context pruning (behind feature flag).
231+
#[cfg(feature = "dcp")]
232+
dcp: Option<crate::dcp_plugin::DcpPlugin>,
230233
}
231234

232235
impl Agent {
@@ -277,6 +280,8 @@ impl Agent {
277280
rewind_undo_snapshot: None,
278281
stdin_request_tx: None,
279282
provider_runtime_state: ProviderRuntimeState::observed(initial_provider_model),
283+
#[cfg(feature = "dcp")]
284+
dcp: crate::dcp_plugin::DcpPlugin::new().ok(),
280285
};
281286
crate::tool::set_session_tool_policy(
282287
&agent.session.id,
@@ -579,17 +584,49 @@ impl Agent {
579584
}
580585

581586
fn messages_for_provider(&mut self) -> (Vec<Message>, Option<CompactionEvent>) {
587+
// ── Phase 1: DCP Plugin Layer ──────────────────────────────────
588+
#[cfg(feature = "dcp")]
589+
let messages = {
590+
let all_messages = self.session.provider_messages();
591+
if let Some(dcp) = &mut self.dcp {
592+
let output = dcp.transform(&all_messages).unwrap_or_else(|e| {
593+
logging::warn(&format!("DCP transform failed: {e}"));
594+
crate::dcp_plugin::DcpTransformOutput {
595+
messages: all_messages.to_vec(),
596+
tokens_saved: 0,
597+
removed_count: 0,
598+
changed: false,
599+
}
600+
});
601+
602+
if output.changed {
603+
logging::info(&format!(
604+
"DCP: pruned {} messages, saved ~{} tokens",
605+
output.removed_count,
606+
output.tokens_saved,
607+
));
608+
}
609+
610+
output.messages
611+
} else {
612+
all_messages.to_vec()
613+
}
614+
};
615+
616+
#[cfg(not(feature = "dcp"))]
617+
let messages = self.session.provider_messages().to_vec();
618+
619+
// ── Phase 2: CompactionManager (existing) ──────────────────────
582620
if self.provider.supports_compaction() || self.session.compaction.is_some() {
583621
let compaction = self.registry.compaction();
584622
match compaction.try_write() {
585623
Ok(mut manager) => {
586624
let discarded_oversized_native =
587625
manager.discard_oversized_openai_native_compaction();
588626
let messages = {
589-
let all_messages = self.session.provider_messages();
590627
if self.provider.uses_jcode_compaction() {
591628
let action =
592-
manager.ensure_context_fits(all_messages, self.provider.clone());
629+
manager.ensure_context_fits(&messages, self.provider.clone());
593630
match action {
594631
crate::compaction::CompactionAction::BackgroundStarted {
595632
trigger,
@@ -608,7 +645,7 @@ impl Agent {
608645
crate::compaction::CompactionAction::None => {}
609646
}
610647
}
611-
manager.messages_for_api_with(all_messages)
648+
manager.messages_for_api_with(&messages)
612649
};
613650
let event = manager.take_compaction_event();
614651
if event.is_some() || discarded_oversized_native {
@@ -637,8 +674,6 @@ impl Agent {
637674
};
638675
}
639676

640-
let all_messages = self.session.provider_messages();
641-
let messages = all_messages.to_vec();
642677
let user_count = messages
643678
.iter()
644679
.filter(|message| matches!(message.role, Role::User))

src/dcp_bridge.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
//! (see PLAN.md §9.1).
99
1010
use crate::message::{ContentBlock, Message as JMsg, Role as JRole};
11-
use dcp_types::{Message as DcpMessage, Part, Role as DcpRole, ToolStatus};
11+
use dynamic_context_pruning::{Message as DcpMessage, Part, Role as DcpRole, ToolStatus};
1212

1313
/// Convert jcode messages to DCP canonical IR.
14-
pub fn jcode_to_dcp(msgs: &[JMsg]) -> Vec<DcpMessage> {
14+
pub fn jcode_to_dcp(msgs:&[JMsg]) -> Vec<DcpMessage> {
1515
msgs.iter().map(jmsg_to_dcp).collect()
1616
}
1717

@@ -28,9 +28,8 @@ fn jmsg_to_dcp(m: &JMsg) -> DcpMessage {
2828
.unwrap_or(0);
2929

3030
// Generate a stable ID from the message content
31-
let id = crate::message::stable_message_hash(m)
32-
.map(|h| format!("{:x}", h))
33-
.unwrap_or_else(|| format!("msg_{}", time));
31+
let hash = crate::message::stable_message_hash(m);
32+
let id = format!("{:x}", hash);
3433

3534
let parts: Vec<Part> = m
3635
.content
@@ -43,6 +42,7 @@ fn jmsg_to_dcp(m: &JMsg) -> DcpMessage {
4342
role,
4443
parts,
4544
time,
45+
ignored: false,
4646
}
4747
}
4848

@@ -90,6 +90,7 @@ fn dcp_msg_to_jcode(m: DcpMessage) -> JMsg {
9090
DcpRole::User => JRole::User,
9191
DcpRole::Assistant => JRole::Assistant,
9292
DcpRole::System => JRole::User, // shouldn't happen in practice
93+
_ => JRole::User, // exhaustive fallback for non-exhaustive enum
9394
};
9495

9596
let timestamp = if m.time != 0 {
@@ -136,6 +137,7 @@ fn part_to_content(p: Part) -> Option<ContentBlock> {
136137
is_error: matches!(status, ToolStatus::Error).then_some(true),
137138
},
138139
Part::Image { media_type, data } => ContentBlock::Image { media_type, data },
140+
_ => return None, // exhaustive fallback for non-exhaustive enum
139141
})
140142
}
141143

@@ -167,7 +169,7 @@ mod tests {
167169

168170
#[test]
169171
fn test_tool_call_roundtrip() {
170-
use dcp_types::Part;
172+
use dynamic_context_pruning::Part;
171173

172174
let dcp_msg = DcpMessage {
173175
id: "test123".to_string(),
@@ -181,10 +183,11 @@ mod tests {
181183
},
182184
],
183185
time: 1234567890,
186+
ignored: false,
184187
};
185188

186189
let jmsg = dcp_msg_to_jcode(dcp_msg);
187190
assert_eq!(jmsg.role, JRole::Assistant);
188191
assert!(jmsg.content.iter().any(|c| matches!(c, ContentBlock::ToolUse { .. })));
189192
}
190-
}
193+
}

0 commit comments

Comments
 (0)