klieo_runlog/
compaction.rs1use crate::error::RunLogError;
7use crate::types::{RunLog, Step, StepKind};
8use async_trait::async_trait;
9use std::time::Duration;
10
11#[async_trait]
13pub trait CompactionPolicy: Send + Sync {
14 async fn compact(&self, run_log: &mut RunLog) -> Result<(), RunLogError>;
16}
17
18pub struct DeterministicCompaction {
27 pub max_steps: u32,
29 pub drop_kinds: Vec<StepKind>,
31}
32
33impl Default for DeterministicCompaction {
34 fn default() -> Self {
35 Self {
36 max_steps: 100,
37 drop_kinds: vec![],
38 }
39 }
40}
41
42#[async_trait]
43impl CompactionPolicy for DeterministicCompaction {
44 async fn compact(&self, run_log: &mut RunLog) -> Result<(), RunLogError> {
45 if !self.drop_kinds.is_empty() {
47 run_log.steps.retain(|s| !self.drop_kinds.contains(&s.kind));
48 }
49
50 let max = self.max_steps as usize;
52 if run_log.steps.len() > max {
53 let total = run_log.steps.len();
54 let dropped = total - max;
55 let kept_tail: Vec<Step> = run_log.steps.split_off(dropped);
56 run_log.steps = vec![Step {
58 idx: 0,
59 kind: StepKind::ToolCall,
60 name: Some("compacted".into()),
61 prompt_tokens: None,
62 completion_tokens: None,
63 cost_usd: None,
64 input: serde_json::json!({ "dropped_count": dropped }),
65 output: serde_json::Value::Null,
66 error: None,
67 latency: Duration::ZERO,
68 span_id: None,
69 }];
70 run_log.steps.extend(kept_tail);
71 }
72
73 for (i, s) in run_log.steps.iter_mut().enumerate() {
75 s.idx = i as u32;
76 }
77 Ok(())
78 }
79}
80
81#[cfg(feature = "compaction-llm")]
82mod llm {
83 use super::{CompactionPolicy, RunLogError, Step, StepKind};
84 use crate::types::RunLog;
85 use async_trait::async_trait;
86 use klieo_core::llm::{ChatRequest, LlmClient, Message, Role};
87 use std::sync::Arc;
88 use std::time::Duration;
89
90 pub struct LlmCompaction {
97 pub llm: Arc<dyn LlmClient>,
99 pub max_steps: u32,
101 }
102
103 #[async_trait]
104 impl CompactionPolicy for LlmCompaction {
105 async fn compact(&self, run_log: &mut RunLog) -> Result<(), RunLogError> {
106 let max = self.max_steps as usize;
107 if run_log.steps.len() <= max {
108 return Ok(());
109 }
110 let total = run_log.steps.len();
111 let dropped = total - max;
112 let older_json = serde_json::to_string(&run_log.steps[..dropped])
114 .map_err(|e| RunLogError::Compaction(e.to_string()))?;
115 let prompt = format!(
116 "Summarise the following {dropped} agent steps in one sentence:\n{older_json}"
117 );
118 let req = ChatRequest::new(vec![Message {
122 role: Role::User,
123 content: prompt,
124 tool_calls: vec![],
125 tool_call_id: None,
126 }]);
127 let resp = self
128 .llm
129 .complete(req)
130 .await
131 .map_err(|e| RunLogError::Compaction(e.to_string()))?;
132 let summary = resp.message.content;
133 let kept_tail: Vec<Step> = run_log.steps.split_off(dropped);
134 run_log.steps = vec![Step {
135 idx: 0,
136 kind: StepKind::ToolCall,
137 name: Some("compacted".into()),
138 prompt_tokens: None,
139 completion_tokens: None,
140 cost_usd: None,
141 input: serde_json::json!({ "dropped_count": dropped }),
142 output: serde_json::Value::String(summary),
143 error: None,
144 latency: Duration::ZERO,
145 span_id: None,
146 }];
147 run_log.steps.extend(kept_tail);
148 for (i, s) in run_log.steps.iter_mut().enumerate() {
149 s.idx = i as u32;
150 }
151 Ok(())
152 }
153 }
154}
155
156#[cfg(feature = "compaction-llm")]
157pub use llm::LlmCompaction;